vdi-cloud/platform/internal/server/task_history_test.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)
}
}