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// }