Migrate all test files from resetTestData(t) to testutils.SetupTestDB(t) for isolated per-package test databases. - Add new feature tests: name history assertions, referral discount preview, time blockers, email validation, GDPR export, loyalty manual redemption - Update existing tests to use batch queries and SetupTestDB - Remove test_helpers.go resetTestData infrastructure - Add comprehensive user profile tests (442 new lines) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
699 lines
25 KiB
Go
699 lines
25 KiB
Go
//go:build test
|
|
// +build test
|
|
|
|
package admin
|
|
|
|
// Package admin contains tests for admin user management endpoints.
|
|
//
|
|
// Test Coverage:
|
|
// - ListAdminUsersHandler: GET /api/admin/users - List all users with pagination
|
|
// - GetAdminUserHandler: GET /api/admin/users/{id} - Get single user details
|
|
// - GetEligiblePatchTestServicesHandler: GET /api/admin/users/{id}/eligible-patch-tests
|
|
// - AddPatchTestHandler: POST /api/admin/users/{id}/patch-tests - Add patch test record
|
|
// - RequireAdmin middleware: All endpoints require admin role (403 for non-admins)
|
|
//
|
|
// Database State: Tests create and clean up users in the users table.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"crussell/db"
|
|
"crussell/testutils"
|
|
"crussell/handlers/user"
|
|
"crussell/mw"
|
|
)
|
|
|
|
// TestAdminUsers_List verifies that an admin can list all users in the
|
|
// system with their details including account role and type.
|
|
func TestAdminUsers_List(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test users with name history
|
|
var ninaID, bobID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Nina', 'Smith', 'nina@test.com', '+447123456789', '1990-01-01', 'hash1', 'admin', 'email')
|
|
RETURNING id
|
|
`).Scan(&ninaID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create nina: %v", err)
|
|
}
|
|
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Bob', 'Jones', 'bob@test.com', '+447123456789', '1990-01-01', 'hash2', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&bobID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create bob: %v", err)
|
|
}
|
|
|
|
// Create a completed booking for Nina so she has a completed_count
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO bookings (user_id, start_time, status) VALUES ($1, NOW(), 'completed')
|
|
`, ninaID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking for nina: %v", err)
|
|
}
|
|
|
|
// Insert name history for Bob (previous name that differs from current)
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
|
|
VALUES ($1, 'Bobby', 'Jones')
|
|
`, bobID)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert name_history for bob: %v", err)
|
|
}
|
|
|
|
userID, _ := json.Marshal(bobID)
|
|
_ = userID
|
|
|
|
handler := http.HandlerFunc(user.ListAdminUsersHandler)
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users", nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response user.UserListResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if response.Total != 2 {
|
|
t.Errorf("expected 2 users, got %d", response.Total)
|
|
}
|
|
|
|
if len(response.Users) != 2 {
|
|
t.Errorf("expected 2 users in list, got %d", len(response.Users))
|
|
}
|
|
|
|
// Verify name history and completed_count in user list
|
|
var bobFound bool
|
|
for _, u := range response.Users {
|
|
if u.ID == bobID {
|
|
bobFound = true
|
|
if u.PreviousFirstName == nil || *u.PreviousFirstName != "Bobby" {
|
|
t.Errorf("expected bob previousFirstName 'Bobby', got %v", u.PreviousFirstName)
|
|
}
|
|
if u.PreviousLastName == nil || *u.PreviousLastName != "Jones" {
|
|
t.Errorf("expected bob previousLastName 'Jones', got %v", u.PreviousLastName)
|
|
}
|
|
}
|
|
if u.ID == ninaID {
|
|
if u.CompletedCount != 1 {
|
|
t.Errorf("expected nina completed_count 1, got %d", u.CompletedCount)
|
|
}
|
|
// Nina has no name history — should be nil
|
|
if u.PreviousFirstName != nil {
|
|
t.Errorf("expected nina previousFirstName nil, got %v", *u.PreviousFirstName)
|
|
}
|
|
}
|
|
}
|
|
if !bobFound {
|
|
t.Error("expected bob in user list")
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_List_Page verifies the page parameter is echoed in the response.
|
|
// Note: The user list uses cursor-based pagination, not offset-based, so page
|
|
// is metadata only — actual page navigation is driven by the next_cursor field.
|
|
func TestAdminUsers_List_Page(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
for i := 0; i < 5; i++ {
|
|
_, err := db.DB.Exec(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('User', $1, $2, '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
`, fmt.Sprintf("LastName_%d", i), fmt.Sprintf("user%d@test.com", i))
|
|
if err != nil {
|
|
t.Fatalf("failed to create user %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.ListAdminUsersHandler)
|
|
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users?page=2", nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var resp user.UserListResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if resp.Page != 2 {
|
|
t.Errorf("expected page 2 echoed back, got %d", resp.Page)
|
|
}
|
|
if resp.Total != 5 {
|
|
t.Errorf("expected total 5, got %d", resp.Total)
|
|
}
|
|
if resp.PerPage == 0 {
|
|
t.Error("expected per_page to be set")
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_Get tests that an admin can retrieve detailed information
|
|
// about a specific user including their profile and account settings.
|
|
func TestAdminUsers_Get(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.GetAdminUserHandler)
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response user.AdminUserDetail
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if response.ID != userID {
|
|
t.Errorf("expected user ID %s, got %s", userID, response.ID)
|
|
}
|
|
|
|
if response.AccountType != "email" {
|
|
t.Errorf("expected account type 'email', got %s", response.AccountType)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_Get_NotFound verifies that requesting details for a
|
|
// non-existent user returns HTTP 404 Not Found.
|
|
func TestAdminUsers_Get_NotFound(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
handler := http.HandlerFunc(user.GetAdminUserHandler)
|
|
// Use 12-char or less ID to avoid CHAR(12) constraint error
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users/nonexist", nil)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_PatchTests_Eligible tests that the system correctly
|
|
// identifies which services require patch tests and returns only those services
|
|
// the user is eligible for based on age requirements.
|
|
func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create services - some with patch test, some without
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
|
VALUES
|
|
('Basic Manicure', 'Basic manicure', 25.00, 30, true, 0),
|
|
('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16),
|
|
('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16),
|
|
('Inactive Service', 'Inactive', 30.00, 30, false, 16)
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("failed to create services: %v", err)
|
|
}
|
|
|
|
// Get service IDs for patch test services
|
|
var gelPolishID, luxuryGelID string
|
|
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Gel Polish Full Set'").Scan(&gelPolishID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get gel polish service ID: %v", err)
|
|
}
|
|
err = db.DB.QueryRow(context.Background(), "SELECT id FROM services WHERE name = 'Luxury Gel Manicure'").Scan(&luxuryGelID)
|
|
if err != nil {
|
|
t.Fatalf("failed to get luxury gel service ID: %v", err)
|
|
}
|
|
|
|
// Create patch tests that link to these services
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
|
|
`, []string{gelPolishID})
|
|
if err != nil {
|
|
t.Fatalf("failed to create patch test for gel polish: %v", err)
|
|
}
|
|
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ('Luxury Gel Test', 'Patch test for luxury gel', 24, 6, $1)
|
|
`, []string{luxuryGelID})
|
|
if err != nil {
|
|
t.Fatalf("failed to create patch test for luxury gel: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response []user.ServiceForPatchTest
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
// Should return 2 services (the two with patch tests that are active)
|
|
if len(response) != 2 {
|
|
t.Errorf("expected 2 eligible services, got %d. body: %s", len(response), w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_PatchTests_Eligible_WithExisting verifies that when a
|
|
// user already has a valid patch test on file, that service is filtered out
|
|
// from the eligible list (since they've already completed it).
|
|
func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create services
|
|
var serviceID1, serviceID2 string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
|
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
|
|
RETURNING id
|
|
`).Scan(&serviceID1)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service 1: %v", err)
|
|
}
|
|
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
|
VALUES ('Luxury Gel Manicure', 'Luxury gel', 55.00, 75, true, 16)
|
|
RETURNING id
|
|
`).Scan(&serviceID2)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service 2: %v", err)
|
|
}
|
|
|
|
// Create patch tests
|
|
var patchTestID1 string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
|
|
RETURNING id
|
|
`, []string{serviceID1}).Scan(&patchTestID1)
|
|
if err != nil {
|
|
t.Fatalf("failed to create patch test 1: %v", err)
|
|
}
|
|
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ('Luxury Gel Test', 'Patch test for luxury gel', 24, 6, $1)
|
|
`, []string{serviceID2})
|
|
if err != nil {
|
|
t.Fatalf("failed to create patch test 2: %v", err)
|
|
}
|
|
|
|
// Add one patch test for the user (valid - within expiry)
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO user_patch_tests (user_id, patch_test_id, tested_at)
|
|
VALUES ($1, $2, NOW() - INTERVAL '2 months')
|
|
`, userID, patchTestID1)
|
|
if err != nil {
|
|
t.Fatalf("failed to add patch test: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.GetEligiblePatchTestServicesHandler)
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response []user.ServiceForPatchTest
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
// Should return only 1 service (the one not already added)
|
|
if len(response) != 1 {
|
|
t.Errorf("expected 1 eligible service, got %d. body: %s", len(response), w.Body.String())
|
|
}
|
|
|
|
if len(response) > 0 && response[0].ID != serviceID2 {
|
|
t.Errorf("expected service %s, got %s", serviceID2, response[0].ID)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_AddPatchTest verifies that an admin can record a patch
|
|
// test completion for a user, creating a user_patch_tests record.
|
|
func TestAdminUsers_AddPatchTest(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create a service
|
|
var serviceID string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
|
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
|
|
RETURNING id
|
|
`).Scan(&serviceID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
// Create a patch test that links to this service
|
|
var patchTestID string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
|
|
RETURNING id
|
|
`, []string{serviceID}).Scan(&patchTestID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create patch test: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.AddPatchTestHandler)
|
|
|
|
reqBody := user.AddPatchTestRequest{PatchTestID: patchTestID}
|
|
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Verify patch test was added
|
|
var count int
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
SELECT COUNT(*) FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
|
|
`, userID, patchTestID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to check patch test: %v", err)
|
|
}
|
|
|
|
if count != 1 {
|
|
t.Errorf("expected 1 patch test record, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestAdminUsers_AddPatchTest_InvalidPatchTest(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.AddPatchTestHandler)
|
|
|
|
// Try to add a non-existent patch test
|
|
reqBody := user.AddPatchTestRequest{PatchTestID: "nonexist123"}
|
|
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_NonAdmin verifies that non-admin users receive HTTP 403
|
|
// Forbidden when attempting to list users, get user details, or manage patch tests.
|
|
func TestAdminUsers_NonAdmin(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create regular user in DB
|
|
_, err := db.DB.Exec(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Regular', 'User', 'user@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create test user for GET
|
|
var targetUserID string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Target', 'User', 'target@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&targetUserID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create target user: %v", err)
|
|
}
|
|
|
|
// Test LIST - should get 403 when using middleware
|
|
listHandler := mw.RequireAdmin(http.HandlerFunc(user.ListAdminUsersHandler))
|
|
w := makeUserRequest(listHandler, "GET", "/api/admin/users", nil)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("LIST: expected status 403, got %d", w.Code)
|
|
}
|
|
|
|
// Test GET - should get 403 when using middleware
|
|
getHandler := mw.RequireAdmin(http.HandlerFunc(user.GetAdminUserHandler))
|
|
w = makeUserRequest(getHandler, "GET", "/api/admin/users/"+targetUserID, nil)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("GET: expected status 403, got %d", w.Code)
|
|
}
|
|
|
|
// Test eligible patch tests - should get 403 when using middleware
|
|
eligibleHandler := mw.RequireAdmin(http.HandlerFunc(user.GetEligiblePatchTestServicesHandler))
|
|
w = makeUserRequest(eligibleHandler, "GET", "/api/admin/users/"+targetUserID+"/patch-tests/eligible", nil)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("ELIGIBLE: expected status 403, got %d", w.Code)
|
|
}
|
|
|
|
// Test add patch test - should get 403 when using middleware
|
|
addHandler := mw.RequireAdmin(http.HandlerFunc(user.AddPatchTestHandler))
|
|
w = makeUserRequest(addHandler, "POST", "/api/admin/users/"+targetUserID+"/patch-tests", map[string]string{"patch_test_id": "some-test-id"})
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("ADD: expected status 403, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_Get_Success is an additional test verifying admin can
|
|
// retrieve user details including ID, name, email, and account role.
|
|
func TestAdminUsers_Get_Success(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create a test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('John', 'Doe', 'john.doe@test.com', '+447700900000', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
// Insert name history (simulating a previous name change)
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
|
|
VALUES ($1, 'OldFirst', 'OldLast')
|
|
`, userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert name_history: %v", err)
|
|
}
|
|
|
|
// Call admin get user endpoint
|
|
handler := http.HandlerFunc(user.GetAdminUserHandler)
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", w.Code)
|
|
t.Logf("response body: %s", w.Body.String())
|
|
return
|
|
}
|
|
|
|
var resp user.AdminUserDetail
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if resp.ID != userID {
|
|
t.Errorf("expected user ID %s, got %s", userID, resp.ID)
|
|
}
|
|
if resp.FirstName != "John" {
|
|
t.Errorf("expected first name 'John', got '%s'", resp.FirstName)
|
|
}
|
|
if resp.LastName != "Doe" {
|
|
t.Errorf("expected last name 'Doe', got '%s'", resp.LastName)
|
|
}
|
|
if resp.Email == nil || *resp.Email != "john.doe@test.com" {
|
|
t.Errorf("expected email 'john.doe@test.com', got '%v'", resp.Email)
|
|
}
|
|
if resp.AccountRole != "verified_email" {
|
|
t.Errorf("expected account_role 'verified_email', got '%s'", resp.AccountRole)
|
|
}
|
|
|
|
// Verify previous name from history is returned
|
|
if resp.PreviousFirstName == nil || *resp.PreviousFirstName != "OldFirst" {
|
|
t.Errorf("expected previousFirstName 'OldFirst', got %v", resp.PreviousFirstName)
|
|
}
|
|
if resp.PreviousLastName == nil || *resp.PreviousLastName != "OldLast" {
|
|
t.Errorf("expected previousLastName 'OldLast', got %v", resp.PreviousLastName)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent verifies that previous
|
|
// name is omitted when the name_history entry matches the current user name.
|
|
func TestAdminUsers_Get_ShowsPreviousNameOnlyWhenDifferent(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Alice', 'Smith', 'alice@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Insert name_history with the SAME name as current — should be omitted
|
|
_, err = db.DB.Exec(context.Background(), `
|
|
INSERT INTO name_history (user_id, previous_first_name, previous_last_name)
|
|
VALUES ($1, 'Alice', 'Smith')
|
|
`, userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to insert name_history: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.GetAdminUserHandler)
|
|
w := makeAdminRequest(handler, "GET", "/api/admin/users/"+userID, nil)
|
|
|
|
var resp user.AdminUserDetail
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal: %v", err)
|
|
}
|
|
|
|
if resp.PreviousFirstName != nil {
|
|
t.Errorf("expected previousFirstName nil (same as current), got %v", *resp.PreviousFirstName)
|
|
}
|
|
if resp.PreviousLastName != nil {
|
|
t.Errorf("expected previousLastName nil (same as current), got %v", *resp.PreviousLastName)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_AddPatchTest_Duplicate verifies that recording the same patch test
|
|
// twice updates the tested_at timestamp (upsert behavior).
|
|
func TestAdminUsers_AddPatchTest_Duplicate(t *testing.T) {
|
|
testutils.SetupTestDB(t)
|
|
|
|
// Create test user
|
|
var userID string
|
|
err := db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Test', 'User', 'testuser@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create a service
|
|
var serviceID string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO services (name, description, price, duration_minutes, is_active, minimum_age_required)
|
|
VALUES ('Gel Polish Full Set', 'Gel polish service', 45.00, 60, true, 16)
|
|
RETURNING id
|
|
`).Scan(&serviceID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
// Create a patch test that links to this service
|
|
var patchTestID string
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
INSERT INTO patch_tests (name, description, notice_duration_hours, expiry_months, service_ids)
|
|
VALUES ('Gel Allergy Test', 'Patch test for gel products', 24, 6, $1)
|
|
RETURNING id
|
|
`, []string{serviceID}).Scan(&patchTestID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create patch test: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(user.AddPatchTestHandler)
|
|
|
|
// Record patch test first time - should return 201 Created
|
|
reqBody := user.AddPatchTestRequest{PatchTestID: patchTestID}
|
|
w := makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
|
|
if w.Code != http.StatusCreated {
|
|
t.Errorf("first record: expected status 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Query tested_at time T1
|
|
var t1 time.Time
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
SELECT tested_at FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
|
|
`, userID, patchTestID).Scan(&t1)
|
|
if err != nil {
|
|
t.Fatalf("failed to get tested_at: %v", err)
|
|
}
|
|
|
|
// Wait 100ms to ensure timestamp will change
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Record same patch test again - should return 201 or 200 (upsert updates)
|
|
reqBody = user.AddPatchTestRequest{PatchTestID: patchTestID}
|
|
w = makeAdminRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody)
|
|
if w.Code != http.StatusCreated && w.Code != http.StatusOK {
|
|
t.Errorf("second record: expected status 200 or 201, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Query tested_at time T2
|
|
var t2 time.Time
|
|
err = db.DB.QueryRow(context.Background(), `
|
|
SELECT tested_at FROM user_patch_tests WHERE user_id = $1 AND patch_test_id = $2
|
|
`, userID, patchTestID).Scan(&t2)
|
|
if err != nil {
|
|
t.Fatalf("failed to get tested_at: %v", err)
|
|
}
|
|
|
|
// Assert T2 > T1 (upsert updated the timestamp)
|
|
if !t2.After(t1) {
|
|
t.Errorf("expected t2 %v after t1 %v, but it's not", t2, t1)
|
|
}
|
|
}
|