1package domain
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/assert"
7 "github.com/stretchr/testify/require"
8)
9
10func TestParseScope(t *testing.T) {
11 tests := []struct {
12 name string
13 input string
14 wantScope *Scope
15 wantErr bool
16 errContains string
17 }{
18 // Valid cases
19 {
20 name: "simple repository scope",
21 input: "repository:myrepo:pull",
22 wantScope: &Scope{
23 Type: "repository",
24 Name: "myrepo",
25 Actions: []string{"pull"},
26 },
27 },
28 {
29 name: "multiple actions",
30 input: "repository:myrepo:push,pull",
31 wantScope: &Scope{
32 Type: "repository",
33 Name: "myrepo",
34 Actions: []string{"push", "pull"},
35 },
36 },
37 {
38 name: "wildcard repository",
39 input: "repository:*:pull",
40 wantScope: &Scope{
41 Type: "repository",
42 Name: "*",
43 Actions: []string{"pull"},
44 },
45 },
46 {
47 name: "org wildcard",
48 input: "repository:myorg/*:push,pull",
49 wantScope: &Scope{
50 Type: "repository",
51 Name: "myorg/*",
52 Actions: []string{"push", "pull"},
53 },
54 },
55 {
56 name: "wildcard action",
57 input: "repository:myrepo:*",
58 wantScope: &Scope{
59 Type: "repository",
60 Name: "myrepo",
61 Actions: []string{"*"},
62 },
63 },
64 {
65 name: "nested repository name",
66 input: "repository:myorg/team/app:pull",
67 wantScope: &Scope{
68 Type: "repository",
69 Name: "myorg/team/app",
70 Actions: []string{"pull"},
71 },
72 },
73 {
74 name: "actions with whitespace trimmed",
75 input: "repository:myrepo:push, pull",
76 wantScope: &Scope{
77 Type: "repository",
78 Name: "myrepo",
79 Actions: []string{"push", "pull"},
80 },
81 },
82 {
83 name: "registry scope type",
84 input: "registry:catalog:*",
85 wantScope: &Scope{
86 Type: "registry",
87 Name: "catalog",
88 Actions: []string{"*"},
89 },
90 },
91
92 // Invalid cases
93 {
94 name: "empty string",
95 input: "",
96 wantErr: true,
97 errContains: "invalid scope format",
98 },
99 {
100 name: "missing actions",
101 input: "repository:myrepo",
102 wantErr: true,
103 errContains: "invalid scope format",
104 },
105 {
106 name: "missing name and actions",
107 input: "repository",
108 wantErr: true,
109 errContains: "invalid scope format",
110 },
111 {
112 name: "only type and name",
113 input: "repository:myrepo:",
114 wantScope: &Scope{
115 Type: "repository",
116 Name: "myrepo",
117 Actions: []string{""},
118 },
119 },
120 }
121
122 for _, tt := range tests {
123 t.Run(tt.name, func(t *testing.T) {
124 scope, err := ParseScope(tt.input)
125
126 if tt.wantErr {
127 require.Error(t, err)
128 if tt.errContains != "" {
129 assert.Contains(t, err.Error(), tt.errContains)
130 }
131 return
132 }
133
134 require.NoError(t, err)
135 assert.Equal(t, tt.wantScope.Type, scope.Type)
136 assert.Equal(t, tt.wantScope.Name, scope.Name)
137 assert.Equal(t, tt.wantScope.Actions, scope.Actions)
138 })
139 }
140}
141
142func TestScope_CanAccess(t *testing.T) {
143 tests := []struct {
144 name string
145 scope *Scope
146 repoName string
147 action string
148 want bool
149 }{
150 // Exact match cases
151 {
152 name: "exact repo match with pull",
153 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
154 repoName: "myrepo",
155 action: "pull",
156 want: true,
157 },
158 {
159 name: "exact repo match with push",
160 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push"}},
161 repoName: "myrepo",
162 action: "push",
163 want: true,
164 },
165 {
166 name: "exact repo match wrong action",
167 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
168 repoName: "myrepo",
169 action: "push",
170 want: false,
171 },
172 {
173 name: "wrong repo name",
174 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
175 repoName: "otherrepo",
176 action: "pull",
177 want: false,
178 },
179
180 // Wildcard repository cases
181 {
182 name: "wildcard repo matches any",
183 scope: &Scope{Type: "repository", Name: "*", Actions: []string{"pull"}},
184 repoName: "anyrepo",
185 action: "pull",
186 want: true,
187 },
188 {
189 name: "wildcard repo matches nested",
190 scope: &Scope{Type: "repository", Name: "*", Actions: []string{"pull"}},
191 repoName: "myorg/myapp",
192 action: "pull",
193 want: true,
194 },
195
196 // Org-level wildcard cases
197 {
198 name: "org wildcard matches repo in org",
199 scope: &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
200 repoName: "myorg/myapp",
201 action: "pull",
202 want: true,
203 },
204 {
205 name: "org wildcard matches nested repo in org",
206 scope: &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
207 repoName: "myorg/team/myapp",
208 action: "pull",
209 want: true,
210 },
211 {
212 name: "org wildcard does not match other org",
213 scope: &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
214 repoName: "otherorg/myapp",
215 action: "pull",
216 want: false,
217 },
218 {
219 name: "org wildcard does not match repo without slash",
220 scope: &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"pull"}},
221 repoName: "myorg",
222 action: "pull",
223 want: false,
224 },
225
226 // Wildcard action cases
227 {
228 name: "wildcard action grants pull",
229 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"*"}},
230 repoName: "myrepo",
231 action: "pull",
232 want: true,
233 },
234 {
235 name: "wildcard action grants push",
236 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"*"}},
237 repoName: "myrepo",
238 action: "push",
239 want: true,
240 },
241
242 // Multiple actions
243 {
244 name: "multiple actions includes requested",
245 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push", "pull"}},
246 repoName: "myrepo",
247 action: "pull",
248 want: true,
249 },
250 {
251 name: "multiple actions includes push",
252 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push", "pull"}},
253 repoName: "myrepo",
254 action: "push",
255 want: true,
256 },
257
258 // Non-repository type
259 {
260 name: "registry type does not grant repo access",
261 scope: &Scope{Type: "registry", Name: "catalog", Actions: []string{"*"}},
262 repoName: "myrepo",
263 action: "pull",
264 want: false,
265 },
266
267 // Edge cases
268 {
269 name: "empty actions",
270 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{}},
271 repoName: "myrepo",
272 action: "pull",
273 want: false,
274 },
275 {
276 name: "empty repo name in scope",
277 scope: &Scope{Type: "repository", Name: "", Actions: []string{"pull"}},
278 repoName: "myrepo",
279 action: "pull",
280 want: false,
281 },
282 }
283
284 for _, tt := range tests {
285 t.Run(tt.name, func(t *testing.T) {
286 got := tt.scope.CanAccess(tt.repoName, tt.action)
287 assert.Equal(t, tt.want, got)
288 })
289 }
290}
291
292func TestScope_String(t *testing.T) {
293 tests := []struct {
294 name string
295 scope *Scope
296 want string
297 }{
298 {
299 name: "simple scope",
300 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"pull"}},
301 want: "repository:myrepo:pull",
302 },
303 {
304 name: "multiple actions",
305 scope: &Scope{Type: "repository", Name: "myrepo", Actions: []string{"push", "pull"}},
306 want: "repository:myrepo:push,pull",
307 },
308 {
309 name: "wildcard",
310 scope: &Scope{Type: "repository", Name: "*", Actions: []string{"*"}},
311 want: "repository:*:*",
312 },
313 {
314 name: "org wildcard",
315 scope: &Scope{Type: "repository", Name: "myorg/*", Actions: []string{"push", "pull"}},
316 want: "repository:myorg/*:push,pull",
317 },
318 }
319
320 for _, tt := range tests {
321 t.Run(tt.name, func(t *testing.T) {
322 got := tt.scope.String()
323 assert.Equal(t, tt.want, got)
324 })
325 }
326}
327
328func TestParseScope_RoundTrip(t *testing.T) {
329 // Test that parsing and stringifying produces the same result
330 inputs := []string{
331 "repository:myrepo:pull",
332 "repository:myrepo:push,pull",
333 "repository:*:*",
334 "repository:myorg/*:push,pull",
335 "repository:myorg/team/app:pull",
336 }
337
338 for _, input := range inputs {
339 t.Run(input, func(t *testing.T) {
340 scope, err := ParseScope(input)
341 require.NoError(t, err)
342
343 output := scope.String()
344 assert.Equal(t, input, output)
345 })
346 }
347}