A community based topic aggregation platform built on atproto
1package oauth
2
3import (
4 "crypto/rand"
5 "encoding/base64"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14// generateSealSecret generates a random 32-byte seal secret for testing
15func generateSealSecret() []byte {
16 secret := make([]byte, 32)
17 if _, err := rand.Read(secret); err != nil {
18 panic(err)
19 }
20 return secret
21}
22
23func TestSealSession_RoundTrip(t *testing.T) {
24 // Create client with seal secret
25 client := &OAuthClient{
26 SealSecret: generateSealSecret(),
27 }
28
29 did := "did:plc:abc123"
30 sessionID := "session-xyz"
31 ttl := 1 * time.Hour
32
33 // Seal the session
34 token, err := client.SealSession(did, sessionID, ttl)
35 require.NoError(t, err)
36 require.NotEmpty(t, token)
37
38 // Token should be base64url encoded
39 _, err = base64.RawURLEncoding.DecodeString(token)
40 require.NoError(t, err, "token should be valid base64url")
41
42 // Unseal the session
43 session, err := client.UnsealSession(token)
44 require.NoError(t, err)
45 require.NotNil(t, session)
46
47 // Verify data
48 assert.Equal(t, did, session.DID)
49 assert.Equal(t, sessionID, session.SessionID)
50
51 // Verify expiration is approximately correct (within 1 second)
52 expectedExpiry := time.Now().Add(ttl).Unix()
53 assert.InDelta(t, expectedExpiry, session.ExpiresAt, 1.0)
54}
55
56func TestSealSession_ExpirationValidation(t *testing.T) {
57 client := &OAuthClient{
58 SealSecret: generateSealSecret(),
59 }
60
61 did := "did:plc:abc123"
62 sessionID := "session-xyz"
63 ttl := 2 * time.Second // Short TTL (must be >= 1 second due to Unix timestamp granularity)
64
65 // Seal the session
66 token, err := client.SealSession(did, sessionID, ttl)
67 require.NoError(t, err)
68
69 // Should work immediately
70 session, err := client.UnsealSession(token)
71 require.NoError(t, err)
72 assert.Equal(t, did, session.DID)
73
74 // Wait well past expiration
75 time.Sleep(2500 * time.Millisecond)
76
77 // Should fail after expiration
78 session, err = client.UnsealSession(token)
79 assert.Error(t, err)
80 assert.Nil(t, session)
81 assert.Contains(t, err.Error(), "token expired")
82}
83
84func TestSealSession_TamperedTokenDetection(t *testing.T) {
85 client := &OAuthClient{
86 SealSecret: generateSealSecret(),
87 }
88
89 did := "did:plc:abc123"
90 sessionID := "session-xyz"
91 ttl := 1 * time.Hour
92
93 // Seal the session
94 token, err := client.SealSession(did, sessionID, ttl)
95 require.NoError(t, err)
96
97 // Tamper with the token by modifying one character
98 tampered := token[:len(token)-5] + "XXXX" + token[len(token)-1:]
99
100 // Should fail to unseal tampered token
101 session, err := client.UnsealSession(tampered)
102 assert.Error(t, err)
103 assert.Nil(t, session)
104 assert.Contains(t, err.Error(), "failed to decrypt token")
105}
106
107func TestSealSession_InvalidTokenFormats(t *testing.T) {
108 client := &OAuthClient{
109 SealSecret: generateSealSecret(),
110 }
111
112 tests := []struct {
113 name string
114 token string
115 }{
116 {
117 name: "empty token",
118 token: "",
119 },
120 {
121 name: "invalid base64",
122 token: "not-valid-base64!@#$",
123 },
124 {
125 name: "too short",
126 token: base64.RawURLEncoding.EncodeToString([]byte("short")),
127 },
128 {
129 name: "random bytes",
130 token: base64.RawURLEncoding.EncodeToString(make([]byte, 50)),
131 },
132 }
133
134 for _, tt := range tests {
135 t.Run(tt.name, func(t *testing.T) {
136 session, err := client.UnsealSession(tt.token)
137 assert.Error(t, err)
138 assert.Nil(t, session)
139 })
140 }
141}
142
143func TestSealSession_DifferentSecrets(t *testing.T) {
144 // Create two clients with different secrets
145 client1 := &OAuthClient{
146 SealSecret: generateSealSecret(),
147 }
148 client2 := &OAuthClient{
149 SealSecret: generateSealSecret(),
150 }
151
152 did := "did:plc:abc123"
153 sessionID := "session-xyz"
154 ttl := 1 * time.Hour
155
156 // Seal with client1
157 token, err := client1.SealSession(did, sessionID, ttl)
158 require.NoError(t, err)
159
160 // Try to unseal with client2 (different secret)
161 session, err := client2.UnsealSession(token)
162 assert.Error(t, err)
163 assert.Nil(t, session)
164 assert.Contains(t, err.Error(), "failed to decrypt token")
165}
166
167func TestSealSession_NoSecretConfigured(t *testing.T) {
168 client := &OAuthClient{
169 SealSecret: nil,
170 }
171
172 did := "did:plc:abc123"
173 sessionID := "session-xyz"
174 ttl := 1 * time.Hour
175
176 // Should fail to seal without secret
177 token, err := client.SealSession(did, sessionID, ttl)
178 assert.Error(t, err)
179 assert.Empty(t, token)
180 assert.Contains(t, err.Error(), "seal secret not configured")
181
182 // Should fail to unseal without secret
183 session, err := client.UnsealSession("dummy-token")
184 assert.Error(t, err)
185 assert.Nil(t, session)
186 assert.Contains(t, err.Error(), "seal secret not configured")
187}
188
189func TestSealSession_MissingRequiredFields(t *testing.T) {
190 client := &OAuthClient{
191 SealSecret: generateSealSecret(),
192 }
193
194 ttl := 1 * time.Hour
195
196 tests := []struct {
197 name string
198 did string
199 sessionID string
200 errorMsg string
201 }{
202 {
203 name: "missing DID",
204 did: "",
205 sessionID: "session-123",
206 errorMsg: "DID is required",
207 },
208 {
209 name: "missing session ID",
210 did: "did:plc:abc123",
211 sessionID: "",
212 errorMsg: "session ID is required",
213 },
214 }
215
216 for _, tt := range tests {
217 t.Run(tt.name, func(t *testing.T) {
218 token, err := client.SealSession(tt.did, tt.sessionID, ttl)
219 assert.Error(t, err)
220 assert.Empty(t, token)
221 assert.Contains(t, err.Error(), tt.errorMsg)
222 })
223 }
224}
225
226func TestSealSession_UniquenessPerCall(t *testing.T) {
227 client := &OAuthClient{
228 SealSecret: generateSealSecret(),
229 }
230
231 did := "did:plc:abc123"
232 sessionID := "session-xyz"
233 ttl := 1 * time.Hour
234
235 // Seal the same session twice
236 token1, err := client.SealSession(did, sessionID, ttl)
237 require.NoError(t, err)
238
239 token2, err := client.SealSession(did, sessionID, ttl)
240 require.NoError(t, err)
241
242 // Tokens should be different (different nonces)
243 assert.NotEqual(t, token1, token2, "tokens should be unique due to different nonces")
244
245 // But both should unseal to the same session data
246 session1, err := client.UnsealSession(token1)
247 require.NoError(t, err)
248
249 session2, err := client.UnsealSession(token2)
250 require.NoError(t, err)
251
252 assert.Equal(t, session1.DID, session2.DID)
253 assert.Equal(t, session1.SessionID, session2.SessionID)
254}
255
256func TestSealSession_LongDIDAndSessionID(t *testing.T) {
257 client := &OAuthClient{
258 SealSecret: generateSealSecret(),
259 }
260
261 // Test with very long DID and session ID
262 did := "did:plc:" + strings.Repeat("a", 200)
263 sessionID := "session-" + strings.Repeat("x", 200)
264 ttl := 1 * time.Hour
265
266 // Should work with long values
267 token, err := client.SealSession(did, sessionID, ttl)
268 require.NoError(t, err)
269
270 session, err := client.UnsealSession(token)
271 require.NoError(t, err)
272 assert.Equal(t, did, session.DID)
273 assert.Equal(t, sessionID, session.SessionID)
274}
275
276func TestSealSession_URLSafeEncoding(t *testing.T) {
277 client := &OAuthClient{
278 SealSecret: generateSealSecret(),
279 }
280
281 did := "did:plc:abc123"
282 sessionID := "session-xyz"
283 ttl := 1 * time.Hour
284
285 // Seal multiple times to get different nonces
286 for i := 0; i < 100; i++ {
287 token, err := client.SealSession(did, sessionID, ttl)
288 require.NoError(t, err)
289
290 // Token should not contain URL-unsafe characters
291 assert.NotContains(t, token, "+", "token should not contain '+'")
292 assert.NotContains(t, token, "/", "token should not contain '/'")
293 assert.NotContains(t, token, "=", "token should not contain '='")
294
295 // Should unseal successfully
296 session, err := client.UnsealSession(token)
297 require.NoError(t, err)
298 assert.Equal(t, did, session.DID)
299 }
300}
301
302func TestSealSession_ConcurrentAccess(t *testing.T) {
303 client := &OAuthClient{
304 SealSecret: generateSealSecret(),
305 }
306
307 did := "did:plc:abc123"
308 sessionID := "session-xyz"
309 ttl := 1 * time.Hour
310
311 // Run concurrent seal/unseal operations
312 done := make(chan bool)
313 for i := 0; i < 10; i++ {
314 go func() {
315 for j := 0; j < 100; j++ {
316 token, err := client.SealSession(did, sessionID, ttl)
317 require.NoError(t, err)
318
319 session, err := client.UnsealSession(token)
320 require.NoError(t, err)
321 assert.Equal(t, did, session.DID)
322 }
323 done <- true
324 }()
325 }
326
327 // Wait for all goroutines
328 for i := 0; i < 10; i++ {
329 <-done
330 }
331}