A community based topic aggregation platform built on atproto
1package integration
2
3import (
4 "context"
5 "fmt"
6 "testing"
7 "time"
8
9 "Coves/internal/core/communities"
10 "Coves/internal/db/postgres"
11)
12
13func TestCommunityRepository_Create(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 ctx := context.Background()
23
24 t.Run("creates community successfully", func(t *testing.T) {
25 // Generate unique handle and DID using timestamp to avoid collisions
26 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
27 communityDID := generateTestDID(uniqueSuffix)
28 community := &communities.Community{
29 DID: communityDID,
30 Handle: fmt.Sprintf("!test-gaming-%s@coves.local", uniqueSuffix),
31 Name: "test-gaming",
32 DisplayName: "Test Gaming Community",
33 Description: "A community for testing",
34 OwnerDID: "did:web:coves.local",
35 CreatedByDID: "did:plc:user123",
36 HostedByDID: "did:web:coves.local",
37 Visibility: "public",
38 AllowExternalDiscovery: true,
39 CreatedAt: time.Now(),
40 UpdatedAt: time.Now(),
41 }
42
43 created, err := repo.Create(ctx, community)
44 if err != nil {
45 t.Fatalf("Failed to create community: %v", err)
46 }
47
48 if created.ID == 0 {
49 t.Error("Expected non-zero ID")
50 }
51 if created.DID != communityDID {
52 t.Errorf("Expected DID %s, got %s", communityDID, created.DID)
53 }
54 })
55
56 t.Run("returns error for duplicate DID", func(t *testing.T) {
57 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
58 communityDID := generateTestDID(uniqueSuffix)
59 community := &communities.Community{
60 DID: communityDID,
61 Handle: fmt.Sprintf("!duplicate-test-%s@coves.local", uniqueSuffix),
62 Name: "duplicate-test",
63 OwnerDID: "did:web:coves.local",
64 CreatedByDID: "did:plc:user123",
65 HostedByDID: "did:web:coves.local",
66 Visibility: "public",
67 CreatedAt: time.Now(),
68 UpdatedAt: time.Now(),
69 }
70
71 // Create first time
72 if _, err := repo.Create(ctx, community); err != nil {
73 t.Fatalf("First create failed: %v", err)
74 }
75
76 // Try to create again with same DID
77 if _, err := repo.Create(ctx, community); err != communities.ErrCommunityAlreadyExists {
78 t.Errorf("Expected ErrCommunityAlreadyExists, got: %v", err)
79 }
80 })
81
82 t.Run("returns error for duplicate handle", func(t *testing.T) {
83 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
84 handle := fmt.Sprintf("!unique-handle-%s@coves.local", uniqueSuffix)
85
86 // First community
87 did1 := generateTestDID(uniqueSuffix + "1")
88 community1 := &communities.Community{
89 DID: did1,
90 Handle: handle,
91 Name: "unique-handle",
92 OwnerDID: "did:web:coves.local",
93 CreatedByDID: "did:plc:user123",
94 HostedByDID: "did:web:coves.local",
95 Visibility: "public",
96 CreatedAt: time.Now(),
97 UpdatedAt: time.Now(),
98 }
99
100 if _, err := repo.Create(ctx, community1); err != nil {
101 t.Fatalf("First create failed: %v", err)
102 }
103
104 // Second community with different DID but same handle
105 did2 := generateTestDID(uniqueSuffix + "2")
106 community2 := &communities.Community{
107 DID: did2,
108 Handle: handle, // Same handle!
109 Name: "unique-handle",
110 OwnerDID: "did:web:coves.local",
111 CreatedByDID: "did:plc:user456",
112 HostedByDID: "did:web:coves.local",
113 Visibility: "public",
114 CreatedAt: time.Now(),
115 UpdatedAt: time.Now(),
116 }
117
118 if _, err := repo.Create(ctx, community2); err != communities.ErrHandleTaken {
119 t.Errorf("Expected ErrHandleTaken, got: %v", err)
120 }
121 })
122}
123
124func TestCommunityRepository_GetByDID(t *testing.T) {
125 db := setupTestDB(t)
126 defer func() {
127 if err := db.Close(); err != nil {
128 t.Logf("Failed to close database: %v", err)
129 }
130 }()
131
132 repo := postgres.NewCommunityRepository(db)
133 ctx := context.Background()
134
135 t.Run("retrieves existing community", func(t *testing.T) {
136 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
137 communityDID := generateTestDID(uniqueSuffix)
138 community := &communities.Community{
139 DID: communityDID,
140 Handle: fmt.Sprintf("!getbyid-test-%s@coves.local", uniqueSuffix),
141 Name: "getbyid-test",
142 DisplayName: "Get By ID Test",
143 Description: "Testing retrieval",
144 OwnerDID: "did:web:coves.local",
145 CreatedByDID: "did:plc:user123",
146 HostedByDID: "did:web:coves.local",
147 Visibility: "public",
148 CreatedAt: time.Now(),
149 UpdatedAt: time.Now(),
150 }
151
152 created, err := repo.Create(ctx, community)
153 if err != nil {
154 t.Fatalf("Failed to create community: %v", err)
155 }
156
157 retrieved, err := repo.GetByDID(ctx, communityDID)
158 if err != nil {
159 t.Fatalf("Failed to get community: %v", err)
160 }
161
162 if retrieved.DID != created.DID {
163 t.Errorf("Expected DID %s, got %s", created.DID, retrieved.DID)
164 }
165 if retrieved.Handle != created.Handle {
166 t.Errorf("Expected Handle %s, got %s", created.Handle, retrieved.Handle)
167 }
168 if retrieved.DisplayName != created.DisplayName {
169 t.Errorf("Expected DisplayName %s, got %s", created.DisplayName, retrieved.DisplayName)
170 }
171 })
172
173 t.Run("returns error for non-existent community", func(t *testing.T) {
174 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
175 fakeDID := generateTestDID(uniqueSuffix)
176 if _, err := repo.GetByDID(ctx, fakeDID); err != communities.ErrCommunityNotFound {
177 t.Errorf("Expected ErrCommunityNotFound, got: %v", err)
178 }
179 })
180}
181
182func TestCommunityRepository_GetByHandle(t *testing.T) {
183 db := setupTestDB(t)
184 defer func() {
185 if err := db.Close(); err != nil {
186 t.Logf("Failed to close database: %v", err)
187 }
188 }()
189
190 repo := postgres.NewCommunityRepository(db)
191 ctx := context.Background()
192
193 t.Run("retrieves community by handle", func(t *testing.T) {
194 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
195 communityDID := generateTestDID(uniqueSuffix)
196 handle := fmt.Sprintf("!handle-lookup-%s@coves.local", uniqueSuffix)
197
198 community := &communities.Community{
199 DID: communityDID,
200 Handle: handle,
201 Name: "handle-lookup",
202 OwnerDID: "did:web:coves.local",
203 CreatedByDID: "did:plc:user123",
204 HostedByDID: "did:web:coves.local",
205 Visibility: "public",
206 CreatedAt: time.Now(),
207 UpdatedAt: time.Now(),
208 }
209
210 if _, err := repo.Create(ctx, community); err != nil {
211 t.Fatalf("Failed to create community: %v", err)
212 }
213
214 retrieved, err := repo.GetByHandle(ctx, handle)
215 if err != nil {
216 t.Fatalf("Failed to get community by handle: %v", err)
217 }
218
219 if retrieved.Handle != handle {
220 t.Errorf("Expected handle %s, got %s", handle, retrieved.Handle)
221 }
222 if retrieved.DID != communityDID {
223 t.Errorf("Expected DID %s, got %s", communityDID, retrieved.DID)
224 }
225 })
226}
227
228func TestCommunityRepository_Subscriptions(t *testing.T) {
229 db := setupTestDB(t)
230 defer func() {
231 if err := db.Close(); err != nil {
232 t.Logf("Failed to close database: %v", err)
233 }
234 }()
235
236 repo := postgres.NewCommunityRepository(db)
237 ctx := context.Background()
238
239 // Create a community for subscription tests
240 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
241 communityDID := generateTestDID(uniqueSuffix)
242 community := &communities.Community{
243 DID: communityDID,
244 Handle: fmt.Sprintf("!subscription-test-%s@coves.local", uniqueSuffix),
245 Name: "subscription-test",
246 OwnerDID: "did:web:coves.local",
247 CreatedByDID: "did:plc:user123",
248 HostedByDID: "did:web:coves.local",
249 Visibility: "public",
250 CreatedAt: time.Now(),
251 UpdatedAt: time.Now(),
252 }
253
254 if _, err := repo.Create(ctx, community); err != nil {
255 t.Fatalf("Failed to create community: %v", err)
256 }
257
258 t.Run("creates subscription successfully", func(t *testing.T) {
259 sub := &communities.Subscription{
260 UserDID: "did:plc:subscriber1",
261 CommunityDID: communityDID,
262 ContentVisibility: 3, // Default visibility
263 SubscribedAt: time.Now(),
264 }
265
266 created, err := repo.Subscribe(ctx, sub)
267 if err != nil {
268 t.Fatalf("Failed to subscribe: %v", err)
269 }
270
271 if created.ID == 0 {
272 t.Error("Expected non-zero subscription ID")
273 }
274 })
275
276 t.Run("prevents duplicate subscriptions", func(t *testing.T) {
277 sub := &communities.Subscription{
278 UserDID: "did:plc:duplicate-sub",
279 CommunityDID: communityDID,
280 ContentVisibility: 3, // Default visibility
281 SubscribedAt: time.Now(),
282 }
283
284 if _, err := repo.Subscribe(ctx, sub); err != nil {
285 t.Fatalf("First subscription failed: %v", err)
286 }
287
288 // Try to subscribe again
289 _, err := repo.Subscribe(ctx, sub)
290 if err != communities.ErrSubscriptionAlreadyExists {
291 t.Errorf("Expected ErrSubscriptionAlreadyExists, got: %v", err)
292 }
293 })
294
295 t.Run("unsubscribes successfully", func(t *testing.T) {
296 userDID := "did:plc:unsub-user"
297 sub := &communities.Subscription{
298 UserDID: userDID,
299 CommunityDID: communityDID,
300 ContentVisibility: 3, // Default visibility
301 SubscribedAt: time.Now(),
302 }
303
304 _, err := repo.Subscribe(ctx, sub)
305 if err != nil {
306 t.Fatalf("Failed to subscribe: %v", err)
307 }
308
309 err = repo.Unsubscribe(ctx, userDID, communityDID)
310 if err != nil {
311 t.Fatalf("Failed to unsubscribe: %v", err)
312 }
313
314 // Verify subscription is gone
315 _, err = repo.GetSubscription(ctx, userDID, communityDID)
316 if err != communities.ErrSubscriptionNotFound {
317 t.Errorf("Expected ErrSubscriptionNotFound after unsubscribe, got: %v", err)
318 }
319 })
320}
321
322func TestCommunityRepository_List(t *testing.T) {
323 db := setupTestDB(t)
324 defer func() {
325 if err := db.Close(); err != nil {
326 t.Logf("Failed to close database: %v", err)
327 }
328 }()
329
330 repo := postgres.NewCommunityRepository(db)
331 ctx := context.Background()
332
333 t.Run("lists communities with pagination", func(t *testing.T) {
334 // Create multiple communities
335 baseSuffix := time.Now().UnixNano()
336 for i := 0; i < 5; i++ {
337 uniqueSuffix := fmt.Sprintf("%d%d", baseSuffix, i)
338 communityDID := generateTestDID(uniqueSuffix)
339 community := &communities.Community{
340 DID: communityDID,
341 Handle: fmt.Sprintf("!list-test-%d-%d@coves.local", baseSuffix, i),
342 Name: fmt.Sprintf("list-test-%d", i),
343 OwnerDID: "did:web:coves.local",
344 CreatedByDID: "did:plc:user123",
345 HostedByDID: "did:web:coves.local",
346 Visibility: "public",
347 CreatedAt: time.Now(),
348 UpdatedAt: time.Now(),
349 }
350 if _, err := repo.Create(ctx, community); err != nil {
351 t.Fatalf("Failed to create community %d: %v", i, err)
352 }
353 time.Sleep(10 * time.Millisecond) // Ensure different timestamps
354 }
355
356 // List with limit
357 req := communities.ListCommunitiesRequest{
358 Limit: 3,
359 Offset: 0,
360 }
361
362 results, total, err := repo.List(ctx, req)
363 if err != nil {
364 t.Fatalf("Failed to list communities: %v", err)
365 }
366
367 if len(results) != 3 {
368 t.Errorf("Expected 3 communities, got %d", len(results))
369 }
370
371 if total < 5 {
372 t.Errorf("Expected total >= 5, got %d", total)
373 }
374 })
375
376 t.Run("filters by visibility", func(t *testing.T) {
377 // Create an unlisted community
378 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
379 communityDID := generateTestDID(uniqueSuffix)
380 community := &communities.Community{
381 DID: communityDID,
382 Handle: fmt.Sprintf("!unlisted-test-%s@coves.local", uniqueSuffix),
383 Name: "unlisted-test",
384 OwnerDID: "did:web:coves.local",
385 CreatedByDID: "did:plc:user123",
386 HostedByDID: "did:web:coves.local",
387 Visibility: "unlisted",
388 CreatedAt: time.Now(),
389 UpdatedAt: time.Now(),
390 }
391
392 if _, err := repo.Create(ctx, community); err != nil {
393 t.Fatalf("Failed to create unlisted community: %v", err)
394 }
395
396 // List only public communities
397 req := communities.ListCommunitiesRequest{
398 Limit: 100,
399 Offset: 0,
400 Visibility: "public",
401 }
402
403 results, _, err := repo.List(ctx, req)
404 if err != nil {
405 t.Fatalf("Failed to list public communities: %v", err)
406 }
407
408 // Verify no unlisted communities in results
409 for _, c := range results {
410 if c.Visibility != "public" {
411 t.Errorf("Found non-public community in public-only results: %s", c.Handle)
412 }
413 }
414 })
415}
416
417// TODO: Implement search functionality before re-enabling this test
418// func TestCommunityRepository_Search(t *testing.T) {
419// db := setupTestDB(t)
420// defer func() {
421// if err := db.Close(); err != nil {
422// t.Logf("Failed to close database: %v", err)
423// }
424// }()
425//
426// repo := postgres.NewCommunityRepository(db)
427// ctx := context.Background()
428//
429// t.Run("searches communities by name", func(t *testing.T) {
430// // Create a community with searchable name
431// uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano())
432// communityDID := generateTestDID(uniqueSuffix)
433// community := &communities.Community{
434// DID: communityDID,
435// Handle: fmt.Sprintf("!golang-search-%s@coves.local", uniqueSuffix),
436// Name: "golang-search",
437// DisplayName: "Go Programming",
438// Description: "A community for Go developers",
439// OwnerDID: "did:web:coves.local",
440// CreatedByDID: "did:plc:user123",
441// HostedByDID: "did:web:coves.local",
442// Visibility: "public",
443// CreatedAt: time.Now(),
444// UpdatedAt: time.Now(),
445// }
446//
447// if _, err := repo.Create(ctx, community); err != nil {
448// t.Fatalf("Failed to create community: %v", err)
449// }
450//
451// // Search for it
452// req := communities.SearchCommunitiesRequest{
453// Query: "golang",
454// Limit: 10,
455// Offset: 0,
456// }
457//
458// results, total, err := repo.Search(ctx, req)
459// if err != nil {
460// t.Fatalf("Failed to search communities: %v", err)
461// }
462//
463// if total == 0 {
464// t.Error("Expected to find at least one result")
465// }
466//
467// // Verify our community is in results
468// found := false
469// for _, c := range results {
470// if c.DID == communityDID {
471// found = true
472// break
473// }
474// }
475//
476// if !found {
477// t.Error("Expected to find created community in search results")
478// }
479// })
480// }