package main import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" ) func TestUpdateProfileSelfEdit(t *testing.T) { t.Run("PUT /api/auth/profile - Update all fields successfully", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token updateData := map[string]interface{}{ "username": "updateduser", "email": "updated@tests.bookhoard.internal", "first_name": "Updated", "last_name": "Name", "theme": "dracula", } jsonData, _ := json.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) user, err := setup.DB.GetUserByUsername(context.Background(), "updateduser") require.NoError(t, err) assert.Equal(t, "updated@tests.bookhoard.internal", user.Email) assert.Equal(t, "Updated", user.FirstName.String) assert.Equal(t, "Name", user.LastName.String) assert.Equal(t, "dracula", user.Theme.String) }) t.Run("PUT /api/auth/profile - Update username conflict", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) regularUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password) updateData := map[string]interface{}{ "username": "testuser", } jsonData, _ := json.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusConflict, rec.Code) assert.Contains(t, rec.Body.String(), "username already taken") }) t.Run("PUT /api/auth/profile - Update email conflict", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) regularUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password) updateData := map[string]interface{}{ "email": "testuser@tests.bookhoard.internal", } jsonData, _ := json.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusConflict, rec.Code) assert.Contains(t, rec.Body.String(), "email already taken") }) t.Run("PUT /api/auth/profile - Regular user tries to set role", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) regularUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password) updateData := map[string]interface{}{ "role": "admin", } jsonData, _ := json.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) user, err := setup.DB.GetUser(context.Background(), uuidToPGType(regularUser.ID)) require.NoError(t, err) assert.Equal(t, "user", user.Role) }) t.Run("PUT /api/auth/profile - Partial update (first name only)", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token updateData := map[string]interface{}{ "first_name": "OnlyFirstName", } jsonData, _ := json.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) }) t.Run("PUT /api/auth/profile - No auth", func(t *testing.T) { setup := setupTestServer(t) updateData := map[string]interface{}{ "first_name": "NoAuth", } jsonData, _ := json.Marshal(updateData) req := httptest.NewRequest("PUT", "/api/auth/profile", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) } func TestUpdateProfileAdminMode(t *testing.T) { t.Run("PUT /api/auth/profile/:id - Admin update username", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) newUsername := "updateduser-admin-test" jsonData, _ := json.Marshal(map[string]interface{}{ "username": newUsername, }) req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetUser.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) updatedUser, err := setup.DB.GetUserByUsername(context.Background(), newUsername) require.NoError(t, err) assert.Equal(t, newUsername, updatedUser.Username) }) t.Run("PUT /api/auth/profile/:id - Admin promote user to admin", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) jsonData, _ := json.Marshal(map[string]interface{}{ "role": "admin", }) req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetUser.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) updatedUser, err := setup.DB.GetUser(context.Background(), uuidToPGType(targetUser.ID)) require.NoError(t, err) assert.Equal(t, "admin", updatedUser.Role) }) t.Run("PUT /api/auth/profile/:id - Try to demote last admin", func(t *testing.T) { setup := setupTestServer(t) // Delete the regular user so we have only 1 admin (testuser) regularUser := createRegularUserOnce(t, setup.DB) err := setup.DB.DeleteUser(context.Background(), uuidToPGType(regularUser.ID)) require.NoError(t, err, "Failed to delete regular user") // Verify we have only 1 admin users, _ := setup.DB.ListUsers(context.Background()) adminCount := 0 for _, u := range users { if u.Role == "admin" { adminCount++ } } require.Equal(t, 1, adminCount, "Expected exactly 1 admin after deleting regular user") // Now try to demote the only remaining admin lastAdmin := createTestUserOnce(t, setup.DB) token := setup.Token jsonData, _ := json.Marshal(map[string]interface{}{ "role": "user", }) req := httptest.NewRequest("PUT", "/api/auth/profile/"+lastAdmin.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "cannot demote the last admin") }) t.Run("PUT /api/auth/profile/:id - Non-admin tries update", func(t *testing.T) { setup := setupTestServer(t) targetAdmin := createTestUserOnce(t, setup.DB) regularUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password) jsonData, _ := json.Marshal(map[string]interface{}{ "username": "hacked", }) req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetAdmin.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("PUT /api/auth/profile/:id - Invalid role", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) jsonData, _ := json.Marshal(map[string]interface{}{ "role": "superadmin", }) req := httptest.NewRequest("PUT", "/api/auth/profile/"+targetUser.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "oneof") }) } func TestUpdatePasswordAdminMode(t *testing.T) { t.Run("PUT /api/auth/password/:id - Admin reset password", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) newPassword := "NewPassword123!" jsonData, _ := json.Marshal(map[string]interface{}{ "new_password": newPassword, "confirm_password": newPassword, }) req := httptest.NewRequest("PUT", "/api/auth/password/"+targetUser.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) loginData, _ := json.Marshal(map[string]interface{}{ "login": targetUser.Username, "password": newPassword, }) loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(loginData)) loginReq.Header.Set("Content-Type", "application/json") loginRec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(loginRec, loginReq) assert.Equal(t, http.StatusOK, loginRec.Code) }) t.Run("PUT /api/auth/password/:id - Non-admin tries reset", func(t *testing.T) { setup := setupTestServer(t) targetAdmin := createTestUserOnce(t, setup.DB) regularUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password) jsonData, _ := json.Marshal(map[string]interface{}{ "new_password": "hacked", "confirm_password": "hacked", }) req := httptest.NewRequest("PUT", "/api/auth/password/"+targetAdmin.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("PUT /api/auth/password/:id - Mismatched passwords", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) jsonData, _ := json.Marshal(map[string]interface{}{ "new_password": "NewPassword123!", "confirm_password": "DifferentPass123!", }) req := httptest.NewRequest("PUT", "/api/auth/password/"+targetUser.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "passwords do not match") }) t.Run("PUT /api/auth/password/:id - Invalid password format", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) jsonData, _ := json.Marshal(map[string]interface{}{ "new_password": "123", "confirm_password": "123", }) req := httptest.NewRequest("PUT", "/api/auth/password/"+targetUser.ID.String(), bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) }) } func TestDeleteUserConsolidated(t *testing.T) { t.Run("DELETE /api/auth/profile - User deletes self successfully", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) selfDeletingUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, selfDeletingUser.Email, selfDeletingUser.Password) req := httptest.NewRequest("DELETE", "/api/auth/profile", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) _, err := setup.DB.GetUser(context.Background(), uuidToPGType(selfDeletingUser.ID)) assert.Error(t, err) }) t.Run("DELETE /api/auth/profile - Delete without auth", func(t *testing.T) { setup := setupTestServer(t) req := httptest.NewRequest("DELETE", "/api/auth/profile", nil) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) }) t.Run("DELETE /api/auth/profile - Last admin tries self-delete", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token req := httptest.NewRequest("DELETE", "/api/auth/profile", nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "cannot delete the last admin") }) t.Run("DELETE /api/auth/profile/:id - Admin deletes user", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token targetUser := createRegularUserOnce(t, setup.DB) req := httptest.NewRequest("DELETE", "/api/auth/profile/"+targetUser.ID.String(), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) _, err := setup.DB.GetUser(context.Background(), uuidToPGType(targetUser.ID)) assert.Error(t, err) }) t.Run("DELETE /api/auth/profile/:id - Admin tries delete last admin", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token users, err := setup.DB.ListUsers(context.Background()) require.NoError(t, err) var lastAdminID string for _, u := range users { if u.Role == "admin" { lastAdminID = uuid.UUID(u.ID.Bytes).String() break } } req := httptest.NewRequest("DELETE", "/api/auth/profile/"+lastAdminID, nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "cannot delete the last admin") }) t.Run("DELETE /api/auth/profile/:id - Non-admin tries delete", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) regularUser := createRegularUserOnce(t, setup.DB) token := loginUserWithCredentials(t, setup.Server, regularUser.Email, regularUser.Password) users, err := setup.DB.ListUsers(context.Background()) require.NoError(t, err) var adminID string for _, u := range users { if u.Role == "admin" { adminID = uuid.UUID(u.ID.Bytes).String() break } } req := httptest.NewRequest("DELETE", "/api/auth/profile/"+adminID, nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code) }) t.Run("DELETE /api/auth/profile/:id - Delete non-existent user", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token fakeUUID := uuid.New() req := httptest.NewRequest("DELETE", "/api/auth/profile/"+fakeUUID.String(), nil) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusNotFound, rec.Code) }) } func TestUpdatePasswordSelfChange(t *testing.T) { t.Run("PUT /api/auth/password - Successful password change", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token newPassword := "NewTest@Pass123!" jsonData, _ := json.Marshal(map[string]interface{}{ "current_password": "Test@Pass123!", "new_password": newPassword, "confirm_password": newPassword, }) req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) loginData, _ := json.Marshal(map[string]interface{}{ "login": "testuser", "password": newPassword, }) loginReq := httptest.NewRequest("POST", "/api/auth/login", bytes.NewBuffer(loginData)) loginReq.Header.Set("Content-Type", "application/json") loginRec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(loginRec, loginReq) assert.Equal(t, http.StatusOK, loginRec.Code) }) t.Run("PUT /api/auth/password - Wrong current password", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token jsonData, _ := json.Marshal(map[string]interface{}{ "current_password": "wrongpassword", "new_password": "NewTest@Pass123!", "confirm_password": "NewTest@Pass123!", }) req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusUnauthorized, rec.Code) assert.Contains(t, rec.Body.String(), "current password is incorrect") }) t.Run("PUT /api/auth/password - Missing current password", func(t *testing.T) { setup := setupTestServer(t) _ = createTestUserOnce(t, setup.DB) token := setup.Token jsonData, _ := json.Marshal(map[string]interface{}{ "new_password": "NewTest@Pass123!", "confirm_password": "NewTest@Pass123!", }) req := httptest.NewRequest("PUT", "/api/auth/password", bytes.NewBuffer(jsonData)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() setup.Server.Config.Handler.ServeHTTP(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "current password required") }) } func verifyPasswordHash(t *testing.T, hashedPassword, password string) { err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) assert.NoError(t, err, "Password should match hash") }