package api import ( "errors" "net/http/httptest" "strings" "testing" "time" "git.amiirkhl.ir/starcoach-hackathon/zarinpal-analytics-backend/internal/analytics" ) func TestServerCookieName(t *testing.T) { t.Parallel() tests := []struct { name string secure bool expected string }{ {name: "local development", secure: false, expected: "zpa_session"}, {name: "secure deployment", secure: true, expected: "__Host-zpa_session"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := &Server{cookieSecure: tt.secure} if got := server.cookieName(); got != tt.expected { t.Fatalf("cookieName() = %q, want %q", got, tt.expected) } }) } } func TestRenderExportCSVIsBoundedAndFormulaSafe(t *testing.T) { t.Parallel() item := analytics.ExportDetail{Detail: analytics.Detail{ SessionID: "=session", TerminalID: "+terminal", SessionStatus: "Verified", AmountRial: 1000, FeeAmountRial: 10, CreatedAt: time.Date(2026, time.June, 20, 8, 0, 0, 0, time.UTC), }} encoded, err := renderExportCSV([]analytics.ExportDetail{item}, 4096) if err != nil { t.Fatal(err) } text := string(encoded) if !strings.Contains(text, "'=session") || !strings.Contains(text, "'+terminal") { t.Fatalf("renderExportCSV() did not neutralize formulas: %q", text) } if _, err := renderExportCSV([]analytics.ExportDetail{item}, 10); !errors.Is(err, errExportBytesExceeded) { t.Fatalf("renderExportCSV() error = %v, want errExportBytesExceeded", err) } } func TestPersianStatus(t *testing.T) { t.Parallel() if got := persianStatus("Verified"); got != "تأیید شده" { t.Fatalf("persianStatus() = %q", got) } } func TestOptionalNonNegativeInt64(t *testing.T) { t.Parallel() tests := []struct { name, input string want *int64 valid bool }{ {name: "omitted", valid: true}, {name: "zero", input: "0", want: int64Pointer(0), valid: true}, {name: "positive", input: "125000", want: int64Pointer(125000), valid: true}, {name: "negative", input: "-1"}, {name: "not a number", input: "یک"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, valid := optionalNonNegativeInt64(tt.input) if valid != tt.valid || !equalInt64Pointers(got, tt.want) { t.Fatalf("optionalNonNegativeInt64(%q) = %v,%v, want %v,%v", tt.input, got, valid, tt.want, tt.valid) } }) } } func int64Pointer(value int64) *int64 { return &value } func equalInt64Pointers(left, right *int64) bool { if left == nil || right == nil { return left == right } return *left == *right } func TestDetailFilterFromRequestParsesCanonicalFilters(t *testing.T) { t.Parallel() request := httptest.NewRequest("GET", "/?view=ALL&failureStage=&terminalId=T318&status=Verified&minAmountRial=1000&maxAmountRial=9000&cursor=opaque", nil) filter, err := detailFilterFromRequest(request, 25) if err != nil { t.Fatal(err) } if filter.View != "ALL" || filter.TerminalID != "T318" || filter.Status != "Verified" || filter.Cursor != "opaque" || filter.Limit != 25 { t.Fatalf("detailFilterFromRequest() = %+v", filter) } if filter.MinAmountRial == nil || *filter.MinAmountRial != 1000 || filter.MaxAmountRial == nil || *filter.MaxAmountRial != 9000 { t.Fatalf("detailFilterFromRequest() amount bounds = %v/%v", filter.MinAmountRial, filter.MaxAmountRial) } } func TestDetailFilterFromRequestRejectsDescendingAmountRange(t *testing.T) { t.Parallel() request := httptest.NewRequest("GET", "/?view=ALL&minAmountRial=9000&maxAmountRial=1000", nil) if _, err := detailFilterFromRequest(request, 25); err == nil { t.Fatal("detailFilterFromRequest() error = nil") } } func TestFilterPeriodsByType(t *testing.T) { t.Parallel() periods := []analytics.Period{{PeriodType: "DAY"}, {PeriodType: "WEEK"}} filtered, err := filterPeriodsByType(periods, "DAY") if err != nil || len(filtered) != 1 || filtered[0].PeriodType != "DAY" || !filtered[0].IsDefault { t.Fatalf("filterPeriodsByType() = %+v, %v", filtered, err) } if _, err := filterPeriodsByType(periods, "MONTH"); err == nil { t.Fatal("filterPeriodsByType() accepted invalid type") } }