package server import ( "encoding/csv" "fmt" "net/http/httptest" "net/url" "strings" "testing" ) func TestAuditHistoryFiltersAndDatabasePagination(t *testing.T) { f := newFixtureWorker(t, false) token := f.login("viewer") _, err := f.db.Exec(`INSERT INTO audit_logs SELECT 'history_'||lpad(i::text,5,'0'),'worker','save_alert_rule','node_'||i,'success','request_'||i,'2025-01-01T00:00:00Z'::timestamptz FROM generate_series(1,1050) i`) if err != nil { t.Fatal(err) } if _, err = f.db.Exec(`INSERT INTO audit_logs VALUES('needle','Root_100%','save_alert_rule','研发,"一组"','denied','req_trace','2025-01-02T00:00:00Z')`); err != nil { t.Fatal(err) } code, out := f.request("GET", "/audit-logs?action=save_alert_rule&page=11&pageSize=100", token, nil, "") expect(t, 200, code, out) items := out["items"].([]any) if out["total"] != float64(1051) || len(items) != 51 || items[50].(map[string]any)["id"] != "history_00001" { t.Fatal(out) } q := url.Values{"keyword": {"req_trace"}, "actor": {"_100%"}, "action": {"save_alert_rule"}, "result": {"denied"}, "from": {"2025-01-01T08:00:00+08:00"}, "to": {"2025-01-02T08:00:00+08:00"}} code, out = f.request("GET", "/audit-logs?"+q.Encode(), token, nil, "") expect(t, 200, code, out) if out["total"] != float64(1) || out["items"].([]any)[0].(map[string]any)["id"] != "needle" { t.Fatal(out) } for _, term := range []string{"_100%", "研发,", "' OR true --"} { code, out = f.request("GET", "/audit-logs?keyword="+url.QueryEscape(term), token, nil, "") expect(t, 200, code, out) want := float64(0) if term == "_100%" || term == "研发," { want = 1 } if out["total"] != want { t.Fatal(term, out) } } code, out = f.request("GET", "/audit-logs?page=99999999&pageSize=999", token, nil, "") expect(t, 200, code, out) if out["page"] != float64(1000000) || out["pageSize"] != float64(100) || len(out["items"].([]any)) != 0 { t.Fatal(out) } for _, query := range []string{"from=broken", "to=broken", "from=2025-01-02T00:00:00Z&to=2025-01-01T00:00:00Z", "keyword=" + strings.Repeat("x", 201), "actor=%00"} { for _, path := range []string{"/audit-logs?", "/audit-logs/export?"} { code, out = f.request("GET", path+query, token, nil, "") expect(t, 400, code, out) } } } func auditDownload(f *fixture, token, query string) *httptest.ResponseRecorder { r := httptest.NewRequest("GET", "/api/v1/audit-logs/export"+query, nil) if token != "" { r.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() f.server.Router.ServeHTTP(w, r) return w } func TestAuditCSVExportPermissionsCompletenessAndSafety(t *testing.T) { f := newFixtureWorker(t, false) if _, err := f.db.Exec(`INSERT INTO audit_logs VALUES('csv-special','=malicious','custom_action',$1,'success','req-csv','2025-01-01T00:00:00Z')`, "研发,\"一组\"\r\n第二行"); err != nil { t.Fatal(err) } w := auditDownload(f, "", "") if w.Code != 401 { t.Fatal(w.Code) } for _, role := range []string{"admin", "operator", "viewer"} { token := f.login(role) w = auditDownload(f, token, "?action=custom_action&page=999&pageSize=1") if w.Code != 200 || w.Header().Get("X-Export-Count") != "1" || !strings.HasPrefix(w.Header().Get("Content-Type"), "text/csv") || !strings.Contains(w.Header().Get("Content-Disposition"), ".csv") { t.Fatal(w.Code, w.Body.String()) } body := w.Body.String() if !strings.HasPrefix(body, "\ufeff") { t.Fatal("missing Excel UTF-8 BOM") } rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(body, "\ufeff"))).ReadAll() if err != nil || len(rows) != 2 || len(rows[0]) != 7 || rows[1][1] != "'=malicious" || rows[1][3] != "研发,\"一组\"\n第二行" || rows[1][6] != "csv-special" { t.Fatal(rows, err) } var logged int if err := f.db.Get(&logged, "SELECT count(*) FROM audit_logs WHERE action='export_audit_logs' AND actor=$1 AND target='rows=1'", role); err != nil || logged != 1 { t.Fatal(logged, err) } } for _, cell := range []string{"=1+2", "+cmd", "-1", "@SUM(A1)", "\t=HYPERLINK(\"x\")", "\r\n @cmd", "\ufeff=cmd"} { if safeCSVCell(cell) != "'"+cell { t.Fatalf("unsafe: %q", cell) } } for _, cell := range []string{"研发", "", "request-id", "success", "2025-01-01"} { if safeCSVCell(cell) != cell { t.Fatal(cell) } } } func TestAuditExportLimitAndEmptyResults(t *testing.T) { f := newFixtureWorker(t, false) token := f.login("admin") if _, err := f.db.Exec(`INSERT INTO audit_logs SELECT 'bulk_'||i,'worker','bulk','target','success','request','2025-01-01T00:00:00Z'::timestamptz FROM generate_series(1,10001) i`); err != nil { t.Fatal(err) } w := auditDownload(f, token, "?action=bulk") if w.Code != 413 || strings.Contains(w.Header().Get("Content-Type"), "csv") { t.Fatal(w.Code, w.Body.String()) } if _, err := f.db.Exec("DELETE FROM audit_logs WHERE id='bulk_10001'"); err != nil { t.Fatal(err) } w = auditDownload(f, token, "?action=bulk&pageSize=1") if w.Code != 200 || w.Header().Get("X-Export-Count") != fmt.Sprint(auditExportLimit) { t.Fatal(w.Code) } rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(w.Body.String(), "\ufeff"))).ReadAll() if err != nil || len(rows) != auditExportLimit+1 { t.Fatal(len(rows), err) } w = auditDownload(f, token, "?action=nonexistent") if w.Code != 200 || w.Header().Get("X-Export-Count") != "0" { t.Fatal(w.Code) } }