diff --git a/docs/aggregators/PRD_AGGREGATORS.md b/docs/aggregators/PRD_AGGREGATORS.md index be59f1b..5afa22d 100644 --- a/docs/aggregators/PRD_AGGREGATORS.md +++ b/docs/aggregators/PRD_AGGREGATORS.md @@ -212,6 +212,69 @@ AppView-only tracking for rate limiting and stats (not from lexicon). --- +## 🚨 Alpha Blockers + +### Aggregator User Registration +**Status:** ❌ BLOCKING ALPHA - Must implement before aggregators can post +**Priority:** CRITICAL +**Discovered:** 2025-10-24 during Kagi News aggregator E2E testing + +**Problem:** +Aggregators cannot create posts because they aren't indexed as users in the AppView database. The post consumer rejects posts with: +``` +🚨 SECURITY: Rejecting post event: author not found: - cannot index post before author +``` + +This security check (in `post_consumer.go:181-196`) ensures referential integrity by requiring all post authors to exist as users before posts can be indexed. + +**Root Cause:** +Users are normally indexed through Jetstream identity events when they create accounts on a PDS. Aggregators don't have PDSs connected to Jetstream, so they never emit identity events and are never automatically indexed. + +**Solution: Aggregator Registration Endpoint** + +Implement `social.coves.aggregator.register` XRPC endpoint to allow aggregators to self-register as users. + +**Implementation:** +```go +// Handler: internal/api/handlers/aggregator/register.go +// POST /xrpc/social.coves.aggregator.register + +type RegisterRequest struct { + AggregatorDID string `json:"aggregatorDid"` + Handle string `json:"handle"` +} + +func (h *Handler) Register(ctx context.Context, req *RegisterRequest) error { + // 1. Validate aggregator DID format + // 2. Validate handle is available + // 3. Verify aggregator controls the DID (via DID document) + // 4. Create user entry in database + _, err := h.userService.CreateUser(ctx, users.CreateUserRequest{ + DID: req.AggregatorDID, + Handle: req.Handle, + PDSURL: "https://api.coves.social", // Aggregators "hosted" by Coves + }) + return err +} +``` + +**Acceptance Criteria:** +- [ ] Endpoint implemented and tested +- [ ] Aggregator can register with DID + handle +- [ ] Registration validates DID ownership +- [ ] Duplicate registrations handled gracefully +- [ ] Kagi News aggregator can successfully post after registration +- [ ] Documentation updated with registration flow + +**Alternative (Quick Fix for Testing):** +Manual SQL insert for known aggregators during bootstrap: +```sql +INSERT INTO users (did, handle, pds_url, created_at, updated_at) +VALUES ('did:plc:...', 'aggregator-name.coves.social', 'https://api.coves.social', NOW(), NOW()); +``` + +--- + ### Phase 2: Aggregator SDK (Post-Alpha) **Deferred** - Will build SDK after Phase 1 is validated in production. @@ -325,10 +388,11 @@ Monitors sports APIs, creates post-game threads with scores and stats. ### Alpha Goals - ✅ Lexicons validated - ✅ Database migrations tested -- ⏳ Jetstream consumer indexes records -- ⏳ Post creation validates aggregator auth -- ⏳ Rate limiting prevents spam -- ⏳ Integration tests passing +- ✅ Jetstream consumer indexes records +- ✅ Post creation validates aggregator auth +- ✅ Rate limiting prevents spam +- ✅ Integration tests passing +- ❌ **BLOCKER:** Aggregator registration endpoint (see Alpha Blockers section) ### Beta Goals (Future) - First aggregator deployed in production diff --git a/internal/api/handlers/aggregator/get_services.go b/internal/api/handlers/aggregator/get_services.go index caf4a30..eb01ba6 100644 --- a/internal/api/handlers/aggregator/get_services.go +++ b/internal/api/handlers/aggregator/get_services.go @@ -105,16 +105,16 @@ type AggregatorView struct { // AggregatorViewDetailed matches social.coves.aggregator.defs#aggregatorViewDetailed (with stats) type AggregatorViewDetailed struct { - DID string `json:"did"` - DisplayName string `json:"displayName"` - Description *string `json:"description,omitempty"` - Avatar *string `json:"avatar,omitempty"` - ConfigSchema interface{} `json:"configSchema,omitempty"` - SourceURL *string `json:"sourceUrl,omitempty"` - MaintainerDID *string `json:"maintainer,omitempty"` - CreatedAt string `json:"createdAt"` - RecordUri string `json:"recordUri"` - Stats AggregatorStats `json:"stats"` + DID string `json:"did"` + DisplayName string `json:"displayName"` + Description *string `json:"description,omitempty"` + Avatar *string `json:"avatar,omitempty"` + ConfigSchema interface{} `json:"configSchema,omitempty"` + SourceURL *string `json:"sourceUrl,omitempty"` + MaintainerDID *string `json:"maintainer,omitempty"` + CreatedAt string `json:"createdAt"` + RecordUri string `json:"recordUri"` + Stats AggregatorStats `json:"stats"` } // AggregatorStats matches social.coves.aggregator.defs#aggregatorStats diff --git a/internal/atproto/jetstream/aggregator_consumer.go b/internal/atproto/jetstream/aggregator_consumer.go index bb367dd..9add373 100644 --- a/internal/atproto/jetstream/aggregator_consumer.go +++ b/internal/atproto/jetstream/aggregator_consumer.go @@ -272,13 +272,13 @@ func (c *AggregatorEventConsumer) deleteAuthorization(ctx context.Context, commu // AggregatorServiceRecord represents the service declaration record structure type AggregatorServiceRecord struct { Type string `json:"$type"` - DID string `json:"did"` // DID of aggregator (must match repo DID) + DID string `json:"did"` // DID of aggregator (must match repo DID) DisplayName string `json:"displayName"` Description string `json:"description,omitempty"` - Avatar map[string]interface{} `json:"avatar,omitempty"` // Blob reference (CID will be extracted) - ConfigSchema map[string]interface{} `json:"configSchema,omitempty"` // JSON Schema - MaintainerDID string `json:"maintainer,omitempty"` // Fixed: was maintainerDid - SourceURL string `json:"sourceUrl,omitempty"` // Fixed: was homepageUrl + Avatar map[string]interface{} `json:"avatar,omitempty"` // Blob reference (CID will be extracted) + ConfigSchema map[string]interface{} `json:"configSchema,omitempty"` // JSON Schema + MaintainerDID string `json:"maintainer,omitempty"` // Fixed: was maintainerDid + SourceURL string `json:"sourceUrl,omitempty"` // Fixed: was homepageUrl CreatedAt string `json:"createdAt"` } @@ -310,8 +310,8 @@ type AggregatorAuthorizationRecord struct { Aggregator string `json:"aggregatorDid"` // Aggregator DID - fixed field name CommunityDid string `json:"communityDid"` // Community DID (must match repo DID) Enabled bool `json:"enabled"` - Config map[string]interface{} `json:"config,omitempty"` // Aggregator-specific config - CreatedBy string `json:"createdBy"` // Required: DID of moderator who authorized + Config map[string]interface{} `json:"config,omitempty"` // Aggregator-specific config + CreatedBy string `json:"createdBy"` // Required: DID of moderator who authorized DisabledBy string `json:"disabledBy,omitempty"` DisabledAt string `json:"disabledAt,omitempty"` // When authorization was disabled (for modlog/audit) CreatedAt string `json:"createdAt"` diff --git a/internal/core/aggregators/aggregator.go b/internal/core/aggregators/aggregator.go index 0f4e8d4..2d118f3 100644 --- a/internal/core/aggregators/aggregator.go +++ b/internal/core/aggregators/aggregator.go @@ -6,36 +6,36 @@ import "time" // Aggregators are autonomous services that can post content to communities after authorization // Following Bluesky's pattern: app.bsky.feed.generator and app.bsky.labeler.service type Aggregator struct { - DID string `json:"did" db:"did"` // Aggregator's DID (primary key) - DisplayName string `json:"displayName" db:"display_name"` // Human-readable name - Description string `json:"description,omitempty" db:"description"` // What the aggregator does - AvatarURL string `json:"avatarUrl,omitempty" db:"avatar_url"` // Optional avatar image URL - ConfigSchema []byte `json:"configSchema,omitempty" db:"config_schema"` // JSON Schema for configuration (JSONB) + DID string `json:"did" db:"did"` // Aggregator's DID (primary key) + DisplayName string `json:"displayName" db:"display_name"` // Human-readable name + Description string `json:"description,omitempty" db:"description"` // What the aggregator does + AvatarURL string `json:"avatarUrl,omitempty" db:"avatar_url"` // Optional avatar image URL + ConfigSchema []byte `json:"configSchema,omitempty" db:"config_schema"` // JSON Schema for configuration (JSONB) MaintainerDID string `json:"maintainerDid,omitempty" db:"maintainer_did"` // Contact for support/issues - SourceURL string `json:"sourceUrl,omitempty" db:"source_url"` // Source code URL (transparency) - CommunitiesUsing int `json:"communitiesUsing" db:"communities_using"` // Auto-updated by trigger - PostsCreated int `json:"postsCreated" db:"posts_created"` // Auto-updated by trigger - CreatedAt time.Time `json:"createdAt" db:"created_at"` // When aggregator was created (from lexicon) - IndexedAt time.Time `json:"indexedAt" db:"indexed_at"` // When we indexed this record - RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://did/social.coves.aggregator.service/self - RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // Content hash + SourceURL string `json:"sourceUrl,omitempty" db:"source_url"` // Source code URL (transparency) + CommunitiesUsing int `json:"communitiesUsing" db:"communities_using"` // Auto-updated by trigger + PostsCreated int `json:"postsCreated" db:"posts_created"` // Auto-updated by trigger + CreatedAt time.Time `json:"createdAt" db:"created_at"` // When aggregator was created (from lexicon) + IndexedAt time.Time `json:"indexedAt" db:"indexed_at"` // When we indexed this record + RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://did/social.coves.aggregator.service/self + RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // Content hash } // Authorization represents a community's authorization for an aggregator // Stored in community's repository: at://community_did/social.coves.aggregator.authorization/{rkey} type Authorization struct { - ID int `json:"id" db:"id"` // Database ID - AggregatorDID string `json:"aggregatorDid" db:"aggregator_did"` // Which aggregator - CommunityDID string `json:"communityDid" db:"community_did"` // Which community - Enabled bool `json:"enabled" db:"enabled"` // Current status - Config []byte `json:"config,omitempty" db:"config"` // Aggregator-specific config (JSONB) - CreatedBy string `json:"createdBy,omitempty" db:"created_by"` // Moderator DID who enabled it - DisabledBy string `json:"disabledBy,omitempty" db:"disabled_by"` // Moderator DID who disabled it - CreatedAt time.Time `json:"createdAt" db:"created_at"` // When authorization was created - DisabledAt *time.Time `json:"disabledAt,omitempty" db:"disabled_at"` // When authorization was disabled (for modlog/audit) - IndexedAt time.Time `json:"indexedAt" db:"indexed_at"` // When we indexed this record - RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://community_did/social.coves.aggregator.authorization/{rkey} - RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // Content hash + ID int `json:"id" db:"id"` // Database ID + AggregatorDID string `json:"aggregatorDid" db:"aggregator_did"` // Which aggregator + CommunityDID string `json:"communityDid" db:"community_did"` // Which community + Enabled bool `json:"enabled" db:"enabled"` // Current status + Config []byte `json:"config,omitempty" db:"config"` // Aggregator-specific config (JSONB) + CreatedBy string `json:"createdBy,omitempty" db:"created_by"` // Moderator DID who enabled it + DisabledBy string `json:"disabledBy,omitempty" db:"disabled_by"` // Moderator DID who disabled it + CreatedAt time.Time `json:"createdAt" db:"created_at"` // When authorization was created + DisabledAt *time.Time `json:"disabledAt,omitempty" db:"disabled_at"` // When authorization was disabled (for modlog/audit) + IndexedAt time.Time `json:"indexedAt" db:"indexed_at"` // When we indexed this record + RecordURI string `json:"recordUri,omitempty" db:"record_uri"` // at://community_did/social.coves.aggregator.authorization/{rkey} + RecordCID string `json:"recordCid,omitempty" db:"record_cid"` // Content hash } // AggregatorPost represents tracking of posts created by aggregators @@ -51,28 +51,28 @@ type AggregatorPost struct { // EnableAggregatorRequest represents input for enabling an aggregator in a community type EnableAggregatorRequest struct { - CommunityDID string `json:"communityDid"` // Which community (resolved from identifier) - AggregatorDID string `json:"aggregatorDid"` // Which aggregator - Config map[string]interface{} `json:"config,omitempty"` // Aggregator-specific configuration - EnabledByDID string `json:"enabledByDid"` // Moderator making the change (from JWT) - EnabledByToken string `json:"-"` // User's access token for PDS write + CommunityDID string `json:"communityDid"` // Which community (resolved from identifier) + AggregatorDID string `json:"aggregatorDid"` // Which aggregator + Config map[string]interface{} `json:"config,omitempty"` // Aggregator-specific configuration + EnabledByDID string `json:"enabledByDid"` // Moderator making the change (from JWT) + EnabledByToken string `json:"-"` // User's access token for PDS write } // DisableAggregatorRequest represents input for disabling an aggregator type DisableAggregatorRequest struct { - CommunityDID string `json:"communityDid"` // Which community (resolved from identifier) - AggregatorDID string `json:"aggregatorDid"` // Which aggregator - DisabledByDID string `json:"disabledByDid"` // Moderator making the change (from JWT) + CommunityDID string `json:"communityDid"` // Which community (resolved from identifier) + AggregatorDID string `json:"aggregatorDid"` // Which aggregator + DisabledByDID string `json:"disabledByDid"` // Moderator making the change (from JWT) DisabledByToken string `json:"-"` // User's access token for PDS write } // UpdateConfigRequest represents input for updating an aggregator's configuration type UpdateConfigRequest struct { - CommunityDID string `json:"communityDid"` // Which community (resolved from identifier) - AggregatorDID string `json:"aggregatorDid"` // Which aggregator - Config map[string]interface{} `json:"config"` // New configuration - UpdatedByDID string `json:"updatedByDid"` // Moderator making the change (from JWT) - UpdatedByToken string `json:"-"` // User's access token for PDS write + CommunityDID string `json:"communityDid"` // Which community (resolved from identifier) + AggregatorDID string `json:"aggregatorDid"` // Which aggregator + Config map[string]interface{} `json:"config"` // New configuration + UpdatedByDID string `json:"updatedByDid"` // Moderator making the change (from JWT) + UpdatedByToken string `json:"-"` // User's access token for PDS write } // GetServicesRequest represents query parameters for fetching aggregator details diff --git a/internal/core/aggregators/errors.go b/internal/core/aggregators/errors.go index b2cbfa4..51c8db8 100644 --- a/internal/core/aggregators/errors.go +++ b/internal/core/aggregators/errors.go @@ -7,15 +7,15 @@ import ( // Domain errors var ( - ErrAggregatorNotFound = errors.New("aggregator not found") - ErrAuthorizationNotFound = errors.New("authorization not found") - ErrNotAuthorized = errors.New("aggregator not authorized for this community") - ErrAlreadyAuthorized = errors.New("aggregator already authorized for this community") - ErrRateLimitExceeded = errors.New("aggregator rate limit exceeded") - ErrInvalidConfig = errors.New("invalid aggregator configuration") - ErrConfigSchemaValidation = errors.New("configuration does not match aggregator's schema") - ErrNotModerator = errors.New("user is not a moderator of this community") - ErrNotImplemented = errors.New("feature not yet implemented") // For Phase 2 write-forward operations + ErrAggregatorNotFound = errors.New("aggregator not found") + ErrAuthorizationNotFound = errors.New("authorization not found") + ErrNotAuthorized = errors.New("aggregator not authorized for this community") + ErrAlreadyAuthorized = errors.New("aggregator already authorized for this community") + ErrRateLimitExceeded = errors.New("aggregator rate limit exceeded") + ErrInvalidConfig = errors.New("invalid aggregator configuration") + ErrConfigSchemaValidation = errors.New("configuration does not match aggregator's schema") + ErrNotModerator = errors.New("user is not a moderator of this community") + ErrNotImplemented = errors.New("feature not yet implemented") // For Phase 2 write-forward operations ) // ValidationError represents a validation error with field details diff --git a/internal/core/aggregators/interfaces.go b/internal/core/aggregators/interfaces.go index 1825acc..c49bd76 100644 --- a/internal/core/aggregators/interfaces.go +++ b/internal/core/aggregators/interfaces.go @@ -55,7 +55,7 @@ type Service interface { // Validation and authorization checks (used by post creation handler) ValidateAggregatorPost(ctx context.Context, aggregatorDID, communityDID string) error // Checks authorization + rate limits - IsAggregator(ctx context.Context, did string) (bool, error) // Check if DID is a registered aggregator + IsAggregator(ctx context.Context, did string) (bool, error) // Check if DID is a registered aggregator // Post tracking (called after successful post creation) RecordAggregatorPost(ctx context.Context, aggregatorDID, communityDID, postURI, postCID string) error diff --git a/internal/db/postgres/aggregator_repo.go b/internal/db/postgres/aggregator_repo.go index 28e4582..6f205c4 100644 --- a/internal/db/postgres/aggregator_repo.go +++ b/internal/db/postgres/aggregator_repo.go @@ -61,7 +61,6 @@ func (r *postgresAggregatorRepo) CreateAggregator(ctx context.Context, agg *aggr nullString(agg.RecordURI), nullString(agg.RecordCID), ) - if err != nil { return fmt.Errorf("failed to create aggregator: %w", err) } @@ -230,7 +229,6 @@ func (r *postgresAggregatorRepo) UpdateAggregator(ctx context.Context, agg *aggr nullString(agg.RecordURI), nullString(agg.RecordCID), ) - if err != nil { return fmt.Errorf("failed to update aggregator: %w", err) } @@ -393,7 +391,6 @@ func (r *postgresAggregatorRepo) CreateAuthorization(ctx context.Context, auth * nullString(auth.RecordURI), nullString(auth.RecordCID), ).Scan(&auth.ID) - if err != nil { // Check for foreign key violations if strings.Contains(err.Error(), "fk_aggregator") { @@ -553,7 +550,6 @@ func (r *postgresAggregatorRepo) UpdateAuthorization(ctx context.Context, auth * nullString(auth.RecordURI), nullString(auth.RecordCID), ) - if err != nil { return fmt.Errorf("failed to update authorization: %w", err) } diff --git a/tests/integration/aggregator_e2e_test.go b/tests/integration/aggregator_e2e_test.go index d9d00e2..5eab32b 100644 --- a/tests/integration/aggregator_e2e_test.go +++ b/tests/integration/aggregator_e2e_test.go @@ -228,18 +228,18 @@ func TestAggregator_E2E_WithJetstream(t *testing.T) { // In production, this would come from Jetstream indexing community.profile records // For this E2E test, we create it directly testCommunity := &communities.Community{ - DID: communityDID, - Handle: communityHandle, - Name: fmt.Sprintf("e2e-%d", timestamp), - DisplayName: "E2E Test Community", - OwnerDID: communityDID, - CreatedByDID: communityDID, - HostedByDID: "did:web:test.coves.social", - Visibility: "public", - ModerationType: "moderator", - RecordURI: fmt.Sprintf("at://%s/social.coves.community.profile/self", communityDID), - RecordCID: "fakecid123", - PDSAccessToken: communityToken, + DID: communityDID, + Handle: communityHandle, + Name: fmt.Sprintf("e2e-%d", timestamp), + DisplayName: "E2E Test Community", + OwnerDID: communityDID, + CreatedByDID: communityDID, + HostedByDID: "did:web:test.coves.social", + Visibility: "public", + ModerationType: "moderator", + RecordURI: fmt.Sprintf("at://%s/social.coves.community.profile/self", communityDID), + RecordCID: "fakecid123", + PDSAccessToken: communityToken, PDSRefreshToken: communityToken, } _, err = communityRepo.Create(ctx, testCommunity) @@ -748,10 +748,10 @@ func TestAggregator_E2E_WithJetstream(t *testing.T) { "feedUrl": "https://example.com/feed.xml", "updateInterval": 15, }, - "createdBy": communityDID, - "disabledBy": communityDID, - "disabledAt": time.Now().Format(time.RFC3339), - "createdAt": time.Now().Add(-1 * time.Hour).Format(time.RFC3339), + "createdBy": communityDID, + "disabledBy": communityDID, + "disabledAt": time.Now().Format(time.RFC3339), + "createdAt": time.Now().Add(-1 * time.Hour).Format(time.RFC3339), }, }, } diff --git a/tests/integration/aggregator_test.go b/tests/integration/aggregator_test.go index 9a4f26c..8523f7d 100644 --- a/tests/integration/aggregator_test.go +++ b/tests/integration/aggregator_test.go @@ -45,17 +45,17 @@ func TestAggregatorRepository_Create(t *testing.T) { schemaBytes, _ := json.Marshal(configSchema) agg := &aggregators.Aggregator{ - DID: aggregatorDID, - DisplayName: "Test RSS Aggregator", - Description: "A test aggregator for integration testing", - AvatarURL: "bafytest123", - ConfigSchema: schemaBytes, + DID: aggregatorDID, + DisplayName: "Test RSS Aggregator", + Description: "A test aggregator for integration testing", + AvatarURL: "bafytest123", + ConfigSchema: schemaBytes, MaintainerDID: "did:plc:maintainer123", - SourceURL: "https://example.com/aggregator", - CreatedAt: time.Now(), - IndexedAt: time.Now(), - RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), - RecordCID: "bagtest456", + SourceURL: "https://example.com/aggregator", + CreatedAt: time.Now(), + IndexedAt: time.Now(), + RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), + RecordCID: "bagtest456", } err := repo.CreateAggregator(ctx, agg) @@ -87,7 +87,7 @@ func TestAggregatorRepository_Create(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Original Name", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest789", @@ -136,7 +136,7 @@ func TestAggregatorRepository_IsAggregator(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Test Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -190,7 +190,7 @@ func TestAggregatorAuthorization_Create(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Test Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -201,14 +201,14 @@ func TestAggregatorAuthorization_Create(t *testing.T) { // Create community community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!test-comm-%s@coves.local", uniqueSuffix), - Name: "test-comm", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!test-comm-%s@coves.local", uniqueSuffix), + Name: "test-comm", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community: %v", err) @@ -261,7 +261,7 @@ func TestAggregatorAuthorization_Create(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Test Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -272,14 +272,14 @@ func TestAggregatorAuthorization_Create(t *testing.T) { // Create community community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!test-unique-%s@coves.local", uniqueSuffix), - Name: "test-unique", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!test-unique-%s@coves.local", uniqueSuffix), + Name: "test-unique", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community: %v", err) @@ -348,7 +348,7 @@ func TestAggregatorAuthorization_IsAuthorized(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Test Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -358,14 +358,14 @@ func TestAggregatorAuthorization_IsAuthorized(t *testing.T) { } community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!test-auth-%s@coves.local", uniqueSuffix), - Name: "test-auth", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!test-auth-%s@coves.local", uniqueSuffix), + Name: "test-auth", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community: %v", err) @@ -405,7 +405,7 @@ func TestAggregatorAuthorization_IsAuthorized(t *testing.T) { agg2 := &aggregators.Aggregator{ DID: aggregatorDID2, DisplayName: "Test Aggregator 2", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID2), RecordCID: "bagtest456", @@ -415,14 +415,14 @@ func TestAggregatorAuthorization_IsAuthorized(t *testing.T) { } community2 := &communities.Community{ - DID: communityDID2, - Handle: fmt.Sprintf("!test-disabled-%s@coves.local", uniqueSuffix2), - Name: "test-disabled", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID2, + Handle: fmt.Sprintf("!test-disabled-%s@coves.local", uniqueSuffix2), + Name: "test-disabled", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community2); err != nil { t.Fatalf("Failed to create community: %v", err) @@ -488,7 +488,7 @@ func TestAggregatorService_PostCreationIntegration(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Test RSS Feed", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -499,14 +499,14 @@ func TestAggregatorService_PostCreationIntegration(t *testing.T) { // Setup community community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!test-post-%s@coves.local", uniqueSuffix), - Name: "test-post", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!test-post-%s@coves.local", uniqueSuffix), + Name: "test-post", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community: %v", err) @@ -587,7 +587,7 @@ func TestAggregatorService_RateLimiting(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Rate Limited Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -597,14 +597,14 @@ func TestAggregatorService_RateLimiting(t *testing.T) { } community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!test-ratelimit-%s@coves.local", uniqueSuffix), - Name: "test-ratelimit", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!test-ratelimit-%s@coves.local", uniqueSuffix), + Name: "test-ratelimit", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community: %v", err) @@ -676,7 +676,7 @@ func TestAggregatorPostService_Integration(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Test Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -726,7 +726,7 @@ func TestAggregatorTriggers(t *testing.T) { agg := &aggregators.Aggregator{ DID: aggregatorDID, DisplayName: "Trigger Test Aggregator", - CreatedAt: time.Now(), + CreatedAt: time.Now(), IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.service/self", aggregatorDID), RecordCID: "bagtest123", @@ -742,14 +742,14 @@ func TestAggregatorTriggers(t *testing.T) { communityDID := generateTestDID(commSuffix + "comm") community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!trigger-test-%s@coves.local", commSuffix), - Name: fmt.Sprintf("trigger-test-%d", i), - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!trigger-test-%s@coves.local", commSuffix), + Name: fmt.Sprintf("trigger-test-%d", i), + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community %d: %v", i, err) @@ -760,8 +760,8 @@ func TestAggregatorTriggers(t *testing.T) { CommunityDID: communityDID, Enabled: true, CreatedBy: "did:plc:moderator123", - CreatedAt: time.Now(), - IndexedAt: time.Now(), + CreatedAt: time.Now(), + IndexedAt: time.Now(), RecordURI: fmt.Sprintf("at://%s/social.coves.aggregator.authorization/auth%d", communityDID, i), RecordCID: fmt.Sprintf("bagauth%d", i), } @@ -786,14 +786,14 @@ func TestAggregatorTriggers(t *testing.T) { // Create community community := &communities.Community{ - DID: communityDID, - Handle: fmt.Sprintf("!post-trigger-%s@coves.local", uniqueSuffix), - Name: "post-trigger", - OwnerDID: "did:web:coves.local", - HostedByDID: "did:web:coves.local", - Visibility: "public", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + DID: communityDID, + Handle: fmt.Sprintf("!post-trigger-%s@coves.local", uniqueSuffix), + Name: "post-trigger", + OwnerDID: "did:web:coves.local", + HostedByDID: "did:web:coves.local", + Visibility: "public", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), } if _, err := commRepo.Create(ctx, community); err != nil { t.Fatalf("Failed to create community: %v", err)