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}