Files
dsh-web/internal/api/admission_test.go
T
m1saka 9a71772771 Harden image admission, resource recovery and session renewal
Add exact-image compatibility policy, experimental admission, paginated build history and shared reauthentication checks. Preserve uncertain Docker creation records, bound frontend resource waits and renew runtime sessions proactively.

Lazy-load and split Monaco, add regression and opt-in live Docker fault tests, and record full race and real-browser acceptance results.
2026-10-14 13:41:54 +08:00

275 lines
9.7 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"git.misaka.ren/m1saka/dsh-web/internal/store"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestExperimentalBuildSuccessRequiresAcceptance(t *testing.T) {
id := "sha256:" + strings.Repeat("d", 64)
a, sid := 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)
}
w := imageRequest(a.builds, sid, "POST", `{"version":"9.8.7","experimental":true}`)
if w.Code != 202 {
t.Fatal(w.Code, w.Body.String())
}
var b buildRecord
if err := json.Unmarshal(w.Body.Bytes(), &b); err != nil || !b.Experimental {
t.Fatal(b, err)
}
deadline := time.Now().Add(5 * time.Second)
for {
var state string
if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE id=?", b.ID).Scan(&state); err != nil {
t.Fatal(err)
}
if state == "success" {
break
}
if state == "failed" || time.Now().After(deadline) {
t.Fatal(state)
}
time.Sleep(10 * time.Millisecond)
}
im, err := a.evaluateImage(context.Background(), id)
if err != nil || im.Selectable || im.Status != "experimental" {
t.Fatal(im, err)
}
}
func TestQueuedUnsupportedPlatformFailsBeforeDocker(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, "experimental") {
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 TestAdmissionHistoricalCreateAndBlock(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": "4"}}})
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("UPDATE image_builds SET recipe_rev='4'; 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: "4", 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":"legacy 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":"legacy","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) != 2 || mounts[0].(map[string]any)["Target"] != "/data/dsh-home" || mounts[1].(map[string]any)["Target"] != "/workspace" || !strings.Contains(fmt.Sprint(body["Env"]), "DSH_HOME=/data/dsh-home") {
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"}`, 400}, {a.builds, `{"version":"9.8.7","experimental":true}`, 403}, {a.intents, `{"name":"unknown","version":"9.8.7"}`, 400}, {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.QueryRow("SELECT count(*) FROM creation_intents").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)
}
}