130 lines
5.5 KiB
Go
130 lines
5.5 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTaskHistoryPaginationFiltersAndOldPending(t *testing.T) {
|
|
f := newFixtureWorker(t, false)
|
|
token := f.login("viewer")
|
|
_, err := f.db.Exec(`INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key,created_at,updated_at)
|
|
SELECT 'history_'||lpad(i::text,5,'0'),'desktop','start','target_'||i,'测试桌面_'||i,'worker','success','完成','history_'||i,'2025-01-02T00:00:00Z'::timestamptz,'2025-01-02T00:00:01Z'::timestamptz FROM generate_series(1,1050) i;
|
|
INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key,created_at) VALUES
|
|
('old-pending','snapshot','snapshot-create','pending-target','旧快照','Root_100%','timeout','结果未确认','old-pending','2024-01-01T00:00:00Z'),
|
|
('old-running','terminal','collect-logs','old-terminal','诊断设备','worker','running','正在执行','old-running','2024-01-01T00:00:00Z')`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, out := f.request("GET", "/tasks?page=11&pageSize=100", token, nil, "")
|
|
expect(t, 200, code, out)
|
|
items := out["items"].([]any)
|
|
if out["total"] != float64(1052) || len(items) != 52 || items[len(items)-1].(map[string]any)["id"] != "old-pending" {
|
|
t.Fatal(out)
|
|
}
|
|
code, out = f.request("GET", "/tasks?active=true", token, nil, "")
|
|
expect(t, 200, code, out)
|
|
if out["total"] != float64(2) {
|
|
t.Fatal(out)
|
|
}
|
|
q := url.Values{"actor": {"_100%"}, "status": {"timeout"}, "action": {"snapshot-create"}, "kind": {"snapshot"}, "targetId": {"pending-target"}, "keyword": {"结果未确认"}, "from": {"2024-01-01T08:00:00+08:00"}, "to": {"2024-01-02T08:00:00+08:00"}}
|
|
code, out = f.request("GET", "/tasks?"+q.Encode(), token, nil, "")
|
|
expect(t, 200, code, out)
|
|
if out["total"] != float64(1) || out["items"].([]any)[0].(map[string]any)["id"] != "old-pending" {
|
|
t.Fatal(out)
|
|
}
|
|
for _, term := range []string{"桌面_1", "%", "' OR true --"} {
|
|
code, out = f.request("GET", "/tasks?keyword="+url.QueryEscape(term), token, nil, "")
|
|
expect(t, 200, code, out)
|
|
want := float64(0)
|
|
if term == "桌面_1" {
|
|
want = 162
|
|
}
|
|
if term == "%" {
|
|
want = 1
|
|
}
|
|
if out["total"] != want {
|
|
t.Fatal(term, out["total"])
|
|
}
|
|
}
|
|
for _, query := range []string{"from=broken", "from=2025-01-02T00:00:00Z&to=2025-01-01T00:00:00Z", "status=invalid", "active=maybe", "actor=%00", "keyword=" + strings.Repeat("x", 201)} {
|
|
for _, path := range []string{"/tasks?", "/tasks/export?"} {
|
|
code, out = f.request("GET", path+query, token, nil, "")
|
|
expect(t, 400, code, out)
|
|
}
|
|
}
|
|
code, out = f.request("GET", "/tasks?page=999999999&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)
|
|
}
|
|
}
|
|
|
|
func downloadTaskCSV(f *fixture, token, query string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest("GET", "/api/v1/tasks/export"+query, nil)
|
|
if token != "" {
|
|
r.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
f.server.Router.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
func TestTaskHistoryExportRolesAndPrivateFields(t *testing.T) {
|
|
f := newFixtureWorker(t, false)
|
|
_, err := f.db.Exec(`INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key,provider_ref) VALUES ('csv-task','desktop','start','csv-target',$1,'worker','failed',$2,'private-idempotency','private-provider-payload')`, "研发,\"一组\"\n第二行", "\t=HYPERLINK(\"x\")")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w := downloadTaskCSV(f, "", ""); w.Code != 401 {
|
|
t.Fatal(w.Code)
|
|
}
|
|
for _, role := range []string{"admin", "operator", "viewer"} {
|
|
token := f.login(role)
|
|
w := downloadTaskCSV(f, token, "?keyword=csv-task&page=100&pageSize=1")
|
|
if w.Code != 200 || w.Header().Get("X-Export-Count") != "1" {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
body := w.Body.String()
|
|
if !strings.HasPrefix(body, "\ufeff") || strings.Contains(body, "private-") {
|
|
t.Fatal("CSV exposed internal fields")
|
|
}
|
|
rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(body, "\ufeff"))).ReadAll()
|
|
if err != nil || len(rows) != 2 || len(rows[0]) != 10 || rows[1][2] != "csv-task" || rows[1][7] != "研发,\"一组\"\n第二行" || rows[1][9] != "'\t=HYPERLINK(\"x\")" {
|
|
t.Fatal(rows, err)
|
|
}
|
|
var count int
|
|
err = f.db.Get(&count, "SELECT count(*) FROM audit_logs WHERE action='export_tasks' AND actor=$1 AND target='rows=1'", role)
|
|
if err != nil || count != 1 {
|
|
t.Fatal(count, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTaskHistoryExportCompleteAndLimit(t *testing.T) {
|
|
f := newFixtureWorker(t, false)
|
|
token := f.login("admin")
|
|
_, err := f.db.Exec(`INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,idempotency_key) SELECT 'bulk_'||i,'desktop','start','target_'||i,'桌面','worker','success','bulk_'||i FROM generate_series(1,10001) i`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w := downloadTaskCSV(f, token, ""); w.Code != 413 {
|
|
t.Fatal(w.Code)
|
|
}
|
|
if _, err = f.db.Exec("DELETE FROM tasks WHERE id='bulk_10001'"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := downloadTaskCSV(f, token, "?pageSize=1")
|
|
rows, err := csv.NewReader(strings.NewReader(strings.TrimPrefix(w.Body.String(), "\ufeff"))).ReadAll()
|
|
if w.Code != 200 || w.Header().Get("X-Export-Count") != "10000" || err != nil || len(rows) != 10001 {
|
|
t.Fatal(w.Code, len(rows), err)
|
|
}
|
|
w = downloadTaskCSV(f, token, "?keyword=nonexistent")
|
|
if w.Code != 200 || w.Header().Get("X-Export-Count") != "0" {
|
|
t.Fatal(w.Code)
|
|
}
|
|
}
|