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}