package app import ( "encoding/json" "fmt" "net/url" "strings" "testing" ) func adminResult(t *testing.T, c *testClient, path string) adminListResult { t.Helper() w := c.request(t, "GET", path, nil) expect(t, w, 200) var p adminListResult if err := json.Unmarshal(w.Body.Bytes(), &p); err != nil { t.Fatal(err) } if p.Items == nil { t.Fatal("items must be an array, including when empty") } return p } func TestAdminParcelPaginationAndSearch(t *testing.T) { c := fixture(t, false) expect(t, c.request(t, "GET", "/admin/parcels?page_size=10", nil), 401) loginTestAdmin(t, c) tx, err := c.a.db.Begin() if err != nil { t.Fatal(err) } defer tx.Rollback() for i := 1; i <= 25; i++ { id := fmt.Sprintf("%032x", i) state := "ready" if i%2 == 0 { state = "revoked" } _, err = tx.Exec("INSERT INTO parcels(id,visitor,ip,state,created,password_hash,manage_hash,days,downloads,renewal_days,renewal_mode,lifetime_days,upload_expires,usage_day) VALUES(?,?,'203.0.113.1',?,1000,'','',1,1,0,'add',30,2000,'2026-10-05')", id, "visitor", state) if err != nil { t.Fatal(err) } name := fmt.Sprintf("document-%d.txt", i) if i == 5 { name = "说明_%_Report.txt" } _, err = tx.Exec("INSERT INTO files(id,parcel,name,size,status,reserved,remaining) VALUES(?,?,?,12,'deleted',0,0)", id, id, name) if err != nil { t.Fatal(err) } } if err = tx.Commit(); err != nil { t.Fatal(err) } first := adminResult(t, c, "/admin/parcels") if first.Total != 25 || first.PageSize != 10 || first.Pages != 3 || len(first.Items) != 10 || first.Items[0]["id"] != fmt.Sprintf("%032x", 25) { t.Fatalf("bad first page: %+v", first) } second := adminResult(t, c, "/admin/parcels?page=2") if second.Page != 2 || second.Items[0]["id"] != fmt.Sprintf("%032x", 15) { t.Fatalf("bad second page: %+v", second) } last := adminResult(t, c, "/admin/parcels?page=999999999") if last.Page != 3 || len(last.Items) != 5 { t.Fatalf("out-of-range page was not clamped: %+v", last) } for _, q := range []string{"REPORT", "说明", "%", fmt.Sprintf("%032x", 5)} { p := adminResult(t, c, "/admin/parcels?q="+url.QueryEscape(q)) if p.Total != 1 || p.Items[0]["id"] != fmt.Sprintf("%032x", 5) { t.Fatalf("literal filename/ID query %q: %+v", q, p) } } p := adminResult(t, c, "/admin/parcels?state=revoked&page_size=20") if p.Total != 12 || p.PageSize != 20 || len(p.Items) != 12 { t.Fatalf("state filter: %+v", p) } p = adminResult(t, c, "/admin/parcels?q="+url.QueryEscape("' OR 1=1 --")) if p.Total != 0 || len(p.Items) != 0 || p.Page != 1 || p.Pages != 1 { t.Fatalf("empty/literal injection query: %+v", p) } for _, q := range []string{"page=0", "page=-1", "page=oops", "page_size=0", "page_size=101", "page_size=oops", "state=unknown", "q=" + strings.Repeat("a", 257)} { expect(t, c.request(t, "GET", "/admin/parcels?"+q, nil), 400) } } func TestAdminAuditPaginationAndCombinedFilters(t *testing.T) { c := fixture(t, false) expect(t, c.request(t, "GET", "/admin/audit", nil), 401) loginTestAdmin(t, c) if _, err := c.a.db.Exec("DELETE FROM audit"); err != nil { t.Fatal(err) } base := c.a.now().Unix() tx, err := c.a.db.Begin() if err != nil { t.Fatal(err) } defer tx.Rollback() for i := 1; i <= 54; i++ { action := "upload.create" if i%2 == 0 { action = "admin.delete" } source := "2001:db8::1" if i%3 == 0 { source = "203.0.113.1" } target := fmt.Sprintf("job-%d", i) if i == 18 { target = "job_%_18" } if _, err = tx.Exec("INSERT INTO audit(created,action,target,source) VALUES(?,?,?,?)", base+int64(i)*60, action, target, source); err != nil { t.Fatal(err) } } if err = tx.Commit(); err != nil { t.Fatal(err) } p := adminResult(t, c, "/admin/audit") if p.Total != 54 || p.PageSize != 20 || p.Pages != 3 || len(p.Items) != 20 || p.Items[0]["target"] != "job-54" { t.Fatalf("bad audit page: %+v", p) } p2 := adminResult(t, c, "/admin/audit?page=2") if p2.Items[0]["target"] != "job-34" { t.Fatalf("audit page did not change: %+v", p2) } last := adminResult(t, c, "/admin/audit?page=99999") if last.Page != 3 || len(last.Items) != 14 { t.Fatalf("bad audit last page: %+v", last) } query := fmt.Sprintf("/admin/audit?action=admin.delete&source=203.0.113.1&target=JOB&from=%d&to=%d", base+10*60, base+30*60) filtered := adminResult(t, c, query) if filtered.Total != 4 || len(filtered.Items) != 4 { t.Fatalf("combined/inclusive date filters: %+v", filtered) } for _, item := range filtered.Items { if item["action"] != "admin.delete" || item["source"] != "203.0.113.1" { t.Fatalf("wrong filter result: %+v", item) } } literal := adminResult(t, c, "/admin/audit?target=%25") if literal.Total != 1 || literal.Items[0]["target"] != "job_%_18" { t.Fatalf("wildcard interpreted as pattern: %+v", literal) } ipv6 := adminResult(t, c, "/admin/audit?source=2001%3ADB8") if ipv6.Total != 36 { t.Fatalf("IPv6 fragment filter: %+v", ipv6) } empty := adminResult(t, c, "/admin/audit?action=unknown") if empty.Total != 0 || empty.Pages != 1 || len(empty.Items) != 0 { t.Fatal("empty audit search malformed") } for _, q := range []string{"page=0", "page_size=101", "from=-1", "from=nope", "to=1.5", "from=20&to=10", "action=" + strings.Repeat("a", 65), "source=" + strings.Repeat("a", 129), "target=" + strings.Repeat("a", 257)} { expect(t, c.request(t, "GET", "/admin/audit?"+q, nil), 400) } } func TestAdminAuditOneHundredThousandRecordsRemainPaged(t *testing.T) { c := fixture(t, false) loginTestAdmin(t, c) _, err := c.a.db.Exec("DELETE FROM audit; WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i+1 FROM n WHERE i<100000) INSERT INTO audit(created,action,target,source) SELECT 1000,'upload.create',printf('target-%d',i),'203.0.113.1' FROM n") if err != nil { t.Fatal(err) } first := adminResult(t, c, "/admin/audit") if first.Total != 100000 || first.Pages != 5000 || len(first.Items) != 20 || first.Items[0]["target"] != "target-100000" { t.Fatalf("large audit query unbounded: %+v", first) } last := adminResult(t, c, "/admin/audit?page=5000") if last.Page != 5000 || len(last.Items) != 20 || last.Items[19]["target"] != "target-1" { t.Fatalf("large audit final page incorrect: %+v", last) } filtered := adminResult(t, c, "/admin/audit?target=target-12345") if filtered.Total != 1 || len(filtered.Items) != 1 || filtered.Items[0]["target"] != "target-12345" { t.Fatal("search did not include older/all retained audit events") } }