1package registry
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "testing"
7
8 "github.com/bnema/zerowrap"
9 "github.com/stretchr/testify/assert"
10)
11
12func tokenTestLogger() zerowrap.Logger {
13 return zerowrap.Default()
14}
15
16func TestParseRequestedScopes_MultipleScopeParams(t *testing.T) {
17 log := tokenTestLogger()
18 h := &TokenHandler{log: log}
19
20 req := httptest.NewRequest(http.MethodGet, "/v2/token?scope=repository:repo1:pull&scope=repository:repo2:push", nil)
21
22 scopes := h.parseRequestedScopes(req, log)
23
24 assert.Len(t, scopes, 2)
25 assert.Contains(t, scopes, "repository:repo1:pull")
26 assert.Contains(t, scopes, "repository:repo2:push")
27}
28
29func TestParseRequestedScopes_InvalidScopeFiltered(t *testing.T) {
30 log := tokenTestLogger()
31 h := &TokenHandler{log: log}
32
33 tests := []struct {
34 name string
35 query string
36 wantScopes []string
37 wantDefault bool
38 }{
39 {
40 name: "invalid format is skipped",
41 query: "scope=invalid-no-colons&scope=repository:valid:pull",
42 wantScopes: []string{"repository:valid:pull"},
43 wantDefault: false,
44 },
45 {
46 name: "missing actions is skipped",
47 query: "scope=repository:myrepo&scope=repository:valid:pull",
48 wantScopes: []string{"repository:valid:pull"},
49 wantDefault: false,
50 },
51 {
52 name: "all invalid returns default",
53 query: "scope=invalid&scope=also-invalid",
54 wantScopes: []string{"repository:*:pull"},
55 wantDefault: true,
56 },
57 }
58
59 for _, tt := range tests {
60 t.Run(tt.name, func(t *testing.T) {
61 req := httptest.NewRequest(http.MethodGet, "/v2/token?"+tt.query, nil)
62 scopes := h.parseRequestedScopes(req, log)
63
64 assert.Equal(t, tt.wantScopes, scopes)
65 })
66 }
67}
68
69func TestParseRequestedScopes_NonRepositoryScopeRejected(t *testing.T) {
70 log := tokenTestLogger()
71 h := &TokenHandler{log: log}
72
73 tests := []struct {
74 name string
75 query string
76 wantScopes []string
77 }{
78 {
79 name: "registry scope is rejected",
80 query: "scope=registry:catalog:*&scope=repository:myrepo:pull",
81 wantScopes: []string{"repository:myrepo:pull"},
82 },
83 {
84 name: "only non-repository scopes returns default",
85 query: "scope=registry:catalog:*&scope=custom:something:action",
86 wantScopes: []string{"repository:*:pull"},
87 },
88 }
89
90 for _, tt := range tests {
91 t.Run(tt.name, func(t *testing.T) {
92 req := httptest.NewRequest(http.MethodGet, "/v2/token?"+tt.query, nil)
93 scopes := h.parseRequestedScopes(req, log)
94
95 assert.Equal(t, tt.wantScopes, scopes)
96 })
97 }
98}
99
100func TestParseRequestedScopes_NoScopeReturnsDefault(t *testing.T) {
101 log := tokenTestLogger()
102 h := &TokenHandler{log: log}
103
104 req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
105 scopes := h.parseRequestedScopes(req, log)
106
107 assert.Equal(t, []string{"repository:*:pull"}, scopes)
108}
109
110func TestParseRequestedScopes_EmptyScopeParamReturnsDefault(t *testing.T) {
111 log := tokenTestLogger()
112 h := &TokenHandler{log: log}
113
114 req := httptest.NewRequest(http.MethodGet, "/v2/token?scope=", nil)
115 scopes := h.parseRequestedScopes(req, log)
116
117 // Empty scope param is invalid and should return default
118 assert.Equal(t, []string{"repository:*:pull"}, scopes)
119}
120
121func TestParseRequestedScopes_ValidScopesPreserved(t *testing.T) {
122 log := tokenTestLogger()
123 h := &TokenHandler{log: log}
124
125 tests := []struct {
126 name string
127 query string
128 wantScopes []string
129 }{
130 {
131 name: "single scope",
132 query: "scope=repository:myrepo:pull",
133 wantScopes: []string{"repository:myrepo:pull"},
134 },
135 {
136 name: "multiple actions",
137 query: "scope=repository:myrepo:push,pull",
138 wantScopes: []string{"repository:myrepo:push,pull"},
139 },
140 {
141 name: "wildcard repo",
142 query: "scope=repository:*:pull",
143 wantScopes: []string{"repository:*:pull"},
144 },
145 {
146 name: "org wildcard",
147 query: "scope=repository:myorg/*:push,pull",
148 wantScopes: []string{"repository:myorg/*:push,pull"},
149 },
150 {
151 name: "nested repo",
152 query: "scope=repository:myorg/team/app:pull",
153 wantScopes: []string{"repository:myorg/team/app:pull"},
154 },
155 }
156
157 for _, tt := range tests {
158 t.Run(tt.name, func(t *testing.T) {
159 req := httptest.NewRequest(http.MethodGet, "/v2/token?"+tt.query, nil)
160 scopes := h.parseRequestedScopes(req, log)
161
162 assert.Equal(t, tt.wantScopes, scopes)
163 })
164 }
165}
166
167func TestIsLocalhostRequest(t *testing.T) {
168 tests := []struct {
169 name string
170 remoteAddr string
171 want bool
172 }{
173 {"127.0.0.1 is localhost", "127.0.0.1:12345", true},
174 {"127.0.0.2 is localhost", "127.0.0.2:12345", true},
175 {"::1 is localhost", "[::1]:12345", true},
176 {"::1 without brackets", "::1:12345", true},
177 {"192.168.1.1 is not localhost", "192.168.1.1:12345", false},
178 {"10.0.0.1 is not localhost", "10.0.0.1:12345", false},
179 {"public IP is not localhost", "8.8.8.8:12345", false},
180 }
181
182 for _, tt := range tests {
183 t.Run(tt.name, func(t *testing.T) {
184 req := httptest.NewRequest(http.MethodGet, "/v2/token", nil)
185 req.RemoteAddr = tt.remoteAddr
186
187 result := isLocalhostRequest(req)
188
189 assert.Equal(t, tt.want, result)
190 })
191 }
192}