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