A community based topic aggregation platform built on atproto
1package integration 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "testing" 8 "time" 9 10 "Coves/internal/core/aggregators" 11 "Coves/internal/core/communities" 12 "Coves/internal/db/postgres" 13) 14 15// TestAggregatorRepository_Create tests basic aggregator creation 16func TestAggregatorRepository_Create(t *testing.T) { 17 db := setupTestDB(t) 18 defer func() { 19 if err := db.Close(); err != nil { 20 t.Logf("Failed to close database: %v", err) 21 } 22 }() 23 24 repo := postgres.NewAggregatorRepository(db) 25 ctx := context.Background() 26 27 t.Run("creates aggregator successfully", func(t *testing.T) { 28 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 29 aggregatorDID := generateTestDID(uniqueSuffix) 30 31 // Create config schema (JSON Schema) 32 configSchema := map[string]interface{}{ 33 "type": "object", 34 "properties": map[string]interface{}{ 35 "maxItems": map[string]interface{}{ 36 "type": "number", 37 "minimum": 1, 38 "maximum": 50, 39 }, 40 "category": map[string]interface{}{ 41 "type": "string", 42 "enum": []string{"news", "sports", "tech"}, 43 }, 44 }, 45 } 46 schemaBytes, _ := json.Marshal(configSchema) 47 48 agg := &aggregators.Aggregator{ 49 DID: aggregatorDID, 50 DisplayName: "Test RSS Aggregator", 51 Description: "A test aggregator for integration testing", 52 AvatarURL: "bafytest123", 53 ConfigSchema: schemaBytes, 54 MaintainerDID: "did:plc:maintainer123", 55 SourceURL: "https://example.com/aggregator", 56 CreatedAt: time.Now(), 57 IndexedAt: time.Now(), 58 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 59 RecordCID: "bagtest456", 60 } 61 62 err := repo.CreateAggregator(ctx, agg) 63 if err != nil { 64 t.Fatalf("Failed to create aggregator: %v", err) 65 } 66 67 // Verify it was created 68 retrieved, err := repo.GetAggregator(ctx, aggregatorDID) 69 if err != nil { 70 t.Fatalf("Failed to retrieve aggregator: %v", err) 71 } 72 73 if retrieved.DID != aggregatorDID { 74 t.Errorf("Expected DID %s, got %s", aggregatorDID, retrieved.DID) 75 } 76 if retrieved.DisplayName != "Test RSS Aggregator" { 77 t.Errorf("Expected display name 'Test RSS Aggregator', got %s", retrieved.DisplayName) 78 } 79 if len(retrieved.ConfigSchema) == 0 { 80 t.Error("Expected config schema to be stored") 81 } 82 }) 83 84 t.Run("upserts on duplicate DID", func(t *testing.T) { 85 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 86 aggregatorDID := generateTestDID(uniqueSuffix) 87 88 agg := &aggregators.Aggregator{ 89 DID: aggregatorDID, 90 DisplayName: "Original Name", 91 CreatedAt: time.Now(), 92 IndexedAt: time.Now(), 93 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 94 RecordCID: "bagtest789", 95 } 96 97 // Create first time 98 if err := repo.CreateAggregator(ctx, agg); err != nil { 99 t.Fatalf("First create failed: %v", err) 100 } 101 102 // Create again with different name (should update) 103 agg.DisplayName = "Updated Name" 104 agg.RecordCID = "bagtest999" 105 if err := repo.CreateAggregator(ctx, agg); err != nil { 106 t.Fatalf("Upsert failed: %v", err) 107 } 108 109 // Verify it was updated 110 retrieved, err := repo.GetAggregator(ctx, aggregatorDID) 111 if err != nil { 112 t.Fatalf("Failed to retrieve aggregator: %v", err) 113 } 114 115 if retrieved.DisplayName != "Updated Name" { 116 t.Errorf("Expected display name 'Updated Name', got %s", retrieved.DisplayName) 117 } 118 }) 119} 120 121// TestAggregatorRepository_IsAggregator tests the fast existence check 122func TestAggregatorRepository_IsAggregator(t *testing.T) { 123 db := setupTestDB(t) 124 defer func() { 125 if err := db.Close(); err != nil { 126 t.Logf("Failed to close database: %v", err) 127 } 128 }() 129 130 repo := postgres.NewAggregatorRepository(db) 131 ctx := context.Background() 132 133 t.Run("returns true for existing aggregator", func(t *testing.T) { 134 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 135 aggregatorDID := generateTestDID(uniqueSuffix) 136 137 agg := &aggregators.Aggregator{ 138 DID: aggregatorDID, 139 DisplayName: "Test Aggregator", 140 CreatedAt: time.Now(), 141 IndexedAt: time.Now(), 142 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 143 RecordCID: "bagtest123", 144 } 145 146 if err := repo.CreateAggregator(ctx, agg); err != nil { 147 t.Fatalf("Failed to create aggregator: %v", err) 148 } 149 150 exists, err := repo.IsAggregator(ctx, aggregatorDID) 151 if err != nil { 152 t.Fatalf("IsAggregator failed: %v", err) 153 } 154 155 if !exists { 156 t.Error("Expected aggregator to exist") 157 } 158 }) 159 160 t.Run("returns false for non-existent aggregator", func(t *testing.T) { 161 exists, err := repo.IsAggregator(ctx, "did:plc:nonexistent123") 162 if err != nil { 163 t.Fatalf("IsAggregator failed: %v", err) 164 } 165 166 if exists { 167 t.Error("Expected aggregator to not exist") 168 } 169 }) 170} 171 172// TestAggregatorAuthorization_Create tests authorization creation 173func TestAggregatorAuthorization_Create(t *testing.T) { 174 db := setupTestDB(t) 175 defer func() { 176 if err := db.Close(); err != nil { 177 t.Logf("Failed to close database: %v", err) 178 } 179 }() 180 181 aggRepo := postgres.NewAggregatorRepository(db) 182 commRepo := postgres.NewCommunityRepository(db) 183 ctx := context.Background() 184 185 t.Run("creates authorization successfully", func(t *testing.T) { 186 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 187 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 188 communityDID := generateTestDID(uniqueSuffix + "comm") 189 190 // Create aggregator first 191 agg := &aggregators.Aggregator{ 192 DID: aggregatorDID, 193 DisplayName: "Test Aggregator", 194 CreatedAt: time.Now(), 195 IndexedAt: time.Now(), 196 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 197 RecordCID: "bagtest123", 198 } 199 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 200 t.Fatalf("Failed to create aggregator: %v", err) 201 } 202 203 // Create community 204 community := &communities.Community{ 205 DID: communityDID, 206 Handle: fmt.Sprintf("!test-comm-%s@coves.local", uniqueSuffix), 207 Name: "test-comm", 208 OwnerDID: "did:web:coves.local", 209 HostedByDID: "did:web:coves.local", 210 Visibility: "public", 211 CreatedAt: time.Now(), 212 UpdatedAt: time.Now(), 213 } 214 if _, err := commRepo.Create(ctx, community); err != nil { 215 t.Fatalf("Failed to create community: %v", err) 216 } 217 218 // Create authorization 219 config := map[string]interface{}{ 220 "maxItems": 10, 221 "category": "tech", 222 } 223 configBytes, _ := json.Marshal(config) 224 225 auth := &aggregators.Authorization{ 226 AggregatorDID: aggregatorDID, 227 CommunityDID: communityDID, 228 Enabled: true, 229 Config: configBytes, 230 CreatedBy: "did:plc:moderator123", 231 CreatedAt: time.Now(), 232 IndexedAt: time.Now(), 233 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/abc123", communityDID), 234 RecordCID: "bagauth456", 235 } 236 237 err := aggRepo.CreateAuthorization(ctx, auth) 238 if err != nil { 239 t.Fatalf("Failed to create authorization: %v", err) 240 } 241 242 // Verify it was created 243 retrieved, err := aggRepo.GetAuthorization(ctx, aggregatorDID, communityDID) 244 if err != nil { 245 t.Fatalf("Failed to retrieve authorization: %v", err) 246 } 247 248 if !retrieved.Enabled { 249 t.Error("Expected authorization to be enabled") 250 } 251 if len(retrieved.Config) == 0 { 252 t.Error("Expected config to be stored") 253 } 254 }) 255 256 t.Run("enforces unique constraint on (aggregator_did, community_did)", func(t *testing.T) { 257 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 258 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 259 communityDID := generateTestDID(uniqueSuffix + "comm") 260 261 // Create aggregator 262 agg := &aggregators.Aggregator{ 263 DID: aggregatorDID, 264 DisplayName: "Test Aggregator", 265 CreatedAt: time.Now(), 266 IndexedAt: time.Now(), 267 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 268 RecordCID: "bagtest123", 269 } 270 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 271 t.Fatalf("Failed to create aggregator: %v", err) 272 } 273 274 // Create community 275 community := &communities.Community{ 276 DID: communityDID, 277 Handle: fmt.Sprintf("!test-unique-%s@coves.local", uniqueSuffix), 278 Name: "test-unique", 279 OwnerDID: "did:web:coves.local", 280 HostedByDID: "did:web:coves.local", 281 Visibility: "public", 282 CreatedAt: time.Now(), 283 UpdatedAt: time.Now(), 284 } 285 if _, err := commRepo.Create(ctx, community); err != nil { 286 t.Fatalf("Failed to create community: %v", err) 287 } 288 289 // Create first authorization 290 auth1 := &aggregators.Authorization{ 291 AggregatorDID: aggregatorDID, 292 CommunityDID: communityDID, 293 Enabled: true, 294 CreatedBy: "did:plc:moderator123", 295 CreatedAt: time.Now(), 296 IndexedAt: time.Now(), 297 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/first", communityDID), 298 RecordCID: "bagauth1", 299 } 300 if err := aggRepo.CreateAuthorization(ctx, auth1); err != nil { 301 t.Fatalf("First authorization failed: %v", err) 302 } 303 304 // Try to create duplicate (should update via ON CONFLICT) 305 auth2 := &aggregators.Authorization{ 306 AggregatorDID: aggregatorDID, 307 CommunityDID: communityDID, 308 Enabled: false, // Different value 309 CreatedBy: "did:plc:moderator123", 310 CreatedAt: time.Now(), 311 IndexedAt: time.Now(), 312 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/second", communityDID), 313 RecordCID: "bagauth2", 314 } 315 if err := aggRepo.CreateAuthorization(ctx, auth2); err != nil { 316 t.Fatalf("Second authorization (update) failed: %v", err) 317 } 318 319 // Verify it was updated 320 retrieved, err := aggRepo.GetAuthorization(ctx, aggregatorDID, communityDID) 321 if err != nil { 322 t.Fatalf("Failed to retrieve authorization: %v", err) 323 } 324 325 if retrieved.Enabled { 326 t.Error("Expected authorization to be disabled after update") 327 } 328 }) 329} 330 331// TestAggregatorAuthorization_IsAuthorized tests fast authorization check 332func TestAggregatorAuthorization_IsAuthorized(t *testing.T) { 333 db := setupTestDB(t) 334 defer func() { 335 if err := db.Close(); err != nil { 336 t.Logf("Failed to close database: %v", err) 337 } 338 }() 339 340 aggRepo := postgres.NewAggregatorRepository(db) 341 commRepo := postgres.NewCommunityRepository(db) 342 ctx := context.Background() 343 344 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 345 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 346 communityDID := generateTestDID(uniqueSuffix + "comm") 347 348 // Setup aggregator and community 349 agg := &aggregators.Aggregator{ 350 DID: aggregatorDID, 351 DisplayName: "Test Aggregator", 352 CreatedAt: time.Now(), 353 IndexedAt: time.Now(), 354 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 355 RecordCID: "bagtest123", 356 } 357 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 358 t.Fatalf("Failed to create aggregator: %v", err) 359 } 360 361 community := &communities.Community{ 362 DID: communityDID, 363 Handle: fmt.Sprintf("!test-auth-%s@coves.local", uniqueSuffix), 364 Name: "test-auth", 365 OwnerDID: "did:web:coves.local", 366 HostedByDID: "did:web:coves.local", 367 Visibility: "public", 368 CreatedAt: time.Now(), 369 UpdatedAt: time.Now(), 370 } 371 if _, err := commRepo.Create(ctx, community); err != nil { 372 t.Fatalf("Failed to create community: %v", err) 373 } 374 375 t.Run("returns true for enabled authorization", func(t *testing.T) { 376 auth := &aggregators.Authorization{ 377 AggregatorDID: aggregatorDID, 378 CommunityDID: communityDID, 379 Enabled: true, 380 CreatedBy: "did:plc:moderator123", 381 CreatedAt: time.Now(), 382 IndexedAt: time.Now(), 383 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/enabled", communityDID), 384 RecordCID: "bagauth123", 385 } 386 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 387 t.Fatalf("Failed to create authorization: %v", err) 388 } 389 390 authorized, err := aggRepo.IsAuthorized(ctx, aggregatorDID, communityDID) 391 if err != nil { 392 t.Fatalf("IsAuthorized failed: %v", err) 393 } 394 395 if !authorized { 396 t.Error("Expected aggregator to be authorized") 397 } 398 }) 399 400 t.Run("returns false for disabled authorization", func(t *testing.T) { 401 uniqueSuffix2 := fmt.Sprintf("%d", time.Now().UnixNano()) 402 aggregatorDID2 := generateTestDID(uniqueSuffix2 + "agg") 403 communityDID2 := generateTestDID(uniqueSuffix2 + "comm") 404 405 // Setup 406 agg2 := &aggregators.Aggregator{ 407 DID: aggregatorDID2, 408 DisplayName: "Test Aggregator 2", 409 CreatedAt: time.Now(), 410 IndexedAt: time.Now(), 411 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID2), 412 RecordCID: "bagtest456", 413 } 414 if err := aggRepo.CreateAggregator(ctx, agg2); err != nil { 415 t.Fatalf("Failed to create aggregator: %v", err) 416 } 417 418 community2 := &communities.Community{ 419 DID: communityDID2, 420 Handle: fmt.Sprintf("!test-disabled-%s@coves.local", uniqueSuffix2), 421 Name: "test-disabled", 422 OwnerDID: "did:web:coves.local", 423 HostedByDID: "did:web:coves.local", 424 Visibility: "public", 425 CreatedAt: time.Now(), 426 UpdatedAt: time.Now(), 427 } 428 if _, err := commRepo.Create(ctx, community2); err != nil { 429 t.Fatalf("Failed to create community: %v", err) 430 } 431 432 // Create disabled authorization 433 auth := &aggregators.Authorization{ 434 AggregatorDID: aggregatorDID2, 435 CommunityDID: communityDID2, 436 Enabled: false, 437 CreatedBy: "did:plc:moderator123", 438 CreatedAt: time.Now(), 439 IndexedAt: time.Now(), 440 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/disabled", communityDID2), 441 RecordCID: "bagauth789", 442 } 443 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 444 t.Fatalf("Failed to create authorization: %v", err) 445 } 446 447 authorized, err := aggRepo.IsAuthorized(ctx, aggregatorDID2, communityDID2) 448 if err != nil { 449 t.Fatalf("IsAuthorized failed: %v", err) 450 } 451 452 if authorized { 453 t.Error("Expected aggregator to NOT be authorized (disabled)") 454 } 455 }) 456 457 t.Run("returns false for non-existent authorization", func(t *testing.T) { 458 authorized, err := aggRepo.IsAuthorized(ctx, "did:plc:fake123", "did:plc:fake456") 459 if err != nil { 460 t.Fatalf("IsAuthorized failed: %v", err) 461 } 462 463 if authorized { 464 t.Error("Expected non-existent authorization to return false") 465 } 466 }) 467} 468 469// TestAggregatorService_PostCreationIntegration tests the full post creation flow with aggregators 470func TestAggregatorService_PostCreationIntegration(t *testing.T) { 471 db := setupTestDB(t) 472 defer func() { 473 if err := db.Close(); err != nil { 474 t.Logf("Failed to close database: %v", err) 475 } 476 }() 477 478 aggRepo := postgres.NewAggregatorRepository(db) 479 commRepo := postgres.NewCommunityRepository(db) 480 481 aggService := aggregators.NewAggregatorService(aggRepo, nil) // nil community service for this test 482 ctx := context.Background() 483 484 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 485 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 486 communityDID := generateTestDID(uniqueSuffix + "comm") 487 488 // Setup aggregator 489 agg := &aggregators.Aggregator{ 490 DID: aggregatorDID, 491 DisplayName: "Test RSS Feed", 492 CreatedAt: time.Now(), 493 IndexedAt: time.Now(), 494 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 495 RecordCID: "bagtest123", 496 } 497 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 498 t.Fatalf("Failed to create aggregator: %v", err) 499 } 500 501 // Setup community 502 community := &communities.Community{ 503 DID: communityDID, 504 Handle: fmt.Sprintf("!test-post-%s@coves.local", uniqueSuffix), 505 Name: "test-post", 506 OwnerDID: "did:web:coves.local", 507 HostedByDID: "did:web:coves.local", 508 Visibility: "public", 509 CreatedAt: time.Now(), 510 UpdatedAt: time.Now(), 511 } 512 if _, err := commRepo.Create(ctx, community); err != nil { 513 t.Fatalf("Failed to create community: %v", err) 514 } 515 516 // Create authorization 517 auth := &aggregators.Authorization{ 518 AggregatorDID: aggregatorDID, 519 CommunityDID: communityDID, 520 Enabled: true, 521 CreatedBy: "did:plc:moderator123", 522 CreatedAt: time.Now(), 523 IndexedAt: time.Now(), 524 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/test", communityDID), 525 RecordCID: "bagauth123", 526 } 527 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 528 t.Fatalf("Failed to create authorization: %v", err) 529 } 530 531 t.Run("validates aggregator post successfully", func(t *testing.T) { 532 // This should pass (authorization exists and enabled) 533 err := aggService.ValidateAggregatorPost(ctx, aggregatorDID, communityDID) 534 if err != nil { 535 t.Errorf("Expected validation to pass, got error: %v", err) 536 } 537 }) 538 539 t.Run("rejects post without authorization", func(t *testing.T) { 540 fakeAggDID := generateTestDID(uniqueSuffix + "fake") 541 err := aggService.ValidateAggregatorPost(ctx, fakeAggDID, communityDID) 542 if !aggregators.IsUnauthorized(err) { 543 t.Errorf("Expected unauthorized error, got: %v", err) 544 } 545 }) 546 547 t.Run("records aggregator post for rate limiting", func(t *testing.T) { 548 postURI := fmt.Sprintf("at://%s/social.coves.community.post/post1", communityDID) 549 550 err := aggRepo.RecordAggregatorPost(ctx, aggregatorDID, communityDID, postURI, "bafy123") 551 if err != nil { 552 t.Fatalf("Failed to record post: %v", err) 553 } 554 555 // Count recent posts 556 since := time.Now().Add(-1 * time.Hour) 557 count, err := aggRepo.CountRecentPosts(ctx, aggregatorDID, communityDID, since) 558 if err != nil { 559 t.Fatalf("Failed to count posts: %v", err) 560 } 561 562 if count != 1 { 563 t.Errorf("Expected 1 post, got %d", count) 564 } 565 }) 566} 567 568// TestAggregatorService_RateLimiting tests rate limit enforcement 569func TestAggregatorService_RateLimiting(t *testing.T) { 570 db := setupTestDB(t) 571 defer func() { 572 if err := db.Close(); err != nil { 573 t.Logf("Failed to close database: %v", err) 574 } 575 }() 576 577 aggRepo := postgres.NewAggregatorRepository(db) 578 commRepo := postgres.NewCommunityRepository(db) 579 580 aggService := aggregators.NewAggregatorService(aggRepo, nil) 581 ctx := context.Background() 582 583 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 584 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 585 communityDID := generateTestDID(uniqueSuffix + "comm") 586 587 // Setup 588 agg := &aggregators.Aggregator{ 589 DID: aggregatorDID, 590 DisplayName: "Rate Limited Aggregator", 591 CreatedAt: time.Now(), 592 IndexedAt: time.Now(), 593 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 594 RecordCID: "bagtest123", 595 } 596 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 597 t.Fatalf("Failed to create aggregator: %v", err) 598 } 599 600 community := &communities.Community{ 601 DID: communityDID, 602 Handle: fmt.Sprintf("!test-ratelimit-%s@coves.local", uniqueSuffix), 603 Name: "test-ratelimit", 604 OwnerDID: "did:web:coves.local", 605 HostedByDID: "did:web:coves.local", 606 Visibility: "public", 607 CreatedAt: time.Now(), 608 UpdatedAt: time.Now(), 609 } 610 if _, err := commRepo.Create(ctx, community); err != nil { 611 t.Fatalf("Failed to create community: %v", err) 612 } 613 614 auth := &aggregators.Authorization{ 615 AggregatorDID: aggregatorDID, 616 CommunityDID: communityDID, 617 Enabled: true, 618 CreatedBy: "did:plc:moderator123", 619 CreatedAt: time.Now(), 620 IndexedAt: time.Now(), 621 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/test", communityDID), 622 RecordCID: "bagauth123", 623 } 624 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 625 t.Fatalf("Failed to create authorization: %v", err) 626 } 627 628 t.Run("allows posts within rate limit", func(t *testing.T) { 629 // Create 9 posts (under the 10/hour limit) 630 for i := 0; i < 9; i++ { 631 postURI := fmt.Sprintf("at://%s/social.coves.community.post/post%d", communityDID, i) 632 if err := aggRepo.RecordAggregatorPost(ctx, aggregatorDID, communityDID, postURI, "bafy123"); err != nil { 633 t.Fatalf("Failed to record post %d: %v", i, err) 634 } 635 } 636 637 // Should still pass validation (9 < 10) 638 err := aggService.ValidateAggregatorPost(ctx, aggregatorDID, communityDID) 639 if err != nil { 640 t.Errorf("Expected validation to pass with 9 posts, got error: %v", err) 641 } 642 }) 643 644 t.Run("enforces rate limit at 10 posts/hour", func(t *testing.T) { 645 // Add one more post to hit the limit (total = 10) 646 postURI := fmt.Sprintf("at://%s/social.coves.community.post/post10", communityDID) 647 if err := aggRepo.RecordAggregatorPost(ctx, aggregatorDID, communityDID, postURI, "bafy123"); err != nil { 648 t.Fatalf("Failed to record 10th post: %v", err) 649 } 650 651 // Now should fail (10 >= 10) 652 err := aggService.ValidateAggregatorPost(ctx, aggregatorDID, communityDID) 653 if !aggregators.IsRateLimited(err) { 654 t.Errorf("Expected rate limit error after 10 posts, got: %v", err) 655 } 656 }) 657} 658 659// TestAggregatorPostService_Integration tests the posts service integration 660func TestAggregatorPostService_Integration(t *testing.T) { 661 db := setupTestDB(t) 662 defer func() { 663 if err := db.Close(); err != nil { 664 t.Logf("Failed to close database: %v", err) 665 } 666 }() 667 668 aggRepo := postgres.NewAggregatorRepository(db) 669 aggService := aggregators.NewAggregatorService(aggRepo, nil) 670 ctx := context.Background() 671 672 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 673 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 674 userDID := generateTestDID(uniqueSuffix + "user") 675 676 // Create aggregator 677 agg := &aggregators.Aggregator{ 678 DID: aggregatorDID, 679 DisplayName: "Test Aggregator", 680 CreatedAt: time.Now(), 681 IndexedAt: time.Now(), 682 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 683 RecordCID: "bagtest123", 684 } 685 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 686 t.Fatalf("Failed to create aggregator: %v", err) 687 } 688 689 t.Run("identifies aggregator DID correctly", func(t *testing.T) { 690 isAgg, err := aggService.IsAggregator(ctx, aggregatorDID) 691 if err != nil { 692 t.Fatalf("IsAggregator failed: %v", err) 693 } 694 if !isAgg { 695 t.Error("Expected DID to be identified as aggregator") 696 } 697 }) 698 699 t.Run("identifies regular user DID correctly", func(t *testing.T) { 700 isAgg, err := aggService.IsAggregator(ctx, userDID) 701 if err != nil { 702 t.Fatalf("IsAggregator failed: %v", err) 703 } 704 if isAgg { 705 t.Error("Expected user DID to NOT be identified as aggregator") 706 } 707 }) 708} 709 710// TestAggregatorTriggers tests database triggers for auto-updating stats 711func TestAggregatorTriggers(t *testing.T) { 712 db := setupTestDB(t) 713 defer func() { 714 if err := db.Close(); err != nil { 715 t.Logf("Failed to close database: %v", err) 716 } 717 }() 718 719 aggRepo := postgres.NewAggregatorRepository(db) 720 commRepo := postgres.NewCommunityRepository(db) 721 ctx := context.Background() 722 723 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 724 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 725 726 // Create aggregator 727 agg := &aggregators.Aggregator{ 728 DID: aggregatorDID, 729 DisplayName: "Trigger Test Aggregator", 730 CreatedAt: time.Now(), 731 IndexedAt: time.Now(), 732 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 733 RecordCID: "bagtest123", 734 } 735 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 736 t.Fatalf("Failed to create aggregator: %v", err) 737 } 738 739 t.Run("communities_using count updates via trigger", func(t *testing.T) { 740 // Create 3 communities and authorize aggregator for each 741 for i := 0; i < 3; i++ { 742 commSuffix := fmt.Sprintf("%s%d", uniqueSuffix, i) 743 communityDID := generateTestDID(commSuffix + "comm") 744 745 community := &communities.Community{ 746 DID: communityDID, 747 Handle: fmt.Sprintf("!trigger-test-%s@coves.local", commSuffix), 748 Name: fmt.Sprintf("trigger-test-%d", i), 749 OwnerDID: "did:web:coves.local", 750 HostedByDID: "did:web:coves.local", 751 Visibility: "public", 752 CreatedAt: time.Now(), 753 UpdatedAt: time.Now(), 754 } 755 if _, err := commRepo.Create(ctx, community); err != nil { 756 t.Fatalf("Failed to create community %d: %v", i, err) 757 } 758 759 auth := &aggregators.Authorization{ 760 AggregatorDID: aggregatorDID, 761 CommunityDID: communityDID, 762 Enabled: true, 763 CreatedBy: "did:plc:moderator123", 764 CreatedAt: time.Now(), 765 IndexedAt: time.Now(), 766 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/auth%d", communityDID, i), 767 RecordCID: fmt.Sprintf("bagauth%d", i), 768 } 769 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 770 t.Fatalf("Failed to create authorization %d: %v", i, err) 771 } 772 } 773 774 // Retrieve aggregator and check communities_using count 775 retrieved, err := aggRepo.GetAggregator(ctx, aggregatorDID) 776 if err != nil { 777 t.Fatalf("Failed to retrieve aggregator: %v", err) 778 } 779 780 if retrieved.CommunitiesUsing != 3 { 781 t.Errorf("Expected communities_using = 3, got %d", retrieved.CommunitiesUsing) 782 } 783 }) 784 785 t.Run("posts_created count updates via trigger", func(t *testing.T) { 786 communityDID := generateTestDID(uniqueSuffix + "postcomm") 787 788 // Create community 789 community := &communities.Community{ 790 DID: communityDID, 791 Handle: fmt.Sprintf("!post-trigger-%s@coves.local", uniqueSuffix), 792 Name: "post-trigger", 793 OwnerDID: "did:web:coves.local", 794 HostedByDID: "did:web:coves.local", 795 Visibility: "public", 796 CreatedAt: time.Now(), 797 UpdatedAt: time.Now(), 798 } 799 if _, err := commRepo.Create(ctx, community); err != nil { 800 t.Fatalf("Failed to create community: %v", err) 801 } 802 803 // Record 5 posts 804 for i := 0; i < 5; i++ { 805 postURI := fmt.Sprintf("at://%s/social.coves.community.post/triggerpost%d", communityDID, i) 806 if err := aggRepo.RecordAggregatorPost(ctx, aggregatorDID, communityDID, postURI, "bafy123"); err != nil { 807 t.Fatalf("Failed to record post %d: %v", i, err) 808 } 809 } 810 811 // Retrieve aggregator and check posts_created count 812 retrieved, err := aggRepo.GetAggregator(ctx, aggregatorDID) 813 if err != nil { 814 t.Fatalf("Failed to retrieve aggregator: %v", err) 815 } 816 817 // Note: posts_created accumulates across all tests, so check >= 5 818 if retrieved.PostsCreated < 5 { 819 t.Errorf("Expected posts_created >= 5, got %d", retrieved.PostsCreated) 820 } 821 }) 822} 823 824// TestAggregatorAuthorization_DisabledAtField tests that disabledAt is properly stored and retrieved 825func TestAggregatorAuthorization_DisabledAtField(t *testing.T) { 826 db := setupTestDB(t) 827 defer func() { 828 if err := db.Close(); err != nil { 829 t.Logf("Failed to close database: %v", err) 830 } 831 }() 832 833 aggRepo := postgres.NewAggregatorRepository(db) 834 commRepo := postgres.NewCommunityRepository(db) 835 ctx := context.Background() 836 837 uniqueSuffix := fmt.Sprintf("%d", time.Now().UnixNano()) 838 aggregatorDID := generateTestDID(uniqueSuffix + "agg") 839 communityDID := generateTestDID(uniqueSuffix + "comm") 840 841 // Create aggregator 842 agg := &aggregators.Aggregator{ 843 DID: aggregatorDID, 844 DisplayName: "Disabled Test Aggregator", 845 CreatedAt: time.Now(), 846 IndexedAt: time.Now(), 847 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), 848 RecordCID: "bagtest123", 849 } 850 if err := aggRepo.CreateAggregator(ctx, agg); err != nil { 851 t.Fatalf("Failed to create aggregator: %v", err) 852 } 853 854 // Create community 855 community := &communities.Community{ 856 DID: communityDID, 857 Handle: fmt.Sprintf("!disabled-test-%s@coves.local", uniqueSuffix), 858 Name: "disabled-test", 859 OwnerDID: "did:plc:owner123", 860 HostedByDID: "did:web:coves.local", 861 Visibility: "public", 862 CreatedAt: time.Now(), 863 UpdatedAt: time.Now(), 864 } 865 if _, err := commRepo.Create(ctx, community); err != nil { 866 t.Fatalf("Failed to create community: %v", err) 867 } 868 869 t.Run("stores and retrieves disabledAt timestamp for audit trail", func(t *testing.T) { 870 disabledTime := time.Now().UTC().Truncate(time.Microsecond) 871 872 // Create authorization with disabledAt set 873 auth := &aggregators.Authorization{ 874 AggregatorDID: aggregatorDID, 875 CommunityDID: communityDID, 876 Enabled: false, 877 CreatedBy: "did:plc:moderator123", 878 DisabledBy: "did:plc:moderator456", 879 DisabledAt: &disabledTime, // Pointer to time.Time for nullable field 880 CreatedAt: time.Now(), 881 IndexedAt: time.Now(), 882 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/test", communityDID), 883 RecordCID: "bagauth123", 884 } 885 886 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 887 t.Fatalf("Failed to create authorization: %v", err) 888 } 889 890 // Retrieve and verify disabledAt is stored 891 retrieved, err := aggRepo.GetAuthorization(ctx, aggregatorDID, communityDID) 892 if err != nil { 893 t.Fatalf("Failed to retrieve authorization: %v", err) 894 } 895 896 if retrieved.DisabledAt == nil { 897 t.Fatal("Expected disabledAt to be set, got nil") 898 } 899 900 // Compare timestamps (truncate to microseconds for postgres precision) 901 if !retrieved.DisabledAt.Truncate(time.Microsecond).Equal(disabledTime) { 902 t.Errorf("Expected disabledAt %v, got %v", disabledTime, *retrieved.DisabledAt) 903 } 904 905 if retrieved.DisabledBy != "did:plc:moderator456" { 906 t.Errorf("Expected disabledBy 'did:plc:moderator456', got %s", retrieved.DisabledBy) 907 } 908 }) 909 910 t.Run("handles nil disabledAt for enabled authorizations", func(t *testing.T) { 911 uniqueSuffix2 := fmt.Sprintf("%d", time.Now().UnixNano()) 912 communityDID2 := generateTestDID(uniqueSuffix2 + "comm2") 913 914 // Create another community 915 community2 := &communities.Community{ 916 DID: communityDID2, 917 Handle: fmt.Sprintf("!enabled-test-%s@coves.local", uniqueSuffix2), 918 Name: "enabled-test", 919 OwnerDID: "did:plc:owner123", 920 HostedByDID: "did:web:coves.local", 921 Visibility: "public", 922 CreatedAt: time.Now(), 923 UpdatedAt: time.Now(), 924 } 925 if _, err := commRepo.Create(ctx, community2); err != nil { 926 t.Fatalf("Failed to create community2: %v", err) 927 } 928 929 // Create enabled authorization without disabledAt 930 auth := &aggregators.Authorization{ 931 AggregatorDID: aggregatorDID, 932 CommunityDID: communityDID2, 933 Enabled: true, 934 CreatedBy: "did:plc:moderator123", 935 DisabledAt: nil, // Explicitly nil for enabled authorization 936 CreatedAt: time.Now(), 937 IndexedAt: time.Now(), 938 RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/test2", communityDID2), 939 RecordCID: "bagauth456", 940 } 941 942 if err := aggRepo.CreateAuthorization(ctx, auth); err != nil { 943 t.Fatalf("Failed to create authorization: %v", err) 944 } 945 946 // Retrieve and verify disabledAt is nil 947 retrieved, err := aggRepo.GetAuthorization(ctx, aggregatorDID, communityDID2) 948 if err != nil { 949 t.Fatalf("Failed to retrieve authorization: %v", err) 950 } 951 952 if retrieved.DisabledAt != nil { 953 t.Errorf("Expected disabledAt to be nil for enabled authorization, got %v", *retrieved.DisabledAt) 954 } 955 }) 956}