package httpapi import ( "net/http/httptest" "testing" "time" ) func TestParseHistoryFilterUsesRecentSevenDaysByDefault(t *testing.T) { server := &Server{now: func() time.Time { return time.Date(2026, 7, 16, 8, 0, 0, 0, time.UTC) }} filter, err := server.parseHistoryFilter(httptest.NewRequest("GET", "/api/admin/history/tickets", nil), historyTicketStatuses) if err != nil { t.Fatal(err) } if filter.From != "2026-07-10" || filter.To != "2026-07-16" || filter.Page != 1 || filter.PageSize != 20 { t.Fatalf("default history filter = %#v", filter) } } func TestParseHistoryFilterRejectsUnsafeRangeAndPageSize(t *testing.T) { server := &Server{now: time.Now} for _, path := range []string{ "/api/admin/history/tickets?from=2026-01-01&to=2026-02-01", "/api/admin/history/tickets?from=2026-07-10&to=2026-07-16&page_size=101", } { if _, err := server.parseHistoryFilter(httptest.NewRequest("GET", path, nil), historyTicketStatuses); err == nil { t.Fatalf("expected history filter to be rejected: %s", path) } } } func TestHistoryTicketViewMasksPersonalDataAndHonorsAnonymization(t *testing.T) { server, ticket := encryptedTicketFixture(t) row := historyTicketRow{ ID: ticket.ID, ProjectID: ticket.ProjectID, ProjectName: "云岭漂流", ProjectCode: "RIDE", QueueSessionID: "session-a", BusinessDate: time.Date(2026, 7, 16, 0, 0, 0, 0, time.UTC), TicketNumber: 42, DisplayNumber: ticket.DisplayNumber, PartySize: ticket.PartySize, Status: ticket.Status, JoinedAt: ticket.JoinedAt, PhoneCiphertext: ticket.PhoneCiphertext, PhoneNonce: ticket.PhoneNonce, PhoneHMAC: ticket.PhoneHMAC, LastNameCiphertext: ticket.LastNameCiphertext, LastNameNonce: ticket.LastNameNonce, Honorific: ticket.Honorific, } masked, err := server.historyTicketView(row, false) if err != nil { t.Fatal(err) } if masked["phone"] != nil || masked["last_name"] != nil || masked["phone_masked"] != "****5678" { t.Fatalf("masked history view leaked personal data: %#v", masked) } revealed, err := server.historyTicketView(row, true) if err != nil || revealed["phone"] != "+8613812345678" || revealed["last_name"] != "林" { t.Fatalf("revealed history view = %#v, err = %v", revealed, err) } marked := time.Date(2026, 10, 14, 0, 0, 0, 0, time.UTC) row.HistoryAnonymizedAt = &marked anonymized, err := server.historyTicketView(row, true) if err != nil { t.Fatal(err) } if anonymized["personal_data_status"] != "ANONYMIZED" || anonymized["phone"] != nil || anonymized["phone_masked"] != nil { t.Fatalf("anonymized history view = %#v", anonymized) } if anonymized["honorific"] != nil { t.Fatalf("anonymized history view leaked honorific: %#v", anonymized) } } func TestHistorySummaryAlertsUseMinimumSampleAndOperationalThresholds(t *testing.T) { longWait := 15.0 * 60 alerts := historySummaryAlerts([]historyProjectRow{ {ProjectID: "project-alert", ProjectName: "峡谷漂流", TotalTickets: 10, MissedTickets: 3, CompletedTickets: 6, AverageWait: &longWait}, {ProjectID: "project-small", ProjectName: "小样本项目", TotalTickets: 4, MissedTickets: 4, CompletedTickets: 0, AverageWait: &longWait}, }) if len(alerts) != 1 { t.Fatalf("alerts = %#v, want one alert after minimum sample filtering", alerts) } if alerts[0]["type"] != "MISSED_RATE" || alerts[0]["project_id"] != "project-alert" { t.Fatalf("alert = %#v", alerts[0]) } }