A community based topic aggregation platform built on atproto
1package user
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "strings"
9 "testing"
10
11 "Coves/internal/api/middleware"
12 "Coves/internal/core/users"
13
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/mock"
16)
17
18// MockUserService is a mock implementation of users.UserService
19type MockUserService struct {
20 mock.Mock
21}
22
23func (m *MockUserService) CreateUser(ctx context.Context, req users.CreateUserRequest) (*users.User, error) {
24 args := m.Called(ctx, req)
25 if args.Get(0) == nil {
26 return nil, args.Error(1)
27 }
28 return args.Get(0).(*users.User), args.Error(1)
29}
30
31func (m *MockUserService) GetUserByDID(ctx context.Context, did string) (*users.User, error) {
32 args := m.Called(ctx, did)
33 if args.Get(0) == nil {
34 return nil, args.Error(1)
35 }
36 return args.Get(0).(*users.User), args.Error(1)
37}
38
39func (m *MockUserService) GetUserByHandle(ctx context.Context, handle string) (*users.User, error) {
40 args := m.Called(ctx, handle)
41 if args.Get(0) == nil {
42 return nil, args.Error(1)
43 }
44 return args.Get(0).(*users.User), args.Error(1)
45}
46
47func (m *MockUserService) UpdateHandle(ctx context.Context, did, newHandle string) (*users.User, error) {
48 args := m.Called(ctx, did, newHandle)
49 if args.Get(0) == nil {
50 return nil, args.Error(1)
51 }
52 return args.Get(0).(*users.User), args.Error(1)
53}
54
55func (m *MockUserService) ResolveHandleToDID(ctx context.Context, handle string) (string, error) {
56 args := m.Called(ctx, handle)
57 return args.String(0), args.Error(1)
58}
59
60func (m *MockUserService) RegisterAccount(ctx context.Context, req users.RegisterAccountRequest) (*users.RegisterAccountResponse, error) {
61 args := m.Called(ctx, req)
62 if args.Get(0) == nil {
63 return nil, args.Error(1)
64 }
65 return args.Get(0).(*users.RegisterAccountResponse), args.Error(1)
66}
67
68func (m *MockUserService) IndexUser(ctx context.Context, did, handle, pdsURL string) error {
69 args := m.Called(ctx, did, handle, pdsURL)
70 return args.Error(0)
71}
72
73func (m *MockUserService) GetProfile(ctx context.Context, did string) (*users.ProfileViewDetailed, error) {
74 args := m.Called(ctx, did)
75 if args.Get(0) == nil {
76 return nil, args.Error(1)
77 }
78 return args.Get(0).(*users.ProfileViewDetailed), args.Error(1)
79}
80
81func (m *MockUserService) DeleteAccount(ctx context.Context, did string) error {
82 args := m.Called(ctx, did)
83 return args.Error(0)
84}
85
86func (m *MockUserService) UpdateProfile(ctx context.Context, did string, input users.UpdateProfileInput) (*users.User, error) {
87 args := m.Called(ctx, did, input)
88 if args.Get(0) == nil {
89 return nil, args.Error(1)
90 }
91 return args.Get(0).(*users.User), args.Error(1)
92}
93
94// TestDeleteAccountHandler_Success tests successful account deletion via XRPC
95// Uses the actual production handler with middleware context injection
96func TestDeleteAccountHandler_Success(t *testing.T) {
97 mockService := new(MockUserService)
98 handler := NewDeleteHandler(mockService)
99
100 testDID := "did:plc:testdelete123"
101 mockService.On("DeleteAccount", mock.Anything, testDID).Return(nil)
102
103 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
104 // Use middleware context injection instead of X-User-DID header
105 ctx := middleware.SetTestUserDID(req.Context(), testDID)
106 req = req.WithContext(ctx)
107
108 w := httptest.NewRecorder()
109 handler.HandleDeleteAccount(w, req)
110
111 assert.Equal(t, http.StatusOK, w.Code)
112 assert.Contains(t, w.Body.String(), `"success":true`)
113 assert.Contains(t, w.Body.String(), "atProto identity remains intact")
114
115 mockService.AssertExpectations(t)
116}
117
118// TestDeleteAccountHandler_Unauthenticated tests deletion without authentication
119func TestDeleteAccountHandler_Unauthenticated(t *testing.T) {
120 mockService := new(MockUserService)
121 handler := NewDeleteHandler(mockService)
122
123 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
124 // No context injection - simulates unauthenticated request
125
126 w := httptest.NewRecorder()
127 handler.HandleDeleteAccount(w, req)
128
129 assert.Equal(t, http.StatusUnauthorized, w.Code)
130 assert.Contains(t, w.Body.String(), "AuthRequired")
131
132 mockService.AssertNotCalled(t, "DeleteAccount", mock.Anything, mock.Anything)
133}
134
135// TestDeleteAccountHandler_UserNotFound tests deletion of non-existent user
136func TestDeleteAccountHandler_UserNotFound(t *testing.T) {
137 mockService := new(MockUserService)
138 handler := NewDeleteHandler(mockService)
139
140 testDID := "did:plc:nonexistent"
141 mockService.On("DeleteAccount", mock.Anything, testDID).Return(users.ErrUserNotFound)
142
143 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
144 ctx := middleware.SetTestUserDID(req.Context(), testDID)
145 req = req.WithContext(ctx)
146
147 w := httptest.NewRecorder()
148 handler.HandleDeleteAccount(w, req)
149
150 assert.Equal(t, http.StatusNotFound, w.Code)
151 assert.Contains(t, w.Body.String(), "AccountNotFound")
152
153 mockService.AssertExpectations(t)
154}
155
156// TestDeleteAccountHandler_MethodNotAllowed tests that only POST is accepted
157func TestDeleteAccountHandler_MethodNotAllowed(t *testing.T) {
158 mockService := new(MockUserService)
159 handler := NewDeleteHandler(mockService)
160
161 methods := []string{http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodPatch}
162
163 for _, method := range methods {
164 t.Run(method, func(t *testing.T) {
165 req := httptest.NewRequest(method, "/xrpc/social.coves.actor.deleteAccount", nil)
166 ctx := middleware.SetTestUserDID(req.Context(), "did:plc:test")
167 req = req.WithContext(ctx)
168
169 w := httptest.NewRecorder()
170 handler.HandleDeleteAccount(w, req)
171
172 assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
173 })
174 }
175
176 mockService.AssertNotCalled(t, "DeleteAccount", mock.Anything, mock.Anything)
177}
178
179// TestDeleteAccountHandler_InternalError tests handling of internal errors
180func TestDeleteAccountHandler_InternalError(t *testing.T) {
181 mockService := new(MockUserService)
182 handler := NewDeleteHandler(mockService)
183
184 testDID := "did:plc:erroruser"
185 mockService.On("DeleteAccount", mock.Anything, testDID).Return(assert.AnError)
186
187 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
188 ctx := middleware.SetTestUserDID(req.Context(), testDID)
189 req = req.WithContext(ctx)
190
191 w := httptest.NewRecorder()
192 handler.HandleDeleteAccount(w, req)
193
194 assert.Equal(t, http.StatusInternalServerError, w.Code)
195 assert.Contains(t, w.Body.String(), "InternalServerError")
196
197 mockService.AssertExpectations(t)
198}
199
200// TestDeleteAccountHandler_ContextTimeout tests handling of context timeout
201func TestDeleteAccountHandler_ContextTimeout(t *testing.T) {
202 mockService := new(MockUserService)
203 handler := NewDeleteHandler(mockService)
204
205 testDID := "did:plc:timeoutuser"
206 mockService.On("DeleteAccount", mock.Anything, testDID).Return(context.DeadlineExceeded)
207
208 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
209 ctx := middleware.SetTestUserDID(req.Context(), testDID)
210 req = req.WithContext(ctx)
211
212 w := httptest.NewRecorder()
213 handler.HandleDeleteAccount(w, req)
214
215 assert.Equal(t, http.StatusGatewayTimeout, w.Code)
216 assert.Contains(t, w.Body.String(), "Timeout")
217
218 mockService.AssertExpectations(t)
219}
220
221// TestDeleteAccountHandler_ContextCanceled tests handling of context cancellation
222func TestDeleteAccountHandler_ContextCanceled(t *testing.T) {
223 mockService := new(MockUserService)
224 handler := NewDeleteHandler(mockService)
225
226 testDID := "did:plc:canceluser"
227 mockService.On("DeleteAccount", mock.Anything, testDID).Return(context.Canceled)
228
229 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
230 ctx := middleware.SetTestUserDID(req.Context(), testDID)
231 req = req.WithContext(ctx)
232
233 w := httptest.NewRecorder()
234 handler.HandleDeleteAccount(w, req)
235
236 assert.Equal(t, http.StatusBadRequest, w.Code)
237 assert.Contains(t, w.Body.String(), "RequestCanceled")
238
239 mockService.AssertExpectations(t)
240}
241
242// TestDeleteAccountHandler_InvalidDID tests handling of invalid DID format
243func TestDeleteAccountHandler_InvalidDID(t *testing.T) {
244 mockService := new(MockUserService)
245 handler := NewDeleteHandler(mockService)
246
247 testDID := "did:plc:invaliddid"
248 mockService.On("DeleteAccount", mock.Anything, testDID).Return(&users.InvalidDIDError{DID: "invalid", Reason: "must start with 'did:'"})
249
250 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
251 ctx := middleware.SetTestUserDID(req.Context(), testDID)
252 req = req.WithContext(ctx)
253
254 w := httptest.NewRecorder()
255 handler.HandleDeleteAccount(w, req)
256
257 assert.Equal(t, http.StatusBadRequest, w.Code)
258 assert.Contains(t, w.Body.String(), "InvalidDID")
259
260 mockService.AssertExpectations(t)
261}
262
263// TestWebDeleteAccount_FormSubmission tests the web form-based deletion flow
264func TestWebDeleteAccount_FormSubmission(t *testing.T) {
265 mockService := new(MockUserService)
266
267 testDID := "did:plc:webdeleteuser"
268 mockService.On("DeleteAccount", mock.Anything, testDID).Return(nil)
269
270 // Simulate form submission
271 form := url.Values{}
272 form.Add("confirm", "true")
273
274 req := httptest.NewRequest(http.MethodPost, "/delete-account", strings.NewReader(form.Encode()))
275 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
276 ctx := middleware.SetTestUserDID(req.Context(), testDID)
277 req = req.WithContext(ctx)
278
279 // The web handler would parse the form and call DeleteAccount
280 // This test verifies the service layer is called correctly
281 err := mockService.DeleteAccount(ctx, testDID)
282 assert.NoError(t, err)
283
284 mockService.AssertExpectations(t)
285}
286
287// TestWebDeleteAccount_MissingConfirmation tests that confirmation is required
288func TestWebDeleteAccount_MissingConfirmation(t *testing.T) {
289 form := url.Values{}
290 // NOT adding confirm=true
291
292 req := httptest.NewRequest(http.MethodPost, "/delete-account", strings.NewReader(form.Encode()))
293 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
294
295 err := req.ParseForm()
296 assert.NoError(t, err)
297 assert.NotEqual(t, "true", req.FormValue("confirm"))
298}
299
300// TestWebDeleteAccount_ConfirmationPresent tests confirmation checkbox validation
301func TestWebDeleteAccount_ConfirmationPresent(t *testing.T) {
302 form := url.Values{}
303 form.Add("confirm", "true")
304
305 req := httptest.NewRequest(http.MethodPost, "/delete-account", strings.NewReader(form.Encode()))
306 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
307
308 err := req.ParseForm()
309 assert.NoError(t, err)
310 assert.Equal(t, "true", req.FormValue("confirm"))
311}
312
313// TestDeleteAccountHandler_DIDWithWhitespace tests DID handling with whitespace
314// The service layer should handle trimming whitespace from DIDs
315func TestDeleteAccountHandler_DIDWithWhitespace(t *testing.T) {
316 mockService := new(MockUserService)
317 handler := NewDeleteHandler(mockService)
318
319 // In reality, the middleware would provide a clean DID from the OAuth session.
320 // This test verifies the handler correctly passes the DID to the service.
321 trimmedDID := "did:plc:whitespaceuser"
322 mockService.On("DeleteAccount", mock.Anything, trimmedDID).Return(nil)
323
324 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
325 ctx := middleware.SetTestUserDID(req.Context(), trimmedDID)
326 req = req.WithContext(ctx)
327
328 w := httptest.NewRecorder()
329 handler.HandleDeleteAccount(w, req)
330
331 assert.Equal(t, http.StatusOK, w.Code)
332 mockService.AssertExpectations(t)
333}
334
335// TestDeleteAccountHandler_ConcurrentRequests tests handling of concurrent deletion attempts
336// Verifies that repeated deletion attempts are handled gracefully
337func TestDeleteAccountHandler_ConcurrentRequests(t *testing.T) {
338 mockService := new(MockUserService)
339 handler := NewDeleteHandler(mockService)
340
341 testDID := "did:plc:concurrentuser"
342
343 // First call succeeds, second call returns not found (already deleted)
344 mockService.On("DeleteAccount", mock.Anything, testDID).Return(nil).Once()
345 mockService.On("DeleteAccount", mock.Anything, testDID).Return(users.ErrUserNotFound).Once()
346
347 // First request
348 req1 := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
349 ctx1 := middleware.SetTestUserDID(req1.Context(), testDID)
350 req1 = req1.WithContext(ctx1)
351
352 w1 := httptest.NewRecorder()
353 handler.HandleDeleteAccount(w1, req1)
354 assert.Equal(t, http.StatusOK, w1.Code)
355
356 // Second request (simulating concurrent attempt that arrives after first completes)
357 req2 := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.deleteAccount", nil)
358 ctx2 := middleware.SetTestUserDID(req2.Context(), testDID)
359 req2 = req2.WithContext(ctx2)
360
361 w2 := httptest.NewRecorder()
362 handler.HandleDeleteAccount(w2, req2)
363 assert.Equal(t, http.StatusNotFound, w2.Code)
364
365 mockService.AssertExpectations(t)
366}