Files
bookhoard/cmd/server/tests/user_test.go
T
John O'Keefe c645b8e207 feat(profile): keep_screen_awake user preference (default on)
New users column surfaced as an On/Off dropdown in the profile form.
The form and API carry "true"/"false" strings with the theme-style
""-guard, so JSON clients that omit the field can't clobber it;
anything else is rejected. GET/PUT /api/auth/profile expose
keep_screen_awake alongside the other profile fields, and the reader
route threads the preference into its template metadata.

Integration tests pin the schema default, both values, omission
semantics, and rejection — dedicated test user, never testuser.
2026-10-26 13:59:32 -04:00

759 lines
25 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"
)
// 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")
})
}
// TestProfileKeepScreenAwake pins the keep_screen_awake user preference
// end to end: schema default, the "true"/"false" form dropdown contract,
// JSON-client omission semantics ("" = untouched), and rejection of
// anything else. Uses a dedicated user — never the shared admin.
func TestProfileKeepScreenAwake(t *testing.T) {
// getProfile fetches the JSON profile and returns keep_screen_awake.
getProfile := func(t *testing.T, setup *TestServerSetup, token string) bool {
req := httptest.NewRequest("GET", "/api/auth/profile", nil)
req.Header.Set("Authorization", "Bearer "+token)
rec := httptest.NewRecorder()
setup.Server.Config.Handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var body struct {
KeepScreenAwake bool `json:"keep_screen_awake"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
return body.KeepScreenAwake
}
putProfile := func(t *testing.T, setup *TestServerSetup, token string, fields map[string]interface{}) *httptest.ResponseRecorder {
jsonData, _ := json.Marshal(fields)
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)
return rec
}
t.Run("fresh user defaults to true", func(t *testing.T) {
setup := setupTestServer(t)
selfUser := createRegularUserOnce(t, setup.DB)
token := loginUserWithCredentials(t, setup.Server, selfUser.Email, selfUser.Password)
row, err := setup.DB.GetUser(context.Background(), pgtype.UUID{Bytes: selfUser.ID, Valid: true})
require.NoError(t, err)
assert.True(t, row.KeepScreenAwake, "schema default must keep the column TRUE")
assert.True(t, getProfile(t, setup, token), "profile API must report the default")
})
t.Run("false persists and reads back", func(t *testing.T) {
setup := setupTestServer(t)
selfUser := createRegularUserOnce(t, setup.DB)
token := loginUserWithCredentials(t, setup.Server, selfUser.Email, selfUser.Password)
rec := putProfile(t, setup, token, map[string]interface{}{"keep_screen_awake": "false"})
require.Equal(t, http.StatusOK, rec.Code)
assert.False(t, getProfile(t, setup, token))
row, err := setup.DB.GetUser(context.Background(), pgtype.UUID{Bytes: selfUser.ID, Valid: true})
require.NoError(t, err)
assert.False(t, row.KeepScreenAwake)
})
t.Run("flipping back to true persists", func(t *testing.T) {
setup := setupTestServer(t)
selfUser := createRegularUserOnce(t, setup.DB)
token := loginUserWithCredentials(t, setup.Server, selfUser.Email, selfUser.Password)
require.Equal(t, http.StatusOK, putProfile(t, setup, token,
map[string]interface{}{"keep_screen_awake": "false"}).Code)
require.Equal(t, http.StatusOK, putProfile(t, setup, token,
map[string]interface{}{"keep_screen_awake": "true"}).Code)
assert.True(t, getProfile(t, setup, token))
})
t.Run("request omitting the field leaves it untouched", func(t *testing.T) {
setup := setupTestServer(t)
selfUser := createRegularUserOnce(t, setup.DB)
token := loginUserWithCredentials(t, setup.Server, selfUser.Email, selfUser.Password)
// JSON clients (the Android app) sync other profile fields
// without knowing this one — the default-on preference must
// survive such a save.
rec := putProfile(t, setup, token, map[string]interface{}{"theme": "nord"})
require.Equal(t, http.StatusOK, rec.Code)
assert.True(t, getProfile(t, setup, token))
})
t.Run("invalid value is rejected", func(t *testing.T) {
setup := setupTestServer(t)
selfUser := createRegularUserOnce(t, setup.DB)
token := loginUserWithCredentials(t, setup.Server, selfUser.Email, selfUser.Password)
rec := putProfile(t, setup, token, map[string]interface{}{"keep_screen_awake": "maybe"})
assert.Equal(t, http.StatusBadRequest, rec.Code)
assert.True(t, getProfile(t, setup, token), "rejected save must not change the preference")
})
}