CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s
New test files cover previously untested paths across DAV, validators, S3, Square, mw, bookings, user, and payments packages. Includes mock fix: HoldCheckouts flag on MockClient allows tests to pause auto-complete goroutine for testing PENDING checkout states. Coverage: 50.4% → 65.0% (+14.6pp)
543 lines
18 KiB
Go
543 lines
18 KiB
Go
//go:build test
|
|
|
|
package user
|
|
|
|
// Package user contains in-package tests for admin user management handlers.
|
|
//
|
|
// These tests live in the `user` package (not `handlers/admin`) to ensure
|
|
// Go coverage counts the handler code. The admin-package tests in
|
|
// handlers/admin/users_test.go exercise the same handlers from outside the
|
|
// package, which does not contribute to coverage.
|
|
//
|
|
// Test Coverage:
|
|
// - GetAdminUserHandler: GET /api/admin/users/{id}
|
|
// - ListAdminUsersHandler: GET /api/admin/users
|
|
// - GetEligiblePatchTestServicesHandler: GET /api/admin/users/{id}/patch-tests/eligible
|
|
// - AddPatchTestHandler: POST /api/admin/users/{id}/patch-tests
|
|
//
|
|
// Database State: Tests create and clean up users in the users table.
|
|
// DO NOT use t.Parallel() — tests share db.Conn state.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"crussell/mw"
|
|
"crussell/testutils"
|
|
"crussell/testutils/fixtures"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// makeAdminHandlerRequest creates an HTTP request with admin-level context
|
|
// and chi route parameters extracted from the path.
|
|
func makeAdminHandlerRequest(handler http.HandlerFunc, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
|
|
var req *http.Request
|
|
if body != nil {
|
|
bodyBytes, _ := json.Marshal(body)
|
|
req = httptest.NewRequest(method, path, strings.NewReader(string(bodyBytes)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(method, path, nil)
|
|
}
|
|
|
|
rctx := chi.NewRouteContext()
|
|
if id, ok := extractAdminUserID(path); ok {
|
|
rctx.URLParams.Add("id", id)
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
ctx = context.WithValue(ctx, mw.UserIDKey, "admin-test-id")
|
|
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
|
|
req = req.WithContext(ctx)
|
|
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
// extractAdminUserID extracts the user ID segment from admin API paths.
|
|
// Supported patterns:
|
|
//
|
|
// /api/admin/users/{id}
|
|
// /api/admin/users/{id}/patch-tests/eligible
|
|
// /api/admin/users/{id}/patch-tests
|
|
func extractAdminUserID(path string) (string, bool) {
|
|
prefix := "/api/admin/users/"
|
|
if !strings.HasPrefix(path, prefix) {
|
|
return "", false
|
|
}
|
|
rest := path[len(prefix):]
|
|
// The first path segment is the ID
|
|
for i := 0; i < len(rest); i++ {
|
|
if rest[i] == '/' {
|
|
return rest[:i], true
|
|
}
|
|
}
|
|
return rest, true
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /api/admin/users/{id}
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestAdminUsers_Get verifies that an admin can retrieve detailed information
|
|
// about a specific user.
|
|
func TestAdminUsers_Get(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)
|
|
}
|
|
|
|
handler := http.HandlerFunc(GetAdminUserHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/"+userID, nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response 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 a non-existent user
|
|
// returns HTTP 404.
|
|
func TestAdminUsers_Get_NotFound(t *testing.T) {
|
|
ctx, _ := testutils.SetupTestTx(t)
|
|
|
|
handler := http.HandlerFunc(GetAdminUserHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/nonexist", nil, ctx)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_Get_ValidFormatNotFound tests that a valid-format hex ID
|
|
// that doesn't exist in the DB triggers the pgx.ErrNoRows path (404).
|
|
func TestAdminUsers_Get_ValidFormatNotFound(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
_ = tx
|
|
|
|
handler := http.HandlerFunc(GetAdminUserHandler)
|
|
req := httptest.NewRequest("GET", "/api/admin/users/000000000000", nil)
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("id", "000000000000")
|
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
|
ctx = context.WithValue(ctx, mw.UserIDKey, "admin-id")
|
|
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
|
|
req = req.WithContext(ctx)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /api/admin/users
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestAdminUsers_List verifies that an admin can list all users with
|
|
// their details including name history and completed booking counts.
|
|
func TestAdminUsers_List(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
// Create test users
|
|
var ninaID, bobID string
|
|
err := tx.QueryRow(ctx, `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Nina', 'Smith', 'nina.adminlist@test.com', '+447123456789', '1990-01-01', 'hash', 'admin', 'email')
|
|
RETURNING id
|
|
`).Scan(&ninaID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create nina: %v", err)
|
|
}
|
|
|
|
err = tx.QueryRow(ctx, `
|
|
INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, password_hash, account_role, account_type)
|
|
VALUES ('Bob', 'Jones', 'bob.adminlist@test.com', '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
RETURNING id
|
|
`).Scan(&bobID)
|
|
if err != nil {
|
|
t.Fatalf("failed to create bob: %v", err)
|
|
}
|
|
|
|
// Create a completed booking for Nina
|
|
_, err = tx.Exec(ctx, `
|
|
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
|
|
_, err = tx.Exec(ctx, `
|
|
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)
|
|
}
|
|
|
|
handler := http.HandlerFunc(ListAdminUsersHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users", nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 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)
|
|
}
|
|
|
|
// The baseline seed creates some users too, so we check at least 2
|
|
if response.Total < 2 {
|
|
t.Errorf("expected at least 2 users, got %d", response.Total)
|
|
}
|
|
|
|
// Verify name history and completed_count
|
|
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)
|
|
}
|
|
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.
|
|
func TestAdminUsers_List_Page(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
for i := 0; i < 5; 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 ('User', $1, $2, '+447123456789', '1990-01-01', 'hash', 'verified_email', 'email')
|
|
`, fmt.Sprintf("LastName_%d", i), fmt.Sprintf("user.pagelist.%d@test.com", i))
|
|
if err != nil {
|
|
t.Fatalf("failed to create user %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
handler := http.HandlerFunc(ListAdminUsersHandler)
|
|
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users?page=2", nil, ctx)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp 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 at least 5 users total, got %d", resp.Total)
|
|
}
|
|
if resp.PerPage == 0 {
|
|
t.Error("expected per_page to be set")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /api/admin/users/{id}/patch-tests/eligible
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestAdminUsers_PatchTests_Eligible verifies that eligible patch test
|
|
// services are correctly returned.
|
|
func TestAdminUsers_PatchTests_Eligible(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create services
|
|
_, err = tx.Exec(ctx, `
|
|
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 = tx.QueryRow(ctx, "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 = tx.QueryRow(ctx, "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 = tx.Exec(ctx, `
|
|
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 = tx.Exec(ctx, `
|
|
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(GetEligiblePatchTestServicesHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response []ServiceForPatchTest
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
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 a service is
|
|
// filtered out when the user already has a valid (non-expired) patch test on file.
|
|
func TestAdminUsers_PatchTests_Eligible_WithExisting(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create services
|
|
var serviceID1, serviceID2 string
|
|
err = tx.QueryRow(ctx, `
|
|
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 = tx.QueryRow(ctx, `
|
|
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 = tx.QueryRow(ctx, `
|
|
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 = tx.Exec(ctx, `
|
|
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 (within expiry window)
|
|
_, err = tx.Exec(ctx, `
|
|
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 user patch test: %v", err)
|
|
}
|
|
|
|
handler := http.HandlerFunc(GetEligiblePatchTestServicesHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/"+userID+"/patch-tests/eligible", nil, ctx)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var response []ServiceForPatchTest
|
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /api/admin/users/{id}/patch-tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestAdminUsers_AddPatchTest verifies that an admin can record a patch test
|
|
// for a user, creating a user_patch_tests record.
|
|
func TestAdminUsers_AddPatchTest(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create user: %v", err)
|
|
}
|
|
|
|
// Create a service
|
|
var serviceID string
|
|
err = tx.QueryRow(ctx, `
|
|
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 = tx.QueryRow(ctx, `
|
|
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(AddPatchTestHandler)
|
|
|
|
reqBody := AddPatchTestRequest{PatchTestID: patchTestID}
|
|
w := makeAdminHandlerRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
|
|
|
|
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 = tx.QueryRow(ctx, `
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestAdminUsers_AddPatchTest_InvalidPatchTest verifies that providing a
|
|
// non-existent patch_test_id returns HTTP 400.
|
|
func TestAdminUsers_AddPatchTest_InvalidPatchTest(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: "nonexist123"}
|
|
w := makeAdminHandlerRequest(handler, "POST", "/api/admin/users/"+userID+"/patch-tests", reqBody, ctx)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /api/admin/users/{id} — social logins
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestAdminUsers_Get_WithSocialLogins verifies that the admin user detail
|
|
// response includes social logins when the user has linked OAuth providers.
|
|
func TestAdminUsers_Get_WithSocialLogins(t *testing.T) {
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
userID, err := fixtures.CreateTestUser(tx)
|
|
require.NoError(t, err)
|
|
|
|
// Insert social login rows
|
|
_, err = tx.Exec(ctx, `INSERT INTO user_social_logins (user_id, provider, immutable_id) VALUES ($1, 'google', 'google-123')`, userID)
|
|
require.NoError(t, err)
|
|
_, err = tx.Exec(ctx, `INSERT INTO user_social_logins (user_id, provider, immutable_id) VALUES ($1, 'microsoft', 'ms-456')`, userID)
|
|
require.NoError(t, err)
|
|
|
|
handler := http.HandlerFunc(GetAdminUserHandler)
|
|
w := makeAdminHandlerRequest(handler, "GET", "/api/admin/users/"+userID, nil, ctx)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var resp map[string]any
|
|
err = json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, resp, "socialLogins")
|
|
// socialLogins should be a non-empty array
|
|
logins, ok := resp["socialLogins"].([]any)
|
|
if assert.True(t, ok, "socialLogins should be an array") {
|
|
assert.Len(t, logins, 2)
|
|
}
|
|
}
|