A community based topic aggregation platform built on atproto

fix(auth): use DPoP authentication for user write operations

Replace plain Bearer token authentication with proper DPoP-authenticated
OAuth sessions for subscribe, unsubscribe, block, and unblock operations.

The issue was that atProto OAuth tokens are DPoP-bound and require both
an access token AND a DPoP proof header. The community service was using
plain Bearer authentication which caused "Malformed token" errors.

Changes:
- Update Service interface to use *oauth.ClientSessionData
- Add PDSClientFactory pattern for testability
- Update handlers to get OAuth session from middleware
- Update unit tests to inject OAuth session into context

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+3 -2
cmd/server/main.go
··· 276 log.Printf(" - Communities will be created at: %s", defaultPDS) 277 log.Printf(" - PDS will generate and manage all DIDs and keys") 278 279 - // Initialize community service (no longer needs didGenerator directly) 280 - communityService := communities.NewCommunityService(communityRepo, defaultPDS, instanceDID, instanceDomain, provisioner) 281 282 // Authenticate Coves instance with PDS to enable community record writes 283 // The instance needs a PDS account to write community records it owns
··· 276 log.Printf(" - Communities will be created at: %s", defaultPDS) 277 log.Printf(" - PDS will generate and manage all DIDs and keys") 278 279 + // Initialize community service with OAuth client for user DPoP authentication 280 + // OAuth client is required for subscribe/unsubscribe/block/unblock operations 281 + communityService := communities.NewCommunityService(communityRepo, defaultPDS, instanceDID, instanceDomain, provisioner, oauthClient, oauthStore) 282 283 // Authenticate Coves instance with PDS to enable community record writes 284 // The instance needs a PDS account to write community records it owns
+14 -56
internal/api/handlers/community/block.go
··· 47 return 48 } 49 50 - // Extract authenticated user DID and access token from request context (injected by auth middleware) 51 - userDID := middleware.GetUserDID(r) 52 - if userDID == "" { 53 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 54 return 55 } 56 57 - userAccessToken := middleware.GetUserAccessToken(r) 58 - if userAccessToken == "" { 59 - writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 60 - return 61 - } 62 - 63 - // Resolve community identifier (handle or DID) to DID 64 - // This allows users to block by handle: @gaming.community.coves.social or !gaming@coves.social 65 - communityDID, err := h.service.ResolveCommunityIdentifier(r.Context(), req.Community) 66 - if err != nil { 67 - if communities.IsNotFound(err) { 68 - writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not found") 69 - return 70 - } 71 - if communities.IsValidationError(err) { 72 - writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) 73 - return 74 - } 75 - log.Printf("Failed to resolve community identifier %s: %v", req.Community, err) 76 - writeError(w, http.StatusInternalServerError, "InternalError", "Failed to resolve community") 77 - return 78 - } 79 - 80 - // Block via service (write-forward to PDS) using resolved DID 81 - block, err := h.service.BlockCommunity(r.Context(), userDID, userAccessToken, communityDID) 82 if err != nil { 83 handleServiceError(w, err) 84 return ··· 125 return 126 } 127 128 - // Extract authenticated user DID and access token from request context (injected by auth middleware) 129 - userDID := middleware.GetUserDID(r) 130 - if userDID == "" { 131 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 132 return 133 } 134 135 - userAccessToken := middleware.GetUserAccessToken(r) 136 - if userAccessToken == "" { 137 - writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 138 - return 139 - } 140 - 141 - // Resolve community identifier (handle or DID) to DID 142 - // This allows users to unblock by handle: @gaming.community.coves.social or !gaming@coves.social 143 - communityDID, err := h.service.ResolveCommunityIdentifier(r.Context(), req.Community) 144 - if err != nil { 145 - if communities.IsNotFound(err) { 146 - writeError(w, http.StatusNotFound, "CommunityNotFound", "Community not found") 147 - return 148 - } 149 - if communities.IsValidationError(err) { 150 - writeError(w, http.StatusBadRequest, "InvalidRequest", err.Error()) 151 - return 152 - } 153 - log.Printf("Failed to resolve community identifier %s: %v", req.Community, err) 154 - writeError(w, http.StatusInternalServerError, "InternalError", "Failed to resolve community") 155 - return 156 - } 157 - 158 - // Unblock via service (delete record on PDS) using resolved DID 159 - err = h.service.UnblockCommunity(r.Context(), userDID, userAccessToken, communityDID) 160 if err != nil { 161 handleServiceError(w, err) 162 return
··· 47 return 48 } 49 50 + // Get OAuth session from context (injected by auth middleware) 51 + // The session contains the user's DID and credentials needed for DPoP authentication 52 + session := middleware.GetOAuthSession(r) 53 + if session == nil { 54 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 55 return 56 } 57 58 + // Block via service (write-forward to PDS with DPoP authentication) 59 + // Service handles identifier resolution (DIDs, handles, scoped identifiers) 60 + block, err := h.service.BlockCommunity(r.Context(), session, req.Community) 61 if err != nil { 62 handleServiceError(w, err) 63 return ··· 104 return 105 } 106 107 + // Get OAuth session from context (injected by auth middleware) 108 + // The session contains the user's DID and credentials needed for DPoP authentication 109 + session := middleware.GetOAuthSession(r) 110 + if session == nil { 111 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 112 return 113 } 114 115 + // Unblock via service (delete record on PDS with DPoP authentication) 116 + // Service handles identifier resolution (DIDs, handles, scoped identifiers) 117 + err := h.service.UnblockCommunity(r.Context(), session, req.Community) 118 if err != nil { 119 handleServiceError(w, err) 120 return
+6 -4
internal/api/handlers/community/create_test.go
··· 10 "net/http/httptest" 11 "testing" 12 "time" 13 ) 14 15 // mockCommunityService implements communities.Service for testing ··· 49 return nil, 0, nil 50 } 51 52 - func (m *mockCommunityService) SubscribeToCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 53 return nil, nil 54 } 55 56 - func (m *mockCommunityService) UnsubscribeFromCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 57 return nil 58 } 59 ··· 65 return nil, nil 66 } 67 68 - func (m *mockCommunityService) BlockCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) (*communities.CommunityBlock, error) { 69 return nil, nil 70 } 71 72 - func (m *mockCommunityService) UnblockCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 73 return nil 74 } 75
··· 10 "net/http/httptest" 11 "testing" 12 "time" 13 + 14 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 15 ) 16 17 // mockCommunityService implements communities.Service for testing ··· 51 return nil, 0, nil 52 } 53 54 + func (m *mockCommunityService) SubscribeToCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 55 return nil, nil 56 } 57 58 + func (m *mockCommunityService) UnsubscribeFromCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 59 return nil 60 } 61 ··· 67 return nil, nil 68 } 69 70 + func (m *mockCommunityService) BlockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) (*communities.CommunityBlock, error) { 71 return nil, nil 72 } 73 74 + func (m *mockCommunityService) UnblockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 75 return nil 76 } 77
+12 -23
internal/api/handlers/community/subscribe.go
··· 51 return 52 } 53 54 - // Extract authenticated user DID and access token from request context (injected by auth middleware) 55 - // Note: contentVisibility defaults and clamping handled by service layer 56 - userDID := middleware.GetUserDID(r) 57 - if userDID == "" { 58 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 59 return 60 } 61 62 - userAccessToken := middleware.GetUserAccessToken(r) 63 - if userAccessToken == "" { 64 - writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 65 - return 66 - } 67 - 68 - // Subscribe via service (write-forward to PDS) 69 // Service handles identifier resolution (DIDs, handles, scoped identifiers) 70 - subscription, err := h.service.SubscribeToCommunity(r.Context(), userDID, userAccessToken, req.Community, req.ContentVisibility) 71 if err != nil { 72 handleServiceError(w, err) 73 return ··· 117 return 118 } 119 120 - // Extract authenticated user DID and access token from request context (injected by auth middleware) 121 - userDID := middleware.GetUserDID(r) 122 - if userDID == "" { 123 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 124 return 125 } 126 127 - userAccessToken := middleware.GetUserAccessToken(r) 128 - if userAccessToken == "" { 129 - writeError(w, http.StatusUnauthorized, "AuthRequired", "Missing access token") 130 - return 131 - } 132 - 133 - // Unsubscribe via service (delete record on PDS) 134 // Service handles identifier resolution (DIDs, handles, scoped identifiers) 135 - err := h.service.UnsubscribeFromCommunity(r.Context(), userDID, userAccessToken, req.Community) 136 if err != nil { 137 handleServiceError(w, err) 138 return
··· 51 return 52 } 53 54 + // Get OAuth session from context (injected by auth middleware) 55 + // The session contains the user's DID and credentials needed for DPoP authentication 56 + session := middleware.GetOAuthSession(r) 57 + if session == nil { 58 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 59 return 60 } 61 62 + // Subscribe via service (write-forward to PDS with DPoP authentication) 63 // Service handles identifier resolution (DIDs, handles, scoped identifiers) 64 + subscription, err := h.service.SubscribeToCommunity(r.Context(), session, req.Community, req.ContentVisibility) 65 if err != nil { 66 handleServiceError(w, err) 67 return ··· 111 return 112 } 113 114 + // Get OAuth session from context (injected by auth middleware) 115 + // The session contains the user's DID and credentials needed for DPoP authentication 116 + session := middleware.GetOAuthSession(r) 117 + if session == nil { 118 writeError(w, http.StatusUnauthorized, "AuthRequired", "Authentication required") 119 return 120 } 121 122 + // Unsubscribe via service (delete record on PDS with DPoP authentication) 123 // Service handles identifier resolution (DIDs, handles, scoped identifiers) 124 + err := h.service.UnsubscribeFromCommunity(r.Context(), session, req.Community) 125 if err != nil { 126 handleServiceError(w, err) 127 return
+49 -29
internal/api/handlers/community/subscribe_test.go
··· 11 "net/http/httptest" 12 "testing" 13 "time" 14 ) 15 16 // subscribeTestService implements communities.Service for subscribe handler tests 17 type subscribeTestService struct { 18 - subscribeFunc func(ctx context.Context, userDID, accessToken, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) 19 - unsubscribeFunc func(ctx context.Context, userDID, accessToken, communityIdentifier string) error 20 } 21 22 func (m *subscribeTestService) CreateCommunity(ctx context.Context, req communities.CreateCommunityRequest) (*communities.Community, error) { ··· 39 return nil, 0, nil 40 } 41 42 - func (m *subscribeTestService) SubscribeToCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 43 if m.subscribeFunc != nil { 44 - return m.subscribeFunc(ctx, userDID, accessToken, communityIdentifier, contentVisibility) 45 } 46 return &communities.Subscription{ 47 UserDID: userDID, ··· 52 }, nil 53 } 54 55 - func (m *subscribeTestService) UnsubscribeFromCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 56 if m.unsubscribeFunc != nil { 57 - return m.unsubscribeFunc(ctx, userDID, accessToken, communityIdentifier) 58 } 59 return nil 60 } ··· 67 return nil, nil 68 } 69 70 - func (m *subscribeTestService) BlockCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) (*communities.CommunityBlock, error) { 71 return nil, nil 72 } 73 74 - func (m *subscribeTestService) UnblockCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 75 return nil 76 } 77 ··· 144 t.Run(tc.name, func(t *testing.T) { 145 var receivedIdentifier string 146 mockService := &subscribeTestService{ 147 - subscribeFunc: func(ctx context.Context, userDID, accessToken, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 148 receivedIdentifier = communityIdentifier 149 return &communities.Subscription{ 150 UserDID: userDID, 151 CommunityDID: "did:plc:resolved", ··· 167 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 168 req.Header.Set("Content-Type", "application/json") 169 170 - // Inject auth context 171 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 172 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 173 req = req.WithContext(ctx) 174 175 w := httptest.NewRecorder() ··· 244 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 245 req.Header.Set("Content-Type", "application/json") 246 247 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 248 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 249 req = req.WithContext(ctx) 250 251 w := httptest.NewRecorder() ··· 286 for _, tc := range tests { 287 t.Run(tc.name, func(t *testing.T) { 288 mockService := &subscribeTestService{ 289 - subscribeFunc: func(ctx context.Context, userDID, accessToken, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 290 return nil, tc.serviceErr 291 }, 292 } ··· 302 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 303 req.Header.Set("Content-Type", "application/json") 304 305 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 306 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 307 req = req.WithContext(ctx) 308 309 w := httptest.NewRecorder() ··· 353 t.Run(tc.name, func(t *testing.T) { 354 var receivedIdentifier string 355 mockService := &subscribeTestService{ 356 - unsubscribeFunc: func(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 357 receivedIdentifier = communityIdentifier 358 return nil 359 }, ··· 369 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.unsubscribe", bytes.NewBuffer(bodyBytes)) 370 req.Header.Set("Content-Type", "application/json") 371 372 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 373 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 374 req = req.WithContext(ctx) 375 376 w := httptest.NewRecorder() ··· 399 400 func TestSubscribeHandler_Unsubscribe_SubscriptionNotFound(t *testing.T) { 401 mockService := &subscribeTestService{ 402 - unsubscribeFunc: func(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 403 return communities.ErrSubscriptionNotFound 404 }, 405 } ··· 414 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.unsubscribe", bytes.NewBuffer(bodyBytes)) 415 req.Header.Set("Content-Type", "application/json") 416 417 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 418 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 419 req = req.WithContext(ctx) 420 421 w := httptest.NewRecorder() ··· 456 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBufferString("invalid json")) 457 req.Header.Set("Content-Type", "application/json") 458 459 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 460 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 461 req = req.WithContext(ctx) 462 463 w := httptest.NewRecorder() ··· 468 } 469 } 470 471 - func TestSubscribeHandler_RequiresAccessToken(t *testing.T) { 472 mockService := &subscribeTestService{} 473 handler := NewSubscribeHandler(mockService) 474 ··· 480 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 481 req.Header.Set("Content-Type", "application/json") 482 483 - // User DID but no access token 484 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:testuser") 485 - req = req.WithContext(ctx) 486 487 w := httptest.NewRecorder() 488 handler.HandleSubscribe(w, req)
··· 11 "net/http/httptest" 12 "testing" 13 "time" 14 + 15 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 16 + "github.com/bluesky-social/indigo/atproto/syntax" 17 ) 18 19 + // createTestOAuthSession creates a mock OAuth session for testing 20 + func createTestOAuthSession(did string) *oauth.ClientSessionData { 21 + parsedDID, _ := syntax.ParseDID(did) 22 + return &oauth.ClientSessionData{ 23 + AccountDID: parsedDID, 24 + SessionID: "test-session", 25 + HostURL: "http://localhost:3001", 26 + AccessToken: "test-access-token", 27 + } 28 + } 29 + 30 // subscribeTestService implements communities.Service for subscribe handler tests 31 type subscribeTestService struct { 32 + subscribeFunc func(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) 33 + unsubscribeFunc func(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error 34 } 35 36 func (m *subscribeTestService) CreateCommunity(ctx context.Context, req communities.CreateCommunityRequest) (*communities.Community, error) { ··· 53 return nil, 0, nil 54 } 55 56 + func (m *subscribeTestService) SubscribeToCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 57 if m.subscribeFunc != nil { 58 + return m.subscribeFunc(ctx, session, communityIdentifier, contentVisibility) 59 + } 60 + userDID := "" 61 + if session != nil { 62 + userDID = session.AccountDID.String() 63 } 64 return &communities.Subscription{ 65 UserDID: userDID, ··· 70 }, nil 71 } 72 73 + func (m *subscribeTestService) UnsubscribeFromCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 74 if m.unsubscribeFunc != nil { 75 + return m.unsubscribeFunc(ctx, session, communityIdentifier) 76 } 77 return nil 78 } ··· 85 return nil, nil 86 } 87 88 + func (m *subscribeTestService) BlockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) (*communities.CommunityBlock, error) { 89 return nil, nil 90 } 91 92 + func (m *subscribeTestService) UnblockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 93 return nil 94 } 95 ··· 162 t.Run(tc.name, func(t *testing.T) { 163 var receivedIdentifier string 164 mockService := &subscribeTestService{ 165 + subscribeFunc: func(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 166 receivedIdentifier = communityIdentifier 167 + userDID := "" 168 + if session != nil { 169 + userDID = session.AccountDID.String() 170 + } 171 return &communities.Subscription{ 172 UserDID: userDID, 173 CommunityDID: "did:plc:resolved", ··· 189 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 190 req.Header.Set("Content-Type", "application/json") 191 192 + // Inject OAuth session into context 193 + session := createTestOAuthSession("did:plc:testuser") 194 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 195 req = req.WithContext(ctx) 196 197 w := httptest.NewRecorder() ··· 266 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 267 req.Header.Set("Content-Type", "application/json") 268 269 + session := createTestOAuthSession("did:plc:testuser") 270 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 271 req = req.WithContext(ctx) 272 273 w := httptest.NewRecorder() ··· 308 for _, tc := range tests { 309 t.Run(tc.name, func(t *testing.T) { 310 mockService := &subscribeTestService{ 311 + subscribeFunc: func(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 312 return nil, tc.serviceErr 313 }, 314 } ··· 324 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 325 req.Header.Set("Content-Type", "application/json") 326 327 + session := createTestOAuthSession("did:plc:testuser") 328 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 329 req = req.WithContext(ctx) 330 331 w := httptest.NewRecorder() ··· 375 t.Run(tc.name, func(t *testing.T) { 376 var receivedIdentifier string 377 mockService := &subscribeTestService{ 378 + unsubscribeFunc: func(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 379 receivedIdentifier = communityIdentifier 380 return nil 381 }, ··· 391 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.unsubscribe", bytes.NewBuffer(bodyBytes)) 392 req.Header.Set("Content-Type", "application/json") 393 394 + session := createTestOAuthSession("did:plc:testuser") 395 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 396 req = req.WithContext(ctx) 397 398 w := httptest.NewRecorder() ··· 421 422 func TestSubscribeHandler_Unsubscribe_SubscriptionNotFound(t *testing.T) { 423 mockService := &subscribeTestService{ 424 + unsubscribeFunc: func(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 425 return communities.ErrSubscriptionNotFound 426 }, 427 } ··· 436 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.unsubscribe", bytes.NewBuffer(bodyBytes)) 437 req.Header.Set("Content-Type", "application/json") 438 439 + session := createTestOAuthSession("did:plc:testuser") 440 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 441 req = req.WithContext(ctx) 442 443 w := httptest.NewRecorder() ··· 478 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBufferString("invalid json")) 479 req.Header.Set("Content-Type", "application/json") 480 481 + session := createTestOAuthSession("did:plc:testuser") 482 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 483 req = req.WithContext(ctx) 484 485 w := httptest.NewRecorder() ··· 490 } 491 } 492 493 + func TestSubscribeHandler_RequiresOAuthSession(t *testing.T) { 494 mockService := &subscribeTestService{} 495 handler := NewSubscribeHandler(mockService) 496 ··· 502 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.subscribe", bytes.NewBuffer(bodyBytes)) 503 req.Header.Set("Content-Type", "application/json") 504 505 + // No OAuth session in context 506 507 w := httptest.NewRecorder() 508 handler.HandleSubscribe(w, req)
+5
internal/atproto/pds/errors.go
··· 27 func IsAuthError(err error) bool { 28 return errors.Is(err, ErrUnauthorized) || errors.Is(err, ErrForbidden) 29 }
··· 27 func IsAuthError(err error) bool { 28 return errors.Is(err, ErrUnauthorized) || errors.Is(err, ErrForbidden) 29 } 30 + 31 + // IsConflictError returns true if the error indicates a conflict (e.g., duplicate record). 32 + func IsConflictError(err error) bool { 33 + return errors.Is(err, ErrConflict) 34 + }
+11 -5
internal/core/communities/interfaces.go
··· 1 package communities 2 3 - import "context" 4 5 // Repository defines the interface for community data persistence 6 // This is the AppView's indexed view of communities from the firehose ··· 66 SearchCommunities(ctx context.Context, req SearchCommunitiesRequest) ([]*Community, int, error) 67 68 // Subscription operations (write-forward: creates record in user's PDS) 69 - SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string, contentVisibility int) (*Subscription, error) 70 - UnsubscribeFromCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error 71 GetUserSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error) 72 GetCommunitySubscribers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*Subscription, error) 73 74 // Block operations (write-forward: creates record in user's PDS) 75 - BlockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*CommunityBlock, error) 76 - UnblockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error 77 GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*CommunityBlock, error) 78 IsBlocked(ctx context.Context, userDID, communityIdentifier string) (bool, error) 79
··· 1 package communities 2 3 + import ( 4 + "context" 5 + 6 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 7 + ) 8 9 // Repository defines the interface for community data persistence 10 // This is the AppView's indexed view of communities from the firehose ··· 70 SearchCommunities(ctx context.Context, req SearchCommunitiesRequest) ([]*Community, int, error) 71 72 // Subscription operations (write-forward: creates record in user's PDS) 73 + // OAuth session is passed for DPoP authentication to the user's PDS 74 + SubscribeToCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*Subscription, error) 75 + UnsubscribeFromCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error 76 GetUserSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error) 77 GetCommunitySubscribers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*Subscription, error) 78 79 // Block operations (write-forward: creates record in user's PDS) 80 + // OAuth session is passed for DPoP authentication to the user's PDS 81 + BlockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) (*CommunityBlock, error) 82 + UnblockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error 83 GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*CommunityBlock, error) 84 IsBlocked(ctx context.Context, userDID, communityIdentifier string) (bool, error) 85
+142 -44
internal/core/communities/service.go
··· 1 package communities 2 3 import ( 4 "Coves/internal/atproto/utils" 5 "bytes" 6 "context" ··· 14 "strings" 15 "sync" 16 "time" 17 ) 18 19 // Community handle validation regex (DNS-valid handle: name.community.instance.com) ··· 25 26 // Domain validation (simplified - checks for valid DNS hostname structure) 27 var domainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) 28 29 type communityService struct { 30 // Interfaces and pointers first (better alignment) 31 repo Repository 32 provisioner *PDSAccountProvisioner 33 34 // Token refresh concurrency control 35 // Each community gets its own mutex to prevent concurrent refresh attempts 36 refreshMutexes map[string]*sync.Mutex ··· 52 maxMutexCacheSize = 10000 53 ) 54 55 - // NewCommunityService creates a new community service 56 - func NewCommunityService(repo Repository, pdsURL, instanceDID, instanceDomain string, provisioner *PDSAccountProvisioner) Service { 57 // SECURITY: Basic validation that did:web domain matches configured instanceDomain 58 // This catches honest configuration mistakes but NOT malicious code modifications 59 // Full verification (Phase 2) requires fetching DID document from domain ··· 74 instanceDID: instanceDID, 75 instanceDomain: instanceDomain, 76 provisioner: provisioner, 77 refreshMutexes: make(map[string]*sync.Mutex), 78 } 79 } 80 81 // SetPDSAccessToken sets the PDS access token for authentication 82 // This should be called after creating a session for the Coves instance DID on the PDS 83 func (s *communityService) SetPDSAccessToken(token string) { 84 s.pdsAccessToken = token 85 } 86 87 // CreateCommunity creates a new community via write-forward to PDS ··· 585 } 586 587 // SubscribeToCommunity creates a subscription via write-forward to PDS 588 - func (s *communityService) SubscribeToCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string, contentVisibility int) (*Subscription, error) { 589 - if userDID == "" { 590 - return nil, NewValidationError("userDid", "required") 591 - } 592 - if userAccessToken == "" { 593 - return nil, NewValidationError("userAccessToken", "required") 594 } 595 596 // Clamp contentVisibility to valid range (1-5), default to 3 if 0 or invalid 597 if contentVisibility <= 0 || contentVisibility > 5 { ··· 615 return nil, ErrUnauthorized 616 } 617 618 // Build subscription record 619 // CRITICAL: Collection is social.coves.community.subscription (RECORD TYPE), not social.coves.community.subscribe (XRPC procedure) 620 // This record will be created in the USER's repository: at://user_did/social.coves.community.subscription/{tid} ··· 626 "contentVisibility": contentVisibility, 627 } 628 629 - // Write-forward: create subscription record in user's repo using their access token 630 - // The collection parameter refers to the record type in the repository 631 - recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.subscription", "", subRecord, userAccessToken) 632 if err != nil { 633 return nil, fmt.Errorf("failed to create subscription on PDS: %w", err) 634 } 635 ··· 647 } 648 649 // UnsubscribeFromCommunity removes a subscription via PDS delete 650 - func (s *communityService) UnsubscribeFromCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error { 651 - if userDID == "" { 652 - return NewValidationError("userDid", "required") 653 } 654 - if userAccessToken == "" { 655 - return NewValidationError("userAccessToken", "required") 656 - } 657 658 // Resolve community identifier 659 communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier) ··· 673 return fmt.Errorf("invalid subscription record URI") 674 } 675 676 - // Write-forward: delete record from PDS using user's access token 677 // CRITICAL: Delete from social.coves.community.subscription (RECORD TYPE), not social.coves.community.unsubscribe 678 - if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.subscription", rkey, userAccessToken); err != nil { 679 return fmt.Errorf("failed to delete subscription on PDS: %w", err) 680 } 681 ··· 730 } 731 732 // BlockCommunity blocks a community via write-forward to PDS 733 - func (s *communityService) BlockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) (*CommunityBlock, error) { 734 - if userDID == "" { 735 - return nil, NewValidationError("userDid", "required") 736 } 737 - if userAccessToken == "" { 738 - return nil, NewValidationError("userAccessToken", "required") 739 - } 740 741 // Resolve community identifier (also verifies community exists) 742 communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier) 743 if err != nil { 744 return nil, err 745 } 746 747 // Build block record 748 // CRITICAL: Collection is social.coves.community.block (RECORD TYPE) ··· 754 "createdAt": time.Now().Format(time.RFC3339), 755 } 756 757 - // Write-forward: create block record in user's repo using their access token 758 // Note: We don't check for existing blocks first because: 759 // 1. The PDS may reject duplicates (depending on implementation) 760 // 2. The repository layer handles idempotency with ON CONFLICT DO NOTHING 761 // 3. This avoids a race condition where two concurrent requests both pass the check 762 - recordURI, recordCID, err := s.createRecordOnPDSAs(ctx, userDID, "social.coves.community.block", "", blockRecord, userAccessToken) 763 if err != nil { 764 // Check if this is a duplicate/conflict error from PDS 765 - // PDS should return 409 Conflict for duplicate records, but we also check common error messages 766 - // for compatibility with different PDS implementations 767 - errMsg := err.Error() 768 - isDuplicate := strings.Contains(errMsg, "status 409") || // HTTP 409 Conflict 769 - strings.Contains(errMsg, "duplicate") || 770 - strings.Contains(errMsg, "already exists") || 771 - strings.Contains(errMsg, "AlreadyExists") 772 - 773 - if isDuplicate { 774 // Fetch and return existing block from our indexed view 775 existingBlock, getErr := s.repo.GetBlock(ctx, userDID, communityDID) 776 if getErr == nil { ··· 804 } 805 806 // UnblockCommunity removes a block via PDS delete 807 - func (s *communityService) UnblockCommunity(ctx context.Context, userDID, userAccessToken, communityIdentifier string) error { 808 - if userDID == "" { 809 - return NewValidationError("userDid", "required") 810 } 811 - if userAccessToken == "" { 812 - return NewValidationError("userAccessToken", "required") 813 - } 814 815 // Resolve community identifier 816 communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier) ··· 830 return fmt.Errorf("invalid block record URI") 831 } 832 833 - // Write-forward: delete record from PDS using user's access token 834 - if err := s.deleteRecordOnPDSAs(ctx, userDID, "social.coves.community.block", rkey, userAccessToken); err != nil { 835 return fmt.Errorf("failed to delete block on PDS: %w", err) 836 } 837
··· 1 package communities 2 3 import ( 4 + oauthclient "Coves/internal/atproto/oauth" 5 + "Coves/internal/atproto/pds" 6 "Coves/internal/atproto/utils" 7 "bytes" 8 "context" ··· 16 "strings" 17 "sync" 18 "time" 19 + 20 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 21 + "github.com/bluesky-social/indigo/atproto/syntax" 22 ) 23 24 // Community handle validation regex (DNS-valid handle: name.community.instance.com) ··· 30 31 // Domain validation (simplified - checks for valid DNS hostname structure) 32 var domainRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) 33 + 34 + // PDSClientFactory creates PDS clients from session data. 35 + // Used to allow injection of different auth mechanisms (OAuth for production, password for tests). 36 + type PDSClientFactory func(ctx context.Context, session *oauth.ClientSessionData) (pds.Client, error) 37 38 type communityService struct { 39 // Interfaces and pointers first (better alignment) 40 repo Repository 41 provisioner *PDSAccountProvisioner 42 43 + // OAuth client/store for user PDS authentication (DPoP-based) 44 + oauthClient *oauthclient.OAuthClient 45 + oauthStore oauth.ClientAuthStore 46 + pdsClientFactory PDSClientFactory // Optional, for testing. If nil, uses OAuth. 47 + 48 // Token refresh concurrency control 49 // Each community gets its own mutex to prevent concurrent refresh attempts 50 refreshMutexes map[string]*sync.Mutex ··· 66 maxMutexCacheSize = 10000 67 ) 68 69 + // NewCommunityService creates a new community service with OAuth client for user authentication 70 + func NewCommunityService( 71 + repo Repository, 72 + pdsURL, instanceDID, instanceDomain string, 73 + provisioner *PDSAccountProvisioner, 74 + oauthClient *oauthclient.OAuthClient, 75 + oauthStore oauth.ClientAuthStore, 76 + ) Service { 77 // SECURITY: Basic validation that did:web domain matches configured instanceDomain 78 // This catches honest configuration mistakes but NOT malicious code modifications 79 // Full verification (Phase 2) requires fetching DID document from domain ··· 94 instanceDID: instanceDID, 95 instanceDomain: instanceDomain, 96 provisioner: provisioner, 97 + oauthClient: oauthClient, 98 + oauthStore: oauthStore, 99 refreshMutexes: make(map[string]*sync.Mutex), 100 } 101 } 102 103 + // NewCommunityServiceWithPDSFactory creates a community service with a custom PDS client factory. 104 + // This is primarily for testing with password-based authentication. 105 + func NewCommunityServiceWithPDSFactory( 106 + repo Repository, 107 + pdsURL, instanceDID, instanceDomain string, 108 + provisioner *PDSAccountProvisioner, 109 + factory PDSClientFactory, 110 + ) Service { 111 + return &communityService{ 112 + repo: repo, 113 + pdsURL: pdsURL, 114 + instanceDID: instanceDID, 115 + instanceDomain: instanceDomain, 116 + provisioner: provisioner, 117 + pdsClientFactory: factory, 118 + refreshMutexes: make(map[string]*sync.Mutex), 119 + } 120 + } 121 + 122 // SetPDSAccessToken sets the PDS access token for authentication 123 // This should be called after creating a session for the Coves instance DID on the PDS 124 func (s *communityService) SetPDSAccessToken(token string) { 125 s.pdsAccessToken = token 126 + } 127 + 128 + // getPDSClient creates a PDS client from an OAuth session. 129 + // If a custom factory was provided (for testing), uses that. 130 + // Otherwise, uses DPoP authentication via indigo's APIClient for proper OAuth token handling. 131 + func (s *communityService) getPDSClient(ctx context.Context, session *oauth.ClientSessionData) (pds.Client, error) { 132 + // Use custom factory if provided (e.g., for testing with password auth) 133 + if s.pdsClientFactory != nil { 134 + return s.pdsClientFactory(ctx, session) 135 + } 136 + 137 + // Production path: use OAuth with DPoP 138 + if s.oauthClient == nil || s.oauthClient.ClientApp == nil { 139 + return nil, fmt.Errorf("OAuth client not configured") 140 + } 141 + 142 + client, err := pds.NewFromOAuthSession(ctx, s.oauthClient.ClientApp, session) 143 + if err != nil { 144 + return nil, fmt.Errorf("failed to create PDS client: %w", err) 145 + } 146 + 147 + return client, nil 148 } 149 150 // CreateCommunity creates a new community via write-forward to PDS ··· 648 } 649 650 // SubscribeToCommunity creates a subscription via write-forward to PDS 651 + // Uses OAuth session with DPoP authentication for secure PDS communication 652 + func (s *communityService) SubscribeToCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*Subscription, error) { 653 + if session == nil { 654 + return nil, NewValidationError("session", "required") 655 } 656 + 657 + userDID := session.AccountDID.String() 658 659 // Clamp contentVisibility to valid range (1-5), default to 3 if 0 or invalid 660 if contentVisibility <= 0 || contentVisibility > 5 { ··· 678 return nil, ErrUnauthorized 679 } 680 681 + // Create PDS client for this session (DPoP authentication) 682 + pdsClient, err := s.getPDSClient(ctx, session) 683 + if err != nil { 684 + return nil, fmt.Errorf("failed to create PDS client: %w", err) 685 + } 686 + 687 + // Generate TID for record key 688 + tid := syntax.NewTIDNow(0) 689 + 690 // Build subscription record 691 // CRITICAL: Collection is social.coves.community.subscription (RECORD TYPE), not social.coves.community.subscribe (XRPC procedure) 692 // This record will be created in the USER's repository: at://user_did/social.coves.community.subscription/{tid} ··· 698 "contentVisibility": contentVisibility, 699 } 700 701 + // Write-forward: create subscription record in user's repo using DPoP-authenticated client 702 + recordURI, recordCID, err := pdsClient.CreateRecord(ctx, "social.coves.community.subscription", tid.String(), subRecord) 703 if err != nil { 704 + if pds.IsAuthError(err) { 705 + return nil, ErrUnauthorized 706 + } 707 return nil, fmt.Errorf("failed to create subscription on PDS: %w", err) 708 } 709 ··· 721 } 722 723 // UnsubscribeFromCommunity removes a subscription via PDS delete 724 + // Uses OAuth session with DPoP authentication for secure PDS communication 725 + func (s *communityService) UnsubscribeFromCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 726 + if session == nil { 727 + return NewValidationError("session", "required") 728 } 729 + 730 + userDID := session.AccountDID.String() 731 732 // Resolve community identifier 733 communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier) ··· 747 return fmt.Errorf("invalid subscription record URI") 748 } 749 750 + // Create PDS client for this session (DPoP authentication) 751 + pdsClient, err := s.getPDSClient(ctx, session) 752 + if err != nil { 753 + return fmt.Errorf("failed to create PDS client: %w", err) 754 + } 755 + 756 + // Write-forward: delete record from PDS using DPoP-authenticated client 757 // CRITICAL: Delete from social.coves.community.subscription (RECORD TYPE), not social.coves.community.unsubscribe 758 + if err := pdsClient.DeleteRecord(ctx, "social.coves.community.subscription", rkey); err != nil { 759 + if pds.IsAuthError(err) { 760 + return ErrUnauthorized 761 + } 762 return fmt.Errorf("failed to delete subscription on PDS: %w", err) 763 } 764 ··· 813 } 814 815 // BlockCommunity blocks a community via write-forward to PDS 816 + // Uses OAuth session with DPoP authentication for secure PDS communication 817 + func (s *communityService) BlockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) (*CommunityBlock, error) { 818 + if session == nil { 819 + return nil, NewValidationError("session", "required") 820 } 821 + 822 + userDID := session.AccountDID.String() 823 824 // Resolve community identifier (also verifies community exists) 825 communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier) 826 if err != nil { 827 return nil, err 828 } 829 + 830 + // Create PDS client for this session (DPoP authentication) 831 + pdsClient, err := s.getPDSClient(ctx, session) 832 + if err != nil { 833 + return nil, fmt.Errorf("failed to create PDS client: %w", err) 834 + } 835 + 836 + // Generate TID for record key 837 + tid := syntax.NewTIDNow(0) 838 839 // Build block record 840 // CRITICAL: Collection is social.coves.community.block (RECORD TYPE) ··· 846 "createdAt": time.Now().Format(time.RFC3339), 847 } 848 849 + // Write-forward: create block record in user's repo using DPoP-authenticated client 850 // Note: We don't check for existing blocks first because: 851 // 1. The PDS may reject duplicates (depending on implementation) 852 // 2. The repository layer handles idempotency with ON CONFLICT DO NOTHING 853 // 3. This avoids a race condition where two concurrent requests both pass the check 854 + recordURI, recordCID, err := pdsClient.CreateRecord(ctx, "social.coves.community.block", tid.String(), blockRecord) 855 if err != nil { 856 + // Check for auth errors first 857 + if pds.IsAuthError(err) { 858 + return nil, ErrUnauthorized 859 + } 860 + 861 // Check if this is a duplicate/conflict error from PDS 862 + if pds.IsConflictError(err) { 863 // Fetch and return existing block from our indexed view 864 existingBlock, getErr := s.repo.GetBlock(ctx, userDID, communityDID) 865 if getErr == nil { ··· 893 } 894 895 // UnblockCommunity removes a block via PDS delete 896 + // Uses OAuth session with DPoP authentication for secure PDS communication 897 + func (s *communityService) UnblockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 898 + if session == nil { 899 + return NewValidationError("session", "required") 900 } 901 + 902 + userDID := session.AccountDID.String() 903 904 // Resolve community identifier 905 communityDID, err := s.ResolveCommunityIdentifier(ctx, communityIdentifier) ··· 919 return fmt.Errorf("invalid block record URI") 920 } 921 922 + // Create PDS client for this session (DPoP authentication) 923 + pdsClient, err := s.getPDSClient(ctx, session) 924 + if err != nil { 925 + return fmt.Errorf("failed to create PDS client: %w", err) 926 + } 927 + 928 + // Write-forward: delete record from PDS using DPoP-authenticated client 929 + if err := pdsClient.DeleteRecord(ctx, "social.coves.community.block", rkey); err != nil { 930 + if pds.IsAuthError(err) { 931 + return ErrUnauthorized 932 + } 933 return fmt.Errorf("failed to delete block on PDS: %w", err) 934 } 935
+3 -5
tests/e2e/user_signup_test.go
··· 391 } 392 393 var result struct { 394 - DID string `json:"did"` 395 - Profile struct { 396 - Handle string `json:"handle"` 397 - } `json:"profile"` 398 } 399 400 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 401 return "", "", fmt.Errorf("failed to decode response: %w", err) 402 } 403 404 - return result.DID, result.Profile.Handle, nil 405 }
··· 391 } 392 393 var result struct { 394 + DID string `json:"did"` 395 + Handle string `json:"handle"` 396 } 397 398 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 399 return "", "", fmt.Errorf("failed to decode response: %w", err) 400 } 401 402 + return result.DID, result.Handle, nil 403 }
+1 -1
tests/integration/aggregator_e2e_test.go
··· 68 identityConfig := identity.DefaultConfig() 69 identityResolver := identity.NewResolver(db, identityConfig) 70 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 71 - communityService := communities.NewCommunityService(communityRepo, "http://localhost:3001", "did:web:test.coves.social", "coves.social", nil) 72 aggregatorService := aggregators.NewAggregatorService(aggregatorRepo, communityService) 73 postService := posts.NewPostService(postRepo, communityService, aggregatorService, nil, nil, nil, "http://localhost:3001") 74
··· 68 identityConfig := identity.DefaultConfig() 69 identityResolver := identity.NewResolver(db, identityConfig) 70 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 71 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, "http://localhost:3001", "did:web:test.coves.social", "coves.social", nil, nil) 72 aggregatorService := aggregators.NewAggregatorService(aggregatorRepo, communityService) 73 postService := posts.NewPostService(postRepo, communityService, aggregatorService, nil, nil, nil, "http://localhost:3001") 74
+5 -5
tests/integration/author_posts_e2e_test.go
··· 71 // Setup services 72 resolver := identity.NewResolver(db, identity.DefaultConfig()) 73 userService := users.NewUserService(userRepo, resolver, pdsURL) 74 - communityService := communities.NewCommunityService(communityRepo, pdsURL, getTestInstanceDID(), "", nil) 75 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) 76 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 77 ··· 289 290 resolver := identity.NewResolver(db, identity.DefaultConfig()) 291 userService := users.NewUserService(userRepo, resolver, getTestPDSURL()) 292 - communityService := communities.NewCommunityService(communityRepo, getTestPDSURL(), getTestInstanceDID(), "", nil) 293 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, getTestPDSURL()) 294 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 295 ··· 428 429 resolver := identity.NewResolver(db, identity.DefaultConfig()) 430 userService := users.NewUserService(userRepo, resolver, getTestPDSURL()) 431 - communityService := communities.NewCommunityService(communityRepo, getTestPDSURL(), getTestInstanceDID(), "", nil) 432 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, getTestPDSURL()) 433 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 434 ··· 548 // Setup services 549 resolver := identity.NewResolver(db, identity.DefaultConfig()) 550 userService := users.NewUserService(userRepo, resolver, pdsURL) 551 - communityService := communities.NewCommunityService(communityRepo, pdsURL, getTestInstanceDID(), "", nil) 552 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) 553 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 554 ··· 658 659 resolver := identity.NewResolver(db, identity.DefaultConfig()) 660 userService := users.NewUserService(userRepo, resolver, getTestPDSURL()) 661 - communityService := communities.NewCommunityService(communityRepo, getTestPDSURL(), getTestInstanceDID(), "", nil) 662 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, getTestPDSURL()) 663 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 664
··· 71 // Setup services 72 resolver := identity.NewResolver(db, identity.DefaultConfig()) 73 userService := users.NewUserService(userRepo, resolver, pdsURL) 74 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, pdsURL, getTestInstanceDID(), "", nil, nil) 75 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) 76 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 77 ··· 289 290 resolver := identity.NewResolver(db, identity.DefaultConfig()) 291 userService := users.NewUserService(userRepo, resolver, getTestPDSURL()) 292 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, getTestPDSURL(), getTestInstanceDID(), "", nil, nil) 293 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, getTestPDSURL()) 294 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 295 ··· 428 429 resolver := identity.NewResolver(db, identity.DefaultConfig()) 430 userService := users.NewUserService(userRepo, resolver, getTestPDSURL()) 431 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, getTestPDSURL(), getTestInstanceDID(), "", nil, nil) 432 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, getTestPDSURL()) 433 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 434 ··· 548 // Setup services 549 resolver := identity.NewResolver(db, identity.DefaultConfig()) 550 userService := users.NewUserService(userRepo, resolver, pdsURL) 551 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, pdsURL, getTestInstanceDID(), "", nil, nil) 552 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) 553 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 554 ··· 658 659 resolver := identity.NewResolver(db, identity.DefaultConfig()) 660 userService := users.NewUserService(userRepo, resolver, getTestPDSURL()) 661 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, getTestPDSURL(), getTestInstanceDID(), "", nil, nil) 662 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, getTestPDSURL()) 663 voteService := votes.NewServiceWithPDSFactory(voteRepo, nil, nil, PasswordAuthPDSClientFactory()) 664
+21 -5
tests/integration/block_handle_resolution_test.go
··· 13 "testing" 14 15 postgresRepo "Coves/internal/db/postgres" 16 ) 17 18 // TestBlockHandler_HandleResolution tests that the block handler accepts handles 19 // in addition to DIDs and resolves them correctly 20 func TestBlockHandler_HandleResolution(t *testing.T) { ··· 29 30 // Set up repositories and services 31 communityRepo := postgresRepo.NewCommunityRepository(db) 32 - communityService := communities.NewCommunityService( 33 communityRepo, 34 getTestPDSURL(), 35 getTestInstanceDID(), 36 "coves.social", 37 nil, // No PDS HTTP client for this test 38 ) 39 40 blockHandler := community.NewBlockHandler(communityService) ··· 193 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.blockCommunity", bytes.NewBuffer(reqJSON)) 194 req.Header.Set("Content-Type", "application/json") 195 196 - // Add auth context so we get past auth checks and test resolution validation 197 - ctx := context.WithValue(req.Context(), middleware.UserDIDKey, "did:plc:test123") 198 - ctx = context.WithValue(ctx, middleware.UserAccessToken, "test-token") 199 req = req.WithContext(ctx) 200 201 w := httptest.NewRecorder() ··· 265 266 // Set up repositories and services 267 communityRepo := postgresRepo.NewCommunityRepository(db) 268 - communityService := communities.NewCommunityService( 269 communityRepo, 270 getTestPDSURL(), 271 getTestInstanceDID(), 272 "coves.social", 273 nil, 274 ) 275 276 blockHandler := community.NewBlockHandler(communityService)
··· 13 "testing" 14 15 postgresRepo "Coves/internal/db/postgres" 16 + 17 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 18 + "github.com/bluesky-social/indigo/atproto/syntax" 19 ) 20 21 + // createTestOAuthSessionForBlock creates a mock OAuth session for block handler tests 22 + func createTestOAuthSessionForBlock(did string) *oauth.ClientSessionData { 23 + parsedDID, _ := syntax.ParseDID(did) 24 + return &oauth.ClientSessionData{ 25 + AccountDID: parsedDID, 26 + SessionID: "test-session", 27 + HostURL: "http://localhost:3001", 28 + AccessToken: "test-access-token", 29 + } 30 + } 31 + 32 // TestBlockHandler_HandleResolution tests that the block handler accepts handles 33 // in addition to DIDs and resolves them correctly 34 func TestBlockHandler_HandleResolution(t *testing.T) { ··· 43 44 // Set up repositories and services 45 communityRepo := postgresRepo.NewCommunityRepository(db) 46 + communityService := communities.NewCommunityServiceWithPDSFactory( 47 communityRepo, 48 getTestPDSURL(), 49 getTestInstanceDID(), 50 "coves.social", 51 nil, // No PDS HTTP client for this test 52 + nil, // No PDS factory needed for this test 53 ) 54 55 blockHandler := community.NewBlockHandler(communityService) ··· 208 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.blockCommunity", bytes.NewBuffer(reqJSON)) 209 req.Header.Set("Content-Type", "application/json") 210 211 + // Add OAuth session context so we get past auth checks and test resolution validation 212 + session := createTestOAuthSessionForBlock("did:plc:test123") 213 + ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session) 214 req = req.WithContext(ctx) 215 216 w := httptest.NewRecorder() ··· 280 281 // Set up repositories and services 282 communityRepo := postgresRepo.NewCommunityRepository(db) 283 + communityService := communities.NewCommunityServiceWithPDSFactory( 284 communityRepo, 285 getTestPDSURL(), 286 getTestInstanceDID(), 287 "coves.social", 288 nil, 289 + nil, // No PDS factory needed for this test 290 ) 291 292 blockHandler := community.NewBlockHandler(communityService)
+13 -3
tests/integration/community_e2e_test.go
··· 22 "testing" 23 "time" 24 25 "github.com/go-chi/chi/v5" 26 "github.com/gorilla/websocket" 27 _ "github.com/lib/pq" ··· 151 // PDS handles all DID generation and registration automatically 152 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 153 154 - // Create service (no longer needs didGen directly - provisioner owns it) 155 - communityService := communities.NewCommunityService(communityRepo, pdsURL, instanceDID, instanceDomain, provisioner) 156 if svc, ok := communityService.(interface{ SetPDSAccessToken(string) }); ok { 157 svc.SetPDSAccessToken(accessToken) 158 } ··· 950 t.Logf("Initial subscriber count: %d", initialSubscriberCount) 951 952 // Subscribe first (using instance access token for instance user, with contentVisibility=3) 953 - subscription, err := communityService.SubscribeToCommunity(ctx, instanceDID, accessToken, community.DID, 3) 954 if err != nil { 955 t.Fatalf("Failed to subscribe: %v", err) 956 }
··· 22 "testing" 23 "time" 24 25 + oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth" 26 + "github.com/bluesky-social/indigo/atproto/syntax" 27 "github.com/go-chi/chi/v5" 28 "github.com/gorilla/websocket" 29 _ "github.com/lib/pq" ··· 153 // PDS handles all DID generation and registration automatically 154 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 155 156 + // Create service with PDS factory for password-based auth in tests 157 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, pdsURL, instanceDID, instanceDomain, provisioner, CommunityPasswordAuthPDSClientFactory()) 158 if svc, ok := communityService.(interface{ SetPDSAccessToken(string) }); ok { 159 svc.SetPDSAccessToken(accessToken) 160 } ··· 952 t.Logf("Initial subscriber count: %d", initialSubscriberCount) 953 954 // Subscribe first (using instance access token for instance user, with contentVisibility=3) 955 + // Create a session for the instance user 956 + parsedDID, _ := syntax.ParseDID(instanceDID) 957 + instanceSession := &oauthlib.ClientSessionData{ 958 + AccountDID: parsedDID, 959 + SessionID: "test-session-e2e", 960 + HostURL: pdsURL, 961 + AccessToken: accessToken, 962 + } 963 + subscription, err := communityService.SubscribeToCommunity(ctx, instanceSession, community.DID, 3) 964 if err != nil { 965 t.Fatalf("Failed to subscribe: %v", err) 966 }
+8 -4
tests/integration/community_identifier_resolution_test.go
··· 50 instanceDID = "did:web:" + instanceDomain 51 } 52 53 - service := communities.NewCommunityService( 54 repo, 55 pdsURL, 56 instanceDID, 57 instanceDomain, 58 provisioner, 59 ) 60 61 // Create a test community to resolve ··· 244 } 245 246 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 247 - service := communities.NewCommunityService( 248 repo, 249 pdsURL, 250 instanceDID, 251 instanceDomain, 252 provisioner, 253 ) 254 255 tests := []struct { ··· 421 } 422 423 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 424 - service := communities.NewCommunityService( 425 repo, 426 pdsURL, 427 instanceDID, 428 instanceDomain, 429 provisioner, 430 ) 431 432 t.Run("DID error includes identifier", func(t *testing.T) { ··· 486 } 487 488 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 489 - service := communities.NewCommunityService( 490 repo, 491 pdsURL, 492 instanceDID, 493 instanceDomain, 494 provisioner, 495 ) 496 497 // Create a test community
··· 50 instanceDID = "did:web:" + instanceDomain 51 } 52 53 + service := communities.NewCommunityServiceWithPDSFactory( 54 repo, 55 pdsURL, 56 instanceDID, 57 instanceDomain, 58 provisioner, 59 + nil, 60 ) 61 62 // Create a test community to resolve ··· 245 } 246 247 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 248 + service := communities.NewCommunityServiceWithPDSFactory( 249 repo, 250 pdsURL, 251 instanceDID, 252 instanceDomain, 253 provisioner, 254 + nil, 255 ) 256 257 tests := []struct { ··· 423 } 424 425 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 426 + service := communities.NewCommunityServiceWithPDSFactory( 427 repo, 428 pdsURL, 429 instanceDID, 430 instanceDomain, 431 provisioner, 432 + nil, 433 ) 434 435 t.Run("DID error includes identifier", func(t *testing.T) { ··· 489 } 490 491 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 492 + service := communities.NewCommunityServiceWithPDSFactory( 493 repo, 494 pdsURL, 495 instanceDID, 496 instanceDomain, 497 provisioner, 498 + nil, 499 ) 500 501 // Create a test community
+2 -1
tests/integration/community_provisioning_test.go
··· 146 147 repo := postgres.NewCommunityRepository(db) 148 provisioner := communities.NewPDSAccountProvisioner("test.local", "http://localhost:3001") 149 - service := communities.NewCommunityService( 150 repo, 151 "http://localhost:3001", // pdsURL 152 "did:web:test.local", // instanceDID 153 "test.local", // instanceDomain 154 provisioner, 155 ) 156 ctx := context.Background() 157
··· 146 147 repo := postgres.NewCommunityRepository(db) 148 provisioner := communities.NewPDSAccountProvisioner("test.local", "http://localhost:3001") 149 + service := communities.NewCommunityServiceWithPDSFactory( 150 repo, 151 "http://localhost:3001", // pdsURL 152 "did:web:test.local", // instanceDID 153 "test.local", // instanceDomain 154 provisioner, 155 + nil, 156 ) 157 ctx := context.Background() 158
+10 -5
tests/integration/community_service_integration_test.go
··· 57 // Create provisioner and service (production code path) 58 // Use coves.social domain (configured in PDS_SERVICE_HANDLE_DOMAINS as c-{name}.coves.social) 59 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 60 - service := communities.NewCommunityService( 61 repo, 62 pdsURL, 63 "did:web:coves.social", 64 "coves.social", 65 provisioner, 66 ) 67 68 // Generate unique community name (keep short for DNS label limit) ··· 201 202 t.Run("handles PDS errors gracefully", func(t *testing.T) { 203 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 204 - service := communities.NewCommunityService( 205 repo, 206 pdsURL, 207 "did:web:coves.social", 208 "coves.social", 209 provisioner, 210 ) 211 212 // Try to create community with invalid name (should fail validation before PDS) ··· 232 233 t.Run("validates DNS label limits", func(t *testing.T) { 234 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 235 - service := communities.NewCommunityService( 236 repo, 237 pdsURL, 238 "did:web:coves.social", 239 "coves.social", 240 provisioner, 241 ) 242 243 // Try 64-char name (exceeds DNS limit of 63) ··· 301 repo := postgres.NewCommunityRepository(db) 302 303 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 304 - service := communities.NewCommunityService( 305 repo, 306 pdsURL, 307 "did:web:coves.social", 308 "coves.social", 309 provisioner, 310 ) 311 312 t.Run("updates community with real PDS", func(t *testing.T) { ··· 492 repo := postgres.NewCommunityRepository(db) 493 494 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 495 - service := communities.NewCommunityService( 496 repo, 497 pdsURL, 498 "did:web:coves.social", 499 "coves.social", 500 provisioner, 501 ) 502 503 t.Run("generated password works for session creation", func(t *testing.T) {
··· 57 // Create provisioner and service (production code path) 58 // Use coves.social domain (configured in PDS_SERVICE_HANDLE_DOMAINS as c-{name}.coves.social) 59 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 60 + service := communities.NewCommunityServiceWithPDSFactory( 61 repo, 62 pdsURL, 63 "did:web:coves.social", 64 "coves.social", 65 provisioner, 66 + nil, 67 ) 68 69 // Generate unique community name (keep short for DNS label limit) ··· 202 203 t.Run("handles PDS errors gracefully", func(t *testing.T) { 204 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 205 + service := communities.NewCommunityServiceWithPDSFactory( 206 repo, 207 pdsURL, 208 "did:web:coves.social", 209 "coves.social", 210 provisioner, 211 + nil, 212 ) 213 214 // Try to create community with invalid name (should fail validation before PDS) ··· 234 235 t.Run("validates DNS label limits", func(t *testing.T) { 236 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 237 + service := communities.NewCommunityServiceWithPDSFactory( 238 repo, 239 pdsURL, 240 "did:web:coves.social", 241 "coves.social", 242 provisioner, 243 + nil, 244 ) 245 246 // Try 64-char name (exceeds DNS limit of 63) ··· 304 repo := postgres.NewCommunityRepository(db) 305 306 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 307 + service := communities.NewCommunityServiceWithPDSFactory( 308 repo, 309 pdsURL, 310 "did:web:coves.social", 311 "coves.social", 312 provisioner, 313 + nil, 314 ) 315 316 t.Run("updates community with real PDS", func(t *testing.T) { ··· 496 repo := postgres.NewCommunityRepository(db) 497 498 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 499 + service := communities.NewCommunityServiceWithPDSFactory( 500 repo, 501 pdsURL, 502 "did:web:coves.social", 503 "coves.social", 504 provisioner, 505 + nil, 506 ) 507 508 t.Run("generated password works for session creation", func(t *testing.T) {
+2 -1
tests/integration/community_update_e2e_test.go
··· 88 // Setup services 89 communityRepo := postgres.NewCommunityRepository(db) 90 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 91 - communityService := communities.NewCommunityService( 92 communityRepo, 93 pdsURL, 94 instanceDID, 95 "coves.social", 96 provisioner, 97 ) 98 99 consumer := jetstream.NewCommunityEventConsumer(communityRepo, instanceDID, true, identityResolver)
··· 88 // Setup services 89 communityRepo := postgres.NewCommunityRepository(db) 90 provisioner := communities.NewPDSAccountProvisioner("coves.social", pdsURL) 91 + communityService := communities.NewCommunityServiceWithPDSFactory( 92 communityRepo, 93 pdsURL, 94 instanceDID, 95 "coves.social", 96 provisioner, 97 + nil, 98 ) 99 100 consumer := jetstream.NewCommunityEventConsumer(communityRepo, instanceDID, true, identityResolver)
+24 -12
tests/integration/feed_test.go
··· 29 // Setup services 30 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 31 communityRepo := postgres.NewCommunityRepository(db) 32 - communityService := communities.NewCommunityService( 33 communityRepo, 34 "http://localhost:3001", 35 "did:web:test.coves.social", 36 "test.coves.social", 37 nil, 38 ) 39 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 106 // Setup services 107 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 108 communityRepo := postgres.NewCommunityRepository(db) 109 - communityService := communities.NewCommunityService( 110 communityRepo, 111 "http://localhost:3001", 112 "did:web:test.coves.social", 113 "test.coves.social", 114 nil, 115 ) 116 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) 117 handler := communityFeed.NewGetCommunityHandler(feedService, nil, nil) ··· 182 // Setup services 183 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 184 communityRepo := postgres.NewCommunityRepository(db) 185 - communityService := communities.NewCommunityService( 186 communityRepo, 187 "http://localhost:3001", 188 "did:web:test.coves.social", 189 "test.coves.social", 190 nil, 191 ) 192 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 238 // Setup services 239 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 240 communityRepo := postgres.NewCommunityRepository(db) 241 - communityService := communities.NewCommunityService( 242 communityRepo, 243 "http://localhost:3001", 244 "did:web:test.coves.social", 245 "test.coves.social", 246 nil, 247 ) 248 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 329 // Setup services 330 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 331 communityRepo := postgres.NewCommunityRepository(db) 332 - communityService := communities.NewCommunityService( 333 communityRepo, 334 "http://localhost:3001", 335 "did:web:test.coves.social", 336 "test.coves.social", 337 nil, 338 ) 339 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 365 // Setup services 366 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 367 communityRepo := postgres.NewCommunityRepository(db) 368 - communityService := communities.NewCommunityService( 369 communityRepo, 370 "http://localhost:3001", 371 "did:web:test.coves.social", 372 "test.coves.social", 373 nil, 374 ) 375 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 421 // Setup services 422 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 423 communityRepo := postgres.NewCommunityRepository(db) 424 - communityService := communities.NewCommunityService( 425 communityRepo, 426 "http://localhost:3001", 427 "did:web:test.coves.social", 428 "test.coves.social", 429 nil, 430 ) 431 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 465 // Setup services 466 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 467 communityRepo := postgres.NewCommunityRepository(db) 468 - communityService := communities.NewCommunityService( 469 communityRepo, 470 "http://localhost:3001", 471 "did:web:test.coves.social", 472 "test.coves.social", 473 nil, 474 ) 475 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) 476 handler := communityFeed.NewGetCommunityHandler(feedService, nil, nil) ··· 518 // Setup services 519 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 520 communityRepo := postgres.NewCommunityRepository(db) 521 - communityService := communities.NewCommunityService( 522 communityRepo, 523 "http://localhost:3001", 524 "did:web:test.coves.social", 525 "test.coves.social", 526 nil, 527 ) 528 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 619 // Setup services 620 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 621 communityRepo := postgres.NewCommunityRepository(db) 622 - communityService := communities.NewCommunityService( 623 communityRepo, 624 "http://localhost:3001", 625 "did:web:test.coves.social", 626 "test.coves.social", 627 nil, 628 ) 629 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 721 // Setup services 722 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 723 communityRepo := postgres.NewCommunityRepository(db) 724 - communityService := communities.NewCommunityService( 725 communityRepo, 726 "http://localhost:3001", 727 "did:web:test.coves.social", 728 "test.coves.social", 729 nil, 730 ) 731 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) 732 handler := communityFeed.NewGetCommunityHandler(feedService, nil, nil) ··· 822 // Setup services 823 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 824 communityRepo := postgres.NewCommunityRepository(db) 825 - communityService := communities.NewCommunityService( 826 communityRepo, 827 "http://localhost:3001", 828 "did:web:test.coves.social", 829 "test.coves.social", 830 nil, 831 ) 832 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService)
··· 29 // Setup services 30 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 31 communityRepo := postgres.NewCommunityRepository(db) 32 + communityService := communities.NewCommunityServiceWithPDSFactory( 33 communityRepo, 34 "http://localhost:3001", 35 "did:web:test.coves.social", 36 "test.coves.social", 37 + nil, 38 nil, 39 ) 40 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 107 // Setup services 108 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 109 communityRepo := postgres.NewCommunityRepository(db) 110 + communityService := communities.NewCommunityServiceWithPDSFactory( 111 communityRepo, 112 "http://localhost:3001", 113 "did:web:test.coves.social", 114 "test.coves.social", 115 nil, 116 + nil, 117 ) 118 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) 119 handler := communityFeed.NewGetCommunityHandler(feedService, nil, nil) ··· 184 // Setup services 185 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 186 communityRepo := postgres.NewCommunityRepository(db) 187 + communityService := communities.NewCommunityServiceWithPDSFactory( 188 communityRepo, 189 "http://localhost:3001", 190 "did:web:test.coves.social", 191 "test.coves.social", 192 + nil, 193 nil, 194 ) 195 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 241 // Setup services 242 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 243 communityRepo := postgres.NewCommunityRepository(db) 244 + communityService := communities.NewCommunityServiceWithPDSFactory( 245 communityRepo, 246 "http://localhost:3001", 247 "did:web:test.coves.social", 248 "test.coves.social", 249 + nil, 250 nil, 251 ) 252 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 333 // Setup services 334 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 335 communityRepo := postgres.NewCommunityRepository(db) 336 + communityService := communities.NewCommunityServiceWithPDSFactory( 337 communityRepo, 338 "http://localhost:3001", 339 "did:web:test.coves.social", 340 "test.coves.social", 341 + nil, 342 nil, 343 ) 344 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 370 // Setup services 371 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 372 communityRepo := postgres.NewCommunityRepository(db) 373 + communityService := communities.NewCommunityServiceWithPDSFactory( 374 communityRepo, 375 "http://localhost:3001", 376 "did:web:test.coves.social", 377 "test.coves.social", 378 + nil, 379 nil, 380 ) 381 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 427 // Setup services 428 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 429 communityRepo := postgres.NewCommunityRepository(db) 430 + communityService := communities.NewCommunityServiceWithPDSFactory( 431 communityRepo, 432 "http://localhost:3001", 433 "did:web:test.coves.social", 434 "test.coves.social", 435 + nil, 436 nil, 437 ) 438 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 472 // Setup services 473 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 474 communityRepo := postgres.NewCommunityRepository(db) 475 + communityService := communities.NewCommunityServiceWithPDSFactory( 476 communityRepo, 477 "http://localhost:3001", 478 "did:web:test.coves.social", 479 "test.coves.social", 480 nil, 481 + nil, 482 ) 483 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) 484 handler := communityFeed.NewGetCommunityHandler(feedService, nil, nil) ··· 526 // Setup services 527 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 528 communityRepo := postgres.NewCommunityRepository(db) 529 + communityService := communities.NewCommunityServiceWithPDSFactory( 530 communityRepo, 531 "http://localhost:3001", 532 "did:web:test.coves.social", 533 "test.coves.social", 534 + nil, 535 nil, 536 ) 537 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 628 // Setup services 629 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 630 communityRepo := postgres.NewCommunityRepository(db) 631 + communityService := communities.NewCommunityServiceWithPDSFactory( 632 communityRepo, 633 "http://localhost:3001", 634 "did:web:test.coves.social", 635 "test.coves.social", 636 + nil, 637 nil, 638 ) 639 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) ··· 731 // Setup services 732 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 733 communityRepo := postgres.NewCommunityRepository(db) 734 + communityService := communities.NewCommunityServiceWithPDSFactory( 735 communityRepo, 736 "http://localhost:3001", 737 "did:web:test.coves.social", 738 "test.coves.social", 739 nil, 740 + nil, 741 ) 742 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService) 743 handler := communityFeed.NewGetCommunityHandler(feedService, nil, nil) ··· 833 // Setup services 834 feedRepo := postgres.NewCommunityFeedRepository(db, "test-cursor-secret") 835 communityRepo := postgres.NewCommunityRepository(db) 836 + communityService := communities.NewCommunityServiceWithPDSFactory( 837 communityRepo, 838 "http://localhost:3001", 839 "did:web:test.coves.social", 840 "test.coves.social", 841 + nil, 842 nil, 843 ) 844 feedService := communityFeeds.NewCommunityFeedService(feedRepo, communityService)
+17
tests/integration/helpers.go
··· 4 "Coves/internal/api/middleware" 5 "Coves/internal/atproto/oauth" 6 "Coves/internal/atproto/pds" 7 "Coves/internal/core/users" 8 "Coves/internal/core/votes" 9 "bytes" ··· 443 return pds.NewFromAccessToken(session.HostURL, session.AccountDID.String(), session.AccessToken) 444 } 445 }
··· 4 "Coves/internal/api/middleware" 5 "Coves/internal/atproto/oauth" 6 "Coves/internal/atproto/pds" 7 + "Coves/internal/core/communities" 8 "Coves/internal/core/users" 9 "Coves/internal/core/votes" 10 "bytes" ··· 444 return pds.NewFromAccessToken(session.HostURL, session.AccountDID.String(), session.AccessToken) 445 } 446 } 447 + 448 + // CommunityPasswordAuthPDSClientFactory creates a PDSClientFactory for communities that uses password-based Bearer auth. 449 + // This is for E2E tests that use createSession instead of OAuth. 450 + // The factory extracts the access token and host URL from the session data. 451 + func CommunityPasswordAuthPDSClientFactory() communities.PDSClientFactory { 452 + return func(ctx context.Context, session *oauthlib.ClientSessionData) (pds.Client, error) { 453 + if session.AccessToken == "" { 454 + return nil, fmt.Errorf("session has no access token") 455 + } 456 + if session.HostURL == "" { 457 + return nil, fmt.Errorf("session has no host URL") 458 + } 459 + 460 + return pds.NewFromAccessToken(session.HostURL, session.AccountDID.String(), session.AccessToken) 461 + } 462 + }
+2 -1
tests/integration/post_creation_test.go
··· 35 36 communityRepo := postgres.NewCommunityRepository(db) 37 // Note: Provisioner not needed for this test (we're not actually creating communities) 38 - communityService := communities.NewCommunityService( 39 communityRepo, 40 "http://localhost:3001", 41 "did:web:test.coves.social", 42 "test.coves.social", 43 nil, // provisioner 44 ) 45 46 postRepo := postgres.NewPostRepository(db)
··· 35 36 communityRepo := postgres.NewCommunityRepository(db) 37 // Note: Provisioner not needed for this test (we're not actually creating communities) 38 + communityService := communities.NewCommunityServiceWithPDSFactory( 39 communityRepo, 40 "http://localhost:3001", 41 "did:web:test.coves.social", 42 "test.coves.social", 43 nil, // provisioner 44 + nil, // pdsClientFactory 45 ) 46 47 postRepo := postgres.NewPostRepository(db)
+2 -1
tests/integration/post_e2e_test.go
··· 394 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 395 396 // Setup community service with real PDS provisioner 397 - communityService := communities.NewCommunityService( 398 communityRepo, 399 pdsURL, 400 instanceDID, 401 instanceDomain, 402 provisioner, // ✅ Real provisioner for creating communities on PDS 403 ) 404 405 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) // nil aggregatorService, blobService, unfurlService, blueskyService for user-only tests
··· 394 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 395 396 // Setup community service with real PDS provisioner 397 + communityService := communities.NewCommunityServiceWithPDSFactory( 398 communityRepo, 399 pdsURL, 400 instanceDID, 401 instanceDomain, 402 provisioner, // ✅ Real provisioner for creating communities on PDS 403 + nil, // No PDS factory needed - no subscribe/block in this test 404 ) 405 406 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) // nil aggregatorService, blobService, unfurlService, blueskyService for user-only tests
+6 -3
tests/integration/post_handler_test.go
··· 32 33 // Setup services 34 communityRepo := postgres.NewCommunityRepository(db) 35 - communityService := communities.NewCommunityService( 36 communityRepo, 37 "http://localhost:3001", 38 "did:web:test.coves.social", 39 "test.coves.social", 40 nil, 41 ) 42 ··· 400 401 // Setup services 402 communityRepo := postgres.NewCommunityRepository(db) 403 - communityService := communities.NewCommunityService( 404 communityRepo, 405 "http://localhost:3001", 406 "did:web:test.coves.social", 407 "test.coves.social", 408 nil, 409 ) 410 ··· 484 485 // Setup services 486 communityRepo := postgres.NewCommunityRepository(db) 487 - communityService := communities.NewCommunityService( 488 communityRepo, 489 "http://localhost:3001", 490 "did:web:test.coves.social", 491 "test.coves.social", 492 nil, 493 ) 494
··· 32 33 // Setup services 34 communityRepo := postgres.NewCommunityRepository(db) 35 + communityService := communities.NewCommunityServiceWithPDSFactory( 36 communityRepo, 37 "http://localhost:3001", 38 "did:web:test.coves.social", 39 "test.coves.social", 40 + nil, 41 nil, 42 ) 43 ··· 401 402 // Setup services 403 communityRepo := postgres.NewCommunityRepository(db) 404 + communityService := communities.NewCommunityServiceWithPDSFactory( 405 communityRepo, 406 "http://localhost:3001", 407 "did:web:test.coves.social", 408 "test.coves.social", 409 + nil, 410 nil, 411 ) 412 ··· 486 487 // Setup services 488 communityRepo := postgres.NewCommunityRepository(db) 489 + communityService := communities.NewCommunityServiceWithPDSFactory( 490 communityRepo, 491 "http://localhost:3001", 492 "did:web:test.coves.social", 493 "test.coves.social", 494 + nil, 495 nil, 496 ) 497
+2 -1
tests/integration/post_thumb_validation_test.go
··· 55 56 // Setup services 57 communityRepo := postgres.NewCommunityRepository(db) 58 - communityService := communities.NewCommunityService( 59 communityRepo, 60 "http://localhost:3001", 61 "did:web:test.coves.social", 62 "test.coves.social", 63 nil, 64 ) 65
··· 55 56 // Setup services 57 communityRepo := postgres.NewCommunityRepository(db) 58 + communityService := communities.NewCommunityServiceWithPDSFactory( 59 communityRepo, 60 "http://localhost:3001", 61 "did:web:test.coves.social", 62 "test.coves.social", 63 + nil, 64 nil, 65 ) 66
+8 -4
tests/integration/post_unfurl_test.go
··· 51 unfurl.WithCacheTTL(24*time.Hour), 52 ) 53 54 - communityService := communities.NewCommunityService( 55 communityRepo, 56 "http://localhost:3001", 57 "did:web:test.coves.social", 58 "test.coves.social", 59 nil, 60 ) 61 ··· 348 identityResolver := identity.NewResolver(db, identityConfig) 349 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 350 351 - communityService := communities.NewCommunityService( 352 communityRepo, 353 "http://localhost:3001", 354 "did:web:test.coves.social", 355 "test.coves.social", 356 nil, 357 ) 358 ··· 456 unfurl.WithCacheTTL(24*time.Hour), 457 ) 458 459 - communityService := communities.NewCommunityService( 460 communityRepo, 461 "http://localhost:3001", 462 "did:web:test.coves.social", 463 "test.coves.social", 464 nil, 465 ) 466 467 postService := posts.NewPostService( ··· 568 unfurl.WithTimeout(30*time.Second), 569 ) 570 571 - communityService := communities.NewCommunityService( 572 communityRepo, 573 "http://localhost:3001", 574 "did:web:test.coves.social", 575 "test.coves.social", 576 nil, 577 ) 578
··· 51 unfurl.WithCacheTTL(24*time.Hour), 52 ) 53 54 + communityService := communities.NewCommunityServiceWithPDSFactory( 55 communityRepo, 56 "http://localhost:3001", 57 "did:web:test.coves.social", 58 "test.coves.social", 59 + nil, 60 nil, 61 ) 62 ··· 349 identityResolver := identity.NewResolver(db, identityConfig) 350 userService := users.NewUserService(userRepo, identityResolver, "http://localhost:3001") 351 352 + communityService := communities.NewCommunityServiceWithPDSFactory( 353 communityRepo, 354 "http://localhost:3001", 355 "did:web:test.coves.social", 356 "test.coves.social", 357 + nil, 358 nil, 359 ) 360 ··· 458 unfurl.WithCacheTTL(24*time.Hour), 459 ) 460 461 + communityService := communities.NewCommunityServiceWithPDSFactory( 462 communityRepo, 463 "http://localhost:3001", 464 "did:web:test.coves.social", 465 "test.coves.social", 466 nil, 467 + nil, 468 ) 469 470 postService := posts.NewPostService( ··· 571 unfurl.WithTimeout(30*time.Second), 572 ) 573 574 + communityService := communities.NewCommunityServiceWithPDSFactory( 575 communityRepo, 576 "http://localhost:3001", 577 "did:web:test.coves.social", 578 "test.coves.social", 579 + nil, 580 nil, 581 ) 582
+1 -1
tests/integration/user_journey_e2e_test.go
··· 128 } 129 130 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 131 - communityService := communities.NewCommunityService(communityRepo, pdsURL, instanceDID, instanceDomain, provisioner) 132 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) 133 timelineService := timelineCore.NewTimelineService(timelineRepo) 134
··· 128 } 129 130 provisioner := communities.NewPDSAccountProvisioner(instanceDomain, pdsURL) 131 + communityService := communities.NewCommunityServiceWithPDSFactory(communityRepo, pdsURL, instanceDID, instanceDomain, provisioner, CommunityPasswordAuthPDSClientFactory()) 132 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, nil, pdsURL) 133 timelineService := timelineCore.NewTimelineService(timelineRepo) 134