1 package modresolve
2
3 import (
4 "crypto/sha256"
5 "fmt"
6 "strings"
7 "testing"
8
9 "cuelang.org/go/cue"
10 "cuelang.org/go/cue/cuecontext"
11 "github.com/go-quicktest/qt"
12 )
13
14 func TestRegistryConfigSchema(t *testing.T) {
15 schema := RegistryConfigSchema()
16
17
18 ctx := cuecontext.New()
19 v := ctx.CompileString(schema)
20 fileSchema := v.LookupPath(cue.MakePath(cue.Def("#file")))
21 qt.Assert(t, qt.IsNil(fileSchema.Err()))
22 cfgVal := ctx.CompileString(`defaultRegistry: registry: "something.example"`)
23 qt.Assert(t, qt.IsNil(cfgVal.Err()))
24 cfgVal = cfgVal.Unify(fileSchema)
25 qt.Assert(t, qt.IsNil(cfgVal.Err()))
26 }
27
28 func TestParseCUERegistry(t *testing.T) {
29 testCases := []struct {
30 testName string
31 in string
32 catchAllDefault string
33 err string
34 wantAllHosts []Host
35 lookups map[string]*Location
36 }{{
37 testName: "MultipleFallbacks",
38 in: "registry.somewhere,registry.other",
39 err: "duplicate catch-all registry",
40 }, {
41 testName: "NoRegistryOrDefault",
42 catchAllDefault: "",
43 err: "no catch-all registry or default",
44 }, {
45 testName: "InvalidRegistry",
46 in: "$#foo",
47 err: `invalid registry "\$#foo": invalid host name "\$#foo" in registry`,
48 }, {
49 testName: "InvalidSecuritySuffix",
50 in: "foo.com+bogus",
51 err: `invalid registry "foo.com\+bogus": unknown suffix \("\+bogus"\), need \+insecure, \+secure or no suffix\)`,
52 }, {
53 testName: "IPV6AddrWithoutBrackets",
54 in: "::1",
55 err: `invalid registry "::1": invalid host name "::1" in registry`,
56 }, {
57 testName: "EmptyElement",
58 in: "foo.com,",
59 err: `empty registry part`,
60 }, {
61 testName: "MissingPrefix",
62 in: "=foo.com",
63 err: `empty module prefix`,
64 }, {
65 testName: "MissingRegistry",
66 in: "x.com=",
67 err: `empty registry reference`,
68 }, {
69 testName: "InvalidModulePrefix",
70 in: "foo#=foo.com",
71 err: `invalid module path "foo#": invalid char '#'`,
72 }, {
73 testName: "DuplicateModulePrefix",
74 in: "x.com=r.org,x.com=q.org",
75 err: `duplicate module prefix "x.com"`,
76 }, {
77 testName: "NoDefaultCatchAll",
78 in: "x.com=r.org",
79 err: `no default catch-all registry provided`,
80 }, {
81 testName: "InvalidCatchAll",
82 in: "x.com=r.org",
83 catchAllDefault: "bogus",
84 err: `invalid catch-all registry "bogus": invalid host name "bogus" in registry`,
85 }, {
86 testName: "InvalidRegistryRef",
87 in: "foo.com//bar",
88 err: `invalid registry "foo.com//bar": invalid reference syntax \("foo.com//bar"\)`,
89 }, {
90 testName: "RegistryRefWithDigest",
91 in: "foo.com/bar@sha256:f3c16f525a1b7c204fc953d6d7db7168d84ebf4902f83c3a37d113b18c28981f",
92 err: `invalid registry "foo.com/bar@sha256:f3c16f525a1b7c204fc953d6d7db7168d84ebf4902f83c3a37d113b18c28981f": cannot have an associated tag or digest`,
93 }, {
94 testName: "RegistryRefWithTag",
95 in: "foo.com/bar:sometag",
96 err: `invalid registry "foo.com/bar:sometag": cannot have an associated tag or digest`,
97 }, {
98 testName: "MismatchedSecurity",
99 in: "foo.com/bar+secure,other.example=foo.com/bar+insecure",
100 err: `registry host "foo.com" is specified both as secure and insecure`,
101 }, {
102 testName: "SingleCatchAll",
103 catchAllDefault: "registry.somewhere",
104 wantAllHosts: []Host{{"registry.somewhere", false}},
105 lookups: map[string]*Location{
106 "fruit.com/apple": {
107 Host: "registry.somewhere",
108 Repository: "fruit.com/apple",
109 },
110 },
111 }, {
112 testName: "CatchAllWithNoDefault",
113 in: "registry.somewhere",
114 wantAllHosts: []Host{{"registry.somewhere", false}},
115 lookups: map[string]*Location{
116 "fruit.com/apple": {
117 Host: "registry.somewhere",
118 Repository: "fruit.com/apple",
119 },
120 },
121 }, {
122 testName: "CatchAllWithDefault",
123 in: "registry.somewhere",
124 catchAllDefault: "other.cue.somewhere",
125 wantAllHosts: []Host{{"registry.somewhere", false}},
126 lookups: map[string]*Location{
127 "fruit.com/apple": {
128 Host: "registry.somewhere",
129 Repository: "fruit.com/apple",
130 },
131 "": nil,
132 },
133 }, {
134 testName: "PrefixWithCatchAllNoDefault",
135 in: "example.com=registry.example.com/offset,registry.somewhere",
136 wantAllHosts: []Host{{"registry.example.com", false}, {"registry.somewhere", false}},
137 lookups: map[string]*Location{
138 "fruit.com/apple": {
139 Host: "registry.somewhere",
140 Repository: "fruit.com/apple",
141 },
142 "example.com/blah": {
143 Host: "registry.example.com",
144 Repository: "offset/example.com/blah",
145 },
146 "example.com": {
147 Host: "registry.example.com",
148 Repository: "offset/example.com",
149 },
150 },
151 }, {
152 testName: "PrefixWithCatchAllDefault",
153 in: "example.com=registry.example.com/offset",
154 catchAllDefault: "registry.somewhere",
155 wantAllHosts: []Host{{"registry.example.com", false}, {"registry.somewhere", false}},
156 lookups: map[string]*Location{
157 "fruit.com/apple": {
158 Host: "registry.somewhere",
159 Repository: "fruit.com/apple",
160 },
161 "example.com/blah": {
162 Host: "registry.example.com",
163 Repository: "offset/example.com/blah",
164 },
165 },
166 }, {
167 testName: "LocalhostIsInsecure",
168 in: "localhost:5000",
169 wantAllHosts: []Host{{"localhost:5000", true}},
170 lookups: map[string]*Location{
171 "fruit.com/apple": {
172 Host: "localhost:5000",
173 Insecure: true,
174 Repository: "fruit.com/apple",
175 },
176 },
177 }, {
178 testName: "SecureLocalhost",
179 in: "localhost:1234+secure",
180 wantAllHosts: []Host{{"localhost:1234", false}},
181 lookups: map[string]*Location{
182 "fruit.com/apple": {
183 Host: "localhost:1234",
184 Repository: "fruit.com/apple",
185 },
186 },
187 }, {
188 testName: "127.0.0.1IsInsecure",
189 in: "127.0.0.1",
190 wantAllHosts: []Host{{"127.0.0.1", true}},
191 lookups: map[string]*Location{
192 "fruit.com/apple": {
193 Host: "127.0.0.1",
194 Insecure: true,
195 Repository: "fruit.com/apple",
196 },
197 },
198 }, {
199 testName: "[::1]IsInsecure",
200 in: "[::1]",
201 wantAllHosts: []Host{{"[::1]", true}},
202 lookups: map[string]*Location{
203 "fruit.com/apple": {
204 Host: "[::1]",
205 Insecure: true,
206 Repository: "fruit.com/apple",
207 },
208 },
209 }, {
210 testName: "[0:0::1]IsInsecure",
211 in: "[0:0::1]",
212 wantAllHosts: []Host{{"[0:0::1]", true}},
213 lookups: map[string]*Location{
214 "fruit.com/apple": {
215 Host: "[0:0::1]",
216 Insecure: true,
217 Repository: "fruit.com/apple",
218 },
219 },
220 }}
221
222 for _, tc := range testCases {
223 t.Run(tc.testName, func(t *testing.T) {
224 r, err := ParseCUERegistry(tc.in, tc.catchAllDefault)
225 if tc.err != "" {
226 qt.Assert(t, qt.ErrorMatches(err, tc.err))
227 return
228 }
229 qt.Assert(t, qt.IsNil(err))
230 qt.Check(t, qt.DeepEquals(r.AllHosts(), tc.wantAllHosts))
231 testLookups(t, r, tc.lookups)
232 })
233 }
234 }
235
236 func TestParseConfig(t *testing.T) {
237 testCases := []struct {
238 testName string
239 in string
240 catchAllDefault string
241 err string
242 wantAllHosts []Host
243 lookups map[string]*Location
244 }{{
245 testName: "NoRegistryOrDefault",
246 catchAllDefault: "",
247 err: "no default catch-all registry provided",
248 }, {
249 testName: "InvalidRegistry",
250 in: `
251 defaultRegistry: registry: "$#foo"
252 `,
253 err: `invalid default registry configuration: invalid host name "\$#foo" in registry`,
254 }, {
255 testName: "EncHashAsRepo",
256 in: `
257 defaultRegistry: {
258 registry: "registry.somewhere/hello"
259 pathEncoding: "hashAsRepo"
260 prefixForTags: "mod-"
261 }
262 `,
263 wantAllHosts: []Host{{"registry.somewhere", false}},
264 lookups: map[string]*Location{
265 "foo.com/bar v1.2.3": {
266 Host: "registry.somewhere",
267 Repository: "hello/" + hashOf("foo.com/bar"),
268 Tag: "mod-v1.2.3",
269 },
270 },
271 }, {
272 testName: "EncHashAsTag",
273 in: `
274 defaultRegistry: {
275 registry: "registry.somewhere/hello"
276 pathEncoding: "hashAsTag"
277 prefixForTags: "mod-"
278 }
279 `,
280 wantAllHosts: []Host{{"registry.somewhere", false}},
281 lookups: map[string]*Location{
282 "foo.com/bar v1.2.3": {
283 Host: "registry.somewhere",
284 Repository: "hello",
285 Tag: "mod-" + hashOf("foo.com/bar") + "-v1.2.3",
286 },
287 },
288 }, {
289 testName: "DefaultRegistryWithModuleRegistries",
290 in: `
291 defaultRegistry: {
292 registry: "registry.somewhere"
293 }
294 moduleRegistries: {
295 "a.com": {
296 registry: "registry.otherwhere"
297 }
298 }
299 `,
300 wantAllHosts: []Host{{"registry.otherwhere", false}, {"registry.somewhere", false}},
301 lookups: map[string]*Location{
302 "a.com v0.0.1": {
303 Host: "registry.otherwhere",
304 Repository: "a.com",
305 Tag: "v0.0.1",
306 },
307 "b.com v0.0.1": {
308 Host: "registry.somewhere",
309 Repository: "b.com",
310 Tag: "v0.0.1",
311 },
312 },
313 }, {
314 testName: "DiverseRegistries",
315 catchAllDefault: "default.example/foo",
316 in: `
317 moduleRegistries: {
318 "a.com": {
319 registry: "r1.example/a/b+insecure"
320 }
321 "a.com/foo/bar": {
322 registry: "r2.example/xxx"
323 pathEncoding: "hashAsRepo"
324 prefixForTags: "cue-"
325 }
326 "a.com/foo": {
327 registry: "r1.example/hello+insecure"
328 }
329 "stripped.org/bar": {
330 registry: "r3.example/repo"
331 stripPrefix: true
332 }
333 }
334 `,
335 wantAllHosts: []Host{{
336 Name: "default.example",
337 }, {
338 Name: "r1.example",
339 Insecure: true,
340 }, {
341 Name: "r2.example",
342 }, {
343 Name: "r3.example",
344 }},
345 lookups: map[string]*Location{
346 "a.com/other/bar/baz v0.0.1": {
347 Host: "r1.example",
348 Insecure: true,
349 Repository: "a/b/a.com/other/bar/baz",
350 Tag: "v0.0.1",
351 },
352 "a.com/foo/bar v0.0.1": {
353 Host: "r2.example",
354 Repository: "xxx/" + hashOf("a.com/foo/bar"),
355 Tag: "cue-v0.0.1",
356 },
357 "a.com/foo/bar": {
358 Host: "r2.example",
359 Repository: "xxx/" + hashOf("a.com/foo/bar"),
360 Tag: "cue-",
361 },
362 "a.com/foo/baz v0.0.1": {
363 Host: "r1.example",
364 Insecure: true,
365 Repository: "hello/a.com/foo/baz",
366 Tag: "v0.0.1",
367 },
368 "a.com/food v0.0.1": {
369 Host: "r1.example",
370 Insecure: true,
371 Repository: "a/b/a.com/food",
372 Tag: "v0.0.1",
373 },
374 "b.com v0.0.1": {
375 Host: "default.example",
376 Repository: "foo/b.com",
377 Tag: "v0.0.1",
378 },
379 "stripped.org/bar/one/two/three v0.0.1": {
380 Host: "r3.example",
381 Repository: "repo/one/two/three",
382 Tag: "v0.0.1",
383 },
384 "stripped.org/bar v0.0.1": {
385 Host: "r3.example",
386 Repository: "repo",
387 Tag: "v0.0.1",
388 },
389 },
390 }, {
391 testName: "InvalidModulePath",
392 in: `
393 moduleRegistries: "bad+module": {
394 registry: "foo.com"
395 }
396 `,
397 err: `invalid module path "bad\+module": invalid char '\+'`,
398 }, {
399 testName: "InvalidHost",
400 in: `
401 moduleRegistries: "foo.example": {
402 registry: "badhost:"
403 }
404 `,
405 err: `invalid registry configuration in "foo.example": invalid host name "badhost:" in registry`,
406 }, {
407 testName: "InvalidRepository",
408 in: `
409 moduleRegistries: "foo.example": {
410 registry: "ok.com/A"
411 }
412 `,
413 err: `invalid registry configuration in "foo.example": invalid reference syntax \("ok.com/A"\)`,
414 }, {
415 testName: "UnknownField",
416 in: `
417 registiries: "foo.example": {
418 registry: "ok.com/A",
419 }
420 `,
421 err: `invalid configuration file: registiries: field not allowed`,
422 }, {
423 testName: "MismatchedSecurity",
424 catchAllDefault: "c.example",
425 in: `
426 moduleRegistries: {
427 "a.example": {
428 registry: "ok.com+insecure"
429 }
430 "b.example": {
431 registry: "ok.com"
432 }
433 }
434 `,
435 err: `registry host "ok.com" is specified both as secure and insecure`,
436 }, {
437 testName: "StripPrefixWithNoRepo",
438 catchAllDefault: "c.example",
439 in: `
440 moduleRegistries: {
441 "a.example/foo": {
442 registry: "foo.example"
443 stripPrefix: true
444 }
445 }
446 `,
447 err: `invalid registry configuration in "a.example/foo": use of stripPrefix requires a non-empty repository within the registry`,
448 }}
449
450 for _, tc := range testCases {
451 t.Run(tc.testName, func(t *testing.T) {
452 r, err := ParseConfig([]byte(tc.in), "somefile.cue", tc.catchAllDefault)
453 if tc.err != "" {
454 qt.Assert(t, qt.ErrorMatches(err, tc.err))
455 return
456 }
457 qt.Assert(t, qt.IsNil(err))
458 qt.Check(t, qt.DeepEquals(r.AllHosts(), tc.wantAllHosts))
459 testLookups(t, r, tc.lookups)
460 })
461 }
462 }
463
464 func testLookups(t *testing.T, r LocationResolver, lookups map[string]*Location) {
465 for key, want := range lookups {
466 t.Run(key, func(t *testing.T) {
467 m, v, _ := strings.Cut(key, " ")
468 got, ok := r.ResolveToLocation(m, v)
469 if want == nil {
470 qt.Assert(t, qt.IsFalse(ok))
471 } else {
472 qt.Assert(t, qt.DeepEquals(&got, want))
473 }
474 })
475 }
476 }
477
478 func hashOf(s string) string {
479 return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
480 }
481
View as plain text