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}