package e2e import ( "net/http" "net/url" "os" "path/filepath" "strings" "testing" ) func (e *env) blobFile(digest string) string { return filepath.Join(e.DataDir, "registry", "blobs", strings.Replace(digest, ":", "/", 1)) } func TestImagesTab(t *testing.T) { e := newEnv(t, "REGISTRY_PULL", "users") admin := e.admin() alice := e.register("alice", "password123") e.createRepo(admin, "img-repo") e.createRepo(admin, "other-repo") cfgA, layA, _ := pushImage(t, e, "img-repo", "v1", "a") _, _, manB := pushImage(t, e, "img-repo", "v2", "b") pushImage(t, e, "img-repo/web", "latest", "c") // Shared layer: other-repo references the same bytes as img-repo v1. sharedCfg, sharedLay, _ := pushImage(t, e, "other-repo", "v1", "a") if sharedCfg != cfgA || sharedLay != layA { t.Fatal("expected identical digests for identical content") } t.Run("tab visible and lists images with tags", func(t *testing.T) { r := admin.get("/img-repo/images").mustStatus(200) if !contains(admin.get("/img-repo").Texts(".repo-tab"), "Images") { t.Error("Images tab missing") } titles := r.Texts(".release-item-title") if len(titles) != 2 || !contains(titles, "img-repo") || !contains(titles, "img-repo/web") { t.Errorf("images = %v", titles) } if tags := r.Texts(".image-tag .badge"); len(tags) != 3 { t.Errorf("tags = %v", tags) } }) t.Run("access follows REGISTRY_PULL", func(t *testing.T) { alice.get("/img-repo/images").mustStatus(200) if e.anon().get("/img-repo/images").Code != 403 { t.Error("anonymous could open the Images tab with REGISTRY_PULL=users") } if contains(e.anon().get("/img-repo").Texts(".repo-tab"), "Images") { t.Error("Images tab shown to anonymous") } if alice.get("/img-repo/images").Has(`form[action="/img-repo/images/delete"]`) { t.Error("delete form shown to non-admin") } alice.post("/img-repo/images/delete-all", nil).mustStatus(403) }) t.Run("delete tag removes an untagged manifest", func(t *testing.T) { // v2 is unique to this image, so its manifest file must go. admin.post("/img-repo/images/delete", url.Values{"image": {""}, "tag": {"v2"}}).mustRedirect("/img-repo/images") if got := regTags(t, e, "img-repo", ""); len(got) != 1 || got[0] != "v1" { t.Errorf("tags = %v", got) } regAdmin(t, e, http.MethodGet, "/v2/img-repo/manifests/"+manB, nil).mustStatus(404) if _, err := os.Stat(e.blobFile(manB)); !os.IsNotExist(err) { t.Error("manifest file still on disk") } // The v1 layer stays: other-repo links the same bytes. if _, err := os.Stat(e.blobFile(layA)); err != nil { t.Error("shared layer file removed") } }) t.Run("delete image removes only its unshared files", func(t *testing.T) { admin.post("/img-repo/images/delete", url.Values{"image": {""}}).mustRedirect("/img-repo/images") regAdmin(t, e, http.MethodGet, "/v2/img-repo/tags/list", nil).mustStatus(200) if got := regTags(t, e, "img-repo", ""); len(got) != 0 { t.Errorf("tags = %v", got) } if _, err := os.Stat(e.blobFile(layA)); err != nil { t.Error("layer shared with other-repo was removed") } if titles := admin.get("/img-repo/images").Texts(".release-item-title"); len(titles) != 1 { t.Errorf("images after delete = %v", titles) } }) t.Run("delete all empties the tab", func(t *testing.T) { admin.post("/img-repo/images/delete-all", nil).mustRedirect("/img-repo/images") r := admin.get("/img-repo/images") if r.Count(".release-item-title") != 0 || !r.Contains("No images yet") { t.Error("images remain after delete all") } }) t.Run("deleting a repo removes its unshared image files", func(t *testing.T) { _, layD, manD := pushImage(t, e, "other-repo", "v2", "d") admin.post("/other-repo/settings/delete", nil).mustRedirect("/") for _, d := range []string{layD, manD} { if _, err := os.Stat(e.blobFile(d)); !os.IsNotExist(err) { t.Errorf("file %s survived repo delete", d) } } }) } func TestImagesTabPublicAndPrivate(t *testing.T) { e := newEnv(t, "REGISTRY_PULL", "public") admin := e.admin() e.createRepo(admin, "pub-img") e.createRepo(admin, "priv-img", "is_private", "1") pushImage(t, e, "pub-img", "v1", "p") pushImage(t, e, "priv-img", "v1", "q") if !contains(e.anon().get("/pub-img").Texts(".repo-tab"), "Images") { t.Error("Images tab hidden from anonymous with REGISTRY_PULL=public") } e.anon().get("/pub-img/images").mustStatus(200) if e.anon().get("/priv-img/images").Code == 200 { t.Error("private repo images visible to anonymous") } admin.get("/priv-img/images").mustStatus(200) }