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/core/communities"
11 "Coves/internal/db/postgres"
12)
13
14// TestCommunityConsumer_V2RKeyValidation tests that only V2 communities (rkey="self") are accepted
15func TestCommunityConsumer_V2RKeyValidation(t *testing.T) {
16 db := setupTestDB(t)
17 defer func() {
18 if err := db.Close(); err != nil {
19 t.Logf("Failed to close database: %v", err)
20 }
21 }()
22
23 repo := postgres.NewCommunityRepository(db)
24 // Skip verification in tests
25 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs
26 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.local", true, nil)
27 ctx := context.Background()
28
29 t.Run("accepts V2 community with rkey=self", func(t *testing.T) {
30 // Use unique DID and handle to avoid conflicts with other test runs
31 timestamp := time.Now().UnixNano()
32 testDID := fmt.Sprintf("did:plc:testv2rkey%d", timestamp)
33 testHandle := fmt.Sprintf("testv2rkey%d.community.coves.social", timestamp)
34
35 event := &jetstream.JetstreamEvent{
36 Did: testDID,
37 Kind: "commit",
38 Commit: &jetstream.CommitEvent{
39 Operation: "create",
40 Collection: "social.coves.community.profile",
41 RKey: "self", // V2: correct rkey
42 CID: "bafyreigaming123",
43 Record: map[string]interface{}{
44 "$type": "social.coves.community.profile",
45 "handle": testHandle,
46 "name": "testv2rkey",
47 "createdBy": "did:plc:user123",
48 "hostedBy": "did:web:coves.social",
49 "visibility": "public",
50 "federation": map[string]interface{}{
51 "allowExternalDiscovery": true,
52 },
53 "memberCount": 0,
54 "subscriberCount": 0,
55 "createdAt": time.Now().Format(time.RFC3339),
56 },
57 },
58 }
59
60 err := consumer.HandleEvent(ctx, event)
61 if err != nil {
62 t.Errorf("V2 community with rkey=self should be accepted, got error: %v", err)
63 }
64
65 // Verify community was indexed
66 community, err := repo.GetByDID(ctx, testDID)
67 if err != nil {
68 t.Fatalf("Community should have been indexed: %v", err)
69 }
70
71 // Verify V2 self-ownership
72 if community.OwnerDID != community.DID {
73 t.Errorf("V2 community should be self-owned: expected OwnerDID=%s, got %s", community.DID, community.OwnerDID)
74 }
75
76 // Verify record URI uses "self"
77 expectedURI := fmt.Sprintf("at://%s/social.coves.community.profile/self", testDID)
78 if community.RecordURI != expectedURI {
79 t.Errorf("Expected RecordURI %s, got %s", expectedURI, community.RecordURI)
80 }
81 })
82
83 t.Run("rejects V1 community with non-self rkey", func(t *testing.T) {
84 event := &jetstream.JetstreamEvent{
85 Did: "did:plc:community456",
86 Kind: "commit",
87 Commit: &jetstream.CommitEvent{
88 Operation: "create",
89 Collection: "social.coves.community.profile",
90 RKey: "3k2j4h5g6f7d", // V1: TID-based rkey (INVALID for V2!)
91 CID: "bafyreiv1community",
92 Record: map[string]interface{}{
93 "$type": "social.coves.community.profile",
94 "handle": "v1community.community.coves.social",
95 "name": "v1community",
96 "createdBy": "did:plc:user456",
97 "hostedBy": "did:web:coves.social",
98 "visibility": "public",
99 "federation": map[string]interface{}{
100 "allowExternalDiscovery": true,
101 },
102 "memberCount": 0,
103 "subscriberCount": 0,
104 "createdAt": time.Now().Format(time.RFC3339),
105 },
106 },
107 }
108
109 err := consumer.HandleEvent(ctx, event)
110 if err == nil {
111 t.Error("V1 community with TID rkey should be rejected")
112 }
113
114 // Verify error message indicates V1 not supported
115 if err != nil {
116 errMsg := err.Error()
117 if errMsg != "invalid community profile rkey: expected 'self', got '3k2j4h5g6f7d' (V1 communities not supported)" {
118 t.Errorf("Expected V1 rejection error, got: %s", errMsg)
119 }
120 }
121
122 // Verify community was NOT indexed
123 _, err = repo.GetByDID(ctx, "did:plc:community456")
124 if err != communities.ErrCommunityNotFound {
125 t.Errorf("V1 community should not have been indexed, expected ErrCommunityNotFound, got: %v", err)
126 }
127 })
128
129 t.Run("rejects community with custom rkey", func(t *testing.T) {
130 event := &jetstream.JetstreamEvent{
131 Did: "did:plc:community789",
132 Kind: "commit",
133 Commit: &jetstream.CommitEvent{
134 Operation: "create",
135 Collection: "social.coves.community.profile",
136 RKey: "custom-profile-name", // Custom rkey (INVALID!)
137 CID: "bafyreicustom",
138 Record: map[string]interface{}{
139 "$type": "social.coves.community.profile",
140 "handle": "custom.community.coves.social",
141 "name": "custom",
142 "createdBy": "did:plc:user789",
143 "hostedBy": "did:web:coves.social",
144 "visibility": "public",
145 "federation": map[string]interface{}{
146 "allowExternalDiscovery": true,
147 },
148 "memberCount": 0,
149 "subscriberCount": 0,
150 "createdAt": time.Now().Format(time.RFC3339),
151 },
152 },
153 }
154
155 err := consumer.HandleEvent(ctx, event)
156 if err == nil {
157 t.Error("Community with custom rkey should be rejected")
158 }
159
160 // Verify community was NOT indexed
161 _, err = repo.GetByDID(ctx, "did:plc:community789")
162 if err != communities.ErrCommunityNotFound {
163 t.Error("Community with custom rkey should not have been indexed")
164 }
165 })
166
167 t.Run("update event also requires rkey=self", func(t *testing.T) {
168 // First create a V2 community
169 createEvent := &jetstream.JetstreamEvent{
170 Did: "did:plc:updatetest",
171 Kind: "commit",
172 Commit: &jetstream.CommitEvent{
173 Operation: "create",
174 Collection: "social.coves.community.profile",
175 RKey: "self",
176 CID: "bafyreiupdate1",
177 Record: map[string]interface{}{
178 "$type": "social.coves.community.profile",
179 "handle": "updatetest.community.coves.social",
180 "name": "updatetest",
181 "createdBy": "did:plc:userUpdate",
182 "hostedBy": "did:web:coves.social",
183 "visibility": "public",
184 "federation": map[string]interface{}{
185 "allowExternalDiscovery": true,
186 },
187 "memberCount": 0,
188 "subscriberCount": 0,
189 "createdAt": time.Now().Format(time.RFC3339),
190 },
191 },
192 }
193
194 err := consumer.HandleEvent(ctx, createEvent)
195 if err != nil {
196 t.Fatalf("Failed to create community for update test: %v", err)
197 }
198
199 // Try to update with wrong rkey
200 updateEvent := &jetstream.JetstreamEvent{
201 Did: "did:plc:updatetest",
202 Kind: "commit",
203 Commit: &jetstream.CommitEvent{
204 Operation: "update",
205 Collection: "social.coves.community.profile",
206 RKey: "wrong-rkey", // INVALID!
207 CID: "bafyreiupdate2",
208 Record: map[string]interface{}{
209 "$type": "social.coves.community.profile",
210 "handle": "updatetest.community.coves.social",
211 "name": "updatetest",
212 "displayName": "Updated Name",
213 "createdBy": "did:plc:userUpdate",
214 "hostedBy": "did:web:coves.social",
215 "visibility": "public",
216 "federation": map[string]interface{}{
217 "allowExternalDiscovery": true,
218 },
219 "memberCount": 0,
220 "subscriberCount": 0,
221 "createdAt": time.Now().Format(time.RFC3339),
222 },
223 },
224 }
225
226 err = consumer.HandleEvent(ctx, updateEvent)
227 if err == nil {
228 t.Error("Update event with wrong rkey should be rejected")
229 }
230
231 // Verify original community still exists unchanged
232 community, err := repo.GetByDID(ctx, "did:plc:updatetest")
233 if err != nil {
234 t.Fatalf("Original community should still exist: %v", err)
235 }
236
237 if community.DisplayName == "Updated Name" {
238 t.Error("Community should not have been updated with invalid rkey")
239 }
240 })
241}
242
243// TestCommunityConsumer_HandleField tests the V2 handle field
244func TestCommunityConsumer_HandleField(t *testing.T) {
245 db := setupTestDB(t)
246 defer func() {
247 if err := db.Close(); err != nil {
248 t.Logf("Failed to close database: %v", err)
249 }
250 }()
251
252 repo := postgres.NewCommunityRepository(db)
253 // Skip verification in tests
254 // Pass nil for identity resolver - not needed since consumer constructs handles from DIDs
255 consumer := jetstream.NewCommunityEventConsumer(repo, "did:web:coves.local", true, nil)
256 ctx := context.Background()
257
258 t.Run("indexes community with atProto handle", func(t *testing.T) {
259 uniqueDID := "did:plc:handletestunique987"
260 event := &jetstream.JetstreamEvent{
261 Did: uniqueDID,
262 Kind: "commit",
263 Commit: &jetstream.CommitEvent{
264 Operation: "create",
265 Collection: "social.coves.community.profile",
266 RKey: "self",
267 CID: "bafyreihandle",
268 Record: map[string]interface{}{
269 "$type": "social.coves.community.profile",
270 "handle": "gamingtest.community.coves.social", // atProto handle (DNS-resolvable)
271 "name": "gamingtest", // Short name for !mentions
272 "createdBy": "did:plc:user123",
273 "hostedBy": "did:web:coves.social",
274 "visibility": "public",
275 "federation": map[string]interface{}{
276 "allowExternalDiscovery": true,
277 },
278 "memberCount": 0,
279 "subscriberCount": 0,
280 "createdAt": time.Now().Format(time.RFC3339),
281 },
282 },
283 }
284
285 err := consumer.HandleEvent(ctx, event)
286 if err != nil {
287 t.Errorf("Failed to index community with handle: %v", err)
288 }
289
290 community, err := repo.GetByDID(ctx, uniqueDID)
291 if err != nil {
292 t.Fatalf("Community should have been indexed: %v", err)
293 }
294
295 // Verify the atProto handle is stored
296 if community.Handle != "gamingtest.community.coves.social" {
297 t.Errorf("Expected handle gamingtest.community.coves.social, got %s", community.Handle)
298 }
299
300 // Note: The DID is the authoritative identifier for atProto resolution
301 // The handle is DNS-resolvable via .well-known/atproto-did
302 })
303}