//go:build test // +build test package admin import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "crussell/db" "crussell/mw" "crussell/testutils/fixtures" "github.com/go-chi/chi/v5" ) func makeCampaignRequest(handler http.Handler, method, path string, body interface{}) *httptest.ResponseRecorder { var req *http.Request if body != nil { bodyBytes, _ := json.Marshal(body) req = httptest.NewRequest(method, path, strings.NewReader(string(bodyBytes))) req.Header.Set("Content-Type", "application/json") } else { req = httptest.NewRequest(method, path, nil) } rctx := chi.NewRouteContext() prefix := "/api/admin/discount-campaigns/" if strings.HasPrefix(path, prefix) { suffix := path[len(prefix):] if slashIdx := strings.Index(suffix, "/"); slashIdx >= 0 { rctx.URLParams.Add("id", suffix[:slashIdx]) } else if suffix != "" { rctx.URLParams.Add("id", suffix) } } ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx) ctx = context.WithValue(ctx, mw.UserIDKey, testAdminID) ctx = context.WithValue(ctx, mw.UserRoleKey, "admin") req = req.WithContext(ctx) w := httptest.NewRecorder() handler.ServeHTTP(w, req) return w } func insertTimeBasedCampaign(t *testing.T, name string, discount float64, status string) string { t.Helper() startDate := fmt.Sprintf("%sZ", time.Now().Add(-1*time.Hour).Format("2006-01-02T15:04:05")) endDate := fmt.Sprintf("%sZ", time.Now().Add(7*24*time.Hour).Format("2006-01-02T15:04:05")) var id string err := db.DB.QueryRow(context.Background(), ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date, created_by) VALUES ($1, 'time_based', $2, $3, $4::timestamptz, $5::timestamptz, $6) RETURNING id `, name, discount, status, startDate, endDate, testAdminID).Scan(&id) if err != nil { t.Fatalf("failed to insert time_based campaign: %v", err) } return id } func insertMilestoneCampaign(t *testing.T, name string, discount float64, status string) string { t.Helper() mt := "per_user_booking_count" mv := 5 mu := "bookings" var id string err := db.DB.QueryRow(context.Background(), ` INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, milestone_type, milestone_value, milestone_unit, created_by) VALUES ($1, 'milestone', $2, $3, $4, $5, $6, $7) RETURNING id `, name, discount, status, mt, mv, mu, testAdminID).Scan(&id) if err != nil { t.Fatalf("failed to insert milestone campaign: %v", err) } return id } // ============================================================================= // List Campaigns Tests // ============================================================================= func TestGetDiscountCampaigns_Empty(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() handler := http.HandlerFunc(GetDiscountCampaigns) w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String()) } var campaigns []DiscountCampaign if err := json.Unmarshal(w.Body.Bytes(), &campaigns); err != nil { t.Fatalf("failed to parse response: %v", err) } if len(campaigns) != 0 { t.Errorf("expected empty list, got %d items", len(campaigns)) } } func TestGetDiscountCampaigns_WithCampaigns(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() insertTimeBasedCampaign(t, "Summer Sale", 15, "active") handler := http.HandlerFunc(GetDiscountCampaigns) w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String()) } var campaigns []DiscountCampaign if err := json.Unmarshal(w.Body.Bytes(), &campaigns); err != nil { t.Fatalf("failed to parse response: %v", err) } if len(campaigns) != 1 { t.Fatalf("expected 1 campaign, got %d", len(campaigns)) } if campaigns[0].Name != "Summer Sale" { t.Errorf("expected 'Summer Sale', got %q", campaigns[0].Name) } if campaigns[0].DiscountPercent != 15 { t.Errorf("expected 15%% discount, got %.0f%%", campaigns[0].DiscountPercent) } if campaigns[0].CampaignType != "time_based" { t.Errorf("expected 'time_based', got %q", campaigns[0].CampaignType) } if campaigns[0].Status != "active" { t.Errorf("expected 'active', got %q", campaigns[0].Status) } } func TestGetDiscountCampaigns_FilterByStatus(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() insertMilestoneCampaign(t, "Draft Campaign", 10, "draft") insertMilestoneCampaign(t, "Active Campaign", 20, "active") insertMilestoneCampaign(t, "Cancelled Campaign", 5, "cancelled") handler := http.HandlerFunc(GetDiscountCampaigns) t.Run("filter_by_active", func(t *testing.T) { w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns?status=active", nil) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var campaigns []DiscountCampaign json.Unmarshal(w.Body.Bytes(), &campaigns) if len(campaigns) != 1 { t.Errorf("expected 1 active campaign, got %d", len(campaigns)) } }) t.Run("filter_by_invalid_status", func(t *testing.T) { w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns?status=invalid", nil) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid status filter, got %d", w.Code) } }) } // ============================================================================= // Create Campaign Tests // ============================================================================= func TestCreateDiscountCampaign_TimeBased(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() req := CreateCampaignRequest{ Name: "Summer Sale", CampaignType: "time_based", DiscountPercent: 15, StartDate: strPtr(fmt.Sprintf("%sZ", time.Now().Format("2006-01-02T15:04:05"))), EndDate: strPtr(fmt.Sprintf("%sZ", time.Now().Add(7*24*time.Hour).Format("2006-01-02T15:04:05"))), MaxRedemptions: intPtr(100), } handler := http.HandlerFunc(CreateDiscountCampaign) w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, 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 != "Summer Sale" { t.Errorf("expected 'Summer Sale', got %q", campaign.Name) } if campaign.Status != "draft" { t.Errorf("expected initial status 'draft', got %q", campaign.Status) } if campaign.ID == "" { t.Error("expected campaign ID to be set") } } func TestCreateDiscountCampaign_Milestone(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() mt := "per_user_booking_count" mv := 5 mu := "bookings" req := CreateCampaignRequest{ Name: "Loyalty Milestone", CampaignType: "milestone", DiscountPercent: 25, MilestoneType: &mt, MilestoneValue: &mv, MilestoneUnit: &mu, } handler := http.HandlerFunc(CreateDiscountCampaign) w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, 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.CampaignType != "milestone" { t.Errorf("expected 'milestone', got %q", campaign.CampaignType) } if campaign.MilestoneValue == nil || *campaign.MilestoneValue != 5 { t.Errorf("expected milestone value 5, got %v", campaign.MilestoneValue) } } func TestCreateDiscountCampaign_ValidationErrors(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() handler := http.HandlerFunc(CreateDiscountCampaign) t.Run("empty_name", func(t *testing.T) { req := CreateCampaignRequest{ Name: "", CampaignType: "time_based", DiscountPercent: 10, } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for empty name, got %d", w.Code) } }) t.Run("invalid_discount_percent_zero", func(t *testing.T) { req := CreateCampaignRequest{ Name: "Test", CampaignType: "time_based", DiscountPercent: 0, } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for zero discount, got %d", w.Code) } }) t.Run("invalid_discount_percent_over_100", func(t *testing.T) { req := CreateCampaignRequest{ Name: "Test", CampaignType: "time_based", DiscountPercent: 150, } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for discount >100, got %d", w.Code) } }) t.Run("invalid_campaign_type", func(t *testing.T) { req := CreateCampaignRequest{ Name: "Test", CampaignType: "invalid_type", DiscountPercent: 10, } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid type, got %d", w.Code) } }) t.Run("time_based_missing_dates", func(t *testing.T) { req := CreateCampaignRequest{ Name: "Test", CampaignType: "time_based", DiscountPercent: 10, } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing dates, got %d", w.Code) } }) t.Run("time_based_end_before_start", func(t *testing.T) { future := time.Now().Add(7 * 24 * time.Hour) past := time.Now().Add(-7 * 24 * time.Hour) req := CreateCampaignRequest{ Name: "Test", CampaignType: "time_based", DiscountPercent: 10, StartDate: strPtr(fmt.Sprintf("%sZ", future.Format("2006-01-02T15:04:05"))), EndDate: strPtr(fmt.Sprintf("%sZ", past.Format("2006-01-02T15:04:05"))), } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for end before start, got %d", w.Code) } }) t.Run("milestone_missing_required_fields", func(t *testing.T) { req := CreateCampaignRequest{ Name: "Test", CampaignType: "milestone", DiscountPercent: 10, } w := makeCampaignRequest(handler, "POST", "/api/admin/discount-campaigns", req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing milestone fields, got %d", w.Code) } }) } // ============================================================================= // Update Campaign Tests // ============================================================================= func TestUpdateDiscountCampaign_UpdateName(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() campaignID := insertMilestoneCampaign(t, "Old Name", 10, "draft") newName := "New Name" req := UpdateCampaignRequest{Name: &newName} handler := http.HandlerFunc(UpdateDiscountCampaign) w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req) 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 != "New Name" { t.Errorf("expected 'New Name', got %q", campaign.Name) } } func TestUpdateDiscountCampaign_NotFound(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() newName := "Test" req := UpdateCampaignRequest{Name: &newName} handler := http.HandlerFunc(UpdateDiscountCampaign) w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/nonexistent-id", req) if w.Code != http.StatusNotFound { t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code) } } func TestUpdateDiscountCampaign_InvalidStatus(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() campaignID := insertMilestoneCampaign(t, "Test", 10, "draft") badStatus := "invalid_status" req := UpdateCampaignRequest{Status: &badStatus} handler := http.HandlerFunc(UpdateDiscountCampaign) w := makeCampaignRequest(handler, "PUT", "/api/admin/discount-campaigns/"+campaignID, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for invalid status, got %d", w.Code) } } // ============================================================================= // Delete Campaign Tests // ============================================================================= func TestDeleteDiscountCampaign_HappyPath(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() campaignID := insertMilestoneCampaign(t, "Test", 10, "active") handler := http.HandlerFunc(DeleteDiscountCampaign) w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/"+campaignID, nil) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String()) } var status string err = db.DB.QueryRow(context.Background(), "SELECT status FROM discount_campaigns WHERE id = $1", campaignID).Scan(&status) if err != nil { t.Fatalf("failed to query campaign: %v", err) } if status != "cancelled" { t.Errorf("expected status 'cancelled', got %q", status) } } func TestDeleteDiscountCampaign_NotFound(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() handler := http.HandlerFunc(DeleteDiscountCampaign) w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/nonexistent-id", nil) if w.Code != http.StatusNotFound { t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code) } } // ============================================================================= // Campaign Stats Tests // ============================================================================= func TestGetCampaignStats_NoUsage(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() campaignID := insertMilestoneCampaign(t, "Test", 10, "active") handler := http.HandlerFunc(GetCampaignStats) w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/"+campaignID+"/stats", nil) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String()) } var stats CampaignStats if err := json.Unmarshal(w.Body.Bytes(), &stats); err != nil { t.Fatalf("failed to parse response: %v", err) } if stats.Campaign.ID != campaignID { t.Errorf("expected campaign ID %q, got %q", campaignID, stats.Campaign.ID) } if stats.TotalDiscounts != 0 { t.Errorf("expected 0 total discounts, got %.2f", stats.TotalDiscounts) } if stats.BookingCount != 0 { t.Errorf("expected 0 booking count, got %d", stats.BookingCount) } } func TestGetCampaignStats_NotFound(t *testing.T) { resetTestData(t) adminID, err := fixtures.CreateTestAdminUser(db.DB) if err != nil { t.Fatalf("failed to create admin: %v", err) } testAdminID = adminID defer func() { testAdminID = "" }() handler := http.HandlerFunc(GetCampaignStats) w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/nonexistent-id/stats", nil) if w.Code != http.StatusNotFound { t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code) } }