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