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