1// SPDX-License-Identifier: MIT
2
3package registryproxy
4
5import (
6 "fmt"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "sync"
12 "testing"
13)
14
15// Accept is reduced to the known manifest types, so its variants map to
16// a bounded set of keys.
17func TestNormalizeAccept(t *testing.T) {
18 const index, v2 = "application/vnd.oci.image.index.v1+json", "application/vnd.docker.distribution.manifest.v2+json"
19 all := normalizeAccept("")
20 if strings.Count(all, ",") != len(manifestTypes)-1 {
21 t.Fatalf("default key %q", all)
22 }
23 for _, tc := range []struct{ in, want string }{
24 {index + ";q=0.9, " + strings.ToUpper(v2) + " , " + index, v2 + "," + index},
25 {" " + index + " ;q=0.1", index},
26 {"text/html, */*", all},
27 {"", all},
28 } {
29 if got := normalizeAccept(tc.in); got != tc.want {
30 t.Errorf("normalizeAccept(%q) = %q, want %q", tc.in, got, tc.want)
31 }
32 }
33}
34
35// The Accept variants a job sends for one tag reach upstream with every known
36// type they name, and make at most one tag file per subset of the known types.
37// dockerd's legacy client sends one Accept line per type; the 8 KiB bound
38// applies to the joined lines.
39func TestAcceptTagFiles(t *testing.T) {
40 types := []string{
41 "application/vnd.oci.image.index.v1+json",
42 "application/vnd.docker.distribution.manifest.list.v2+json",
43 "application/vnd.docker.distribution.manifest.v2+json",
44 }
45 for _, tc := range []struct {
46 name string
47 accepts func(i int) []string // the Accept lines of request i
48 requests int
49 maxTags int
50 code int
51 }{
52 {"multi-line", func(int) []string { return types }, 2, 1, http.StatusOK},
53 {"variants", func(i int) []string {
54 switch i % 3 {
55 case 1:
56 return []string{fmt.Sprintf("x/only-junk-%d", i)}
57 case 2:
58 return []string{fmt.Sprintf("%s;q=0.%d, x/junk-%d, %s", types[0], i%10, i, types[1])}
59 }
60 return []string{fmt.Sprintf("%s;q=0.%d, x/junk-%d", types[0], i%10, i)}
61 }, 300, 3, http.StatusOK},
62 {"oversized", func(int) []string {
63 lines := make([]string, 200)
64 for i := range lines {
65 lines[i] = strings.Repeat("x", 50)
66 }
67 return lines
68 }, 1, 0, http.StatusNotFound},
69 } {
70 t.Run(tc.name, func(t *testing.T) {
71 var mu sync.Mutex
72 var seen []string
73 srv := upstreamWith(t, func(w http.ResponseWriter, r *http.Request) {
74 mu.Lock()
75 seen = append(seen, strings.Join(r.Header.Values("Accept"), ","))
76 mu.Unlock()
77 w.Header().Set("Content-Type", types[0])
78 w.Header().Set("Docker-Content-Digest", sum(manifestBody))
79 if r.Method == http.MethodGet {
80 _, _ = io.WriteString(w, manifestBody)
81 }
82 })
83 p := proxyWith(t, srv, Config{})
84 for i := range tc.requests {
85 r := httptest.NewRequest(http.MethodGet, "/v2/library/alpine/manifests/3", nil)
86 lines := tc.accepts(i)
87 for _, line := range lines {
88 r.Header.Add("Accept", line)
89 }
90 w := httptest.NewRecorder()
91 p.ServeHTTP(w, r)
92 if w.Code != tc.code {
93 t.Fatalf("request %d: %d", i, w.Code)
94 }
95 if tc.code != http.StatusOK {
96 continue
97 }
98 mu.Lock()
99 got := seen[len(seen)-1]
100 mu.Unlock()
101 for _, typ := range types {
102 if strings.Contains(strings.Join(lines, ","), typ) && !strings.Contains(got, typ) {
103 t.Errorf("upstream Accept %q lacks %s", got, typ)
104 }
105 }
106 }
107 if n := accounted(t, p.s); n > tc.maxTags || (tc.maxTags > 0 && n == 0) {
108 t.Fatalf("%d tag files for %d requests", n, tc.requests)
109 }
110 })
111 }
112}