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