diff --git a/server/internal/handlers/user_handlers.go b/server/internal/handlers/user_handlers.go index 0249327..4a0a7d9 100644 --- a/server/internal/handlers/user_handlers.go +++ b/server/internal/handlers/user_handlers.go @@ -23,24 +23,6 @@ type DeleteAccountRequest struct { Password string `json:"password"` } -// GetUser returns the current user's profile -func (h *Handler) GetUser() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, ok := context.GetRequestContext(w, r) - if !ok { - return - } - - user, err := h.DB.GetUserByID(ctx.UserID) - if err != nil { - http.Error(w, "Failed to get user", http.StatusInternalServerError) - return - } - - respondJSON(w, user) - } -} - // UpdateProfile updates the current user's profile func (h *Handler) UpdateProfile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -62,18 +44,6 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { return } - // Start transaction for atomic updates - tx, err := h.DB.Begin() - if err != nil { - http.Error(w, "Failed to start transaction", http.StatusInternalServerError) - return - } - defer func() { - if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { - http.Error(w, "Failed to rollback transaction", http.StatusInternalServerError) - } - }() - // Handle password update if requested if req.NewPassword != "" { // Current password must be provided to change password @@ -139,11 +109,6 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { return } - if err := tx.Commit(); err != nil { - http.Error(w, "Failed to commit changes", http.StatusInternalServerError) - return - } - // Return updated user data respondJSON(w, user) } diff --git a/server/internal/handlers/user_handlers_integration_test.go b/server/internal/handlers/user_handlers_integration_test.go new file mode 100644 index 0000000..bfcc38e --- /dev/null +++ b/server/internal/handlers/user_handlers_integration_test.go @@ -0,0 +1,212 @@ +//go:build integration + +package handlers_test + +import ( + "encoding/json" + "net/http" + "testing" + + "novamd/internal/handlers" + "novamd/internal/models" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUserHandlers_Integration(t *testing.T) { + h := setupTestHarness(t) + defer h.teardown(t) + + t.Run("get current user", func(t *testing.T) { + t.Run("successful get", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var user models.User + err := json.NewDecoder(rr.Body).Decode(&user) + require.NoError(t, err) + + assert.Equal(t, h.RegularUser.ID, user.ID) + assert.Equal(t, h.RegularUser.Email, user.Email) + assert.Equal(t, h.RegularUser.DisplayName, user.DisplayName) + assert.Equal(t, h.RegularUser.Role, user.Role) + assert.Empty(t, user.PasswordHash, "Password hash should not be included in response") + }) + + t.Run("unauthorized", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, "/api/v1/auth/me", nil, "", nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + }) + + t.Run("update profile", func(t *testing.T) { + t.Run("update display name only", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + DisplayName: "Updated Name", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var user models.User + err := json.NewDecoder(rr.Body).Decode(&user) + require.NoError(t, err) + assert.Equal(t, updateReq.DisplayName, user.DisplayName) + }) + + t.Run("update email", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + Email: "newemail@test.com", + CurrentPassword: "user123", // Regular user's password from test harness + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var user models.User + err := json.NewDecoder(rr.Body).Decode(&user) + require.NoError(t, err) + assert.Equal(t, updateReq.Email, user.Email) + }) + + t.Run("update email without password", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + Email: "anotheremail@test.com", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("update email with wrong password", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + Email: "wrongpass@test.com", + CurrentPassword: "wrongpassword", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("update password", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + CurrentPassword: "user123", + NewPassword: "newpassword123", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + // Verify can login with new password + loginReq := handlers.LoginRequest{ + Email: h.RegularUser.Email, + Password: "newpassword123", + } + + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + assert.Equal(t, http.StatusOK, rr.Code) + }) + + t.Run("update password without current password", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + NewPassword: "newpass123", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("update password with wrong current password", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + CurrentPassword: "wrongpassword", + NewPassword: "newpass123", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("update with short password", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + CurrentPassword: "user123", + NewPassword: "short", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("duplicate email", func(t *testing.T) { + updateReq := handlers.UpdateProfileRequest{ + Email: h.AdminUser.Email, + CurrentPassword: "user123", + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/profile", updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusConflict, rr.Code) + }) + }) + + t.Run("delete account", func(t *testing.T) { + // Create a new user that we can delete + createReq := handlers.CreateUserRequest{ + Email: "todelete@test.com", + DisplayName: "To Delete", + Password: "password123", + Role: models.RoleEditor, + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var newUser models.User + err := json.NewDecoder(rr.Body).Decode(&newUser) + require.NoError(t, err) + + // Get token for new user + loginReq := handlers.LoginRequest{ + Email: createReq.Email, + Password: createReq.Password, + } + + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + require.Equal(t, http.StatusOK, rr.Code) + + var loginResp handlers.LoginResponse + err = json.NewDecoder(rr.Body).Decode(&loginResp) + require.NoError(t, err) + userToken := loginResp.AccessToken + + t.Run("successful delete", func(t *testing.T) { + deleteReq := handlers.DeleteAccountRequest{ + Password: createReq.Password, + } + + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, userToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + // Verify user is deleted + rr = h.makeRequest(t, http.MethodPost, "/api/v1/auth/login", loginReq, "", nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("delete with wrong password", func(t *testing.T) { + deleteReq := handlers.DeleteAccountRequest{ + Password: "wrongpassword", + } + + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, h.RegularToken, nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("prevent last admin deletion", func(t *testing.T) { + deleteReq := handlers.DeleteAccountRequest{ + Password: "admin123", // Admin password from test harness + } + + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/profile", deleteReq, h.AdminToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) + }) +}