test: add coverage tests across backend + fix mock for PENDING checkout support
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
CI / Nginx config check (push) Successful in 13s
CI / Env docs check (push) Successful in 15s
CI / Docker compose check (push) Successful in 15s
CI / Frontend major deps (push) Failing after 24s
CI / Frontend deps check (push) Successful in 30s
CI / Secrets scan (push) Successful in 38s
CI / Go build (push) Successful in 39s
CI / Frontend build (push) Successful in 1m3s
CI / Knip (push) Successful in 45s
CI / Go vet (prod) (push) Failing after 1m42s
CI / Frontend a11y check (push) Successful in 2m34s
CI / Go vet (dev) (push) Successful in 2m29s
CI / Staticcheck (prod) (push) Failing after 2m38s
CI / go mod tidy (push) Successful in 1m3s
CI / Staticcheck (dev) (push) Successful in 2m55s
CI / Frontend QC (audit) (push) Successful in 51s
CI / golangci-lint (push) Successful in 3m22s
CI / Go vulnerabilities (push) Successful in 1m26s
CI / Frontend QC (typecheck) (push) Successful in 2m18s
CI / Security scan (prod) (push) Successful in 4m18s
CI / Security scan (dev) (push) Successful in 4m40s
CI / Tests (prod) (push) Has been skipped
CI / Tests (dev) (push) Has been skipped
CI / Race (prod) (push) Has been skipped
CI / Race (dev) (push) Has been skipped
CI / Frontend QC (lint) (push) Successful in 2m18s
CI / Svelte strict check (push) Successful in 43s
New test files cover previously untested paths across DAV, validators, S3, Square, mw, bookings, user, and payments packages. Includes mock fix: HoldCheckouts flag on MockClient allows tests to pause auto-complete goroutine for testing PENDING checkout states. Coverage: 50.4% → 65.0% (+14.6pp)
This commit is contained in:
@@ -442,6 +442,17 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var existingCount int
|
||||
err = tx.QueryRow(r.Context(), `SELECT COUNT(*) FROM services WHERE name = $1 AND is_active = true`, name.String).Scan(&existingCount)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to check for duplicate name: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if existingCount > 0 {
|
||||
http.Error(w, "A service with this name already exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
var newServiceID string
|
||||
err = tx.QueryRow(r.Context(), `
|
||||
INSERT INTO services (name, description, price, duration_minutes, minimum_age_required, created_by)
|
||||
@@ -449,10 +460,6 @@ func PromoteCustomService(w http.ResponseWriter, r *http.Request) {
|
||||
RETURNING id
|
||||
`, name.String, desc, price, durationMinutes, minimumAgeRequired, createdBy).Scan(&newServiceID)
|
||||
if err != nil {
|
||||
if err.Error() == "pq: duplicate key value violates unique constraint" || err.Error() == "duplicate key value violates unique constraint" {
|
||||
http.Error(w, "A service with this name already exists", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Failed to create service: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -660,6 +660,41 @@ func TestCustomServices_Promote(t *testing.T) {
|
||||
defer fixtures.DeleteService(tx, newServiceID)
|
||||
}
|
||||
|
||||
func TestCustomServices_Promote_DuplicateName(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin user: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteUser(tx, adminID)
|
||||
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteService(tx, serviceID)
|
||||
|
||||
csID, err := fixtures.CreateTestCustomService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create custom service: %v", err)
|
||||
}
|
||||
defer fixtures.DeleteCustomService(tx, csID)
|
||||
|
||||
_, err = tx.Exec(context.Background(),
|
||||
"UPDATE custom_services SET name = 'Test Service' WHERE id = $1", csID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to update custom service name: %v", err)
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(PromoteCustomService)
|
||||
w := makeCustomServiceRequest(handler, "POST", "/api/admin/custom-services/"+csID+"/promote", nil, adminID, ctx)
|
||||
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestCustomServices_Promote_NotFound verifies that promoting a non-existent custom service returns 404.
|
||||
func TestCustomServices_Promote_NotFound(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
@@ -527,3 +527,442 @@ func TestGetCampaignStats_NotFound(t *testing.T) {
|
||||
t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Additional Update Coverage Tests
|
||||
// =============================================================================
|
||||
|
||||
// TestUpdateDiscountCampaign_EmptyID verifies empty campaign ID returns 404.
|
||||
func TestUpdateDiscountCampaign_EmptyID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
req := UpdateCampaignRequest{Name: stringPtr("Test")}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/", req, ctx, adminID)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for empty ID, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_NonExistentValidID verifies a valid-format but
|
||||
// non-existent campaign ID returns 404.
|
||||
func TestUpdateDiscountCampaign_NonExistentValidID(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
newName := "Test"
|
||||
req := UpdateCampaignRequest{Name: &newName}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/aabbccddee00", req, ctx, adminID)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for nonexistent campaign, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_InvalidJSON verifies malformed JSON body returns 400.
|
||||
func TestUpdateDiscountCampaign_InvalidJSON(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
req := httptest.NewRequest("PUT", "/api/admin/discount-campaigns/"+campaignID, strings.NewReader("not json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", campaignID)
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
||||
ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
|
||||
ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
http.HandlerFunc(UpdateDiscountCampaign).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for invalid JSON, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_ValidatorError verifies struct validation errors return 400.
|
||||
func TestUpdateDiscountCampaign_ValidatorError(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
// Empty name (non-nil pointer, empty string) should fail min=1 validation
|
||||
emptyName := ""
|
||||
req := UpdateCampaignRequest{Name: &emptyName}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for validation error, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_InvalidDiscountPercent verifies discount_percent
|
||||
// out of range returns 400.
|
||||
func TestUpdateDiscountCampaign_InvalidDiscountPercent(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
|
||||
t.Run("zero_percent", func(t *testing.T) {
|
||||
zero := 0.0
|
||||
req := UpdateCampaignRequest{DiscountPercent: &zero}
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for zero discount, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("over_100", func(t *testing.T) {
|
||||
over := 150.0
|
||||
req := UpdateCampaignRequest{DiscountPercent: &over}
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for discount >100, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_InvalidStartDateFormat verifies bad start_date
|
||||
// format returns 400.
|
||||
func TestUpdateDiscountCampaign_InvalidStartDateFormat(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertTimeBasedCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
badDate := "not-a-date"
|
||||
req := UpdateCampaignRequest{StartDate: &badDate}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for bad start date, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_InvalidEndDateFormat verifies bad end_date
|
||||
// format returns 400.
|
||||
func TestUpdateDiscountCampaign_InvalidEndDateFormat(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertTimeBasedCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
badDate := "not-a-date"
|
||||
req := UpdateCampaignRequest{EndDate: &badDate}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for bad end date, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_InvalidMilestoneValue verifies milestone_value
|
||||
// out of range returns 400.
|
||||
func TestUpdateDiscountCampaign_InvalidMilestoneValue(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
|
||||
t.Run("zero_value", func(t *testing.T) {
|
||||
zero := 0
|
||||
req := UpdateCampaignRequest{MilestoneValue: &zero}
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for zero milestone value, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative_value", func(t *testing.T) {
|
||||
neg := -1
|
||||
req := UpdateCampaignRequest{MilestoneValue: &neg}
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for negative milestone value, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateDescription verifies updating description
|
||||
// only.
|
||||
func TestUpdateDiscountCampaign_UpdateDescription(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
desc := "New description"
|
||||
req := UpdateCampaignRequest{Description: &desc}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.Description == nil || *campaign.Description != "New description" {
|
||||
t.Errorf("expected description 'New description', got %v", campaign.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateDiscountPercent verifies updating discount
|
||||
// percent successfully.
|
||||
func TestUpdateDiscountCampaign_UpdateDiscountPercent(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
dp := 25.0
|
||||
req := UpdateCampaignRequest{DiscountPercent: &dp}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.DiscountPercent != 25 {
|
||||
t.Errorf("expected discount percent 25, got %f", campaign.DiscountPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateScope verifies updating scope successfully.
|
||||
func TestUpdateDiscountCampaign_UpdateScope(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
scope := "all_bookings"
|
||||
req := UpdateCampaignRequest{Scope: &scope}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.Scope == nil || *campaign.Scope != "all_bookings" {
|
||||
t.Errorf("expected scope 'all_bookings', got %v", campaign.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateDates verifies updating start_date and
|
||||
// end_date successfully.
|
||||
func TestUpdateDiscountCampaign_UpdateDates(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertTimeBasedCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
startDate := fmt.Sprintf("%sZ", clock.Now().Add(24*time.Hour).Format("2006-01-02T15:04:05"))
|
||||
endDate := fmt.Sprintf("%sZ", clock.Now().Add(14*24*time.Hour).Format("2006-01-02T15:04:05"))
|
||||
req := UpdateCampaignRequest{
|
||||
StartDate: &startDate,
|
||||
EndDate: &endDate,
|
||||
}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.StartDate == nil {
|
||||
t.Error("expected start_date to be set")
|
||||
}
|
||||
if campaign.EndDate == nil {
|
||||
t.Error("expected end_date to be set")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateMilestoneFields verifies updating milestone
|
||||
// fields successfully.
|
||||
func TestUpdateDiscountCampaign_UpdateMilestoneFields(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
mt := "per_user_booking_count"
|
||||
mv := 10
|
||||
mu := "bookings"
|
||||
req := UpdateCampaignRequest{
|
||||
MilestoneType: &mt,
|
||||
MilestoneValue: &mv,
|
||||
MilestoneUnit: &mu,
|
||||
}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.MilestoneType == nil || *campaign.MilestoneType != "per_user_booking_count" {
|
||||
t.Errorf("expected milestone type 'per_user_booking_count', got %v", campaign.MilestoneType)
|
||||
}
|
||||
if campaign.MilestoneValue == nil || *campaign.MilestoneValue != 10 {
|
||||
t.Errorf("expected milestone value 10, got %v", campaign.MilestoneValue)
|
||||
}
|
||||
if campaign.MilestoneUnit == nil || *campaign.MilestoneUnit != "bookings" {
|
||||
t.Errorf("expected milestone unit 'bookings', got %v", campaign.MilestoneUnit)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateMaxRedemptions verifies updating
|
||||
// max_redemptions successfully.
|
||||
func TestUpdateDiscountCampaign_UpdateMaxRedemptions(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Test", 10, "draft")
|
||||
|
||||
mr := 200
|
||||
req := UpdateCampaignRequest{MaxRedemptions: &mr}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.MaxRedemptions == nil || *campaign.MaxRedemptions != 200 {
|
||||
t.Errorf("expected max redemptions 200, got %v", campaign.MaxRedemptions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateDiscountCampaign_UpdateMultipleFields verifies updating several
|
||||
// fields at once: name, description, discount_percent, status, max_redemptions.
|
||||
func TestUpdateDiscountCampaign_UpdateMultipleFields(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
adminID, err := fixtures.CreateTestAdminUser(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create admin: %v", err)
|
||||
}
|
||||
testAdminID = adminID
|
||||
|
||||
campaignID := insertMilestoneCampaign(t, ctx, tx, adminID, "Original Name", 10, "draft")
|
||||
|
||||
newName := "Updated Name"
|
||||
newDesc := "Updated description"
|
||||
newDP := 20.0
|
||||
newStatus := "active"
|
||||
newMR := 50
|
||||
req := UpdateCampaignRequest{
|
||||
Name: &newName,
|
||||
Description: &newDesc,
|
||||
DiscountPercent: &newDP,
|
||||
Status: &newStatus,
|
||||
MaxRedemptions: &newMR,
|
||||
}
|
||||
handler := http.HandlerFunc(UpdateDiscountCampaign)
|
||||
w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req, ctx, adminID)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var campaign DiscountCampaign
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &campaign); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
if campaign.Name != "Updated Name" {
|
||||
t.Errorf("expected name 'Updated Name', got %q", campaign.Name)
|
||||
}
|
||||
if campaign.Description == nil || *campaign.Description != "Updated description" {
|
||||
t.Errorf("expected description 'Updated description', got %v", campaign.Description)
|
||||
}
|
||||
if campaign.DiscountPercent != 20 {
|
||||
t.Errorf("expected discount percent 20, got %f", campaign.DiscountPercent)
|
||||
}
|
||||
if campaign.Status != "active" {
|
||||
t.Errorf("expected status 'active', got %q", campaign.Status)
|
||||
}
|
||||
if campaign.MaxRedemptions == nil || *campaign.MaxRedemptions != 50 {
|
||||
t.Errorf("expected max redemptions 50, got %v", campaign.MaxRedemptions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"crussell/testutils"
|
||||
"crussell/testutils/fixtures"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func TestPatchTests_CRUD(t *testing.T) {
|
||||
@@ -63,3 +68,266 @@ func TestPatchTests_CRUD(t *testing.T) {
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Create Patch Test - Validation Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestCreatePatchTest_ValidationErrors(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
|
||||
handler := http.HandlerFunc(CreatePatchTest)
|
||||
|
||||
t.Run("empty_name", func(t *testing.T) {
|
||||
req := CreatePatchTestRequest{
|
||||
Name: "",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
}
|
||||
w := makeAdminRequest(handler, "POST", "/api/admin/patch-tests", req, ctx)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for empty name, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
_, err := tx.Exec(ctx, `SELECT 1`)
|
||||
if err != nil {
|
||||
t.Fatalf("tx check failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Update Patch Test - Error Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestUpdatePatchTest_NotFound(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
newName := "Updated"
|
||||
updateReq := UpdatePatchTestRequest{Name: &newName}
|
||||
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/nonexistent-id", updateReq, ctx)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for nonexistent patch test, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Delete Patch Test - Error Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestDeletePatchTest_NotFound(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
w = makeAdminRequest(http.HandlerFunc(DeletePatchTest), "DELETE", "/api/admin/patch-tests/nonexistent-id", nil, ctx)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404 for nonexistent patch test, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Update Patch Test - Individual Field Update Tests
|
||||
// =============================================================================
|
||||
|
||||
func TestUpdatePatchTest_UpdateDescription(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
parseResponseBody(w, &created)
|
||||
|
||||
updateReq := UpdatePatchTestRequest{Description: strPtr("Updated Description")}
|
||||
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq, ctx)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatchTest_UpdateNoticeDuration(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
parseResponseBody(w, &created)
|
||||
|
||||
noticeDuration := 48
|
||||
updateReq := UpdatePatchTestRequest{NoticeDurationHours: ¬iceDuration}
|
||||
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq, ctx)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatchTest_UpdateExpiryMonths(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
parseResponseBody(w, &created)
|
||||
|
||||
expiryMonths := 12
|
||||
updateReq := UpdatePatchTestRequest{ExpiryMonths: &expiryMonths}
|
||||
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq, ctx)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatchTest_UpdateServiceIDs(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
parseResponseBody(w, &created)
|
||||
|
||||
updateReq := UpdatePatchTestRequest{ServiceIDs: []string{serviceID}}
|
||||
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq, ctx)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatchTest_InvalidJSON(t *testing.T) {
|
||||
ctx, _ := testutils.SetupTestTx(t)
|
||||
|
||||
req := httptest.NewRequest("PUT", "/api/admin/patch-tests/abcdef123456", bytes.NewReader([]byte(`{invalid}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("id", "abcdef123456")
|
||||
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
http.HandlerFunc(UpdatePatchTest).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for invalid JSON, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePatchTest_ValidationError(t *testing.T) {
|
||||
ctx, tx := testutils.SetupTestTx(t)
|
||||
serviceID, err := fixtures.CreateTestService(tx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
createReq := CreatePatchTestRequest{
|
||||
Name: "Test",
|
||||
NoticeDurationHours: 24,
|
||||
ExpiryMonths: 6,
|
||||
ServiceIDs: []string{serviceID},
|
||||
}
|
||||
w := makeAdminRequest(http.HandlerFunc(CreatePatchTest), "POST", "/api/admin/patch-tests", createReq, ctx)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("failed to create patch test: %d", w.Code)
|
||||
}
|
||||
|
||||
var created struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
parseResponseBody(w, &created)
|
||||
|
||||
var longName string
|
||||
for i := 0; i < 201; i++ {
|
||||
longName += "a"
|
||||
}
|
||||
updateReq := UpdatePatchTestRequest{Name: &longName}
|
||||
w = makeAdminRequest(http.HandlerFunc(UpdatePatchTest), "PUT", "/api/admin/patch-tests/"+created.ID, updateReq, ctx)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 for validation error, got %d. body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
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 }
|
||||
@@ -1036,3 +1039,22 @@ func TestUpdateBusinessSettings_DisableVATRegistration(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user