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 606 607 607 // Register XRPC routes 608 608 routes.RegisterUserRoutes(r, userService) 609 - routes.RegisterCommunityRoutes(r, communityService, authMiddleware, allowedCommunityCreators) 609 + routes.RegisterCommunityRoutes(r, communityService, communityRepo, authMiddleware, allowedCommunityCreators) 610 610 log.Println("Community XRPC endpoints registered with OAuth authentication") 611 611 612 612 routes.RegisterPostRoutes(r, postService, dualAuth)
+41
internal/api/handlers/common/viewer_state.go
··· 2 2 3 3 import ( 4 4 "Coves/internal/api/middleware" 5 + "Coves/internal/core/communities" 5 6 "Coves/internal/core/posts" 6 7 "Coves/internal/core/votes" 7 8 "context" ··· 71 72 } 72 73 } 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 1 package community 2 2 3 3 import ( 4 + "Coves/internal/api/handlers/common" 4 5 "Coves/internal/core/communities" 5 6 "encoding/json" 6 7 "net/http" ··· 10 11 // ListHandler handles listing communities 11 12 type ListHandler struct { 12 13 service communities.Service 14 + repo communities.Repository 13 15 } 14 16 15 17 // NewListHandler creates a new list handler 16 - func NewListHandler(service communities.Service) *ListHandler { 18 + func NewListHandler(service communities.Service, repo communities.Repository) *ListHandler { 17 19 return &ListHandler{ 18 20 service: service, 21 + repo: repo, 19 22 } 20 23 } 21 24 ··· 99 102 handleServiceError(w, err) 100 103 return 101 104 } 105 + 106 + // Populate viewer state if authenticated 107 + common.PopulateCommunityViewerState(r.Context(), r, h.repo, results) 102 108 103 109 // Build response 104 110 var cursor string
+2 -2
internal/api/routes/community.go
··· 11 11 // RegisterCommunityRoutes registers community-related XRPC endpoints on the router 12 12 // Implements social.coves.community.* lexicon endpoints 13 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) { 14 + func RegisterCommunityRoutes(r chi.Router, service communities.Service, repo communities.Repository, authMiddleware *middleware.OAuthAuthMiddleware, allowedCommunityCreators []string) { 15 15 // Initialize handlers 16 16 createHandler := community.NewCreateHandler(service, allowedCommunityCreators) 17 17 getHandler := community.NewGetHandler(service) 18 18 updateHandler := community.NewUpdateHandler(service) 19 - listHandler := community.NewListHandler(service) 19 + listHandler := community.NewListHandler(service, repo) 20 20 searchHandler := community.NewSearchHandler(service) 21 21 subscribeHandler := community.NewSubscribeHandler(service) 22 22 blockHandler := community.NewBlockHandler(service)
+4
internal/core/comments/comment_service_test.go
··· 315 315 return nil, nil 316 316 } 317 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 + 318 322 func (m *mockCommunityRepo) BlockCommunity(ctx context.Context, block *communities.CommunityBlock) (*communities.CommunityBlock, error) { 319 323 return nil, nil 320 324 }
+15 -2
internal/core/communities/community.go
··· 41 41 PostCount int `json:"postCount" db:"post_count"` 42 42 SubscriberCount int `json:"subscriberCount" db:"subscriber_count"` 43 43 MemberCount int `json:"memberCount" db:"member_count"` 44 - ID int `json:"id" db:"id"` 45 - AllowExternalDiscovery bool `json:"allowExternalDiscovery" db:"allow_external_discovery"` 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"` 46 59 } 47 60 48 61 // Subscription represents a lightweight feed follow (user subscribes to see posts)
+1
internal/core/communities/interfaces.go
··· 32 32 GetSubscriptionByURI(ctx context.Context, recordURI string) (*Subscription, error) // For Jetstream delete operations 33 33 ListSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*Subscription, error) 34 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) 35 36 36 37 // Community Blocks 37 38 BlockCommunity(ctx context.Context, block *CommunityBlock) (*CommunityBlock, error)
+48
internal/db/postgres/community_repo_subscriptions.go
··· 344 344 345 345 return result, nil 346 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 164 165 165 // Setup HTTP server with XRPC routes 166 166 r := chi.NewRouter() 167 - routes.RegisterCommunityRoutes(r, communityService, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 167 + routes.RegisterCommunityRoutes(r, communityService, communityRepo, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 168 168 httpServer := httptest.NewServer(r) 169 169 defer httpServer.Close() 170 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 409 }) 410 410 } 411 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 + 412 529 // TODO: Implement search functionality before re-enabling this test 413 530 // func TestCommunityRepository_Search(t *testing.T) { 414 531 // db := setupTestDB(t)
+1 -1
tests/integration/user_journey_e2e_test.go
··· 141 141 // Setup HTTP server with all routes using OAuth middleware 142 142 e2eAuth := NewE2EOAuthMiddleware() 143 143 r := chi.NewRouter() 144 - routes.RegisterCommunityRoutes(r, communityService, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 144 + routes.RegisterCommunityRoutes(r, communityService, communityRepo, e2eAuth.OAuthAuthMiddleware, nil) // nil = allow all community creators 145 145 routes.RegisterPostRoutes(r, postService, e2eAuth.OAuthAuthMiddleware) 146 146 routes.RegisterTimelineRoutes(r, timelineService, nil, nil, e2eAuth.OAuthAuthMiddleware) 147 147 httpServer := httptest.NewServer(r)