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