1package route_test
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/assert"
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/gordon/internal/adapters/in/http/registry/route"
10)
11
12// validDigest is a syntactically valid sha256 digest.
13const validDigest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
14
15// validUUID is a syntactically valid v4 UUID.
16const validUUID = "12345678-1234-4123-8123-123456789012"
17
18func TestParse_Routes(t *testing.T) {
19 tests := []struct {
20 path string
21 want route.Operation
22 }{
23 {"/v2/", route.Operation{Kind: route.KindBase}},
24 {"/v2", route.Operation{Kind: route.KindBase}},
25 {"/v2/_catalog", route.Operation{Kind: route.KindCatalog}},
26 {
27 "/v2/myrepo/manifests/latest",
28 route.Operation{Kind: route.KindManifest, Repository: "myrepo", Reference: "latest"},
29 },
30 {
31 "/v2/myorg/myapp/manifests/v1.0",
32 route.Operation{Kind: route.KindManifest, Repository: "myorg/myapp", Reference: "v1.0"},
33 },
34 {
35 "/v2/myrepo/blobs/" + validDigest,
36 route.Operation{Kind: route.KindBlob, Repository: "myrepo", Digest: validDigest},
37 },
38 {"/v2/myrepo/blobs/uploads/", route.Operation{Kind: route.KindUpload, Repository: "myrepo"}},
39 {
40 "/v2/myrepo/blobs/uploads/" + validUUID,
41 route.Operation{Kind: route.KindUpload, Repository: "myrepo", UploadID: validUUID},
42 },
43 {"/v2/myrepo/tags/list", route.Operation{Kind: route.KindTagList, Repository: "myrepo"}},
44 }
45 for _, tt := range tests {
46 t.Run(tt.path, func(t *testing.T) {
47 op, err := route.Parse(tt.path)
48 require.NoError(t, err)
49 assert.Equal(t, tt.want, op)
50 })
51 }
52}
53
54// TestParse_ResolvesRepositoryFromLastMarker proves a reserved-looking
55// component earlier in the path cannot shorten the repository: the parser
56// (and therefore authorization) always sees the repository the handler
57// would serve.
58func TestParse_ResolvesRepositoryFromLastMarker(t *testing.T) {
59 tests := []struct {
60 path string
61 wantRepo string
62 }{
63 {"/v2/manifests/victim/manifests/latest", "manifests/victim"},
64 {"/v2/allowed/tags/victim/manifests/latest", "allowed/tags/victim"},
65 {"/v2/blobs/impostor/blobs/" + validDigest, "blobs/impostor"},
66 }
67 for _, tt := range tests {
68 t.Run(tt.path, func(t *testing.T) {
69 op, err := route.Parse(tt.path)
70 require.NoError(t, err)
71 assert.Equal(t, tt.wantRepo, op.Repository)
72 })
73 }
74}
75
76func TestParse_Errors(t *testing.T) {
77 notFound := []string{
78 "/healthz",
79 "/v2/myrepo",
80 "/v2/myrepo/manifests/",
81 "/v2/myrepo/manifests/a/b",
82 "/v2/myrepo/blobs/",
83 "/v2/myrepo/tags/list/",
84 "/v2//manifests/latest",
85 }
86 for _, path := range notFound {
87 t.Run("not found "+path, func(t *testing.T) {
88 _, err := route.Parse(path)
89 assert.ErrorIs(t, err, route.ErrNotFound)
90 })
91 }
92
93 cases := []struct {
94 path string
95 wantCode string
96 }{
97 {"/v2/BadRepo/manifests/latest", route.CodeNameInvalid},
98 {"/v2/myrepo/manifests/bad tag", route.CodeTagInvalid},
99 {"/v2/myrepo/blobs/notadigest", route.CodeDigestInvalid},
100 {"/v2/myrepo/blobs/uploads/not-a-uuid", route.CodeBlobUploadInvalid},
101 }
102 for _, tc := range cases {
103 t.Run("invalid "+tc.path, func(t *testing.T) {
104 _, err := route.Parse(tc.path)
105 assert.ErrorIs(t, err, route.ErrInvalid)
106 var parseErr *route.ParseError
107 require.ErrorAs(t, err, &parseErr)
108 assert.Equal(t, tc.wantCode, parseErr.Code)
109 })
110 }
111}
112
113func TestRequiresRepositoryAuth_ProtocolRootsOnly(t *testing.T) {
114 base, err := route.Parse("/v2/")
115 require.NoError(t, err)
116 assert.False(t, base.RequiresRepositoryAuth())
117
118 catalog, err := route.Parse("/v2/_catalog")
119 require.NoError(t, err)
120 assert.False(t, catalog.RequiresRepositoryAuth())
121
122 manifest, err := route.Parse("/v2/myrepo/manifests/latest")
123 require.NoError(t, err)
124 assert.True(t, manifest.RequiresRepositoryAuth())
125}