Files
dsh-web/internal/api/images_delete_test.go
T

169 lines
5.3 KiB
Go

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)
}
})
}
}