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