Files
Crussell/backend/handlers/admin/settings_test.go
T
popertotsandSisyphus e4b9003439 refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:43:50 +01:00

1041 lines
36 KiB
Go

//go:build test
// +build test
package admin
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"crussell/testutils"
)
func intPtr(i int) *int { return &i }
func float64Ptr(f float64) *float64 { return &f }
func boolPtr(b bool) *bool { return &b }
// ─── 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")
}
}