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