Files
bookhoard/cmd/server/tests/user_test.go
john-okeefe 19e389f966 test: fix password mismatch test to use valid complex passwords
The test was using simple passwords ('password1', 'password2') that
failed complexity validation before the mismatch check could run.

Changed to use valid complex passwords that don't match:
- new_password: 'NewPassword123!'
- confirm_password: 'DifferentPass123!'

This properly tests the mismatch validation path. All 4 subtests in
TestUpdatePasswordAdminMode now pass.
2026-02-22 11:43:42 -05:00

627 lines
19 KiB
Go

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")
}