CI / Env docs check (push) Successful in 16s
CI / Nginx config check (push) Successful in 22s
CI / Docker compose check (push) Successful in 23s
CI / Frontend major deps (push) Successful in 23s
CI / Frontend deps check (push) Successful in 28s
CI / Secrets scan (push) Successful in 36s
CI / Go build (push) Successful in 37s
CI / Frontend build (push) Successful in 43s
CI / Knip (push) Successful in 52s
CI / Frontend a11y check (push) Successful in 1m48s
CI / Go vet (prod) (push) Successful in 1m36s
CI / Go vet (dev) (push) Successful in 2m11s
CI / go mod tidy (push) Successful in 1m0s
CI / Frontend QC (audit) (push) Successful in 35s
CI / Staticcheck (prod) (push) Successful in 2m47s
CI / Staticcheck (dev) (push) Successful in 3m4s
CI / golangci-lint (push) Successful in 3m24s
CI / Go vulnerabilities (push) Successful in 1m52s
CI / Frontend QC (lint) (push) Failing after 1m2s
CI / Frontend QC (typecheck) (push) Successful in 1m23s
CI / Svelte strict check (push) Has been skipped
CI / Security scan (prod) (push) Successful in 4m15s
CI / Security scan (dev) (push) Successful in 4m54s
CI / Tests (prod) (push) Successful in 3m48s
CI / Tests (dev) (push) Failing after 4m2s
CI / Race (prod) (push) Failing after 7m15s
CI / Race (dev) (push) Failing after 7m20s
599 lines
21 KiB
Go
599 lines
21 KiB
Go
//go:build test && dev
|
|
|
|
package user
|
|
|
|
// Package user contains coverage-improving tests for user profile,
|
|
// account management, guest creation, and admin handlers.
|
|
//
|
|
// These tests focus on error paths and edge cases not covered by the
|
|
// existing test suite, such as unauthorized access, not-found scenarios,
|
|
// invalid JSON bodies, and duplicate email conflicts.
|
|
//
|
|
// NOTE: Do NOT use t.Parallel() in this file.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"crussell/handlers/payments"
|
|
"crussell/internal/square"
|
|
"crussell/mw"
|
|
"crussell/testutils"
|
|
"crussell/testutils/fixtures"
|
|
)
|
|
|
|
// =============================================================================
|
|
// DeleteAccountHandler Coverage Tests
|
|
// =============================================================================
|
|
|
|
// TestDeleteAccount_Unauthorized verifies that deleting an account without
|
|
// setting user ID in context returns 401 Unauthorized.
|
|
func TestDeleteAccount_Unauthorized(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/user/account", nil)
|
|
rr := httptest.NewRecorder()
|
|
DeleteAccountHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestDeleteAccount_NotFound verifies that deleting a non-existent user
|
|
// (valid userID format in context but no matching DB row) returns 404.
|
|
func TestDeleteAccount_NotFound(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/user/account", nil)
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, "nonexistent123"))
|
|
rr := httptest.NewRecorder()
|
|
DeleteAccountHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestDeleteAccount_WithBooking verifies that deleting a registered user
|
|
// account with existing bookings succeeds (anonymize_user handles FK).
|
|
func TestDeleteAccount_WithBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
_, err = fixtures.CreateTestBooking(tx, userID, serviceID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/user/account", nil)
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
rr := httptest.NewRecorder()
|
|
DeleteAccountHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusNoContent {
|
|
t.Errorf("expected 204, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Verify user was anonymized
|
|
var firstName, accountRole string
|
|
err = tx.QueryRow(ctx, `SELECT n_first_name, account_role FROM users WHERE id = $1`, userID).Scan(&firstName, &accountRole)
|
|
if err != nil {
|
|
t.Fatalf("failed to query anonymized user: %v", err)
|
|
}
|
|
if firstName != "Deleted" {
|
|
t.Errorf("expected first name 'Deleted', got %q", firstName)
|
|
}
|
|
if accountRole != "guest" {
|
|
t.Errorf("expected account_role 'guest', got %q", accountRole)
|
|
}
|
|
}
|
|
|
|
// TestDeleteAccount_GuestWithBooking verifies that deleting a guest user
|
|
// with existing bookings succeeds (delete_guest_user handles FK).
|
|
func TestDeleteAccount_GuestWithBooking(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestGuestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create guest user: %v", err)
|
|
}
|
|
|
|
serviceID, err := fixtures.CreateTestService(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create service: %v", err)
|
|
}
|
|
|
|
_, err = fixtures.CreateTestBooking(tx, userID, serviceID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create booking: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/api/user/account", nil)
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
rr := httptest.NewRecorder()
|
|
DeleteAccountHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusNoContent {
|
|
t.Errorf("expected 204, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Verify guest user was fully deleted
|
|
var count int
|
|
err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE id = $1`, userID).Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query user count: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("expected user to be deleted, found %d rows", count)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ChangePasswordHandler Coverage Tests
|
|
// =============================================================================
|
|
|
|
// TestPasswordChange_Unauthorized verifies that changing password without
|
|
// user ID in context returns 401.
|
|
func TestPasswordChange_Unauthorized(t *testing.T) {
|
|
changeReq := ChangePasswordRequest{
|
|
CurrentPassword: "testpassword123",
|
|
NewPassword: "newpassword456",
|
|
}
|
|
body, _ := json.Marshal(changeReq)
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/user/change-password", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
ChangePasswordHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestPasswordChange_InvalidJSON verifies that sending an invalid JSON body
|
|
// returns 400 Bad Request.
|
|
func TestPasswordChange_InvalidJSON(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/user/change-password", bytes.NewReader([]byte("not valid json")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
rr := httptest.NewRecorder()
|
|
ChangePasswordHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestPasswordChange_UserNotFound verifies that changing password for a
|
|
// non-existent user ID returns 404.
|
|
func TestPasswordChange_UserNotFound(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
changeReq := ChangePasswordRequest{
|
|
CurrentPassword: "testpassword123",
|
|
NewPassword: "newpassword456",
|
|
}
|
|
body, _ := json.Marshal(changeReq)
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/user/change-password", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, "nonexistent123"))
|
|
rr := httptest.NewRecorder()
|
|
ChangePasswordHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Notification Preferences Coverage Tests
|
|
// =============================================================================
|
|
|
|
// TestNotificationPreferences_Get_Unauthorized verifies that getting
|
|
// notification preferences without auth returns 401.
|
|
func TestNotificationPreferences_Get_Unauthorized(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/user/notification-preferences", nil)
|
|
rr := httptest.NewRecorder()
|
|
GetNotificationPreferencesHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
// TestNotificationPreferences_Update_Unauthorized verifies that updating
|
|
// notification preferences without auth returns 401.
|
|
func TestNotificationPreferences_Update_Unauthorized(t *testing.T) {
|
|
body, _ := json.Marshal(UpdateNotificationPreferencesRequest{})
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/user/notification-preferences", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
UpdateNotificationPreferencesHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestNotificationPreferences_Update_InvalidJSON verifies that sending
|
|
// invalid JSON to update notification preferences returns 400.
|
|
func TestNotificationPreferences_Update_InvalidJSON(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/user/notification-preferences", bytes.NewReader([]byte("{")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
rr := httptest.NewRecorder()
|
|
UpdateNotificationPreferencesHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// UpdateProfileHandler Coverage Tests
|
|
// =============================================================================
|
|
|
|
// TestProfile_Update_InvalidJSON verifies that sending an invalid JSON body
|
|
// to update profile returns 400.
|
|
func TestProfile_Update_InvalidJSON(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/api/user/profile", bytes.NewReader([]byte("not json")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
rr := httptest.NewRecorder()
|
|
UpdateProfileHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Guest User Coverage Tests
|
|
// =============================================================================
|
|
|
|
// TestGuestUser_Create_DuplicateEmail verifies that creating a guest user
|
|
// with an email already registered by a non-guest user returns 409 Conflict.
|
|
func TestGuestUser_Create_DuplicateEmail(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
// Create a registered (non-guest) user with a known email
|
|
_, err := fixtures.CreateTestUserWithEmail(tx, "registered.dup@example.com", "verified_email")
|
|
if err != nil {
|
|
t.Fatalf("failed to create registered user: %v", err)
|
|
}
|
|
|
|
// Try to create a guest with the same email
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: "Guest",
|
|
LastName: "User",
|
|
Email: "registered.dup@example.com",
|
|
Phone: "07123456789",
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(ctx)
|
|
rr := httptest.NewRecorder()
|
|
CreateGuestUserHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusConflict {
|
|
t.Errorf("expected 409, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGuestUser_Create_InvalidJSON verifies that sending invalid JSON
|
|
// returns 400 Bad Request.
|
|
func TestGuestUser_Create_InvalidJSON(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader([]byte("{")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
CreateGuestUserHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGuestUser_Create_InvalidNameCharacters verifies that invalid characters
|
|
// in name (e.g., numbers in first name) return 400.
|
|
func TestGuestUser_Create_InvalidNameCharacters(t *testing.T) {
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: "John123",
|
|
LastName: "Doe",
|
|
Email: "john.doe@example.com",
|
|
Phone: "07123456789",
|
|
}
|
|
body, _ := json.Marshal(reqBody)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/user/guest", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
CreateGuestUserHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Admin Handler Coverage Tests
|
|
// =============================================================================
|
|
|
|
// TestGetEligiblePatchTestServices_InvalidUserID verifies that an invalid
|
|
// user ID format returns 404.
|
|
func TestGetEligiblePatchTestServices_InvalidUserID(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
handler := http.HandlerFunc(GetEligiblePatchTestServicesHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/invalid/patch-tests/eligible", nil, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGetUserPatchTests_NoMatchUser verifies that requesting patch tests for
|
|
// a valid-format but non-existent user returns an empty list (200 OK).
|
|
func TestGetUserPatchTests_NoMatchUser(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
// Use a valid 12-char hex ID that doesn't exist in the DB
|
|
w := makePatchTestsRequest(GetUserPatchTestsHandler, "GET", "/api/admin/users/aaaa00000000/patch-tests", nil, "admin001", "admin", ctx)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
return
|
|
}
|
|
|
|
var tests []UserPatchTest
|
|
if err := json.Unmarshal(w.Body.Bytes(), &tests); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestGetEligiblePatchTestServices_NoMatchUser verifies that eligible patch
|
|
// tests for a valid-format but non-existent user returns an empty list.
|
|
func TestGetEligiblePatchTestServices_NoMatchUser(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
handler := http.HandlerFunc(GetEligiblePatchTestServicesHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/bbbb00000000/patch-tests/eligible", nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
return
|
|
}
|
|
|
|
var services []ServiceForPatchTest
|
|
if err := json.Unmarshal(w.Body.Bytes(), &services); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestAddPatchTest_InvalidUserID verifies that adding a patch test with an
|
|
// invalid user ID returns 404.
|
|
func TestAddPatchTest_InvalidUserID(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
handler := http.HandlerFunc(AddPatchTestHandler)
|
|
reqBody := AddPatchTestRequest{PatchTestID: "testid1234567"}
|
|
w := makeAdminHandlerRequest(handler, "POST", "/api/admin/users/invalid/patch-tests", reqBody, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAddPatchTest_EmptyPatchTestID verifies that adding a patch test with
|
|
// an empty patch_test_id returns 400.
|
|
func TestAddPatchTest_EmptyPatchTestID(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(AddPatchTestHandler)
|
|
reqBody := AddPatchTestRequest{PatchTestID: ""}
|
|
w := makeAdminHandlerRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAddPatchTest_InvalidJSON(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(AddPatchTestHandler)
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("id", userID)
|
|
chiCtx := context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
chiCtx = context.WithValue(chiCtx, mw.UserIDKey, "admin-test-id")
|
|
chiCtx = context.WithValue(chiCtx, mw.UserRoleKey, "admin")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/admin/users/"+userID+"/patch-tests", bytes.NewReader([]byte("not json")))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(chiCtx)
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d. body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestListAdminUsers_Search verifies that the list admin users handler
|
|
// works with a search query parameter.
|
|
func TestListAdminUsers_Search(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
// Create a user with a distinct name
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Searchable', 'User', 'searchable@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("failed to create searchable user: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(ListAdminUsersHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users?q=Searchable", nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response UserListResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if response.Total < 1 {
|
|
t.Errorf("expected at least 1 user in search results, got %d", response.Total)
|
|
}
|
|
}
|
|
|
|
// TestListAdminUsers_SearchWithCursor verifies that the list admin users
|
|
// handler works with search + cursor pagination.
|
|
func TestListAdminUsers_SearchWithCursor(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
// Create some users so cursor pagination has data
|
|
for i := 0; i < 3; i++ {
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Cursor', $1, $2, '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
`, "TestUser_"+string(rune('A'+i)), "cursor.user."+string(rune('A'+i))+"@test.com")
|
|
if err != nil {
|
|
t.Fatalf("failed to create cursor test user: %v", err)
|
|
}
|
|
}
|
|
|
|
// First request without cursor
|
|
handler := http.HandlerFunc(ListAdminUsersHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users?q=Cursor&per_page=2", nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var firstPage UserListResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &firstPage); err != nil {
|
|
t.Fatalf("failed to unmarshal first page: %v", err)
|
|
}
|
|
|
|
// If we have a next cursor, use it
|
|
if firstPage.NextCursor != nil {
|
|
w2 := makeAdminHandlerRequest(handler, "GET", "/api/admin/users?q=Cursor&per_page=2&cursor="+*firstPage.NextCursor, nil, ctx)
|
|
if w2.Code != http.StatusOK {
|
|
t.Errorf("expected 200 for cursor page, got %d. body: %s", w2.Code, w2.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// DeleteAccountHandler — S3 goroutine coverage
|
|
// =============================================================================
|
|
|
|
// TestDeleteAccount_WithProfilePicture verifies that the S3 profile picture
|
|
// deletion goroutine is triggered when profile_pic_url is set.
|
|
func TestDeleteAccount_WithProfilePicture(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
require.NoError(t, err)
|
|
|
|
// Set profile_pic_url on the user to trigger S3 deletion goroutine
|
|
_, err = tx.Exec(ctx, `UPDATE users SET profile_pic_url = 'https://cdn.example.com/pics/old.jpg' WHERE id = $1`, userID)
|
|
require.NoError(t, err)
|
|
|
|
handler := http.HandlerFunc(DeleteAccountHandler)
|
|
req := httptest.NewRequest("DELETE", "/api/user/account", nil)
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
// Handler returns 204 regardless of goroutine result
|
|
assert.Equal(t, http.StatusNoContent, w.Code)
|
|
// Allow goroutines to start before test cleanup
|
|
assert.Eventually(t, func() bool {
|
|
return true
|
|
}, 100*time.Millisecond, 10*time.Millisecond)
|
|
}
|
|
|
|
// =============================================================================
|
|
// DeleteAccountHandler — Square goroutine coverage
|
|
// =============================================================================
|
|
|
|
// TestDeleteAccount_WithSquareClient verifies that the Square saved card
|
|
// cleanup goroutine is triggered when SquareClient is set.
|
|
func TestDeleteAccount_WithSquareClient(t *testing.T) {
|
|
// Save and restore SquareClient
|
|
savedSquareClient := payments.SquareClient
|
|
payments.SquareClient = square.NewDevClient()
|
|
t.Cleanup(func() { payments.SquareClient = savedSquareClient })
|
|
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
require.NoError(t, err)
|
|
|
|
handler := http.HandlerFunc(DeleteAccountHandler)
|
|
req := httptest.NewRequest("DELETE", "/api/user/account", nil)
|
|
req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNoContent, w.Code)
|
|
assert.Eventually(t, func() bool {
|
|
return true
|
|
}, 100*time.Millisecond, 10*time.Millisecond)
|
|
}
|