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}