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