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