A community based topic aggregation platform built on atproto
1package community 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "net/http" 8 "net/http/httptest" 9 "testing" 10 "time" 11 12 "Coves/internal/api/middleware" 13 "Coves/internal/core/communities" 14) 15 16// mockCommunityService implements communities.Service for testing 17type mockCommunityService struct { 18 createFunc func(ctx context.Context, req communities.CreateCommunityRequest) (*communities.Community, error) 19} 20 21func (m *mockCommunityService) CreateCommunity(ctx context.Context, req communities.CreateCommunityRequest) (*communities.Community, error) { 22 if m.createFunc != nil { 23 return m.createFunc(ctx, req) 24 } 25 return &communities.Community{ 26 DID: "did:plc:test123", 27 Handle: "test.community.coves.social", 28 RecordURI: "at://did:plc:test123/social.coves.community.profile/self", 29 RecordCID: "bafytest123", 30 DisplayName: req.DisplayName, 31 Description: req.Description, 32 Visibility: req.Visibility, 33 CreatedAt: time.Now(), 34 }, nil 35} 36 37func (m *mockCommunityService) GetCommunity(ctx context.Context, identifier string) (*communities.Community, error) { 38 return nil, nil 39} 40 41func (m *mockCommunityService) UpdateCommunity(ctx context.Context, req communities.UpdateCommunityRequest) (*communities.Community, error) { 42 return nil, nil 43} 44 45func (m *mockCommunityService) ListCommunities(ctx context.Context, req communities.ListCommunitiesRequest) ([]*communities.Community, int, error) { 46 return nil, 0, nil 47} 48 49func (m *mockCommunityService) SearchCommunities(ctx context.Context, req communities.SearchCommunitiesRequest) ([]*communities.Community, int, error) { 50 return nil, 0, nil 51} 52 53func (m *mockCommunityService) SubscribeToCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string, contentVisibility int) (*communities.Subscription, error) { 54 return nil, nil 55} 56 57func (m *mockCommunityService) UnsubscribeFromCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 58 return nil 59} 60 61func (m *mockCommunityService) GetUserSubscriptions(ctx context.Context, userDID string, limit, offset int) ([]*communities.Subscription, error) { 62 return nil, nil 63} 64 65func (m *mockCommunityService) GetCommunitySubscribers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*communities.Subscription, error) { 66 return nil, nil 67} 68 69func (m *mockCommunityService) BlockCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) (*communities.CommunityBlock, error) { 70 return nil, nil 71} 72 73func (m *mockCommunityService) UnblockCommunity(ctx context.Context, userDID, accessToken, communityIdentifier string) error { 74 return nil 75} 76 77func (m *mockCommunityService) GetBlockedCommunities(ctx context.Context, userDID string, limit, offset int) ([]*communities.CommunityBlock, error) { 78 return nil, nil 79} 80 81func (m *mockCommunityService) IsBlocked(ctx context.Context, userDID, communityIdentifier string) (bool, error) { 82 return false, nil 83} 84 85func (m *mockCommunityService) GetMembership(ctx context.Context, userDID, communityIdentifier string) (*communities.Membership, error) { 86 return nil, nil 87} 88 89func (m *mockCommunityService) ListCommunityMembers(ctx context.Context, communityIdentifier string, limit, offset int) ([]*communities.Membership, error) { 90 return nil, nil 91} 92 93func (m *mockCommunityService) ValidateHandle(handle string) error { 94 return nil 95} 96 97func (m *mockCommunityService) ResolveCommunityIdentifier(ctx context.Context, identifier string) (string, error) { 98 return identifier, nil 99} 100 101func (m *mockCommunityService) EnsureFreshToken(ctx context.Context, community *communities.Community) (*communities.Community, error) { 102 return community, nil 103} 104 105func (m *mockCommunityService) GetByDID(ctx context.Context, did string) (*communities.Community, error) { 106 return nil, nil 107} 108 109func TestCreateHandler_AllowlistRestriction(t *testing.T) { 110 mockService := &mockCommunityService{} 111 112 tests := []struct { 113 name string 114 allowedDIDs []string 115 requestDID string 116 expectedStatus int 117 expectedError string 118 }{ 119 { 120 name: "allowed DID can create community", 121 allowedDIDs: []string{"did:plc:allowed123"}, 122 requestDID: "did:plc:allowed123", 123 expectedStatus: http.StatusOK, 124 }, 125 { 126 name: "non-allowed DID is forbidden", 127 allowedDIDs: []string{"did:plc:allowed123"}, 128 requestDID: "did:plc:notallowed456", 129 expectedStatus: http.StatusForbidden, 130 expectedError: "CommunityCreationRestricted", 131 }, 132 { 133 name: "empty allowlist allows anyone", 134 allowedDIDs: nil, 135 requestDID: "did:plc:anyuser789", 136 expectedStatus: http.StatusOK, 137 }, 138 { 139 name: "multiple allowed DIDs - first DID", 140 allowedDIDs: []string{"did:plc:admin1", "did:plc:admin2", "did:plc:admin3"}, 141 requestDID: "did:plc:admin1", 142 expectedStatus: http.StatusOK, 143 }, 144 { 145 name: "multiple allowed DIDs - last DID", 146 allowedDIDs: []string{"did:plc:admin1", "did:plc:admin2", "did:plc:admin3"}, 147 requestDID: "did:plc:admin3", 148 expectedStatus: http.StatusOK, 149 }, 150 { 151 name: "multiple allowed DIDs - not in list", 152 allowedDIDs: []string{"did:plc:admin1", "did:plc:admin2"}, 153 requestDID: "did:plc:randomuser", 154 expectedStatus: http.StatusForbidden, 155 expectedError: "CommunityCreationRestricted", 156 }, 157 { 158 name: "allowlist with empty strings filtered - valid DID works", 159 allowedDIDs: []string{"did:plc:admin1", "", "did:plc:admin2"}, 160 requestDID: "did:plc:admin1", 161 expectedStatus: http.StatusOK, 162 }, 163 { 164 name: "allowlist with empty strings filtered - invalid DID blocked", 165 allowedDIDs: []string{"did:plc:admin1", "", "did:plc:admin2"}, 166 requestDID: "did:plc:notallowed", 167 expectedStatus: http.StatusForbidden, 168 expectedError: "CommunityCreationRestricted", 169 }, 170 { 171 name: "all empty strings allows anyone", 172 allowedDIDs: []string{"", "", ""}, 173 requestDID: "did:plc:anyuser", 174 expectedStatus: http.StatusOK, 175 }, 176 } 177 178 for _, tc := range tests { 179 t.Run(tc.name, func(t *testing.T) { 180 handler := NewCreateHandler(mockService, tc.allowedDIDs) 181 182 // Create request body 183 reqBody := map[string]interface{}{ 184 "name": "testcommunity", 185 "displayName": "Test Community", 186 "description": "Test description", 187 "visibility": "public", 188 "allowExternalDiscovery": true, 189 } 190 bodyBytes, err := json.Marshal(reqBody) 191 if err != nil { 192 t.Fatalf("Failed to marshal request: %v", err) 193 } 194 195 // Create HTTP request 196 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.create", bytes.NewBuffer(bodyBytes)) 197 req.Header.Set("Content-Type", "application/json") 198 199 // Inject user DID into context (simulates auth middleware) 200 ctx := context.WithValue(req.Context(), middleware.UserDIDKey, tc.requestDID) 201 req = req.WithContext(ctx) 202 203 // Execute handler 204 w := httptest.NewRecorder() 205 handler.HandleCreate(w, req) 206 207 // Check status code 208 if w.Code != tc.expectedStatus { 209 t.Errorf("Expected status %d, got %d. Body: %s", tc.expectedStatus, w.Code, w.Body.String()) 210 } 211 212 // Check error response if expected 213 if tc.expectedError != "" { 214 var errResp struct { 215 Error string `json:"error"` 216 Message string `json:"message"` 217 } 218 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 219 t.Fatalf("Failed to decode error response: %v", err) 220 } 221 if errResp.Error != tc.expectedError { 222 t.Errorf("Expected error %s, got %s", tc.expectedError, errResp.Error) 223 } 224 } 225 }) 226 } 227} 228 229func TestCreateHandler_RequiresAuth(t *testing.T) { 230 mockService := &mockCommunityService{} 231 handler := NewCreateHandler(mockService, nil) 232 233 // Create request without auth context 234 reqBody := map[string]interface{}{ 235 "name": "testcommunity", 236 "displayName": "Test Community", 237 "description": "Test description", 238 "visibility": "public", 239 } 240 bodyBytes, err := json.Marshal(reqBody) 241 if err != nil { 242 t.Fatalf("Failed to marshal request: %v", err) 243 } 244 245 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.create", bytes.NewBuffer(bodyBytes)) 246 req.Header.Set("Content-Type", "application/json") 247 // No user DID in context 248 249 w := httptest.NewRecorder() 250 handler.HandleCreate(w, req) 251 252 if w.Code != http.StatusUnauthorized { 253 t.Errorf("Expected status 401, got %d. Body: %s", w.Code, w.Body.String()) 254 } 255 256 var errResp struct { 257 Error string `json:"error"` 258 } 259 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil { 260 t.Fatalf("Failed to decode error response: %v", err) 261 } 262 if errResp.Error != "AuthRequired" { 263 t.Errorf("Expected error AuthRequired, got %s", errResp.Error) 264 } 265}