package api import ( "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func TestImageDelete(t *testing.T) { id := "sha256:" + strings.Repeat("a", 64) for _, mode := range []string{"success", "anonymous", "non-admin", "invalid", "unmanaged", "role", "instance", "container-running", "container-exited", "containers-failure", "containers-null", "containers-unknown", "inspect-failure", "delete-failure", "delete-conflict", "queued", "running", "validating", "worker", "journal", "complete", "rolled_back", "db-failure", "task-query-failure", "audit-failure"} { t.Run(mode, func(t *testing.T) { var deletes atomic.Int32 a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodDelete: deletes.Add(1) if r.URL.Path != "/v1.47/images/"+id || r.URL.Query().Get("force") != "false" { t.Errorf("unsafe delete: %s", r.URL) } if mode == "delete-failure" { w.WriteHeader(500) return } if mode == "delete-conflict" { w.WriteHeader(409) return } writeJSON(w, []any{}) case r.URL.Path == "/v1.47/images/"+id+"/json": if mode == "inspect-failure" { w.WriteHeader(500) return } if mode == "role" { writeJSON(w, map[string]any{"Id": id}) return } writeImageInspect(w, id, "1.2.3") case r.URL.Path == "/v1.47/containers/json": if r.URL.RawQuery != "all=1" { t.Errorf("must inspect all containers: %s", r.URL) } if mode == "containers-failure" { w.WriteHeader(500) return } if mode == "containers-null" { writeJSON(w, nil) return } if mode == "containers-unknown" { writeJSON(w, []any{map[string]string{"Image": "tag"}}) return } if strings.HasPrefix(mode, "container-") { writeJSON(w, []any{map[string]string{"ImageID": id, "State": strings.TrimPrefix(mode, "container-")}}) return } writeJSON(w, []any{}) default: t.Errorf("unexpected Docker request: %s %s", r.Method, r.URL) w.WriteHeader(500) } }) exec := func(q string, args ...any) { t.Helper() if _, err := a.s.DB.Exec(q, args...); err != nil { t.Fatal(err) } } seedVerifiedImage(t, a, id, "1.2.3") exec("INSERT INTO images(image_id,version,status,built_at) VALUES(?,'1.2.3','available','now')", id) if mode != "non-admin" { exec("UPDATE users SET admin=1 WHERE id='u'") } want := 409 switch mode { case "success", "complete", "rolled_back": want = 200 case "anonymous": sid = "" want = 401 case "non-admin": want = 403 case "invalid": want = 400 case "unmanaged": exec("DELETE FROM images") want = 404 case "inspect-failure", "containers-failure", "containers-null", "containers-unknown", "delete-failure": want = 502 case "instance": exec("INSERT INTO instances(id,uid,name,image_id,created) VALUES('i','other','i',?,0)", id) case "queued", "running", "validating": exec("INSERT INTO image_builds(id,version,status,created_at) VALUES('active','2.0.0',?,'now')", mode) case "worker": buildQueue.Lock() buildQueue.active[a.s.DB] = true buildQueue.Unlock() defer func() { buildQueue.Lock(); delete(buildQueue.active, a.s.DB); buildQueue.Unlock() }() case "db-failure": exec("ALTER TABLE instances RENAME COLUMN image_id TO broken") want = 500 case "task-query-failure": exec("CREATE TABLE upgrade_journal(broken TEXT)") want = 500 case "audit-failure": exec("CREATE TRIGGER deny_audit BEFORE INSERT ON audit BEGIN SELECT RAISE(FAIL,'unavailable'); END") want = 500 } if mode == "journal" || mode == "complete" || mode == "rolled_back" { exec("CREATE TABLE upgrade_journal(phase TEXT)") exec("INSERT INTO upgrade_journal VALUES(?)", mode) } queryID := id if mode == "invalid" { queryID = "runtime:latest" } r := httptest.NewRequest(http.MethodDelete, "/api/images?image_id="+queryID, nil) if sid != "" { r.AddCookie(&http.Cookie{Name: "dsh_session", Value: sid}) addTestAccess(r, sid) } w := httptest.NewRecorder() a.images(w, r) if w.Code != want { t.Fatalf("status=%d want=%d body=%s", w.Code, want, w.Body.String()) } wantDeletes := int32(0) if want == 200 || mode == "delete-failure" || mode == "delete-conflict" { wantDeletes = 1 } if deletes.Load() != wantDeletes { t.Fatalf("deletes=%d want=%d", deletes.Load(), wantDeletes) } if mode != "unmanaged" { var status string if err := a.s.DB.QueryRow("SELECT status FROM images WHERE image_id=?", id).Scan(&status); err != nil { t.Fatal(err) } wantStatus := "available" if want == 200 { wantStatus = "missing" } if status != wantStatus { t.Fatalf("image status=%s want=%s", status, wantStatus) } } var audits int if err := a.s.DB.QueryRow("SELECT count(*) FROM audit WHERE action='image.delete' AND uid='u' AND target=?", id).Scan(&audits); err != nil { t.Fatal(err) } if (audits == 1) != (want == 200) { t.Fatalf("audits=%d", audits) } var status string if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE image_id=?", id).Scan(&status); err != nil { t.Fatal(err) } if (status == "missing") != (want == 200) { t.Fatalf("build status=%s", status) } }) } }