package api import ( "context" "encoding/json" "fmt" "git.misaka.ren/m1saka/dsh-web/internal/store" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestHistoricalExperimentalBuildRequiresAcceptance(t *testing.T) { id := "sha256:" + strings.Repeat("d", 64) a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/v1.47/images/json": writeJSON(w, []any{map[string]any{"Id": id, "Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "9.8.7", "dsh-mgr.recipe": imageRecipeRev}}}) case "/v1.47/images/" + id + "/json": writeImageInspect(w, id, "9.8.7") case "/v1.47/containers/create": writeJSON(w, map[string]string{"Id": "check"}) case "/v1.47/containers/check/exec": writeJSON(w, map[string]string{"Id": "probe"}) case "/v1.47/exec/probe/json": writeJSON(w, map[string]any{"Running": false, "ExitCode": 0}) case "/v1.47/containers/check/json": writeJSON(w, map[string]any{"State": map[string]bool{"Running": true}}) default: w.WriteHeader(204) } }) if _, err := a.s.DB.Exec("UPDATE users SET admin=1 WHERE id='u'"); err != nil { t.Fatal(err) } seedVerifiedImage(t, a, id, "9.8.7") if _, err := a.s.DB.Exec("DELETE FROM image_admissions; UPDATE image_builds SET experimental=1"); err != nil { t.Fatal(err) } im, err := a.evaluateImage(context.Background(), id) if err != nil || im.Selectable || im.Status != "experimental" { t.Fatal(im, err) } } func TestLegacyQueuedBuildFailsBeforeDocker(t *testing.T) { a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unsupported build contacted Docker") }) buildQueue.Lock() err := a.ensureBuilds() buildQueue.Unlock() if err != nil { t.Fatal(err) } previous := imagePlatform imagePlatform = "linux/arm64" defer func() { imagePlatform = previous }() b := buildRecord{ID: "unsupported", Version: "0.2.0-rc.2", Status: "queued", RecipeRev: imageRecipeRev, Platform: imagePlatform} if err := a.insertBuildRequest(b, "u"); err != nil { t.Fatal(err) } a.runImageBuild(b) var state, detail string if err := a.s.DB.QueryRow("SELECT status,error FROM image_builds WHERE id=?", b.ID).Scan(&state, &detail); err != nil || state != "failed" || !strings.Contains(detail, "Git release identity required") { t.Fatal(state, detail, err) } } func TestAdmissionDowngradeWaitsForCreate(t *testing.T) { id := "sha256:" + strings.Repeat("e", 64) entered, release := make(chan struct{}), make(chan struct{}) a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/images/"): writeImageInspect(w, id, "0.2.0-rc.1") case strings.HasSuffix(r.URL.Path, "/networks/create"): close(entered) <-release w.WriteHeader(204) case strings.HasSuffix(r.URL.Path, "/containers/create"): writeJSON(w, map[string]string{"Id": "created"}) default: w.WriteHeader(204) } }) seedVerifiedImage(t, a, id, "0.2.0-rc.1") if _, err := a.s.DB.Exec("UPDATE users SET admin=1"); err != nil { t.Fatal(err) } created := make(chan int, 1) go func() { w := httptest.NewRecorder() a.instances(w, runtimeRequest("POST", "/api/instances", fmt.Sprintf(`{"name":"race","image_id":%q}`, id), sid)) created <- w.Code }() select { case <-entered: case <-time.After(3 * time.Second): close(release) t.Fatal("create did not reach network") } downgraded := make(chan int, 1) go func() { w := httptest.NewRecorder() a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":"blocked","evidence":"downgrade test"}`, id), sid)) downgraded <- w.Code }() select { case status := <-downgraded: close(release) t.Fatalf("admission raced active create: %d", status) case <-time.After(30 * time.Millisecond): } close(release) if status := <-created; status != 200 { t.Fatal(status) } if status := <-downgraded; status != 200 { t.Fatal(status) } w := httptest.NewRecorder() a.instances(w, runtimeRequest("POST", "/api/instances", fmt.Sprintf(`{"name":"denied","image_id":%q}`, id), sid)) if w.Code != 400 { t.Fatal(w.Code, w.Body.String()) } } func TestAdmissionRejectsLegacyLayouts(t *testing.T) { for _, recipe := range []string{"", "1", "2", "3", "4", "5", "6", "8"} { t.Run(recipe, func(t *testing.T) { id := "sha256:" + strings.Repeat("a", 64) a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.URL.Path, "/images/") { t.Error("unexpected Docker operation", r.Method, r.URL.Path) } writeJSON(w, map[string]any{"Id": id, "Os": "linux", "Architecture": "amd64", "Config": map[string]any{"Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "1.2.3", "dsh-mgr.recipe": recipe}}}) }) seedVerifiedImage(t, a, id, "1.2.3") for _, query := range []string{"UPDATE image_builds SET recipe_rev=?", "UPDATE image_admissions SET recipe_rev=?"} { if _, err := a.s.DB.Exec(query, recipe); err != nil { t.Fatal(err) } } if _, err := a.s.DB.Exec("UPDATE users SET admin=1"); err != nil { t.Fatal(err) } im, err := a.evaluateImage(context.Background(), id) if err != nil || im.Selectable || im.Admission != "existing_only" { t.Fatal(im, err) } for _, state := range []string{"compatible", "recommended"} { w := httptest.NewRecorder() a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":%q,"evidence":"historical acceptance"}`, id, state), sid)) if w.Code != 400 { t.Fatal(w.Code, w.Body.String()) } } }) } } func TestAdmissionCurrentCreateAndBlock(t *testing.T) { id := "sha256:" + strings.Repeat("a", 64) var body map[string]any a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/images/"): writeJSON(w, map[string]any{"Id": id, "Os": "linux", "Architecture": "amd64", "Config": map[string]any{"Labels": map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "1.2.3", "dsh-mgr.recipe": imageRecipeRev}}}) case strings.HasSuffix(r.URL.Path, "/containers/create"): if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Error(err) } writeJSON(w, map[string]string{"Id": "created"}) default: w.WriteHeader(204) } }) seedVerifiedImage(t, a, id, "1.2.3") if _, err := a.s.DB.Exec("DELETE FROM image_admissions; UPDATE users SET admin=1"); err != nil { t.Fatal(err) } im, err := a.evaluateImage(context.Background(), id) if err != nil || im.Status != "existing_only" || im.Selectable { t.Fatal(im, err) } records := []buildRecord{{ID: "unused", ImageID: id, Version: "1.2.3", RecipeRev: imageRecipeRev, Platform: imagePlatform, Status: "success"}} if err := a.reconcileBuilds(context.Background(), records); err != nil || records[0].Status != "success" { t.Fatal(records, err) } admit := func(state string) *httptest.ResponseRecorder { w := httptest.NewRecorder() a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":%q,"evidence":"current layout explicitly accepted"}`, id, state), sid)) return w } if w := admit("compatible"); w.Code != 200 { t.Fatal(w.Code, w.Body.String()) } w := httptest.NewRecorder() a.instances(w, runtimeRequest("POST", "/api/instances", fmt.Sprintf(`{"name":"current","image_id":%q}`, id), sid)) if w.Code != 200 { t.Fatal(w.Code, w.Body.String()) } host := body["HostConfig"].(map[string]any) mounts := host["Mounts"].([]any) if len(mounts) != 1 || mounts[0].(map[string]any)["Target"] != "/workspace" || !strings.Contains(fmt.Sprint(body["Env"]), "DSH_HOME=/workspace") { t.Fatal(body) } var instance store.Instance if err := json.Unmarshal(w.Body.Bytes(), &instance); err != nil { t.Fatal(err) } if w := admit("blocked"); w.Code != 200 { t.Fatal(w.Body.String()) } for _, action := range []string{"restore", "backups", "profile"} { w := httptest.NewRecorder() a.config(w, runtimeRequest("POST", "/api/instances/"+instance.ID+"/"+action, `{}`, sid), instance, action) if w.Code != 409 { t.Fatal(action, w.Code, w.Body.String()) } } for _, action := range []string{"start", "restart"} { w := httptest.NewRecorder() a.instance(w, runtimeRequest("POST", "/api/instances/"+instance.ID+"/"+action, "", sid)) if w.Code != 409 { t.Fatal(action, w.Code, w.Body.String()) } } if _, err := a.s.DB.Exec("CREATE TRIGGER audit_fail BEFORE INSERT ON audit BEGIN SELECT RAISE(FAIL,'audit unavailable'); END"); err != nil { t.Fatal(err) } if w := admit("compatible"); w.Code != 500 { t.Fatal(w.Code, w.Body.String()) } im, err = a.evaluateImage(context.Background(), id) if err != nil || im.Admission != "blocked" { t.Fatal(im, err) } } func TestAdmissionExperimentalAndPreflight(t *testing.T) { id := "sha256:" + strings.Repeat("b", 64) a, sid := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) { writeImageInspect(w, id, "9.8.7") }) seedVerifiedImage(t, a, id, "9.8.7") if _, err := a.s.DB.Exec("DELETE FROM image_admissions; UPDATE image_builds SET experimental=1"); err != nil { t.Fatal(err) } im, err := a.evaluateImage(context.Background(), id) if err != nil || im.Status != "experimental" || im.Selectable { t.Fatal(im, err) } for _, tc := range []struct { handler http.HandlerFunc body string want int }{{a.builds, `{"version":"9.8.7"}`, 405}, {a.builds, `{"version":"9.8.7","experimental":true}`, 405}, {a.instances, fmt.Sprintf(`{"name":"unknown","image_id":%q}`, id), 400}} { w := httptest.NewRecorder() tc.handler(w, runtimeRequest("POST", "/api/test", tc.body, sid)) if w.Code != tc.want { t.Fatal(w.Code, w.Body.String()) } } var n int if err := a.s.DB.QueryRow("SELECT count(*) FROM image_build_requests").Scan(&n); err != nil || n != 0 { t.Fatal(n, err) } if _, err := a.s.DB.Exec("UPDATE users SET admin=1"); err != nil { t.Fatal(err) } w := httptest.NewRecorder() a.imageAdmission(w, runtimeRequest("POST", "/api/images/admission", fmt.Sprintf(`{"image_id":%q,"state":"compatible","evidence":"accepted after startup validation"}`, id), sid)) if w.Code != 200 { t.Fatal(w.Body.String()) } im, err = a.evaluateImage(context.Background(), id) if err != nil || !im.Selectable { t.Fatal(im, err) } if _, err := a.s.DB.Exec("UPDATE image_builds SET status='failed'"); err != nil { t.Fatal(err) } im, err = a.evaluateImage(context.Background(), id) if err != nil || im.Selectable || im.Status != "unverified" { t.Fatal(im, err) } }