A community based topic aggregation platform built on atproto

feat(communities): add viewer.subscribed state to listCommunities endpoint

Fixes the issue where authenticated users couldn't see their subscription
status in the community list response, causing "My Communities" tab to show
no results and all community tiles to show "Join" instead of "Joined".

Changes:
- Add CommunityViewerState struct with tri-state *bool semantics
- Add GetSubscribedCommunityDIDs batch query to repository
- Add PopulateCommunityViewerState helper for viewer enrichment
- Update ListHandler to inject repo and populate viewer state
- Update route registration to pass repository through

The endpoint remains public but now enriches responses with viewer.subscribed
when the request is authenticated.

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

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

+506 -8
+1 -1
cmd/server/main.go
··· 606 607 // Register XRPC routes 608 routes.RegisterUserRoutes(r, userService) 609 - routes.RegisterCommunityRoutes(r, communityService, authMiddleware, allowedCommunityCreators) 610 log.Println("Community XRPC endpoints registered with OAuth authentication") 611 612 routes.RegisterPostRoutes(r, postService, dualAuth)
··· 606 607 // Register XRPC routes 608 routes.RegisterUserRoutes(r, userService) 609 + routes.RegisterCommunityRoutes(r, communityService, communityRepo, authMiddleware, allowedCommunityCreators) 610 log.Println("Community XRPC endpoints registered with OAuth authentication") 611 612 routes.RegisterPostRoutes(r, postService, dualAuth)
+41
internal/api/handlers/common/viewer_state.go
··· 2 3 import ( 4 "Coves/internal/api/middleware" 5 "Coves/internal/core/posts" 6 "Coves/internal/core/votes" 7 "context" ··· 71 } 72 } 73 }
··· 2 3 import ( 4 "Coves/internal/api/middleware" 5 + "Coves/internal/core/communities" 6 "Coves/internal/core/posts" 7 "Coves/internal/core/votes" 8 "context" ··· 72 } 73 } 74 } 75 + 76 + // PopulateCommunityViewerState enriches communities with the authenticated user's subscription state. 77 + // This is a no-op if the request is unauthenticated. 78 + func PopulateCommunityViewerState( 79 + ctx context.Context, 80 + r *http.Request, 81 + repo communities.Repository, 82 + communityList []*communities.Community, 83 + ) { 84 + if repo == nil || len(communityList) == 0 { 85 + return 86 + } 87 + 88 + userDID := middleware.GetUserDID(r) 89 + if userDID == "" { 90 + return // Not authenticated, leave viewer state nil 91 + } 92 + 93 + // Collect community DIDs 94 + communityDIDs := make([]string, len(communityList)) 95 + for i, c := range communityList { 96 + communityDIDs[i] = c.DID 97 + } 98 + 99 + // Batch query subscriptions 100 + subscribed, err := repo.GetSubscribedCommunityDIDs(ctx, userDID, communityDIDs) 101 + if err != nil { 102 + log.Printf("Warning: failed to get subscription state for user %s (%d communities): %v", 103 + userDID, len(communityDIDs), err) 104 + return 105 + } 106 + 107 + // Populate viewer state on each community 108 + for _, c := range communityList { 109 + isSubscribed := subscribed[c.DID] 110 + c.Viewer = &communities.CommunityViewerState{ 111 + Subscribed: &isSubscribed, 112 + } 113 + } 114 + }
+7 -1
internal/api/handlers/community/list.go
··· 1 package community 2 3 import ( 4 "Coves/internal/core/communities" 5 "encoding/json" 6 "net/http" ··· 10 // ListHandler handles listing communities 11 type ListHandler struct { 12 service communities.Service 13 } 14 15 // NewListHandler creates a new list handler 16 - func NewListHandler(service communities.Service) *ListHandler { 17 return &ListHandler{ 18 service: service, 19 } 20 } 21 ··· 99 handleServiceError(w, err) 100 return 101 } 102 103 // Build response 104 var cursor string
··· 1 package community 2 3 import ( 4 + "Coves/internal/api/handlers/common" 5 "Coves/internal/core/communities" 6 "encoding/json" 7 "net/http" ··· 11 // ListHandler handles listing communities 12 type ListHandler struct { 13 service communities.Service 14 + repo communities.Repository 15 } 16 17 // NewListHandler creates a new list handler 18 + func NewListHandler(service communities.Service, repo communities.Repository) *ListHandler { 19 return &ListHandler{ 20 service: service, 21 + repo: repo, 22 } 23 } 24 ··· 102 handleServiceError(w, err) 103 return 104 } 105 + 106 + // Populate viewer state if authenticated 107 + common.PopulateCommunityViewerState(r.Context(), r, h.repo, results) 108 109 // Build response 110 var cursor string
+2 -2
internal/api/routes/community.go
··· 11 // RegisterCommunityRoutes registers community-related XRPC endpoints on the router 12 // Implements social.coves.community.* lexicon endpoints 13 // allowedCommunityCreators restricts who can create communities. If empty, anyone can create. 14 - func RegisterCommunityRoutes(r chi.Router, service communities.Service, authMiddleware *middleware.OAuthAuthMiddleware, allowedCommunityCreators []string) { 15 // Initialize handlers 16 createHandler := community.NewCreateHandler(service, allowedCommunityCreators) 17 getHandler := community.NewGetHandler(service) 18 updateHandler := community.NewUpdateHandler(service) 19 - listHandler := community.NewListHandler(service) 20 searchHandler := community.NewSearchHandler(service) 21 subscribeHandler := community.NewSubscribeHandler(service) 22 blockHandler := community.NewBlockHandler(service)
··· 11 // RegisterCommunityRoutes registers community-related XRPC endpoints on the router 12 // Implements social.coves.community.* lexicon endpoints 13 // allowedCommunityCreators restricts who can create communities. If empty, anyone can create. 14 + func RegisterCommunityRoutes(r chi.Router, service communities.Service, repo communities.Repository, authMiddleware *middleware.OAuthAuthMiddleware, allowedCommunityCreators []string) { 15 // Initialize handlers 16 createHandler := community.NewCreateHandler(service, allowedCommunityCreators) 17 getHandler := community.NewGetHandler(service) 18 updateHandler := community.NewUpdateHandler(service) 19 + listHandler := community.NewListHandler(service, repo) 20 searchHandler := community.NewSearchHandler(service) 21 subscribeHandler := community.NewSubscribeHandler(service) 22 blockHandler := community.NewBlockHandler(service)
+4
internal/core/comments/comment_service_test.go
··· 315 return nil, nil 316 } 317 318 func (m *mockCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) { 319 return nil, nil 320 }
··· 315 return nil, nil 316 } 317 318 + func (m *mockCommunityRepo) GetSubscribedCommunityDIDs(ctx context.Context, userDID string, communityDIDs []string) (map[string]bool, error) { 319 + return map[string]bool{}, nil 320 + } 321 + 322 func (m *mockCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) { 323 return nil, nil 324 }
+15 -2
internal/core/communities/community.go
··· 41 PostCount int `json:"postCount" db:"post_count"` 42 SubscriberCount int `json:"subscriberCount" db:"subscriber_count"` 43 MemberCount int `json:"memberCount" db:"member_count"` 44 - ID int `json:"id" db:"id"` 45 - AllowExternalDiscovery bool `json:"allowExternalDiscovery" db:"allow_external_discovery"` 46 } 47 48 // Subscription represents a lightweight feed follow (user subscribes to see posts)
··· 41 PostCount int `json:"postCount" db:"post_count"` 42 SubscriberCount int `json:"subscriberCount" db:"subscriber_count"` 43 MemberCount int `json:"memberCount" db:"member_count"` 44 + ID int `json:"id" db:"id"` 45 + AllowExternalDiscovery bool `json:"allowExternalDiscovery" db:"allow_external_discovery"` 46 + Viewer *CommunityViewerState `json:"viewer,omitempty" db:"-"` 47 + } 48 + 49 + // CommunityViewerState contains viewer-specific state for community list views. 50 + // This is a simplified version - detailed views use the full viewerState from lexicon. 51 + // 52 + // Fields use *bool to represent three states: 53 + // - nil: State not queried (unauthenticated request) 54 + // - true: User has this relationship 55 + // - false: User does not have this relationship 56 + type CommunityViewerState struct { 57 + Subscribed *bool `json:"subscribed,omitempty"` 58 + Member *bool `json:"member,omitempty"` 59 } 60 61 // Subscription represents a lightweight feed follow (user subscribes to see posts)
+1
internal/core/communities/interfaces.go
··· 32 GetSubscriptionByURI(ctx context.Context, recordURI string) (*Subscription, error) // For Jetstream delete operations 33 ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error) 34 ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*Subscription, error) 35 36 // Community Blocks 37 BlockCommunity(ctx context.Context, block *CommunityBlock) (*CommunityBlock, error)
··· 32 GetSubscriptionByURI(ctx context.Context, recordURI string) (*Subscription, error) // For Jetstream delete operations 33 ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error) 34 ListSubscribers(ctx context.Context, communityDID string, limit, offset int) ([]*Subscription, error) 35 + GetSubscribedCommunityDIDs(ctx context.Context, userDID string, communityDIDs []string) (map[string]bool, error) 36 37 // Community Blocks 38 BlockCommunity(ctx context.Context, block *CommunityBlock) (*CommunityBlock, error)
+48
internal/db/postgres/community_repo_subscriptions.go
··· 344 345 return result, nil 346 }
··· 344 345 return result, nil 346 } 347 + 348 + // GetSubscribedCommunityDIDs returns a map of community DIDs that the user is subscribed to 349 + // This is optimized for batch lookups when populating viewer state 350 + func (r *postgresCommunityRepo) GetSubscribedCommunityDIDs(ctx context.Context, userDID string, communityDIDs []string) (map[string]bool, error) { 351 + if len(communityDIDs) == 0 { 352 + return map[string]bool{}, nil 353 + } 354 + 355 + // Build query with placeholders for IN clause 356 + placeholders := make([]string, len(communityDIDs)) 357 + args := make([]interface{}, len(communityDIDs)+1) 358 + args[0] = userDID 359 + for i, did := range communityDIDs { 360 + placeholders[i] = fmt.Sprintf("$%d", i+2) 361 + args[i+1] = did 362 + } 363 + 364 + query := fmt.Sprintf(` 365 + SELECT community_did 366 + FROM community_subscriptions 367 + WHERE user_did = $1 AND community_did IN (%s)`, 368 + strings.Join(placeholders, ", ")) 369 + 370 + rows, err := r.db.QueryContext(ctx, query, args...) 371 + if err != nil { 372 + return nil, fmt.Errorf("failed to get subscribed communities: %w", err) 373 + } 374 + defer func() { 375 + if closeErr := rows.Close(); closeErr != nil { 376 + log.Printf("Failed to close rows: %v", closeErr) 377 + } 378 + }() 379 + 380 + result := make(map[string]bool) 381 + for rows.Next() { 382 + var communityDID string 383 + if err := rows.Scan(&communityDID); err != nil { 384 + return nil, fmt.Errorf("failed to scan community DID: %w", err) 385 + } 386 + result[communityDID] = true 387 + } 388 + 389 + if err = rows.Err(); err != nil { 390 + return nil, fmt.Errorf("error iterating subscribed communities: %w", err) 391 + } 392 + 393 + return result, nil 394 + }
+1 -1
tests/integration/community_e2e_test.go
··· 164 165 // Setup HTTP server with XRPC routes 166 r := chi.NewRouter() 167 - routes.RegisterCommunityRoutes(r, communityService, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 168 httpServer := httptest.NewServer(r) 169 defer httpServer.Close() 170
··· 164 165 // Setup HTTP server with XRPC routes 166 r := chi.NewRouter() 167 + routes.RegisterCommunityRoutes(r, communityService, communityRepo, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 168 httpServer := httptest.NewServer(r) 169 defer httpServer.Close() 170
+268
tests/integration/community_list_viewer_state_test.go
···
··· 1 + package integration 2 + 3 + import ( 4 + "Coves/internal/api/handlers/community" 5 + "Coves/internal/api/middleware" 6 + "Coves/internal/core/communities" 7 + "Coves/internal/db/postgres" 8 + "context" 9 + "encoding/json" 10 + "fmt" 11 + "net/http" 12 + "net/http/httptest" 13 + "testing" 14 + "time" 15 + 16 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 17 + "github.com/go-chi/chi/v5" 18 + ) 19 + 20 + // TestCommunityList_ViewerState tests that the list communities endpoint 21 + // correctly populates viewer.subscribed field for authenticated users 22 + func TestCommunityList_ViewerState(t *testing.T) { 23 + db := setupTestDB(t) 24 + defer func() { 25 + if err := db.Close(); err != nil { 26 + t.Logf("Failed to close database: %v", err) 27 + } 28 + }() 29 + 30 + repo := postgres.NewCommunityRepository(db) 31 + ctx := context.Background() 32 + 33 + // Create test communities 34 + baseSuffix := time.Now().UnixNano() 35 + communityDIDs := make([]string, 3) 36 + for i := 0; i < 3; i++ { 37 + uniqueSuffix := fmt.Sprintf("%d%d", baseSuffix, i) 38 + communityDID := generateTestDID(uniqueSuffix) 39 + communityDIDs[i] = communityDID 40 + comm := &communities.Community{ 41 + DID: communityDID, 42 + Handle: fmt.Sprintf("c-viewer-test-%d-%d.coves.local", baseSuffix, i), 43 + Name: fmt.Sprintf("viewer-test-%d", i), 44 + DisplayName: fmt.Sprintf("Viewer Test Community %d", i), 45 + OwnerDID: "did:web:coves.local", 46 + CreatedByDID: "did:plc:testcreator", 47 + HostedByDID: "did:web:coves.local", 48 + Visibility: "public", 49 + CreatedAt: time.Now(), 50 + UpdatedAt: time.Now(), 51 + } 52 + if _, err := repo.Create(ctx, comm); err != nil { 53 + t.Fatalf("Failed to create community %d: %v", i, err) 54 + } 55 + } 56 + 57 + // Create a test user and subscribe them to community 0 and 2 58 + testUserDID := fmt.Sprintf("did:plc:viewertestuser%d", baseSuffix) 59 + 60 + sub1 := &communities.Subscription{ 61 + UserDID: testUserDID, 62 + CommunityDID: communityDIDs[0], 63 + ContentVisibility: 3, 64 + SubscribedAt: time.Now(), 65 + } 66 + if _, err := repo.Subscribe(ctx, sub1); err != nil { 67 + t.Fatalf("Failed to subscribe to community 0: %v", err) 68 + } 69 + 70 + sub2 := &communities.Subscription{ 71 + UserDID: testUserDID, 72 + CommunityDID: communityDIDs[2], 73 + ContentVisibility: 3, 74 + SubscribedAt: time.Now(), 75 + } 76 + if _, err := repo.Subscribe(ctx, sub2); err != nil { 77 + t.Fatalf("Failed to subscribe to community 2: %v", err) 78 + } 79 + 80 + // Create mock service that returns our communities 81 + mockService := &mockCommunityService{ 82 + repo: repo, 83 + } 84 + 85 + // Create handler with real repo for viewer state population 86 + listHandler := community.NewListHandler(mockService, repo) 87 + 88 + t.Run("authenticated user sees viewer.subscribed correctly", func(t *testing.T) { 89 + // Setup router with middleware that injects user DID 90 + r := chi.NewRouter() 91 + 92 + // Use test middleware that sets user DID in context 93 + r.Use(func(next http.Handler) http.Handler { 94 + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 95 + ctx := middleware.SetTestUserDID(req.Context(), testUserDID) 96 + next.ServeHTTP(w, req.WithContext(ctx)) 97 + }) 98 + }) 99 + r.Get("/xrpc/social.coves.community.list", listHandler.HandleList) 100 + 101 + req := httptest.NewRequest("GET", "/xrpc/social.coves.community.list?limit=50", nil) 102 + rec := httptest.NewRecorder() 103 + 104 + r.ServeHTTP(rec, req) 105 + 106 + if rec.Code != http.StatusOK { 107 + t.Fatalf("Expected status 200, got %d: %s", rec.Code, rec.Body.String()) 108 + } 109 + 110 + var response struct { 111 + Communities []struct { 112 + DID string `json:"did"` 113 + Viewer *struct { 114 + Subscribed *bool `json:"subscribed"` 115 + } `json:"viewer"` 116 + } `json:"communities"` 117 + } 118 + 119 + if err := json.NewDecoder(rec.Body).Decode(&response); err != nil { 120 + t.Fatalf("Failed to decode response: %v", err) 121 + } 122 + 123 + // Check that viewer state is populated correctly 124 + subscriptionMap := map[string]bool{ 125 + communityDIDs[0]: true, 126 + communityDIDs[1]: false, 127 + communityDIDs[2]: true, 128 + } 129 + 130 + for _, comm := range response.Communities { 131 + expectedSubscribed, inTestSet := subscriptionMap[comm.DID] 132 + if !inTestSet { 133 + continue // Skip communities not in our test set 134 + } 135 + 136 + if comm.Viewer == nil { 137 + t.Errorf("Community %s has nil Viewer, expected populated", comm.DID) 138 + continue 139 + } 140 + 141 + if comm.Viewer.Subscribed == nil { 142 + t.Errorf("Community %s has nil Viewer.Subscribed, expected populated", comm.DID) 143 + continue 144 + } 145 + 146 + if *comm.Viewer.Subscribed != expectedSubscribed { 147 + t.Errorf("Community %s: expected subscribed=%v, got %v", 148 + comm.DID, expectedSubscribed, *comm.Viewer.Subscribed) 149 + } 150 + } 151 + }) 152 + 153 + t.Run("unauthenticated request has nil viewer state", func(t *testing.T) { 154 + // Setup router WITHOUT middleware that sets user DID 155 + r := chi.NewRouter() 156 + r.Get("/xrpc/social.coves.community.list", listHandler.HandleList) 157 + 158 + req := httptest.NewRequest("GET", "/xrpc/social.coves.community.list?limit=50", nil) 159 + rec := httptest.NewRecorder() 160 + 161 + r.ServeHTTP(rec, req) 162 + 163 + if rec.Code != http.StatusOK { 164 + t.Fatalf("Expected status 200, got %d: %s", rec.Code, rec.Body.String()) 165 + } 166 + 167 + var response struct { 168 + Communities []struct { 169 + DID string `json:"did"` 170 + Viewer *struct { 171 + Subscribed *bool `json:"subscribed"` 172 + } `json:"viewer"` 173 + } `json:"communities"` 174 + } 175 + 176 + if err := json.NewDecoder(rec.Body).Decode(&response); err != nil { 177 + t.Fatalf("Failed to decode response: %v", err) 178 + } 179 + 180 + // For unauthenticated requests, viewer should be nil for all communities 181 + for _, comm := range response.Communities { 182 + if comm.Viewer != nil { 183 + t.Errorf("Community %s has non-nil Viewer for unauthenticated request", comm.DID) 184 + } 185 + } 186 + }) 187 + } 188 + 189 + // mockCommunityService implements communities.Service for testing 190 + type mockCommunityService struct { 191 + repo communities.Repository 192 + } 193 + 194 + func (m *mockCommunityService) CreateCommunity(ctx context.Context, req communities.CreateCommunityRequest) (*communities.Community, error) { 195 + return nil, fmt.Errorf("not implemented") 196 + } 197 + 198 + func (m *mockCommunityService) GetCommunity(ctx context.Context, identifier string) (*communities.Community, error) { 199 + return nil, fmt.Errorf("not implemented") 200 + } 201 + 202 + func (m *mockCommunityService) UpdateCommunity(ctx context.Context, req communities.UpdateCommunityRequest) (*communities.Community, error) { 203 + return nil, fmt.Errorf("not implemented") 204 + } 205 + 206 + func (m *mockCommunityService) ListCommunities(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, error) { 207 + return m.repo.List(ctx, req) 208 + } 209 + 210 + func (m *mockCommunityService) SearchCommunities(ctx context.Context, req communities.SearchCommunitiesRequest) ([]*communities.Community, int, error) { 211 + return nil, 0, fmt.Errorf("not implemented") 212 + } 213 + 214 + func (m *mockCommunityService) SubscribeToCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 215 + return nil, fmt.Errorf("not implemented") 216 + } 217 + 218 + func (m *mockCommunityService) UnsubscribeFromCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 219 + return fmt.Errorf("not implemented") 220 + } 221 + 222 + func (m *mockCommunityService) GetUserSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*communities.Subscription, error) { 223 + return nil, fmt.Errorf("not implemented") 224 + } 225 + 226 + func (m *mockCommunityService) GetCommunitySubscribers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*communities.Subscription, error) { 227 + return nil, fmt.Errorf("not implemented") 228 + } 229 + 230 + func (m *mockCommunityService) BlockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) (*communities.CommunityBlock, error) { 231 + return nil, fmt.Errorf("not implemented") 232 + } 233 + 234 + func (m *mockCommunityService) UnblockCommunity(ctx context.Context, session *oauth.ClientSessionData, communityIdentifier string) error { 235 + return fmt.Errorf("not implemented") 236 + } 237 + 238 + func (m *mockCommunityService) GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*communities.CommunityBlock, error) { 239 + return nil, fmt.Errorf("not implemented") 240 + } 241 + 242 + func (m *mockCommunityService) IsBlocked(ctx context.Context, userDID, communityIdentifier string) (bool, error) { 243 + return false, fmt.Errorf("not implemented") 244 + } 245 + 246 + func (m *mockCommunityService) GetMembership(ctx context.Context, userDID, communityIdentifier string) (*communities.Membership, error) { 247 + return nil, fmt.Errorf("not implemented") 248 + } 249 + 250 + func (m *mockCommunityService) ListCommunityMembers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*communities.Membership, error) { 251 + return nil, fmt.Errorf("not implemented") 252 + } 253 + 254 + func (m *mockCommunityService) ValidateHandle(handle string) error { 255 + return nil 256 + } 257 + 258 + func (m *mockCommunityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) { 259 + return identifier, nil 260 + } 261 + 262 + func (m *mockCommunityService) EnsureFreshToken(ctx context.Context, community *communities.Community) (*communities.Community, error) { 263 + return community, nil 264 + } 265 + 266 + func (m *mockCommunityService) GetByDID(ctx context.Context, did string) (*communities.Community, error) { 267 + return m.repo.GetByDID(ctx, did) 268 + }
+117
tests/integration/community_repo_test.go
··· 409 }) 410 } 411 412 // TODO: Implement search functionality before re-enabling this test 413 // func TestCommunityRepository_Search(t *testing.T) { 414 // db := setupTestDB(t)
··· 409 }) 410 } 411 412 + func TestCommunityRepository_GetSubscribedCommunityDIDs(t *testing.T) { 413 + db := setupTestDB(t) 414 + defer func() { 415 + if err := db.Close(); err != nil { 416 + t.Logf("Failed to close database: %v", err) 417 + } 418 + }() 419 + 420 + repo := postgres.NewCommunityRepository(db) 421 + ctx := context.Background() 422 + 423 + // Create test communities 424 + baseSuffix := time.Now().UnixNano() 425 + communityDIDs := make([]string, 3) 426 + for i := 0; i < 3; i++ { 427 + uniqueSuffix := fmt.Sprintf("%d%d", baseSuffix, i) 428 + communityDID := generateTestDID(uniqueSuffix) 429 + communityDIDs[i] = communityDID 430 + community := &communities.Community{ 431 + DID: communityDID, 432 + Handle: fmt.Sprintf("!batch-sub-test-%d-%d@coves.local", baseSuffix, i), 433 + Name: fmt.Sprintf("batch-sub-test-%d", i), 434 + OwnerDID: "did:web:coves.local", 435 + CreatedByDID: "did:plc:user123", 436 + HostedByDID: "did:web:coves.local", 437 + Visibility: "public", 438 + CreatedAt: time.Now(), 439 + UpdatedAt: time.Now(), 440 + } 441 + if _, err := repo.Create(ctx, community); err != nil { 442 + t.Fatalf("Failed to create community %d: %v", i, err) 443 + } 444 + } 445 + 446 + userDID := fmt.Sprintf("did:plc:batchsubuser%d", baseSuffix) 447 + 448 + t.Run("returns empty map when user has no subscriptions", func(t *testing.T) { 449 + result, err := repo.GetSubscribedCommunityDIDs(ctx, userDID, communityDIDs) 450 + if err != nil { 451 + t.Fatalf("Failed to get subscribed community DIDs: %v", err) 452 + } 453 + 454 + if len(result) != 0 { 455 + t.Errorf("Expected empty map, got %d entries", len(result)) 456 + } 457 + }) 458 + 459 + t.Run("returns subscribed communities only", func(t *testing.T) { 460 + // Subscribe to first and third community 461 + sub1 := &communities.Subscription{ 462 + UserDID: userDID, 463 + CommunityDID: communityDIDs[0], 464 + ContentVisibility: 3, 465 + SubscribedAt: time.Now(), 466 + } 467 + if _, err := repo.Subscribe(ctx, sub1); err != nil { 468 + t.Fatalf("Failed to subscribe to community 0: %v", err) 469 + } 470 + 471 + sub3 := &communities.Subscription{ 472 + UserDID: userDID, 473 + CommunityDID: communityDIDs[2], 474 + ContentVisibility: 3, 475 + SubscribedAt: time.Now(), 476 + } 477 + if _, err := repo.Subscribe(ctx, sub3); err != nil { 478 + t.Fatalf("Failed to subscribe to community 2: %v", err) 479 + } 480 + 481 + result, err := repo.GetSubscribedCommunityDIDs(ctx, userDID, communityDIDs) 482 + if err != nil { 483 + t.Fatalf("Failed to get subscribed community DIDs: %v", err) 484 + } 485 + 486 + if len(result) != 2 { 487 + t.Errorf("Expected 2 subscribed communities, got %d", len(result)) 488 + } 489 + 490 + if !result[communityDIDs[0]] { 491 + t.Errorf("Expected community 0 to be subscribed") 492 + } 493 + if result[communityDIDs[1]] { 494 + t.Errorf("Expected community 1 to NOT be subscribed") 495 + } 496 + if !result[communityDIDs[2]] { 497 + t.Errorf("Expected community 2 to be subscribed") 498 + } 499 + }) 500 + 501 + t.Run("returns empty map for empty community DIDs slice", func(t *testing.T) { 502 + result, err := repo.GetSubscribedCommunityDIDs(ctx, userDID, []string{}) 503 + if err != nil { 504 + t.Fatalf("Failed to get subscribed community DIDs: %v", err) 505 + } 506 + 507 + if len(result) != 0 { 508 + t.Errorf("Expected empty map for empty input, got %d entries", len(result)) 509 + } 510 + }) 511 + 512 + t.Run("handles non-existent community DIDs gracefully", func(t *testing.T) { 513 + nonExistentDIDs := []string{ 514 + "did:plc:nonexistent1", 515 + "did:plc:nonexistent2", 516 + } 517 + 518 + result, err := repo.GetSubscribedCommunityDIDs(ctx, userDID, nonExistentDIDs) 519 + if err != nil { 520 + t.Fatalf("Failed to get subscribed community DIDs: %v", err) 521 + } 522 + 523 + if len(result) != 0 { 524 + t.Errorf("Expected empty map for non-existent DIDs, got %d entries", len(result)) 525 + } 526 + }) 527 + } 528 + 529 // TODO: Implement search functionality before re-enabling this test 530 // func TestCommunityRepository_Search(t *testing.T) { 531 // db := setupTestDB(t)
+1 -1
tests/integration/user_journey_e2e_test.go
··· 141 // Setup HTTP server with all routes using OAuth middleware 142 e2eAuth := NewE2EOAuthMiddleware() 143 r := chi.NewRouter() 144 - routes.RegisterCommunityRoutes(r, communityService, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 145 routes.RegisterPostRoutes(r, postService, e2eAuth.OAuthAuthMiddleware) 146 routes.RegisterTimelineRoutes(r, timelineService, nil, nil, e2eAuth.OAuthAuthMiddleware) 147 httpServer := httptest.NewServer(r)
··· 141 // Setup HTTP server with all routes using OAuth middleware 142 e2eAuth := NewE2EOAuthMiddleware() 143 r := chi.NewRouter() 144 + routes.RegisterCommunityRoutes(r, communityService, communityRepo, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 145 routes.RegisterPostRoutes(r, postService, e2eAuth.OAuthAuthMiddleware) 146 routes.RegisterTimelineRoutes(r, timelineService, nil, nil, e2eAuth.OAuthAuthMiddleware) 147 httpServer := httptest.NewServer(r)