default_timezone existed as a system setting (admin-editable, exposed via SettingsRegistry.DefaultTimezone) but nothing read it at user creation — the CreateUser query didn't even carry the column, so every new user got the DB default UTC regardless. Both creation paths (public register + admin create) now seed the user's timezone from the setting, validated with time.LoadLocation and falling back to UTC when unwired/empty/invalid. Test-infra fixes surfaced by the run: TestUpdateProfileSelfEdit was self-editing the SHARED admin (renaming it past the harness cleanup's preserve-list, leaking a second admin row that broke the last-admin tests) — it now edits a dedicated user with a unique name; the last-admin tests get an ensureSingleAdmin precondition helper; the websocket collection name is run-unique (non-test names persist in the shared DB and UNIQUE(user_id,name) 409s on rerun).
676 lines
21 KiB
Go
676 lines
21 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"bookhoard/internal/database"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// ensureSingleAdmin makes the "last admin" precondition deterministic in
|
|
// the shared test DB: only the preserved testuser admin remains. Other
|
|
// suites may have promoted or renamed users; the last-admin semantics
|
|
// under test require a true singleton.
|
|
func ensureSingleAdmin(t *testing.T, setup *TestServerSetup) {
|
|
ctx := context.Background()
|
|
_, err := setup.DBPool.Exec(ctx,
|
|
"DELETE FROM users WHERE role = 'admin' AND email != 'testuser@tests.bookhoard.internal'")
|
|
require.NoError(t, err, "Failed to prune extra admins")
|
|
|
|
if _, err := setup.DB.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal"); err != nil {
|
|
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
|
_, cerr := setup.DB.CreateUser(ctx, database.CreateUserParams{
|
|
Email: "testuser@tests.bookhoard.internal",
|
|
Username: "testuser",
|
|
PasswordHash: passwordHash,
|
|
Role: "admin",
|
|
Timezone: pgtype.Text{String: "UTC", Valid: true},
|
|
})
|
|
require.NoError(t, cerr, "Failed to restore the shared admin")
|
|
}
|
|
|
|
users, _ := setup.DB.ListUsers(ctx)
|
|
adminCount := 0
|
|
for _, u := range users {
|
|
if u.Role == "admin" {
|
|
adminCount++
|
|
}
|
|
}
|
|
require.Equal(t, 1, adminCount, "precondition: exactly one admin")
|
|
}
|
|
|
|
func TestUpdateProfileSelfEdit(t *testing.T) {
|
|
t.Run("PUT /api/auth/profile - Update all fields successfully", func(t *testing.T) {
|
|
setup := setupTestServer(t)
|
|
|
|
// Self-edit a DEDICATED user, never the shared admin: renaming
|
|
// testuser here leaked a second admin row into the shared DB (the
|
|
// renamed email escapes the harness's preserve-list cleanup) and
|
|
// broke the last-admin tests downstream.
|
|
selfUser := createRegularUserOnce(t, setup.DB)
|
|
token := loginUserWithCredentials(t, setup.Server, selfUser.Email, selfUser.Password)
|
|
|
|
// Unique rename target: a fixed name collides with stale rows from
|
|
// earlier runs of this very test (409 username conflict).
|
|
suffix := uuid.New().String()[:8]
|
|
newUsername := "updateduser-" + suffix
|
|
updateData := map[string]interface{}{
|
|
"username": newUsername,
|
|
"email": "updated-" + suffix + "@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(), newUsername)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "updated-"+suffix+"@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)
|
|
|
|
// Deterministic singleton-admin precondition (shared DB may carry
|
|
// promoted/renamed admin rows from other suites).
|
|
ensureSingleAdmin(t, setup)
|
|
|
|
// 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)
|
|
|
|
ensureSingleAdmin(t, setup)
|
|
_ = 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)
|
|
|
|
ensureSingleAdmin(t, setup)
|
|
_ = 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")
|
|
}
|