A community based topic aggregation platform built on atproto
at main 8.1 kB view raw
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}