//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) }