···
10
+
"Coves/internal/atproto/identity"
13
+
// uniqueID generates a unique identifier for test isolation
14
+
func uniqueID() string {
15
+
return fmt.Sprintf("test-%d", time.Now().UnixNano())
18
+
// TestIdentityCache tests the PostgreSQL identity cache operations
19
+
func TestIdentityCache(t *testing.T) {
20
+
db := setupTestDB(t)
23
+
cache := identity.NewPostgresCache(db, 5*time.Minute)
24
+
ctx := context.Background()
26
+
// Generate unique test prefix for parallel safety
27
+
testID := fmt.Sprintf("test-%d", time.Now().UnixNano())
29
+
t.Run("Cache Miss on Empty Cache", func(t *testing.T) {
30
+
_, err := cache.Get(ctx, testID+"-nonexistent.test")
32
+
t.Error("Expected cache miss error, got nil")
36
+
t.Run("Set and Get Identity by Handle", func(t *testing.T) {
37
+
ident := &identity.Identity{
38
+
DID: "did:plc:" + testID + "-test123abc",
39
+
Handle: testID + "-alice.test",
40
+
PDSURL: "https://pds.alice.test",
41
+
ResolvedAt: time.Now().UTC(),
42
+
Method: identity.MethodHTTPS,
45
+
// Set identity in cache
46
+
if err := cache.Set(ctx, ident); err != nil {
47
+
t.Fatalf("Failed to cache identity: %v", err)
51
+
cached, err := cache.Get(ctx, ident.Handle)
53
+
t.Fatalf("Failed to get cached identity by handle: %v", err)
56
+
if cached.DID != ident.DID {
57
+
t.Errorf("Expected DID %s, got %s", ident.DID, cached.DID)
59
+
if cached.Handle != ident.Handle {
60
+
t.Errorf("Expected handle %s, got %s", ident.Handle, cached.Handle)
62
+
if cached.PDSURL != ident.PDSURL {
63
+
t.Errorf("Expected PDS URL %s, got %s", ident.PDSURL, cached.PDSURL)
67
+
t.Run("Get Identity by DID", func(t *testing.T) {
68
+
// Should be able to retrieve by DID as well (bidirectional cache)
69
+
expectedDID := "did:plc:" + testID + "-test123abc"
70
+
expectedHandle := testID + "-alice.test"
72
+
cached, err := cache.Get(ctx, expectedDID)
74
+
t.Fatalf("Failed to get cached identity by DID: %v", err)
77
+
if cached.Handle != expectedHandle {
78
+
t.Errorf("Expected handle %s, got %s", expectedHandle, cached.Handle)
82
+
t.Run("Update Existing Cache Entry", func(t *testing.T) {
83
+
// Update with new PDS URL
84
+
updated := &identity.Identity{
85
+
DID: "did:plc:test123abc",
86
+
Handle: "alice.test",
87
+
PDSURL: "https://new-pds.alice.test",
88
+
ResolvedAt: time.Now(),
89
+
Method: identity.MethodHTTPS,
92
+
if err := cache.Set(ctx, updated); err != nil {
93
+
t.Fatalf("Failed to update cached identity: %v", err)
96
+
cached, err := cache.Get(ctx, "alice.test")
98
+
t.Fatalf("Failed to get updated identity: %v", err)
101
+
if cached.PDSURL != "https://new-pds.alice.test" {
102
+
t.Errorf("Expected updated PDS URL, got %s", cached.PDSURL)
106
+
t.Run("Delete Cache Entry", func(t *testing.T) {
107
+
if err := cache.Delete(ctx, "alice.test"); err != nil {
108
+
t.Fatalf("Failed to delete cache entry: %v", err)
111
+
// Should now be a cache miss
112
+
_, err := cache.Get(ctx, "alice.test")
114
+
t.Error("Expected cache miss after deletion, got nil error")
118
+
t.Run("Purge Removes Both Handle and DID Entries", func(t *testing.T) {
119
+
ident := &identity.Identity{
120
+
DID: "did:plc:purgetest",
121
+
Handle: "purge.test",
122
+
PDSURL: "https://pds.purge.test",
123
+
ResolvedAt: time.Now(),
124
+
Method: identity.MethodDNS,
127
+
if err := cache.Set(ctx, ident); err != nil {
128
+
t.Fatalf("Failed to cache identity: %v", err)
131
+
// Verify both entries exist
132
+
if _, err := cache.Get(ctx, "purge.test"); err != nil {
133
+
t.Errorf("Handle entry should exist: %v", err)
135
+
if _, err := cache.Get(ctx, "did:plc:purgetest"); err != nil {
136
+
t.Errorf("DID entry should exist: %v", err)
140
+
if err := cache.Purge(ctx, "purge.test"); err != nil {
141
+
t.Fatalf("Failed to purge: %v", err)
144
+
// Both should be gone
145
+
if _, err := cache.Get(ctx, "purge.test"); err == nil {
146
+
t.Error("Handle entry should be purged")
148
+
if _, err := cache.Get(ctx, "did:plc:purgetest"); err == nil {
149
+
t.Error("DID entry should be purged")
153
+
t.Run("Handle Normalization - Case Insensitive", func(t *testing.T) {
154
+
ident := &identity.Identity{
155
+
DID: "did:plc:casetest",
156
+
Handle: "Alice.Test",
157
+
PDSURL: "https://pds.alice.test",
158
+
ResolvedAt: time.Now(),
159
+
Method: identity.MethodHTTPS,
162
+
if err := cache.Set(ctx, ident); err != nil {
163
+
t.Fatalf("Failed to cache identity: %v", err)
166
+
// Should be retrievable with different casing
167
+
cached, err := cache.Get(ctx, "ALICE.TEST")
169
+
t.Fatalf("Failed to get identity with different casing: %v", err)
172
+
if cached.DID != "did:plc:casetest" {
173
+
t.Errorf("Expected DID did:plc:casetest, got %s", cached.DID)
177
+
cache.Delete(ctx, "alice.test")
180
+
t.Run("DID is Case Sensitive", func(t *testing.T) {
181
+
ident := &identity.Identity{
182
+
DID: "did:plc:CaseSensitive",
183
+
Handle: "sensitive.test",
184
+
PDSURL: "https://pds.test",
185
+
ResolvedAt: time.Now(),
186
+
Method: identity.MethodHTTPS,
189
+
if err := cache.Set(ctx, ident); err != nil {
190
+
t.Fatalf("Failed to cache identity: %v", err)
193
+
// Should retrieve with exact case
194
+
if _, err := cache.Get(ctx, "did:plc:CaseSensitive"); err != nil {
195
+
t.Errorf("Should retrieve DID with exact case: %v", err)
198
+
// Different case should miss (DIDs are case-sensitive)
199
+
if _, err := cache.Get(ctx, "did:plc:casesensitive"); err == nil {
200
+
t.Error("Should NOT retrieve DID with different case")
204
+
cache.Delete(ctx, "did:plc:CaseSensitive")
208
+
// TestIdentityCacheTTL tests that expired cache entries are not returned
209
+
func TestIdentityCacheTTL(t *testing.T) {
210
+
db := setupTestDB(t)
213
+
// Create cache with very short TTL (reduced from 1s to 100ms for faster, less flaky tests)
214
+
ttl := 100 * time.Millisecond
215
+
cache := identity.NewPostgresCache(db, ttl)
216
+
ctx := context.Background()
218
+
// Use unique ID for test isolation
219
+
testID := uniqueID()
221
+
ident := &identity.Identity{
222
+
DID: "did:plc:" + testID,
223
+
Handle: testID + ".ttl.test",
224
+
PDSURL: "https://pds.ttl.test",
225
+
ResolvedAt: time.Now().UTC(),
226
+
Method: identity.MethodHTTPS,
229
+
if err := cache.Set(ctx, ident); err != nil {
230
+
t.Fatalf("Failed to cache identity: %v", err)
233
+
// Should be retrievable immediately
234
+
if _, err := cache.Get(ctx, ident.Handle); err != nil {
235
+
t.Errorf("Should retrieve fresh cache entry: %v", err)
238
+
// Wait for TTL to expire (1.5x TTL for safety margin on slow systems)
239
+
waitTime := time.Duration(float64(ttl) * 1.5)
240
+
t.Logf("Waiting %s for cache entry to expire (TTL=%s)...", waitTime, ttl)
241
+
time.Sleep(waitTime)
243
+
// Should now be a cache miss
244
+
_, err := cache.Get(ctx, ident.Handle)
246
+
t.Error("Expected cache miss after TTL expiration, got nil error")
250
+
// TestIdentityResolverWithCache tests the caching resolver behavior
251
+
func TestIdentityResolverWithCache(t *testing.T) {
252
+
db := setupTestDB(t)
255
+
cache := identity.NewPostgresCache(db, 5*time.Minute)
258
+
_, _ = db.Exec("TRUNCATE identity_cache")
260
+
// Create resolver with caching
261
+
resolver := identity.NewResolver(db, identity.Config{
262
+
PLCURL: "https://plc.directory",
263
+
CacheTTL: 5 * time.Minute,
266
+
ctx := context.Background()
268
+
t.Run("Resolve Invalid Identifier", func(t *testing.T) {
269
+
_, err := resolver.Resolve(ctx, "")
271
+
t.Error("Expected error for empty identifier")
274
+
_, err = resolver.Resolve(ctx, "invalid format")
276
+
t.Error("Expected error for invalid identifier format")
280
+
t.Run("ResolveHandle Returns DID and PDS URL", func(t *testing.T) {
281
+
// Pre-populate cache with known identity
282
+
ident := &identity.Identity{
283
+
DID: "did:plc:resolvetest",
284
+
Handle: "resolve.test",
285
+
PDSURL: "https://pds.resolve.test",
286
+
ResolvedAt: time.Now(),
287
+
Method: identity.MethodDNS,
290
+
if err := cache.Set(ctx, ident); err != nil {
291
+
t.Fatalf("Failed to pre-populate cache: %v", err)
294
+
did, pdsURL, err := resolver.ResolveHandle(ctx, "resolve.test")
296
+
t.Fatalf("Failed to resolve handle: %v", err)
299
+
if did != "did:plc:resolvetest" {
300
+
t.Errorf("Expected DID did:plc:resolvetest, got %s", did)
302
+
if pdsURL != "https://pds.resolve.test" {
303
+
t.Errorf("Expected PDS URL https://pds.resolve.test, got %s", pdsURL)
307
+
t.Run("Purge Removes from Cache", func(t *testing.T) {
308
+
// Pre-populate cache
309
+
ident := &identity.Identity{
310
+
DID: "did:plc:purge123",
311
+
Handle: "purgetest.test",
312
+
PDSURL: "https://pds.test",
313
+
ResolvedAt: time.Now(),
314
+
Method: identity.MethodHTTPS,
317
+
if err := cache.Set(ctx, ident); err != nil {
318
+
t.Fatalf("Failed to cache identity: %v", err)
321
+
// Verify it's cached
322
+
if _, err := cache.Get(ctx, "purgetest.test"); err != nil {
323
+
t.Fatalf("Identity should be cached: %v", err)
326
+
// Purge via resolver
327
+
if err := resolver.Purge(ctx, "purgetest.test"); err != nil {
328
+
t.Fatalf("Failed to purge: %v", err)
331
+
// Should be gone from cache
332
+
if _, err := cache.Get(ctx, "purgetest.test"); err == nil {
333
+
t.Error("Identity should be purged from cache")
338
+
// TestIdentityResolverRealHandles tests resolution with real atProto handles
339
+
// This is an optional integration test that requires network access
340
+
func TestIdentityResolverRealHandles(t *testing.T) {
341
+
if testing.Short() {
342
+
t.Skip("Skipping real handle resolution test in short mode")
345
+
// Skip if environment variable is not set (opt-in for real network tests)
346
+
if os.Getenv("TEST_REAL_HANDLES") != "1" {
347
+
t.Skip("Skipping real handle resolution - set TEST_REAL_HANDLES=1 to enable")
350
+
db := setupTestDB(t)
353
+
resolver := identity.NewResolver(db, identity.Config{
354
+
PLCURL: "https://plc.directory",
355
+
CacheTTL: 10 * time.Minute,
358
+
ctx := context.Background()
360
+
testCases := []struct {
364
+
expectedMethod identity.ResolutionMethod
367
+
name: "Resolve bsky.app (well-known handle)",
368
+
handle: "bsky.app",
369
+
expectError: false,
370
+
expectedMethod: identity.MethodHTTPS,
373
+
name: "Resolve nonexistent handle",
374
+
handle: "this-handle-definitely-does-not-exist-12345.bsky.social",
379
+
for _, tc := range testCases {
380
+
t.Run(tc.name, func(t *testing.T) {
381
+
ident, err := resolver.Resolve(ctx, tc.handle)
383
+
if tc.expectError {
385
+
t.Error("Expected error for nonexistent handle")
391
+
t.Fatalf("Failed to resolve handle %s: %v", tc.handle, err)
394
+
if ident.Handle != tc.handle {
395
+
t.Errorf("Expected handle %s, got %s", tc.handle, ident.Handle)
398
+
if ident.DID == "" {
399
+
t.Error("Expected non-empty DID")
402
+
if ident.PDSURL == "" {
403
+
t.Error("Expected non-empty PDS URL")
406
+
t.Logf("✅ Resolved %s → %s (PDS: %s, Method: %s)",
407
+
ident.Handle, ident.DID, ident.PDSURL, ident.Method)
409
+
// Second resolution should hit cache
410
+
ident2, err := resolver.Resolve(ctx, tc.handle)
412
+
t.Fatalf("Failed second resolution: %v", err)
415
+
if ident2.Method != identity.MethodCache {
416
+
t.Errorf("Second resolution should be from cache, got method: %s", ident2.Method)
419
+
t.Logf("✅ Second resolution from cache: %s (Method: %s)", tc.handle, ident2.Method)
424
+
// TestResolveDID tests DID document resolution
425
+
func TestResolveDID(t *testing.T) {
426
+
if testing.Short() {
427
+
t.Skip("Skipping DID resolution test in short mode")
430
+
if os.Getenv("TEST_REAL_HANDLES") != "1" {
431
+
t.Skip("Skipping DID resolution - set TEST_REAL_HANDLES=1 to enable")
434
+
db := setupTestDB(t)
437
+
resolver := identity.NewResolver(db, identity.Config{
438
+
PLCURL: "https://plc.directory",
439
+
CacheTTL: 10 * time.Minute,
442
+
ctx := context.Background()
444
+
t.Run("Resolve Real DID Document", func(t *testing.T) {
445
+
// First resolve a handle to get a real DID
446
+
ident, err := resolver.Resolve(ctx, "bsky.app")
448
+
t.Skipf("Failed to resolve handle for DID test: %v", err)
451
+
// Now resolve the DID document
452
+
doc, err := resolver.ResolveDID(ctx, ident.DID)
454
+
t.Fatalf("Failed to resolve DID document: %v", err)
457
+
if doc.DID != ident.DID {
458
+
t.Errorf("Expected DID %s, got %s", ident.DID, doc.DID)
461
+
// Should have at least PDS service
462
+
if len(doc.Service) == 0 {
463
+
t.Error("Expected at least one service in DID document")
466
+
// Find PDS service
468
+
for _, svc := range doc.Service {
469
+
if svc.Type == "AtprotoPersonalDataServer" {
471
+
if svc.ServiceEndpoint == "" {
472
+
t.Error("PDS service endpoint should not be empty")
474
+
t.Logf("✅ PDS Service: %s", svc.ServiceEndpoint)
479
+
t.Error("Expected to find AtprotoPersonalDataServer service in DID document")
483
+
t.Run("Resolve Invalid DID", func(t *testing.T) {
484
+
_, err := resolver.ResolveDID(ctx, "not-a-did")
486
+
t.Error("Expected error for invalid DID format")