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}