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