Files
Crussell/backend/handlers/admin/settings_test.go
T
popertots 3029fd5179
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
test: add coverage tests across backend + fix mock for PENDING checkout support
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)
2026-07-10 18:13:44 +01:00

1061 lines
37 KiB
Go

//go:build test
package admin
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"crussell/testutils"
"github.com/stretchr/testify/assert"
)
func intPtr(i int) *int { return &i }
func float64Ptr(f float64) *float64 { return &f }
// ─── Public info ──────────────────────────────────────────────────────────────
// TestGetPublicBusinessInfo verifies that GET /business-info returns the public
// subset of business settings (no admin-only fields like currency_code,
// website_url, gift_card_expiry_months, voucher_type).
func TestGetPublicBusinessInfo(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET
business_name = 'Test Nail Salon',
business_address = '456 High Street',
business_phone = '+441234567890',
business_email = 'info@testsalon.com',
is_vat_registered = true,
vat_registration_number = 'GB123456789',
default_vat_rate = 20.00,
currency_code = 'GBP',
website_url = 'https://testsalon.com',
gift_card_expiry_months = 24,
voucher_type = 'SPV'
`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
// GetPublicBusinessInfo does NOT require admin auth — it's a public endpoint.
handler := http.HandlerFunc(GetPublicBusinessInfo)
req := httptest.NewRequest("GET", "/business-info", nil)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var info PublicBusinessInfo
if err := parseResponseBody(w, &info); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if info.BusinessName != "Test Nail Salon" {
t.Errorf("expected BusinessName 'Test Nail Salon', got '%s'", info.BusinessName)
}
if info.BusinessAddress != "456 High Street" {
t.Errorf("expected BusinessAddress '456 High Street', got '%s'", info.BusinessAddress)
}
if info.BusinessPhone == nil || *info.BusinessPhone != "+441234567890" {
t.Errorf("expected BusinessPhone '+441234567890', got %v", info.BusinessPhone)
}
if info.BusinessEmail == nil || *info.BusinessEmail != "info@testsalon.com" {
t.Errorf("expected BusinessEmail 'info@testsalon.com', got %v", info.BusinessEmail)
}
if !info.IsVATRegistered {
t.Errorf("expected IsVATRegistered to be true")
}
if info.VATRegistrationNumber == nil || *info.VATRegistrationNumber != "GB123456789" {
t.Errorf("expected VATRegistrationNumber 'GB123456789', got %v", info.VATRegistrationNumber)
}
if info.DefaultVATRate != 20.00 {
t.Errorf("expected DefaultVATRate 20.00, got %f", info.DefaultVATRate)
}
}
// TestGetPublicBusinessInfo_NotAdminSafe verifies that the public info endpoint
// does NOT expose admin-only fields (currency_code, website_url,
// gift_card_expiry_months, voucher_type).
func TestGetPublicBusinessInfo_NotAdminSafe(t *testing.T) {
t.Parallel()
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET
business_name = 'Test Salon', business_address = '123 St',
currency_code = 'GBP', website_url = 'https://secret.admin.url',
gift_card_expiry_months = 48, voucher_type = 'MPV'
`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
handler := http.HandlerFunc(GetPublicBusinessInfo)
req := httptest.NewRequest("GET", "/business-info", nil)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
bodyStr := w.Body.String()
// Admin-only fields MUST NOT appear in the JSON response
if strings.Contains(bodyStr, "currency_code") {
t.Error("expected currency_code to be excluded from public info response")
}
if strings.Contains(bodyStr, "website_url") {
t.Error("expected website_url to be excluded from public info response")
}
if strings.Contains(bodyStr, "gift_card_expiry_months") {
t.Error("expected gift_card_expiry_months to be excluded from public info response")
}
if strings.Contains(bodyStr, "voucher_type") {
t.Error("expected voucher_type to be excluded from public info response")
}
}
// ─── GET ─────────────────────────────────────────────────────────────────────
// TestGetBusinessSettings verifies that GET /api/admin/settings returns the
// current business settings row.
func TestGetBusinessSettings(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET business_name = 'Test Salon', business_address = '123 Test St', currency_code = 'GBP', gift_card_expiry_months = 12, voucher_type = 'SPV'`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
handler := http.HandlerFunc(GetBusinessSettings)
w := makeAdminRequest(handler, "GET", "/api/admin/settings", nil, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessName != "Test Salon" {
t.Errorf("expected BusinessName 'Test Salon', got '%s'", s.BusinessName)
}
if s.BusinessAddress != "123 Test St" {
t.Errorf("expected BusinessAddress '123 Test St', got '%s'", s.BusinessAddress)
}
if s.CurrencyCode != "GBP" {
t.Errorf("expected CurrencyCode 'GBP', got '%s'", s.CurrencyCode)
}
if s.GiftCardExpiryMonths != 12 {
t.Errorf("expected GiftCardExpiryMonths 12, got %d", s.GiftCardExpiryMonths)
}
if s.VoucherType != "SPV" {
t.Errorf("expected VoucherType 'SPV', got '%s'", s.VoucherType)
}
}
// ─── Successful updates ──────────────────────────────────────────────────────
// TestUpdateBusinessSettings_SingleField verifies updating a single field (business_name).
func TestUpdateBusinessSettings_SingleField(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessName: stringPtr("Updated Salon Name"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessName != "Updated Salon Name" {
t.Errorf("expected BusinessName 'Updated Salon Name', got '%s'", s.BusinessName)
}
}
// TestUpdateBusinessSettings_MultipleFields verifies that updating several fields
// at once works correctly.
func TestUpdateBusinessSettings_MultipleFields(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessName: stringPtr("Multi Update Salon"),
BusinessAddress: stringPtr("456 New St"),
BusinessPhone: stringPtr("+441234567890"),
BusinessEmail: stringPtr("salon@example.com"),
WebsiteURL: stringPtr("https://salon.example.com"),
GiftCardExpiryMonths: intPtr(24),
VoucherType: stringPtr("MPV"),
DefaultVATRate: float64Ptr(20.00),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessName != "Multi Update Salon" {
t.Errorf("expected BusinessName 'Multi Update Salon', got '%s'", s.BusinessName)
}
if s.BusinessAddress != "456 New St" {
t.Errorf("expected BusinessAddress '456 New St', got '%s'", s.BusinessAddress)
}
if s.BusinessPhone == nil || *s.BusinessPhone != "+441234567890" {
t.Errorf("expected BusinessPhone '+441234567890', got %v", s.BusinessPhone)
}
if s.BusinessEmail == nil || *s.BusinessEmail != "salon@example.com" {
t.Errorf("expected BusinessEmail 'salon@example.com', got %v", s.BusinessEmail)
}
if s.WebsiteURL == nil || *s.WebsiteURL != "https://salon.example.com" {
t.Errorf("expected WebsiteURL 'https://salon.example.com', got %v", s.WebsiteURL)
}
if s.GiftCardExpiryMonths != 24 {
t.Errorf("expected GiftCardExpiryMonths 24, got %d", s.GiftCardExpiryMonths)
}
if s.VoucherType != "MPV" {
t.Errorf("expected VoucherType 'MPV', got '%s'", s.VoucherType)
}
}
// TestUpdateBusinessSettings_PartialUpdate verifies that updating a single field
// leaves other fields unchanged.
func TestUpdateBusinessSettings_PartialUpdate(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET business_name = 'Test Salon', business_address = '123 Test St', currency_code = 'GBP', gift_card_expiry_months = 12, voucher_type = 'SPV'`)
if err != nil {
t.Fatalf("failed to seed business settings: %v", err)
}
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
GiftCardExpiryMonths: intPtr(36),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.GiftCardExpiryMonths != 36 {
t.Errorf("expected GiftCardExpiryMonths 36, got %d", s.GiftCardExpiryMonths)
}
if s.BusinessName != "Test Salon" {
t.Errorf("expected BusinessName 'Test Salon' (unchanged), got '%s'", s.BusinessName)
}
if s.BusinessAddress != "123 Test St" {
t.Errorf("expected BusinessAddress '123 Test St' (unchanged), got '%s'", s.BusinessAddress)
}
if s.VoucherType != "SPV" {
t.Errorf("expected VoucherType 'SPV' (unchanged), got '%s'", s.VoucherType)
}
if s.CurrencyCode != "GBP" {
t.Errorf("expected CurrencyCode 'GBP' (unchanged), got '%s'", s.CurrencyCode)
}
}
// ─── Empty request ───────────────────────────────────────────────────────────
// TestUpdateBusinessSettings_NoFields verifies that an empty request body
// (no fields to update) returns 400.
func TestUpdateBusinessSettings_NoFields(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", UpdateBusinessSettingsRequest{}, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
if w.Body.String() != "No fields to update\n" {
t.Errorf("unexpected error message: %s", w.Body.String())
}
}
// ─── business_name validation ────────────────────────────────────────────────
// TestUpdateBusinessSettings_Name_Empty rejects an empty business_name.
func TestUpdateBusinessSettings_Name_Empty(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessName: stringPtr(""),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for empty name, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "business_name") {
t.Errorf("expected error about business_name, got: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_Name_TooLong rejects a business_name > 255 chars.
func TestUpdateBusinessSettings_Name_TooLong(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
longName := strings.Repeat("A", 256)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessName: stringPtr(longName),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for long name, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "business_name") {
t.Errorf("expected error about business_name, got: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_Name_Boundary accepts a 255-char name (DB max).
func TestUpdateBusinessSettings_Name_Boundary(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
name := strings.Repeat("A", 255)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessName: stringPtr(name),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for 255-char name, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessName != name {
t.Errorf("expected business name to match, got %q", s.BusinessName)
}
}
// ─── business_address validation ─────────────────────────────────────────────
// TestUpdateBusinessSettings_Address_Empty rejects an empty business_address.
func TestUpdateBusinessSettings_Address_Empty(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessAddress: stringPtr(""),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for empty address, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "business_address") {
t.Errorf("expected error about business_address, got: %s", w.Body.String())
}
}
// ─── business_phone validation ───────────────────────────────────────────────
// TestUpdateBusinessSettings_Phone_TooLong rejects a phone > 20 chars.
func TestUpdateBusinessSettings_Phone_TooLong(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessPhone: stringPtr("+44" + strings.Repeat("1", 18)),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for long phone, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "business_phone") {
t.Errorf("expected error about business_phone, got: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_Phone_Boundary accepts a 20-char phone (DB max).
func TestUpdateBusinessSettings_Phone_Boundary(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
phone := "+44" + strings.Repeat("1", 16) // 19 chars — within limit
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessPhone: stringPtr(phone),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for valid phone, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessPhone == nil || *s.BusinessPhone != phone {
t.Errorf("expected phone %q, got %v", phone, s.BusinessPhone)
}
}
// TestUpdateBusinessSettings_Phone_Null clears phone by setting it to null.
func TestUpdateBusinessSettings_Phone_Null(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
// Seed a phone value first
_, err := tx.Exec(ctx, `UPDATE business_settings SET business_phone = '+441234567890'`)
if err != nil {
t.Fatalf("failed to seed phone: %v", err)
}
handler := http.HandlerFunc(UpdateBusinessSettings)
nullPhone := ""
body := UpdateBusinessSettingsRequest{
BusinessPhone: &nullPhone,
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for clearing phone, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessPhone == nil || *s.BusinessPhone != "" {
t.Errorf("expected BusinessPhone to be cleared, got %v", s.BusinessPhone)
}
}
// ─── business_email validation ───────────────────────────────────────────────
// TestUpdateBusinessSettings_Email_TooLong rejects an email > 254 chars.
func TestUpdateBusinessSettings_Email_TooLong(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
local := strings.Repeat("a", 250)
email := local + "@b.co"
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
BusinessEmail: stringPtr(email),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for long email, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "business_email") {
t.Errorf("expected error about business_email, got: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_Email_Null clears email by setting it to null.
func TestUpdateBusinessSettings_Email_Null(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET business_email = 'test@example.com'`)
if err != nil {
t.Fatalf("failed to seed email: %v", err)
}
handler := http.HandlerFunc(UpdateBusinessSettings)
nullEmail := ""
body := UpdateBusinessSettingsRequest{
BusinessEmail: &nullEmail,
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for clearing email, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.BusinessEmail == nil || *s.BusinessEmail != "" {
t.Errorf("expected BusinessEmail to be cleared, got %v", s.BusinessEmail)
}
}
// ─── vat_registration_number validation ──────────────────────────────────────
// TestUpdateBusinessSettings_VatNumber_TooLong rejects a VAT number > 20 chars.
func TestUpdateBusinessSettings_VatNumber_TooLong(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VATRegistrationNumber: stringPtr(strings.Repeat("A", 21)),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for long VAT number, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "vat_registration_number") {
t.Errorf("expected error about vat_registration_number, got: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_VatNumber_Boundary accepts a valid UK VAT number.
func TestUpdateBusinessSettings_VatNumber_Boundary(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
// Standard UK VAT number: GB + 9 digits
vatNum := "GB123456789"
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VATRegistrationNumber: stringPtr(vatNum),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for valid VAT number, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.VATRegistrationNumber == nil || *s.VATRegistrationNumber != vatNum {
t.Errorf("expected VAT number %q, got %v", vatNum, s.VATRegistrationNumber)
}
// UK branch VAT number: GB + 12 digits
branchVat := "GB123456789012"
body2 := UpdateBusinessSettingsRequest{
VATRegistrationNumber: stringPtr(branchVat),
}
w2 := makeAdminRequest(handler, "PUT", "/api/admin/settings", body2, ctx)
if w2.Code != http.StatusOK {
t.Errorf("expected status 200 for valid branch VAT number, got %d. body: %s", w2.Code, w2.Body.String())
}
}
// ─── website_url validation ──────────────────────────────────────────────────
// TestUpdateBusinessSettings_Website_NoScheme rejects a URL without http/https.
func TestUpdateBusinessSettings_Website_NoScheme(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
WebsiteURL: stringPtr("www.example.com"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for no scheme, got %d. body: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "website_url") {
t.Errorf("expected error about website_url, got: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_Website_FTPScheme rejects a URL with ftp://.
func TestUpdateBusinessSettings_Website_FTPScheme(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
WebsiteURL: stringPtr("ftp://files.example.com"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for ftp scheme, got %d. body: %s", w.Code, w.Body.String())
}
}
// TestUpdateBusinessSettings_Website_EmptyString clears the website URL.
func TestUpdateBusinessSettings_Website_EmptyString(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET website_url = 'https://example.com'`)
if err != nil {
t.Fatalf("failed to seed website: %v", err)
}
handler := http.HandlerFunc(UpdateBusinessSettings)
empty := ""
body := UpdateBusinessSettingsRequest{
WebsiteURL: &empty,
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for clearing website, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.WebsiteURL != nil && *s.WebsiteURL != "" {
t.Errorf("expected WebsiteURL to be cleared, got %v", *s.WebsiteURL)
}
}
// TestUpdateBusinessSettings_Website_ValidHTTPS accepts a valid https URL.
func TestUpdateBusinessSettings_Website_ValidHTTPS(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
WebsiteURL: stringPtr("https://www.example.com"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for valid website, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.WebsiteURL == nil || *s.WebsiteURL != "https://www.example.com" {
t.Errorf("expected website URL, got %v", s.WebsiteURL)
}
}
// TestUpdateBusinessSettings_Website_ValidHTTP accepts a valid http URL.
func TestUpdateBusinessSettings_Website_ValidHTTP(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
WebsiteURL: stringPtr("http://localhost:3000"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for valid website, got %d. body: %s", w.Code, w.Body.String())
}
}
// ─── voucher_type validation ─────────────────────────────────────────────────
// TestUpdateBusinessSettings_InvalidVoucherType verifies that an invalid
// voucher_type value returns 400.
func TestUpdateBusinessSettings_InvalidVoucherType(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VoucherType: stringPtr("INVALID"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
if w.Body.String() != "voucher_type must be 'SPV' or 'MPV'\n" {
t.Errorf("unexpected error message: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_InvalidVoucherType_Empty rejects an empty voucher_type.
func TestUpdateBusinessSettings_InvalidVoucherType_Empty(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VoucherType: stringPtr(""),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for empty voucher_type, got %d. body: %s", w.Code, w.Body.String())
}
}
// TestUpdateBusinessSettings_VoucherType_SPV accepts SPV.
func TestUpdateBusinessSettings_VoucherType_SPV(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VoucherType: stringPtr("SPV"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for SPV, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.VoucherType != "SPV" {
t.Errorf("expected VoucherType 'SPV', got '%s'", s.VoucherType)
}
}
// TestUpdateBusinessSettings_VoucherType_MPV accepts MPV.
func TestUpdateBusinessSettings_VoucherType_MPV(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
VoucherType: stringPtr("MPV"),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for MPV, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.VoucherType != "MPV" {
t.Errorf("expected VoucherType 'MPV', got '%s'", s.VoucherType)
}
}
// ─── gift_card_expiry_months validation ──────────────────────────────────────
// TestUpdateBusinessSettings_NegativeExpiryMonths verifies that a
// gift_card_expiry_months value less than 1 returns 400.
func TestUpdateBusinessSettings_NegativeExpiryMonths(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
GiftCardExpiryMonths: intPtr(0),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d. body: %s", w.Code, w.Body.String())
}
if w.Body.String() != "gift_card_expiry_months must be at least 1\n" {
t.Errorf("unexpected error message: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_ExpiryMonths_Negative rejects negative values.
func TestUpdateBusinessSettings_ExpiryMonths_Negative(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
GiftCardExpiryMonths: intPtr(-5),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for negative expiry, got %d. body: %s", w.Code, w.Body.String())
}
if w.Body.String() != "gift_card_expiry_months must be at least 1\n" {
t.Errorf("unexpected error message: %s", w.Body.String())
}
}
// TestUpdateBusinessSettings_ExpiryMonths_LargeValue accepts a large but valid value.
func TestUpdateBusinessSettings_ExpiryMonths_LargeValue(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
GiftCardExpiryMonths: intPtr(9999),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for large expiry, got %d. body: %s", w.Code, w.Body.String())
}
}
// ─── default_vat_rate validation ─────────────────────────────────────────────
// TestUpdateBusinessSettings_InvalidVATRate verifies that a default_vat_rate
// outside the 0-100 range returns 400.
func TestUpdateBusinessSettings_InvalidVATRate(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(-1),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for negative rate, got %d. body: %s", w.Code, w.Body.String())
}
if w.Body.String() != "default_vat_rate must be between 0 and 100\n" {
t.Errorf("unexpected error message: %s", w.Body.String())
}
body2 := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(101),
}
w2 := makeAdminRequest(handler, "PUT", "/api/admin/settings", body2, ctx)
if w2.Code != http.StatusBadRequest {
t.Errorf("expected status 400 for rate > 100, got %d. body: %s", w2.Code, w2.Body.String())
}
if w2.Body.String() != "default_vat_rate must be between 0 and 100\n" {
t.Errorf("unexpected error message: %s", w2.Body.String())
}
}
// TestUpdateBusinessSettings_VATRate_Boundary_Zero accepts 0% VAT rate.
func TestUpdateBusinessSettings_VATRate_Boundary_Zero(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(0),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for 0%% VAT rate, got %d. body: %s", w.Code, w.Body.String())
}
}
// TestUpdateBusinessSettings_VATRate_Boundary_100 accepts 100% VAT rate.
func TestUpdateBusinessSettings_VATRate_Boundary_100(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(100),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for 100%% VAT rate, got %d. body: %s", w.Code, w.Body.String())
}
}
// TestUpdateBusinessSettings_VATRate_Fractional accepts fractional VAT rates.
func TestUpdateBusinessSettings_VATRate_Fractional(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
body := UpdateBusinessSettingsRequest{
DefaultVATRate: float64Ptr(5.5),
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 for 5.5%% VAT rate, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.DefaultVATRate != 5.5 {
t.Errorf("expected DefaultVATRate 5.5, got %f", s.DefaultVATRate)
}
}
// ─── is_vat_registered toggle ────────────────────────────────────────────────
// TestUpdateBusinessSettings_EnableVATRegistration enables VAT registration.
func TestUpdateBusinessSettings_EnableVATRegistration(t *testing.T) {
ctx, _ := testutils.SetupTestTx(t)
handler := http.HandlerFunc(UpdateBusinessSettings)
trueVal := true
vatRate := 20.0
vatNum := "GB123456789"
body := UpdateBusinessSettingsRequest{
IsVATRegistered: &trueVal,
DefaultVATRate: &vatRate,
VATRegistrationNumber: &vatNum,
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 when enabling VAT, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if !s.IsVATRegistered {
t.Errorf("expected IsVATRegistered to be true")
}
if s.DefaultVATRate != 20.0 {
t.Errorf("expected DefaultVATRate 20.0, got %f", s.DefaultVATRate)
}
if s.VATRegistrationNumber == nil || *s.VATRegistrationNumber != "GB123456789" {
t.Errorf("expected VATRegistrationNumber 'GB123456789', got %v", s.VATRegistrationNumber)
}
}
// TestUpdateBusinessSettings_DisableVATRegistration disables VAT registration.
// ─── validateURL tests ───────────────────────────────────────────────────────
func TestValidateURL_ValidHTTPS(t *testing.T) {
if err := validateURL("https://example.com"); err != nil {
t.Errorf("expected no error, got %v", err)
}
}
func TestValidateURL_ValidHTTP(t *testing.T) {
if err := validateURL("http://example.com"); err != nil {
t.Errorf("expected no error, got %v", err)
}
}
func TestValidateURL_ValidHTTP_Localhost(t *testing.T) {
if err := validateURL("http://localhost:3000"); err != nil {
t.Errorf("expected no error, got %v", err)
}
}
func TestValidateURL_ValidHTTPS_WithPath(t *testing.T) {
if err := validateURL("https://example.com/path/to/page?q=1"); err != nil {
t.Errorf("expected no error, got %v", err)
}
}
func TestValidateURL_NoScheme(t *testing.T) {
err := validateURL("www.example.com")
if err == nil {
t.Fatal("expected error for missing scheme")
}
if !strings.Contains(err.Error(), "http") {
t.Errorf("expected error mentioning http/https, got: %v", err)
}
}
func TestValidateURL_FTPScheme(t *testing.T) {
err := validateURL("ftp://files.example.com")
if err == nil {
t.Fatal("expected error for ftp scheme")
}
if !strings.Contains(err.Error(), "http") {
t.Errorf("expected error mentioning http/https, got: %v", err)
}
}
func TestValidateURL_EmptyHost(t *testing.T) {
err := validateURL("https://")
if err == nil {
t.Fatal("expected error for empty host")
}
if !strings.Contains(err.Error(), "host") {
t.Errorf("expected error about host, got: %v", err)
}
}
func TestValidateURL_InvalidString(t *testing.T) {
err := validateURL("not a url at all")
if err == nil {
t.Fatal("expected error for invalid URL")
}
}
func TestValidateURL_EmptyString(t *testing.T) {
err := validateURL("")
// url.Parse("") returns a valid url.URL with empty Scheme
// So validateURL should reject it (no scheme, no host)
if err == nil {
t.Fatal("expected error for empty string")
}
}
func TestValidateURL_HTTPSOnlyScheme(t *testing.T) {
if err := validateURL("https://"); err == nil {
t.Fatal("expected error for https:// with no host")
}
}
func TestValidateURL_IPAddress(t *testing.T) {
if err := validateURL("https://192.168.1.1"); err != nil {
t.Errorf("expected no error for HTTPS IP, got %v", err)
}
}
// ─── Disable VAT registration ─────────────────────────────────────────────────
func TestUpdateBusinessSettings_DisableVATRegistration(t *testing.T) {
ctx, tx := testutils.SetupTestTx(t)
_, err := tx.Exec(ctx, `UPDATE business_settings SET is_vat_registered = TRUE, default_vat_rate = 20.00`)
if err != nil {
t.Fatalf("failed to enable VAT: %v", err)
}
handler := http.HandlerFunc(UpdateBusinessSettings)
falseVal := false
body := UpdateBusinessSettingsRequest{
IsVATRegistered: &falseVal,
}
w := makeAdminRequest(handler, "PUT", "/api/admin/settings", body, ctx)
if w.Code != http.StatusOK {
t.Errorf("expected status 200 when disabling VAT, got %d. body: %s", w.Code, w.Body.String())
}
var s BusinessSettings
if err := parseResponseBody(w, &s); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if s.IsVATRegistered {
t.Errorf("expected IsVATRegistered to be false")
}
}
func TestGetPublicBusinessInfo_DBError(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
req := httptest.NewRequest("GET", "/business-info", nil).WithContext(ctx)
w := httptest.NewRecorder()
GetPublicBusinessInfo(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestGetBusinessSettings_DBError(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
req := httptest.NewRequest("GET", "/api/admin/settings", nil)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
GetBusinessSettings(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
}