A community based topic aggregation platform built on atproto
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}