A community based topic aggregation platform built on atproto
at main 366 lines 13 kB view raw
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}