package api import ( "context" "database/sql" "encoding/json" "errors" "io" "net/http" "os" "strings" "sync/atomic" "testing" "time" ) func TestImageLegacyMigrationNeverCertifies(t *testing.T) { a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v1.47/images/json" { io.WriteString(w, "[]") return } writeImageInspect(w, "legacy-image", "1.2.3") }) _, err := a.s.DB.Exec(`CREATE TABLE image_builds(id TEXT PRIMARY KEY,version TEXT NOT NULL,status TEXT NOT NULL,logs TEXT NOT NULL DEFAULT '',error TEXT NOT NULL DEFAULT '',image_id TEXT NOT NULL DEFAULT '',created_at TEXT NOT NULL); CREATE UNIQUE INDEX image_builds_version ON image_builds(version) WHERE status IN ('queued','running','validating','success'); INSERT INTO image_builds(id,version,status,image_id,created_at) VALUES('legacy','1.2.3','success','legacy-image','now'),('queued','1.2.4','queued','','now'); CREATE TABLE images(image_id TEXT PRIMARY KEY,version TEXT NOT NULL,tag TEXT NOT NULL DEFAULT '',status TEXT NOT NULL,built_at TEXT NOT NULL); INSERT INTO images VALUES('legacy-image','1.2.3','','available','now');`) if err != nil { t.Fatal(err) } verified, err := a.verifiedImages(context.Background()) if err != nil || len(verified) != 0 { t.Fatalf("%v %v", verified, err) } var recipe, platform sql.NullString if err = a.s.DB.QueryRow("SELECT recipe_rev,platform FROM image_builds WHERE id='legacy'").Scan(&recipe, &platform); err != nil || recipe.Valid || platform.Valid { t.Fatalf("legacy metadata inferred: %v %v %v", recipe, platform, err) } w := imageRequest(a.images, sid, "GET", "") var images []imageRecord if err = json.Unmarshal(w.Body.Bytes(), &images); err != nil || len(images) != 1 || images[0].Status != "unverified" || images[0].RecipeRev != imageRecipeRev || images[0].Platform != imagePlatform { t.Fatal(w.Body.String(), err) } if err = a.s.DB.QueryRow("SELECT recipe_rev,platform FROM image_builds WHERE id='legacy'").Scan(&recipe, &platform); err != nil || recipe.Valid || platform.Valid { t.Fatalf("GET certified legacy metadata: %v %v %v", recipe, platform, err) } var status string if err = a.s.DB.QueryRow("SELECT status FROM image_builds WHERE id='queued'").Scan(&status); err != nil || status != "failed" { t.Fatal(status, err) } seedVerifiedImage(t, a, "new-image", "1.2.3") verified, err = a.verifiedImages(context.Background()) if err != nil || len(verified) != 1 || verified["new-image"] != "1.2.3" { t.Fatal(verified, err) } } func TestImageInspectIdentity(t *testing.T) { for _, mode := range []string{"valid", "recipe", "wrong-key", "platform", "variant", "unknown-platform", "role", "version", "id", "missing", "unavailable"} { t.Run(mode, func(t *testing.T) { a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Fatal("unexpected mutation") } if mode == "missing" { w.WriteHeader(404) return } if mode == "unavailable" { w.WriteHeader(500) return } parts := strings.Split(imagePlatform, "/") labels := map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "1.2.3", "dsh-mgr.recipe": imageRecipeRev} im := map[string]any{"Id": "image", "Os": parts[0], "Architecture": parts[1], "Config": map[string]any{"Labels": labels}} switch mode { case "recipe": labels["dsh-mgr.recipe"] = "old" case "wrong-key": delete(labels, "dsh-mgr.recipe") labels["dsh-mgr.recipe_rev"] = imageRecipeRev case "platform": im["Architecture"] = "other" case "variant": im["Variant"] = "v7" case "unknown-platform": delete(im, "Architecture") case "role": labels["dsh-mgr.role"] = "other" case "version": labels["dsh-mgr.version"] = "2.0.0" case "id": im["Id"] = "other" } writeJSON(w, im) }) ok, err := a.imageMatches(context.Background(), "image", "1.2.3") if ok != (mode == "valid") || (err != nil) != (mode == "unavailable") { t.Fatal(ok, err) } }) } } func TestImageIndexLocalInventory(t *testing.T) { for _, mode := range []string{"external", "old-recipe", "other-platform", "inspect-failure"} { t.Run(mode, func(t *testing.T) { var deleted atomic.Bool a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { t.Errorf("unexpected Docker mutation: %s", r.Method) } if r.URL.Path == "/v1.47/images/json" { if deleted.Load() { io.WriteString(w, "[]") } else { io.WriteString(w, `[{"Id":"image","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.recipe":"2","dsh-mgr.version":"9.9.9"}}]`) } return } if mode == "inspect-failure" { w.WriteHeader(500) return } if deleted.Load() { w.WriteHeader(404) return } parts := strings.Split(imagePlatform, "/") im := inspectedImage{ID: "image", Os: parts[0], Architecture: parts[1], Created: "2026-01-01T00:00:00Z", RepoTags: []string{"dsh-web:1.2.3"}} im.Config.Labels = map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": "1.2.3", "dsh-mgr.recipe": imageRecipeRev} if mode == "old-recipe" { im.Config.Labels["dsh-mgr.recipe"] = "1" } if mode == "other-platform" { im.Architecture, im.Variant = "arm", "v7" } writeJSON(w, im) }) if mode != "external" { seedVerifiedImage(t, a, "image", "1.2.3") } if mode == "old-recipe" { if _, err := a.s.DB.Exec("UPDATE image_builds SET recipe_rev='1'"); err != nil { t.Fatal(err) } } w := imageRequest(a.images, sid, "GET", "") if mode == "inspect-failure" { var status string if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE image_id='image'").Scan(&status); err != nil || status != "success" || w.Code != 502 { t.Fatal(status, w.Code, err) } return } var images []imageRecord if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil || w.Code != 200 || len(images) != 1 || images[0].Status != "unverified" || images[0].Version != "1.2.3" || images[0].Created != "2026-01-01T00:00:00Z" || images[0].Tag != "dsh-web:1.2.3" { t.Fatal(w.Body.String(), err) } if mode == "other-platform" && images[0].Platform != strings.Split(imagePlatform, "/")[0]+"/arm/v7" { t.Fatal(images[0].Platform) } if mode == "old-recipe" && images[0].RecipeRev != "1" { t.Fatal(images[0].RecipeRev) } if w := imageRequest(a.instances, sid, "POST", `{"name":"rejected","image_id":"image"}`); w.Code != 400 { t.Fatal(w.Code, w.Body.String()) } deleted.Store(true) w = imageRequest(a.images, sid, "GET", "") if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil || w.Code != 200 || len(images) != 1 || images[0].Status != "missing" { t.Fatal(w.Body.String(), err) } }) } } func TestImageIndexUsageMissingAndDockerFailure(t *testing.T) { var mode atomic.Int32 a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { t.Error("unexpected mutation") } if r.URL.Path == "/v1.47/images/json" && mode.Load() != 1 { io.WriteString(w, "[]") return } switch mode.Load() { case 1: w.WriteHeader(500) case 2: w.WriteHeader(404) default: writeImageInspect(w, "image", "1.2.3") } }) seedVerifiedImage(t, a, "image", "1.2.3") if _, err := a.s.DB.Exec(`INSERT INTO instances(id,uid,name,image_id,status,created) VALUES('one','u','one','image','running',0),('two','other-user','two','image','stopped',0),('three','u','three','other','running',0)`); err != nil { t.Fatal(err) } check := func(status string, count int) { t.Helper() w := imageRequest(a.images, sid, "GET", "") var images []imageRecord if err := json.Unmarshal(w.Body.Bytes(), &images); err != nil || w.Code != 200 || len(images) != 1 || images[0].Status != status || images[0].UsageCount != count { t.Fatal(w.Body.String(), err) } var storedStatus string var storedCount int if err := a.s.DB.QueryRow("SELECT status,usage_count FROM images WHERE image_id='image'").Scan(&storedStatus, &storedCount); err != nil || storedStatus != status || storedCount != count { t.Fatal(storedStatus, storedCount, err) } } check("available", 2) mode.Store(1) if w := imageRequest(a.images, sid, "GET", ""); w.Code != 502 { t.Fatal(w.Code, w.Body.String()) } var status string if err := a.s.DB.QueryRow("SELECT status FROM image_builds WHERE image_id='image'").Scan(&status); err != nil || status != "success" { t.Fatal(status, err) } mode.Store(2) check("missing", 2) if _, err := a.s.DB.Exec("DELETE FROM instances WHERE id='two'"); err != nil { t.Fatal(err) } check("missing", 1) verified, err := a.verifiedImages(context.Background()) if err != nil || len(verified) != 0 { t.Fatal(verified, err) } } func TestBuildRateLimitDoesNotPenalizeSharedTask(t *testing.T) { a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/v1.47/images/verified/json" { writeImageInspect(w, "verified", "1.2.3") return } io.WriteString(w, `[{"Id":"verified","Labels":{"dsh-mgr.role":"runtime","dsh-mgr.version":"1.2.3","dsh-mgr.recipe":"`+imageRecipeRev+`"}}]`) }) seedVerifiedImage(t, a, "verified", "1.2.3") for _, id := range []string{"a", "b", "c"} { if _, err := a.s.DB.Exec("INSERT INTO image_build_requests VALUES('u',?,?)", id, time.Now().UTC().Format(time.RFC3339Nano)); err != nil { t.Fatal(err) } } if w := imageRequest(a.builds, sid, "POST", `{"version":"1.2.3"}`); w.Code != 202 { t.Fatal(w.Code, w.Body.String()) } if w := imageRequest(a.builds, sid, "POST", `{"version":"1.2.4"}`); w.Code != 429 || w.Header().Get("Retry-After") == "" { t.Fatal(w.Code, w.Body.String()) } var n int a.s.DB.QueryRow("SELECT count(*) FROM image_build_requests").Scan(&n) if n != 3 { t.Fatal(n) } } func TestBuildDiskPolicy(t *testing.T) { a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("unexpected build") }) if err := a.checkBuildDisk(context.Background()); err != nil { t.Fatal(err) } probeErr := errors.New("capacity probe failed") for _, tc := range []struct { name string available uint64 err error want string }{ {"insufficient", minimumBuildDiskBytes - 1, nil, "insufficient"}, {"exact", minimumBuildDiskBytes, nil, ""}, {"probe-error", 0, probeErr, "unavailable"}, } { t.Run(tc.name, func(t *testing.T) { buildDiskAvailable = func(path string) (uint64, error) { if path != os.Getenv("DSH_DOCKER_ROOT_VISIBLE") { t.Fatalf("unexpected probe path %q", path) } return tc.available, tc.err } err := a.checkBuildDisk(context.Background()) if tc.want == "" && err != nil || tc.want != "" && (err == nil || !strings.Contains(err.Error(), tc.want)) { t.Fatal(err) } if tc.err != nil && !errors.Is(err, tc.err) { t.Fatalf("probe error not preserved: %v", err) } }) } buildDiskAvailable = func(string) (uint64, error) { t.Fatal("unconfirmed Docker root must not be probed") return 0, nil } t.Setenv("DSH_DOCKER_ROOT_VISIBLE", "") if err := a.checkBuildDisk(context.Background()); err == nil || !strings.Contains(err.Error(), "unknown") { t.Fatal(err) } if requireBuildDisk(minimumBuildDiskBytes-1) == nil || requireBuildDisk(minimumBuildDiskBytes) != nil { t.Fatal("disk threshold") } } func TestUnknownDockerDiskBlocksBuild(t *testing.T) { a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1.47/images/json" { t.Errorf("unexpected Docker operation %s", r.URL.Path) } io.WriteString(w, "[]") }) t.Setenv("DSH_DOCKER_ROOT_VISIBLE", "") w := imageRequest(a.builds, sid, "POST", `{"version":"1.2.3"}`) if w.Code != 202 { t.Fatal(w.Body.String()) } deadline := time.Now().Add(3 * time.Second) for { var status, detail string if err := a.s.DB.QueryRow("SELECT status,error FROM image_builds").Scan(&status, &detail); err != nil { t.Fatal(err) } if status == "failed" { if !strings.Contains(detail, "capacity unknown") { t.Fatal(detail) } break } if time.Now().After(deadline) { t.Fatal(status, detail) } time.Sleep(time.Millisecond) } } func TestExactNPMMetadata(t *testing.T) { for _, test := range []struct { body string status int ok bool }{ {`{"version":"1.2.3"}`, 200, true}, {`{"version":"1.2.4"}`, 200, false}, {`{"version":"latest"}`, 200, false}, {`{}`, 404, false}, {`invalid`, 200, false}, } { client := &http.Client{Transport: imageRoundTrip(func(r *http.Request) (*http.Response, error) { if r.URL.Host != "registry.npmjs.org" || !strings.HasSuffix(r.URL.Path, "/1.2.3") { t.Error(r.URL) } return &http.Response{StatusCode: test.status, Body: io.NopCloser(strings.NewReader(test.body)), Header: make(http.Header)}, nil })} _, err := fetchNPMVersion(context.Background(), client, "1.2.3") if (err == nil) != test.ok { t.Fatalf("%s: %v", test.body, err) } } } func TestBuildListReconcilesMissingAndLatestTimestamp(t *testing.T) { a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "[]") }) seedVerifiedImage(t, a, "deleted", "1.2.3") w := imageRequest(a.builds, sid, "GET", "") var records []buildRecord if err := json.Unmarshal(w.Body.Bytes(), &records); err != nil || len(records) != 1 || records[0].Status != "missing" { t.Fatal(w.Body.String()) } w = imageRequest(a.latest, sid, "GET", "") var latest map[string]string if err := json.Unmarshal(w.Body.Bytes(), &latest); err != nil { t.Fatal(err) } if _, err := time.Parse(time.RFC3339, latest["checked_at"]); err != nil || latest["version"] != "1.2.3" { t.Fatal(latest, err) } }