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