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)
337 lines
10 KiB
Go
337 lines
10 KiB
Go
//go:build test
|
|
|
|
package user
|
|
|
|
// Package user contains tests for guest user creation endpoints.
|
|
//
|
|
// Test Coverage:
|
|
// - CreateGuestUserHandler: POST /api/user/guest - Create a new guest user
|
|
//
|
|
// Edge case tests for validation:
|
|
// - Invalid phone number format
|
|
// - Empty first name
|
|
// - Name exceeds 50 character limit
|
|
// - Invalid email format
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"crussell/testutils"
|
|
"crussell/testutils/fixtures"
|
|
)
|
|
|
|
// TestGuestUser_Create_InvalidPhone verifies that an invalid phone number returns 400 Bad Request.
|
|
func TestGuestUser_Create_InvalidPhone(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: "Test",
|
|
LastName: "User",
|
|
Email: "test@test.com",
|
|
Phone: "not-a-phone",
|
|
}
|
|
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 status 400, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGuestUser_Create_EmptyFirstName verifies that an empty first name returns 400 Bad Request.
|
|
func TestGuestUser_Create_EmptyFirstName(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: "",
|
|
LastName: "User",
|
|
Email: "test@test.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 status 400, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGuestUser_Create_NameTooLong verifies that a first name exceeding 50 characters returns 400 Bad Request.
|
|
func TestGuestUser_Create_NameTooLong(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: strings.Repeat("a", 51),
|
|
LastName: "User",
|
|
Email: "test@test.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 status 400, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGuestUser_Create_InvalidEmail verifies that an invalid email format returns 400 Bad Request.
|
|
func TestGuestUser_Create_InvalidEmail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: "Test",
|
|
LastName: "User",
|
|
Email: "not-an-email",
|
|
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 status 400, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestCheckEmail_NotRegistered verifies that querying a non-existent email returns suggestion null.
|
|
func TestCheckEmail_NotRegistered(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/check-email?email=nobody@example.com&firstName=Jane&lastName=Doe&phone=%2B447123456789", nil)
|
|
rr := httptest.NewRecorder()
|
|
CheckEmailHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if resp["suggestion"] != nil {
|
|
t.Errorf("expected suggestion null, got %v", resp["suggestion"])
|
|
}
|
|
}
|
|
|
|
// TestCheckEmail_Registered_MatchingDetails verifies that querying an existing registered user's email
|
|
// with matching first name, last name, and phone returns suggestion "login".
|
|
func TestCheckEmail_Registered_MatchingDetails(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUserWithEmail(tx, "jane@example.com", "verified_email")
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
UPDATE users SET n_first_name = 'Jane', n_last_name = 'Doe' WHERE id = $1
|
|
`, userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to update user name: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, `/api/check-email?email=jane@example.com&firstName=Jane&lastName=Doe&phone=%2B447123456789`, nil)
|
|
req = req.WithContext(ctx)
|
|
rr := httptest.NewRecorder()
|
|
CheckEmailHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
suggestion, ok := resp["suggestion"].(string)
|
|
if !ok || suggestion != "login" {
|
|
t.Errorf("expected suggestion 'login', got %v", resp["suggestion"])
|
|
}
|
|
}
|
|
|
|
// TestCheckEmail_Registered_PartialMatch verifies that when the email exists but details don't fully match,
|
|
// the handler returns suggestion "check".
|
|
func TestCheckEmail_Registered_PartialMatch(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
userID, err := fixtures.CreateTestUserWithEmail(tx, "jane@example.com", "verified_email")
|
|
if err != nil {
|
|
t.Fatalf("failed to create test user: %v", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
UPDATE users SET n_first_name = 'Jane', n_last_name = 'Doe' WHERE id = $1
|
|
`, userID)
|
|
if err != nil {
|
|
t.Fatalf("failed to update user name: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, `/api/check-email?email=jane@example.com&firstName=Wrong&lastName=Doe&phone=%2B447123456789`, nil)
|
|
req = req.WithContext(ctx)
|
|
rr := httptest.NewRecorder()
|
|
CheckEmailHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
suggestion, ok := resp["suggestion"].(string)
|
|
if !ok || suggestion != "check" {
|
|
t.Errorf("expected suggestion 'check', got %v", resp["suggestion"])
|
|
}
|
|
}
|
|
|
|
// TestCheckEmail_GuestUser verifies that a guest user's email is treated as not found
|
|
// (suggestion null) because the query excludes account_role = 'guest'.
|
|
func TestCheckEmail_GuestUser(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
_, err := fixtures.CreateTestGuestUser(tx)
|
|
if err != nil {
|
|
t.Fatalf("failed to create guest user: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, `/api/check-email?email=guest@test.com&firstName=Guest&lastName=User&phone=%2B447123456789`, nil)
|
|
req = req.WithContext(ctx)
|
|
rr := httptest.NewRecorder()
|
|
CheckEmailHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected status 200, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
|
|
if resp["suggestion"] != nil {
|
|
t.Errorf("expected suggestion null for guest user, got %v", resp["suggestion"])
|
|
}
|
|
}
|
|
|
|
// TestCheckEmail_InvalidEmail verifies that an invalid email format returns 400 Bad Request.
|
|
func TestCheckEmail_InvalidEmail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/check-email?email=not-an-email", nil)
|
|
rr := httptest.NewRecorder()
|
|
CheckEmailHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestCheckEmail_MissingEmail verifies that omitting the email query parameter returns 400 Bad Request
|
|
// with the appropriate error message.
|
|
func TestCheckEmail_MissingEmail(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/check-email", nil)
|
|
rr := httptest.NewRecorder()
|
|
CheckEmailHandler(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected status 400, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
|
|
if !strings.Contains(rr.Body.String(), "email query parameter required") {
|
|
t.Errorf("expected 'email query parameter required' in body, got %s", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestGuestUser_Create_Success verifies that a valid guest user request
|
|
// creates a guest user and returns 201 with the user's details.
|
|
func TestGuestUser_Create_Success(t *testing.T) {
|
|
// NOT parallel — uses db.Conn state
|
|
ctx, tx := testutils.SetupTestTx(t)
|
|
|
|
reqBody := CreateGuestUserRequest{
|
|
FirstName: "Jane",
|
|
LastName: "Guest",
|
|
Email: "jane.guest.success@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.StatusCreated {
|
|
t.Errorf("expected status 201, got %d", rr.Code)
|
|
t.Logf("response body: %s", rr.Body.String())
|
|
}
|
|
|
|
// Verify user was created in DB
|
|
var count int
|
|
err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE email = $1 AND account_role = 'guest'`, "jane.guest.success@example.com").Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query users: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 guest user (account_role='guest'), got %d", count)
|
|
}
|
|
|
|
// Verify response body contains ID and role
|
|
var resp CreateGuestUserResponse
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to unmarshal response: %v", err)
|
|
}
|
|
if resp.ID == "" {
|
|
t.Error("expected non-empty user ID in response")
|
|
}
|
|
if resp.Role != "guest" {
|
|
t.Errorf("expected role 'guest', got '%s'", resp.Role)
|
|
}
|
|
}
|