images_test.go
⎇
Raw
1package e2e
2
3import (
4 "net/http"
5 "net/url"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func (e *env) blobFile(digest string) string {
13 return filepath.Join(e.DataDir, "registry", "blobs", strings.Replace(digest, ":", "/", 1))
14}
15
16func TestImagesTab(t *testing.T) {
17 e := newEnv(t, "REGISTRY_PULL", "users")
18 admin := e.admin()
19 alice := e.register("alice", "password123")
20 e.createRepo(admin, "img-repo")
21 e.createRepo(admin, "other-repo")
22 cfgA, layA, _ := pushImage(t, e, "img-repo", "v1", "a")
23 _, _, manB := pushImage(t, e, "img-repo", "v2", "b")
24 pushImage(t, e, "img-repo/web", "latest", "c")
25 // Shared layer: other-repo references the same bytes as img-repo v1.
26 sharedCfg, sharedLay, _ := pushImage(t, e, "other-repo", "v1", "a")
27 if sharedCfg != cfgA || sharedLay != layA {
28 t.Fatal("expected identical digests for identical content")
29 }
30
31 t.Run("tab visible and lists images with tags", func(t *testing.T) {
32 r := admin.get("/img-repo/images").mustStatus(200)
33 if !contains(admin.get("/img-repo").Texts(".repo-tab"), "Images") {
34 t.Error("Images tab missing")
35 }
36 titles := r.Texts(".release-item-title")
37 if len(titles) != 2 || !contains(titles, "img-repo") || !contains(titles, "img-repo/web") {
38 t.Errorf("images = %v", titles)
39 }
40 if tags := r.Texts(".image-tag .badge"); len(tags) != 3 {
41 t.Errorf("tags = %v", tags)
42 }
43 })
44
45 t.Run("tags are listed newest first", func(t *testing.T) {
46 for tag, at := range map[string]string{"v1": "2026-01-01T00:00:00Z", "v2": "2026-02-01T00:00:00Z"} {
47 if _, err := e.DB.Exec(`UPDATE registry_tags SET updated_at = ? WHERE tag = ? AND image = ''`, at, tag); err != nil {
48 t.Fatal(err)
49 }
50 }
51 tags := admin.get("/img-repo/images").Texts(".image-tag .badge")
52 if want := []string{"v2", "v1", "latest"}; !eqStrings(tags, want) {
53 t.Errorf("tags = %v, want %v", tags, want)
54 }
55 })
56
57 t.Run("access follows REGISTRY_PULL", func(t *testing.T) {
58 alice.get("/img-repo/images").mustStatus(200)
59 if e.anon().get("/img-repo/images").Code != 403 {
60 t.Error("anonymous could open the Images tab with REGISTRY_PULL=users")
61 }
62 if contains(e.anon().get("/img-repo").Texts(".repo-tab"), "Images") {
63 t.Error("Images tab shown to anonymous")
64 }
65 if alice.get("/img-repo/images").Has(`form[action="/img-repo/images/delete"]`) {
66 t.Error("delete form shown to non-admin")
67 }
68 alice.post("/img-repo/images/delete-all", nil).mustStatus(403)
69 })
70
71 t.Run("delete tag removes an untagged manifest", func(t *testing.T) {
72 // v2 is unique to this image, so its manifest file must go.
73 admin.post("/img-repo/images/delete", url.Values{"image": {""}, "tag": {"v2"}}).mustRedirect("/img-repo/images")
74 if got := regTags(t, e, "img-repo", ""); len(got) != 1 || got[0] != "v1" {
75 t.Errorf("tags = %v", got)
76 }
77 regAdmin(t, e, http.MethodGet, "/v2/img-repo/manifests/"+manB, nil).mustStatus(404)
78 if _, err := os.Stat(e.blobFile(manB)); !os.IsNotExist(err) {
79 t.Error("manifest file still on disk")
80 }
81 // The v1 layer stays: other-repo links the same bytes.
82 if _, err := os.Stat(e.blobFile(layA)); err != nil {
83 t.Error("shared layer file removed")
84 }
85 })
86
87 t.Run("delete image removes only its unshared files", func(t *testing.T) {
88 admin.post("/img-repo/images/delete", url.Values{"image": {""}}).mustRedirect("/img-repo/images")
89 regAdmin(t, e, http.MethodGet, "/v2/img-repo/tags/list", nil).mustStatus(200)
90 if got := regTags(t, e, "img-repo", ""); len(got) != 0 {
91 t.Errorf("tags = %v", got)
92 }
93 if _, err := os.Stat(e.blobFile(layA)); err != nil {
94 t.Error("layer shared with other-repo was removed")
95 }
96 if titles := admin.get("/img-repo/images").Texts(".release-item-title"); len(titles) != 1 {
97 t.Errorf("images after delete = %v", titles)
98 }
99 })
100
101 t.Run("delete all empties the tab", func(t *testing.T) {
102 admin.post("/img-repo/images/delete-all", nil).mustRedirect("/img-repo/images")
103 r := admin.get("/img-repo/images")
104 if r.Count(".release-item-title") != 0 || !r.Contains("No images yet") {
105 t.Error("images remain after delete all")
106 }
107 })
108
109 t.Run("deleting a repo removes its unshared image files", func(t *testing.T) {
110 _, layD, manD := pushImage(t, e, "other-repo", "v2", "d")
111 admin.post("/other-repo/settings/delete", nil).mustRedirect("/")
112 for _, d := range []string{layD, manD} {
113 if _, err := os.Stat(e.blobFile(d)); !os.IsNotExist(err) {
114 t.Errorf("file %s survived repo delete", d)
115 }
116 }
117 })
118}
119
120func TestImagesTabPublicAndPrivate(t *testing.T) {
121 e := newEnv(t, "REGISTRY_PULL", "public")
122 admin := e.admin()
123 e.createRepo(admin, "pub-img")
124 e.createRepo(admin, "priv-img", "is_private", "1")
125 pushImage(t, e, "pub-img", "v1", "p")
126 pushImage(t, e, "priv-img", "v1", "q")
127
128 if !contains(e.anon().get("/pub-img").Texts(".repo-tab"), "Images") {
129 t.Error("Images tab hidden from anonymous with REGISTRY_PULL=public")
130 }
131 e.anon().get("/pub-img/images").mustStatus(200)
132 if e.anon().get("/priv-img/images").Code == 200 {
133 t.Error("private repo images visible to anonymous")
134 }
135 admin.get("/priv-img/images").mustStatus(200)
136}
137