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}