A community based topic aggregation platform built on atproto
1package integration 2 3import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "Coves/internal/atproto/jetstream" 10 "Coves/internal/db/postgres" 11) 12 13// TestHostedByVerification_DomainMatching tests that hostedBy domain must match handle domain 14func TestHostedByVerification_DomainMatching(t *testing.T) { 15 db := setupTestDB(t) 16 defer func() { 17 if err := db.Close(); err != nil { 18 t.Logf("Failed to close database: %v", err) 19 } 20 }() 21 22 repo := postgres.NewCommunityRepository(db) 23 ctx := context.Background() 24 25 t.Run("rejects community with mismatched hostedBy domain", func(t *testing.T) { 26 // Create consumer with verification enabled 27 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 28 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.social", false, nil) 29 30 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 31 communityDID := generateTestDID(uniqueSuffix) 32 uniqueHandle := fmt.Sprintf("gaming%s.community.coves.social", uniqueSuffix) 33 34 // Attempt to create community claiming to be hosted by nintendo.com 35 // but with a coves.social handle (ATTACK!) 36 event := &jetstream.JetstreamEvent{ 37 Did: communityDID, 38 TimeUS: time.Now().UnixMicro(), 39 Kind: "commit", 40 Commit: &jetstream.CommitEvent{ 41 Rev: "rev123", 42 Operation: "create", 43 Collection: "social.coves.community.profile", 44 RKey: "self", 45 CID: "bafy123abc", 46 Record: map[string]interface{}{ 47 "handle": uniqueHandle, // coves.social handle 48 "name": "gaming", 49 "displayName": "Nintendo Gaming", 50 "description": "Fake Nintendo community", 51 "createdBy": "did:plc:attacker123", 52 "hostedBy": "did:web:nintendo.com", // ← SPOOFED! Claiming Nintendo hosting 53 "visibility": "public", 54 "federation": map[string]interface{}{ 55 "allowExternalDiscovery": true, 56 }, 57 "memberCount": 0, 58 "subscriberCount": 0, 59 "createdAt": time.Now().Format(time.RFC3339), 60 }, 61 }, 62 } 63 64 // This should fail verification 65 err := consumer.HandleEvent(ctx, event) 66 if err == nil { 67 t.Fatal("Expected verification error for mismatched hostedBy domain, got nil") 68 } 69 70 // Verify error message mentions domain mismatch 71 errMsg := err.Error() 72 if errMsg == "" { 73 t.Fatal("Expected error message, got empty string") 74 } 75 t.Logf("Got expected error: %v", err) 76 77 // Verify community was NOT indexed 78 _, getErr := repo.GetByDID(ctx, communityDID) 79 if getErr == nil { 80 t.Fatal("Community should not have been indexed, but was found in database") 81 } 82 }) 83 84 t.Run("accepts community with matching hostedBy domain", func(t *testing.T) { 85 // Create consumer with verification enabled 86 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 87 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.social", false, nil) 88 89 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 90 communityDID := generateTestDID(uniqueSuffix) 91 uniqueHandle := fmt.Sprintf("gaming%s.community.coves.social", uniqueSuffix) 92 93 // Create community with matching hostedBy and handle domains 94 event := &jetstream.JetstreamEvent{ 95 Did: communityDID, 96 TimeUS: time.Now().UnixMicro(), 97 Kind: "commit", 98 Commit: &jetstream.CommitEvent{ 99 Rev: "rev123", 100 Operation: "create", 101 Collection: "social.coves.community.profile", 102 RKey: "self", 103 CID: "bafy123abc", 104 Record: map[string]interface{}{ 105 "handle": uniqueHandle, // coves.social handle 106 "name": "gaming", 107 "displayName": "Gaming Community", 108 "description": "Legitimate coves.social community", 109 "createdBy": "did:plc:user123", 110 "hostedBy": "did:web:coves.social", // ✅ Matches handle domain 111 "visibility": "public", 112 "federation": map[string]interface{}{ 113 "allowExternalDiscovery": true, 114 }, 115 "memberCount": 0, 116 "subscriberCount": 0, 117 "createdAt": time.Now().Format(time.RFC3339), 118 }, 119 }, 120 } 121 122 // This should succeed 123 err := consumer.HandleEvent(ctx, event) 124 if err != nil { 125 t.Fatalf("Expected verification to succeed, got error: %v", err) 126 } 127 128 // Verify community was indexed 129 community, getErr := repo.GetByDID(ctx, communityDID) 130 if getErr != nil { 131 t.Fatalf("Community should have been indexed: %v", getErr) 132 } 133 if community.HostedByDID != "did:web:coves.social" { 134 t.Errorf("Expected hostedByDID 'did:web:coves.social', got '%s'", community.HostedByDID) 135 } 136 }) 137 138 t.Run("rejects hostedBy with non-did:web format", func(t *testing.T) { 139 // Create consumer with verification enabled 140 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 141 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.social", false, nil) 142 143 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 144 communityDID := generateTestDID(uniqueSuffix) 145 uniqueHandle := fmt.Sprintf("gaming%s.community.coves.social", uniqueSuffix) 146 147 // Attempt to use did:plc for hostedBy (not allowed) 148 event := &jetstream.JetstreamEvent{ 149 Did: communityDID, 150 TimeUS: time.Now().UnixMicro(), 151 Kind: "commit", 152 Commit: &jetstream.CommitEvent{ 153 Rev: "rev123", 154 Operation: "create", 155 Collection: "social.coves.community.profile", 156 RKey: "self", 157 CID: "bafy123abc", 158 Record: map[string]interface{}{ 159 "handle": uniqueHandle, 160 "name": "gaming", 161 "displayName": "Test Community", 162 "description": "Test", 163 "createdBy": "did:plc:user123", 164 "hostedBy": "did:plc:xyz123", // ← Invalid: must be did:web 165 "visibility": "public", 166 "federation": map[string]interface{}{ 167 "allowExternalDiscovery": true, 168 }, 169 "memberCount": 0, 170 "subscriberCount": 0, 171 "createdAt": time.Now().Format(time.RFC3339), 172 }, 173 }, 174 } 175 176 // This should fail verification 177 err := consumer.HandleEvent(ctx, event) 178 if err == nil { 179 t.Fatal("Expected verification error for non-did:web hostedBy, got nil") 180 } 181 t.Logf("Got expected error: %v", err) 182 }) 183 184 t.Run("skip verification flag bypasses all checks", func(t *testing.T) { 185 // Create consumer with verification DISABLED 186 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 187 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.social", true, nil) 188 189 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 190 communityDID := generateTestDID(uniqueSuffix) 191 uniqueHandle := fmt.Sprintf("gaming%s.community.example.com", uniqueSuffix) 192 193 // Even with mismatched domain, this should succeed with skipVerification=true 194 event := &jetstream.JetstreamEvent{ 195 Did: communityDID, 196 TimeUS: time.Now().UnixMicro(), 197 Kind: "commit", 198 Commit: &jetstream.CommitEvent{ 199 Rev: "rev123", 200 Operation: "create", 201 Collection: "social.coves.community.profile", 202 RKey: "self", 203 CID: "bafy123abc", 204 Record: map[string]interface{}{ 205 "handle": uniqueHandle, 206 "name": "gaming", 207 "displayName": "Test", 208 "description": "Test", 209 "createdBy": "did:plc:user123", 210 "hostedBy": "did:web:nintendo.com", // Mismatched, but verification skipped 211 "visibility": "public", 212 "federation": map[string]interface{}{ 213 "allowExternalDiscovery": true, 214 }, 215 "memberCount": 0, 216 "subscriberCount": 0, 217 "createdAt": time.Now().Format(time.RFC3339), 218 }, 219 }, 220 } 221 222 // Should succeed because verification is skipped 223 err := consumer.HandleEvent(ctx, event) 224 if err != nil { 225 t.Fatalf("Expected success with skipVerification=true, got error: %v", err) 226 } 227 228 // Verify community was indexed 229 _, getErr := repo.GetByDID(ctx, communityDID) 230 if getErr != nil { 231 t.Fatalf("Community should have been indexed: %v", getErr) 232 } 233 }) 234} 235 236// TestExtractDomainFromHandle tests the domain extraction logic for various handle formats 237func TestExtractDomainFromHandle(t *testing.T) { 238 // This is an internal function test - we'll test it through the consumer 239 db := setupTestDB(t) 240 defer func() { 241 if err := db.Close(); err != nil { 242 t.Logf("Failed to close database: %v", err) 243 } 244 }() 245 246 repo := postgres.NewCommunityRepository(db) 247 ctx := context.Background() 248 249 testCases := []struct { 250 name string 251 handle string 252 hostedByDID string 253 shouldSucceed bool 254 }{ 255 { 256 name: "DNS-style handle with subdomain", 257 handle: "gaming.community.coves.social", 258 hostedByDID: "did:web:coves.social", 259 shouldSucceed: true, 260 }, 261 { 262 name: "Simple two-part domain", 263 handle: "gaming.coves.social", 264 hostedByDID: "did:web:coves.social", 265 shouldSucceed: true, 266 }, 267 { 268 name: "Multi-part subdomain", 269 handle: "gaming.test.community.example.com", 270 hostedByDID: "did:web:example.com", 271 shouldSucceed: true, 272 }, 273 { 274 name: "Mismatched domain", 275 handle: "gaming.community.coves.social", 276 hostedByDID: "did:web:example.com", 277 shouldSucceed: false, 278 }, 279 // CRITICAL: Multi-part TLD tests (PR review feedback) 280 { 281 name: "Multi-part TLD: .co.uk", 282 handle: "gaming.community.coves.co.uk", 283 hostedByDID: "did:web:coves.co.uk", 284 shouldSucceed: true, 285 }, 286 { 287 name: "Multi-part TLD: .com.au", 288 handle: "gaming.community.example.com.au", 289 hostedByDID: "did:web:example.com.au", 290 shouldSucceed: true, 291 }, 292 { 293 name: "Multi-part TLD: Reject incorrect .co.uk extraction", 294 handle: "gaming.community.coves.co.uk", 295 hostedByDID: "did:web:co.uk", // Wrong! Should be coves.co.uk 296 shouldSucceed: false, 297 }, 298 { 299 name: "Multi-part TLD: .org.uk", 300 handle: "gaming.community.myinstance.org.uk", 301 hostedByDID: "did:web:myinstance.org.uk", 302 shouldSucceed: true, 303 }, 304 { 305 name: "Multi-part TLD: .ac.uk", 306 handle: "gaming.community.university.ac.uk", 307 hostedByDID: "did:web:university.ac.uk", 308 shouldSucceed: true, 309 }, 310 } 311 312 for _, tc := range testCases { 313 t.Run(tc.name, func(t *testing.T) { 314 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs 315 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.social", false, nil) 316 317 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 318 communityDID := generateTestDID(uniqueSuffix) 319 320 event := &jetstream.JetstreamEvent{ 321 Did: communityDID, 322 TimeUS: time.Now().UnixMicro(), 323 Kind: "commit", 324 Commit: &jetstream.CommitEvent{ 325 Rev: "rev123", 326 Operation: "create", 327 Collection: "social.coves.community.profile", 328 RKey: "self", 329 CID: "bafy123abc", 330 Record: map[string]interface{}{ 331 "handle": tc.handle, 332 "name": "test", 333 "displayName": "Test", 334 "description": "Test", 335 "createdBy": "did:plc:user123", 336 "hostedBy": tc.hostedByDID, 337 "visibility": "public", 338 "federation": map[string]interface{}{ 339 "allowExternalDiscovery": true, 340 }, 341 "memberCount": 0, 342 "subscriberCount": 0, 343 "createdAt": time.Now().Format(time.RFC3339), 344 }, 345 }, 346 } 347 348 err := consumer.HandleEvent(ctx, event) 349 if tc.shouldSucceed && err != nil { 350 t.Errorf("Expected success for %s, got error: %v", tc.handle, err) 351 } else if !tc.shouldSucceed && err == nil { 352 t.Errorf("Expected failure for %s, got success", tc.handle) 353 } 354 }) 355 } 356}