v0.6.0 aba298d

proxy_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package registryproxy
4
5import (
6	"fmt"
7	"io"
8	"net/http"
9	"net/http/httptest"
10	"os"
11	"path/filepath"
12	"strings"
13	"testing"
14)
15
16// A pulled manifest and blob are served from the store the second time: a tag
17// is revalidated by HEAD, a digest is not fetched again.
18func TestPullCached(t *testing.T) {
19	f := fake(t)
20	p := proxyFor(t, f)
21	for range 2 {
22		w := request(t, p, http.MethodGet, "/v2/library/alpine/manifests/3")
23		if w.Code != http.StatusOK || w.Body.String() != manifestBody {
24			t.Fatalf("manifest: %d %s", w.Code, w.Body)
25		}
26		w = request(t, p, http.MethodGet, "/v2/library/alpine/blobs/"+sum(blobBody))
27		if w.Code != http.StatusOK || w.Body.String() != blobBody {
28			t.Fatalf("blob: %d %s", w.Code, w.Body)
29		}
30	}
31	if f.heads.Load() != 2 || f.gets.Load() != 1 || f.blobs.Load() != 1 || f.tokens.Load() != 1 {
32		t.Fatalf("counts HEAD=%d GET=%d blobs=%d tokens=%d", f.heads.Load(), f.gets.Load(), f.blobs.Load(), f.tokens.Load())
33	}
34}
35
36// HEAD of a blob answers its length, cold or stored, with no body; a stored
37// blob serves ranges.
38func TestBlobHeadAndRange(t *testing.T) {
39	f := fake(t)
40	p := proxyFor(t, f)
41	path := "/v2/library/alpine/blobs/" + sum(blobBody)
42	for _, cached := range []bool{false, true} {
43		if cached {
44			if w := request(t, p, http.MethodGet, path); w.Code != http.StatusOK {
45				t.Fatal(w.Code)
46			}
47		}
48		w := request(t, p, http.MethodHead, path)
49		if w.Code != http.StatusOK || w.Body.Len() != 0 || w.Header().Get("Content-Length") != fmt.Sprint(len(blobBody)) {
50			t.Fatalf("HEAD %d %v %q", w.Code, w.Header(), w.Body.String())
51		}
52	}
53	w := request(t, p, http.MethodGet, path, "Range", "bytes=2-5")
54	if w.Code != http.StatusPartialContent || w.Body.String() != blobBody[2:6] {
55		t.Fatalf("range: %d %s", w.Code, w.Body)
56	}
57}
58
59// A blob is verified before any byte of it is sent: a corrupt one is a 502
60// with no body, and is not stored.
61func TestCorruptBlobRejected(t *testing.T) {
62	f := fake(t)
63	f.corrupt.Store(true)
64	p := proxyFor(t, f)
65	w := request(t, p, http.MethodGet, "/v2/library/alpine/blobs/"+sum(blobBody))
66	if w.Code != http.StatusBadGateway || strings.Contains(w.Body.String(), "xxxx") {
67		t.Fatalf("corrupt blob: %d %q", w.Code, w.Body)
68	}
69	if _, err := os.Stat(filepath.Join(p.s.dir, kindBlob.key(sum(blobBody)[7:]))); !os.IsNotExist(err) {
70		t.Fatal("corrupt blob stored")
71	}
72	if ds, _ := os.ReadDir(filepath.Join(p.s.dir, "tmp")); len(ds) != 0 {
73		t.Fatal("partial temp retained")
74	}
75}
76
77// Only GET and HEAD of valid names, tags and digests under /v2/ are served.
78func TestRouteValidation(t *testing.T) {
79	p := proxyFor(t, fake(t))
80	for _, path := range []string{"/v2/../library/alpine/manifests/3", "/v2/Library/alpine/manifests/3", "/v2/library/alpine/manifests/.bad", "/v2/library/alpine/manifests/sha256:bad", "/v2/library/alpine/blobs/sha256:bad", "/v2/library/alpine/tags/list", "/v3/library/alpine/manifests/3"} {
81		if w := request(t, p, http.MethodGet, path); w.Code != http.StatusNotFound {
82			t.Errorf("%s: %d", path, w.Code)
83		}
84	}
85	if w := request(t, p, http.MethodPut, "/v2/"); w.Code != http.StatusMethodNotAllowed {
86		t.Fatal(w.Code)
87	}
88	if w := request(t, p, http.MethodGet, "/v2/"); w.Code != http.StatusOK || w.Body.String() != "{}" {
89		t.Fatalf("/v2/: %d %s", w.Code, w.Body)
90	}
91}
92
93// A manifest whose bytes do not match its digest, or over the size limit, is
94// a 502 and is not stored.
95func TestManifestLimitsAndDigest(t *testing.T) {
96	for _, body := range []string{"tampered", strings.Repeat("x", manifestLimit+1)} {
97		up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98			if r.URL.Path == "/token" {
99				_, _ = io.WriteString(w, `{"token":"public"}`)
100				return
101			}
102			_, _ = io.WriteString(w, body)
103		}))
104		p, err := New(Config{Dir: t.TempDir(), Max: 8 << 20, Upstream: up.URL, AuthURL: up.URL + "/token"})
105		if err != nil {
106			t.Fatal(err)
107		}
108		w := request(t, p, http.MethodGet, "/v2/library/alpine/manifests/"+sum(manifestBody))
109		up.Close()
110		if w.Code != http.StatusBadGateway {
111			t.Fatal(w.Code)
112		}
113		if _, _, err := p.s.open(kindManifest, sum(manifestBody)); err == nil {
114			t.Fatal("invalid manifest stored")
115		}
116	}
117}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close