package api import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" ) func TestBuildHistoryPaginationAndOnDemandLogs(t *testing.T) { a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Errorf("unexpected Docker request: %s", r.URL) }) buildQueue.Lock() err := a.ensureBuilds() buildQueue.Unlock() if err != nil { t.Fatal(err) } for n := 0; n < 125; n++ { _, err := a.s.DB.Exec("INSERT INTO image_builds(id,version,status,logs,created_at) VALUES(?,?,?,?,?)", fmt.Sprintf("task-%03d", n), "1.2.3", "failed", "token=secret-value"+string(rune(10))+strings.Repeat("x", buildLogLimit), "2026-01-01") if err != nil { t.Fatal(err) } } request := func(target, session, method string) *httptest.ResponseRecorder { r := httptest.NewRequest(method, target, nil) if session != "" { r.AddCookie(&http.Cookie{Name: "dsh_session", Value: session}) addTestAccess(r, session) } w := httptest.NewRecorder() if strings.HasPrefix(r.URL.Path, "/api/builds/") { a.buildDetail(w, r) } else { a.builds(w, r) } return w } for _, tc := range []struct { query string count int first, more string }{ {"", 20, "task-124", "true"}, {"?limit=100", 100, "task-124", "true"}, {"?limit=20&offset=120", 5, "task-004", "false"}, {"?offset=125", 0, "", "false"}, {"?limit=20&offset=105", 20, "task-019", "false"}, {"?limit=20&offset=104", 20, "task-020", "true"}, } { w := request("/api/builds"+tc.query, sid, "GET") var records []buildRecord if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &records) != nil || len(records) != tc.count { t.Fatalf("%s: %d %s", tc.query, w.Code, w.Body.String()) } if tc.count > 0 && records[0].ID != tc.first { t.Fatal(records[0].ID) } if w.Header().Get("X-Has-More") != tc.more || strings.Contains(w.Body.String(), "logs") || strings.Contains(w.Body.String(), "secret-value") { t.Fatal("unbounded or invalid summary response", w.Body.String()) } } for _, query := range []string{"?limit=0", "?limit=101", "?limit=-1", "?offset=-1", "?offset=x", "?limit=1&limit=2", "?offset=9999999999999999999999999"} { if w := request("/api/builds"+query, sid, "GET"); w.Code != 400 { t.Fatal(query, w.Code) } } w := request("/api/builds/task-000", sid, "GET") var b buildRecord if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &b) != nil || b.ID != "task-000" || b.Logs == "" || len(b.Logs) > buildLogLimit || strings.Contains(b.Logs, "secret-value") { t.Fatal(w.Code, w.Body.String()) } for _, path := range []string{"/api/builds/missing", "/api/builds/", "/api/builds/task-000/extra"} { if w := request(path, sid, "GET"); w.Code != 404 { t.Fatal(path, w.Code) } } if w := request("/api/builds/task-000", "", "GET"); w.Code != 401 { t.Fatal(w.Code) } if w := request("/api/builds", "", "GET"); w.Code != 401 { t.Fatal(w.Code) } if w := request("/api/builds/task-000", sid, "POST"); w.Code != 405 { t.Fatal(w.Code) } } func TestBuildDetailRouteAndAccess(t *testing.T) { a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unexpected Docker call") }) buildQueue.Lock() err := a.ensureBuilds() buildQueue.Unlock() if err != nil { t.Fatal(err) } if _, err = a.s.DB.Exec("INSERT INTO image_builds(id,version,status,created_at) VALUES('task','1.2.3','failed','now')"); err != nil { t.Fatal(err) } _, csrf, err := a.s.SessionUser(sid) if err != nil { t.Fatal(err) } for _, tc := range []struct { path, method string access bool status int }{ {"/api/builds/task", "GET", true, 200}, {"/api/builds/task", "GET", false, 401}, {"/api/builds/task", "POST", true, 405}, {"/api/builds/", "GET", true, 404}, {"/api/builds/task%2Fextra", "GET", true, 404}, {"/api/builds/task/extra", "GET", true, 404}, } { r := httptest.NewRequest(tc.method, tc.path, nil) r.AddCookie(&http.Cookie{Name: "dsh_session", Value: sid}) if tc.access { addTestAccess(r, sid) } if tc.method != "GET" { r.Header.Set("Origin", "http://example.com") r.Header.Set("X-CSRF-Token", csrf) } w := httptest.NewRecorder() a.Handler().ServeHTTP(w, r) if w.Code != tc.status { t.Fatalf("%s %s: %d %s", tc.method, tc.path, w.Code, w.Body.String()) } } } func TestBuildPageReconcilesOnlySelectedRecords(t *testing.T) { unavailable := false calls := 0 a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { calls++ if unavailable { w.WriteHeader(500) return } if !strings.HasPrefix(r.URL.Path, "/v1.47/images/") { t.Errorf("unexpected request %s", r.URL) } w.WriteHeader(404) }) seedVerifiedImage(t, a, "first-image", "1.2.3") seedVerifiedImage(t, a, "second-image", "1.2.4") if _, err := a.s.DB.Exec("UPDATE image_builds SET created_at=CASE version WHEN '1.2.3' THEN 'z' ELSE 'a' END"); err != nil { t.Fatal(err) } request := func(offset int) *httptest.ResponseRecorder { r := httptest.NewRequest("GET", fmt.Sprintf("/api/builds?limit=1&offset=%d", offset), nil) r.AddCookie(&http.Cookie{Name: "dsh_session", Value: sid}) addTestAccess(r, sid) w := httptest.NewRecorder() a.builds(w, r) return w } w := request(0) if w.Code != 200 || calls != 1 || !strings.Contains(w.Body.String(), "missing") { t.Fatal(w.Code, calls, w.Body.String()) } var state string if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE version='1.2.4'").Scan(&state); err != nil || state != "success" { t.Fatal(state, err) } unavailable = true if w = request(1); w.Code != 502 { t.Fatal(w.Code, w.Body.String()) } }