Files
Crussell/backend/handlers/admin/discount_campaigns_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

532 lines
17 KiB
Go

//go:build test
// +build test
package admin
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"crussell/clock"
"crussell/db"
"crussell/testutils"
"crussell/mw"
"crussell/testutils/fixtures"
"github.com/go-chi/chi/v5"
)
var testAdminID string
func makeCampaignRequest(handler http.Handler, method, path string, body interface{}, ctx context.Context, adminID string) *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(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()
handler.ServeHTTP(w, req)
return w
}
func insertTimeBasedCampaign(t *testing.T, ctx context.Context, tx db.Querier, adminID, name string, discount float64, status string) string {
t.Helper()
startDate := fmt.Sprintf("%sZ", clock.Now().Add(-1*time.Hour).Format("2006-01-02T15:04:05"))
endDate := fmt.Sprintf("%sZ", clock.Now().Add(7*24*time.Hour).Format("2006-01-02T15:04:05"))
var id string
err := tx.QueryRow(ctx, `
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, adminID).Scan(&id)
if err != nil {
t.Fatalf("failed to insert time_based campaign: %v", err)
}
return id
}
func insertMilestoneCampaign(t *testing.T, ctx context.Context, tx db.Querier, adminID, name string, discount float64, status string) string {
t.Helper()
mt := "per_user_booking_count"
mv := 5
mu := "bookings"
var id string
err := tx.QueryRow(ctx, `
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, adminID).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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
handler := http.HandlerFunc(GetDiscountCampaigns)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil, ctx, adminID)
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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
insertTimeBasedCampaign(t, ctx, tx, adminID, "Summer Sale", 15, "active")
handler := http.HandlerFunc(GetDiscountCampaigns)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns", nil, ctx, adminID)
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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
insertMilestoneCampaign(t, ctx, tx, adminID, "Draft Campaign", 10, "draft")
insertMilestoneCampaign(t, ctx, tx, adminID, "Active Campaign", 20, "active")
insertMilestoneCampaign(t, ctx, tx, adminID, "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, ctx, adminID)
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, ctx, adminID)
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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
req := CreateCampaignRequest{
Name: "Summer Sale",
CampaignType: "time_based",
DiscountPercent: 15,
StartDate: strPtr(fmt.Sprintf("%sZ", clock.Now().Format("2006-01-02T15:04:05"))),
EndDate: strPtr(fmt.Sprintf("%sZ", clock.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, ctx, adminID)
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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
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, ctx, adminID)
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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
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, ctx, adminID)
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, ctx, adminID)
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, ctx, adminID)
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, ctx, adminID)
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, ctx, adminID)
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 := clock.Now().Add(7 * 24 * time.Hour)
past := clock.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, ctx, adminID)
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, ctx, adminID)
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) {
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, "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, 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 != "New Name" {
t.Errorf("expected 'New Name', got %q", campaign.Name)
}
}
func TestUpdateDiscountCampaign_NotFound(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/nonexistent-id", req, ctx, adminID)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code)
}
}
func TestUpdateDiscountCampaign_InvalidStatus(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")
badStatus := "invalid_status"
req := UpdateCampaignRequest{Status: &badStatus}
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 invalid status, got %d", w.Code)
}
}
// =============================================================================
// Delete Campaign Tests
// =============================================================================
func TestDeleteDiscountCampaign_HappyPath(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, "active")
handler := http.HandlerFunc(DeleteDiscountCampaign)
w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/"+campaignID, nil, ctx, adminID)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
}
var status string
err = tx.QueryRow(ctx,
"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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
handler := http.HandlerFunc(DeleteDiscountCampaign)
w := makeCampaignRequest(handler, "DELETE", "/api/admin/discount-campaigns/nonexistent-id", nil, ctx, adminID)
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) {
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, "active")
handler := http.HandlerFunc(GetCampaignStats)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/"+campaignID+"/stats", nil, ctx, adminID)
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) {
ctx, tx := testutils.SetupTestTx(t)
adminID, err := fixtures.CreateTestAdminUser(tx)
if err != nil {
t.Fatalf("failed to create admin: %v", err)
}
testAdminID = adminID
handler := http.HandlerFunc(GetCampaignStats)
w := makeCampaignRequest(handler, "GET", "/api/admin/discount-campaigns/nonexistent-id/stats", nil, ctx, adminID)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404 for nonexistent campaign, got %d", w.Code)
}
}