1package validation
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8)
9
10func TestValidateRepositoryName(t *testing.T) {
11 tests := []struct {
12 name string
13 input string
14 wantErr bool
15 errMsg string
16 }{
17 // Valid cases
18 {"simple name", "myapp", false, ""},
19 {"with hyphen", "my-app", false, ""},
20 {"with underscore", "my_app", false, ""},
21 {"with dot", "my.app", false, ""},
22 {"nested path", "myorg/myapp", false, ""},
23 {"deeply nested", "myorg/team/myapp", false, ""},
24 {"with numbers", "app123", false, ""},
25 {"complex valid", "my-org/my_app.v2", false, ""},
26
27 // Invalid cases - path traversal
28 {"path traversal simple", "../etc/passwd", true, "path traversal"},
29 {"path traversal nested", "myorg/../../../etc", true, "path traversal"},
30 {"path traversal in middle", "myorg/..hidden/app", true, "path traversal"},
31 {"double dot only", "..", true, "path traversal"},
32
33 // Invalid cases - format
34 {"empty", "", true, "cannot be empty"},
35 {"starts with hyphen", "-myapp", true, "invalid repository name format"},
36 {"starts with dot", ".myapp", true, "invalid repository name format"},
37 {"ends with hyphen", "myapp-", true, "invalid repository name format"},
38 {"uppercase", "MyApp", true, "invalid repository name format"},
39 {"special chars", "my@app", true, "invalid repository name format"},
40 {"spaces", "my app", true, "invalid repository name format"},
41 {"adjacent separators", "my--app", true, "invalid repository name format"},
42 {"starts with slash", "/myapp", true, "invalid repository name format"},
43 {"ends with slash", "myapp/", true, "invalid repository name format"},
44 {"too long", strings.Repeat("a", 257), true, "too long"},
45 }
46
47 for _, tt := range tests {
48 t.Run(tt.name, func(t *testing.T) {
49 err := ValidateRepositoryName(tt.input)
50 if tt.wantErr {
51 if err == nil {
52 t.Errorf("ValidateRepositoryName(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
53 } else if !strings.Contains(err.Error(), tt.errMsg) {
54 t.Errorf("ValidateRepositoryName(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
55 }
56 } else if err != nil {
57 t.Errorf("ValidateRepositoryName(%q) unexpected error: %v", tt.input, err)
58 }
59 })
60 }
61}
62
63func TestValidateReference(t *testing.T) {
64 tests := []struct {
65 name string
66 input string
67 wantErr bool
68 errMsg string
69 }{
70 // Valid tags
71 {"simple tag", "latest", false, ""},
72 {"version tag", "v1.0.0", false, ""},
73 {"with hyphen", "my-tag", false, ""},
74 {"with underscore", "my_tag", false, ""},
75 {"with dot", "my.tag", false, ""},
76 {"numeric", "123", false, ""},
77 {"alphanumeric", "abc123", false, ""},
78
79 // Valid digests as references
80 {"sha256 digest", "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", false, ""},
81 {"sha512 digest", "sha512:" + strings.Repeat("a", 128), false, ""},
82
83 // Invalid cases - path traversal
84 {"path traversal", "../etc", true, "path traversal"},
85 {"hidden traversal", "tag/../other", true, "path traversal"},
86
87 // Invalid cases - format
88 {"empty", "", true, "cannot be empty"},
89 {"starts with hyphen", "-tag", true, "invalid reference format"},
90 {"starts with dot", ".tag", true, "invalid reference format"},
91 {"too long tag", strings.Repeat("a", 129), true, "invalid reference format"},
92 {"special chars", "tag@latest", true, "invalid reference format"},
93 {"spaces", "my tag", true, "invalid reference format"},
94
95 // Invalid digests
96 {"wrong algorithm", "md5:abc123", true, "invalid reference format"},
97 {"short sha256", "sha256:abc", true, "invalid reference format"},
98 {"uppercase hex", "sha256:ABC123" + strings.Repeat("0", 58), true, "invalid reference format"},
99 }
100
101 for _, tt := range tests {
102 t.Run(tt.name, func(t *testing.T) {
103 err := ValidateReference(tt.input)
104 if tt.wantErr {
105 if err == nil {
106 t.Errorf("ValidateReference(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
107 } else if !strings.Contains(err.Error(), tt.errMsg) {
108 t.Errorf("ValidateReference(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
109 }
110 } else if err != nil {
111 t.Errorf("ValidateReference(%q) unexpected error: %v", tt.input, err)
112 }
113 })
114 }
115}
116
117func TestValidateDigest(t *testing.T) {
118 validSHA256 := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
119 validSHA512 := "sha512:" + strings.Repeat("a", 128)
120
121 tests := []struct {
122 name string
123 input string
124 wantErr bool
125 errMsg string
126 }{
127 // Valid cases
128 {"valid sha256", validSHA256, false, ""},
129 {"valid sha512", validSHA512, false, ""},
130
131 // Invalid cases - path traversal
132 {"path traversal", "sha256:../../../etc/passwd", true, "path traversal"},
133
134 // Invalid cases - format
135 {"empty", "", true, "cannot be empty"},
136 {"no algorithm", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", true, "invalid digest format"},
137 {"wrong algorithm", "md5:abc123", true, "invalid digest format"},
138 {"sha1 not allowed", "sha1:" + strings.Repeat("a", 40), true, "invalid digest format"},
139 {"short hash", "sha256:abc", true, "invalid digest format"},
140 {"uppercase hex", "sha256:" + strings.Repeat("A", 64), true, "invalid digest format"},
141 {"invalid chars", "sha256:" + strings.Repeat("g", 64), true, "invalid digest format"},
142 {"missing colon", "sha256" + strings.Repeat("a", 64), true, "invalid digest format"},
143 }
144
145 for _, tt := range tests {
146 t.Run(tt.name, func(t *testing.T) {
147 err := ValidateDigest(tt.input)
148 if tt.wantErr {
149 if err == nil {
150 t.Errorf("ValidateDigest(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
151 } else if !strings.Contains(err.Error(), tt.errMsg) {
152 t.Errorf("ValidateDigest(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
153 }
154 } else if err != nil {
155 t.Errorf("ValidateDigest(%q) unexpected error: %v", tt.input, err)
156 }
157 })
158 }
159}
160
161func TestValidateUUID(t *testing.T) {
162 tests := []struct {
163 name string
164 input string
165 wantErr bool
166 errMsg string
167 }{
168 // Valid cases - standard UUID v4
169 {"valid uuid v4", "550e8400-e29b-41d4-a716-446655440000", false, ""},
170 {"another valid uuid v4", "f47ac10b-58cc-4372-a567-0e02b2c3d479", false, ""},
171
172 // Invalid cases - not UUID v4
173 {"uuid v1 wrong version", "6ba7b810-9dad-11d1-80b4-00c04fd430c8", true, "invalid UUID format"},
174 {"all zeros not v4", "00000000-0000-0000-0000-000000000000", true, "invalid UUID format"},
175 {"wrong variant nibble", "550e8400-e29b-41d4-7716-446655440000", true, "invalid UUID format"},
176
177 // Invalid cases - path traversal
178 {"path traversal", "../etc/passwd", true, "path traversal"},
179 {"traversal in middle", "app/../etc", true, "path traversal"},
180
181 // Invalid cases - format
182 {"empty", "", true, "cannot be empty"},
183 {"random string", "myapp", true, "invalid UUID format"},
184 {"old timestamp format", "1234567890-myapp", true, "invalid UUID format"},
185 {"uppercase hex", "550E8400-E29B-41D4-A716-446655440000", true, "invalid UUID format"},
186 {"spaces", "550e8400 e29b 41d4 a716 446655440000", true, "invalid UUID format"},
187 {"missing section", "550e8400-e29b-41d4-a716", true, "invalid UUID format"},
188 }
189
190 for _, tt := range tests {
191 t.Run(tt.name, func(t *testing.T) {
192 err := ValidateUUID(tt.input)
193 if tt.wantErr {
194 if err == nil {
195 t.Errorf("ValidateUUID(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
196 } else if !strings.Contains(err.Error(), tt.errMsg) {
197 t.Errorf("ValidateUUID(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
198 }
199 } else if err != nil {
200 t.Errorf("ValidateUUID(%q) unexpected error: %v", tt.input, err)
201 }
202 })
203 }
204}
205
206func TestValidateDomainParam(t *testing.T) {
207 tests := []struct {
208 name string
209 input string
210 valid bool
211 }{
212 {"valid domain", "example.com", true},
213 {"valid subdomain", "app.example.com", true},
214 {"path traversal", "../etc/passwd", false},
215 {"null byte", "example\x00.com", false},
216 {"newline", "example\n.com", false},
217 {"carriage return", "example\r.com", false},
218 {"empty", "", false},
219 {"double dot in middle", "foo..bar", false},
220 {"forward slash", "example/com", false},
221 {"tab character", "example\t.com", false},
222 }
223 for _, tt := range tests {
224 t.Run(tt.name, func(t *testing.T) {
225 err := ValidateDomainParam(tt.input)
226 if tt.valid {
227 require.NoError(t, err, "input: %q", tt.input)
228 } else {
229 require.Error(t, err, "input: %q", tt.input)
230 }
231 })
232 }
233}
234
235func TestValidatePath(t *testing.T) {
236 tests := []struct {
237 name string
238 input string
239 wantErr bool
240 errMsg string
241 wantPath string
242 }{
243 // Valid cases
244 {"simple", "myfile", false, "", "myfile"},
245 {"nested", "dir/file", false, "", "dir/file"},
246 {"with dots in name", "file.txt", false, "", "file.txt"},
247
248 // Invalid cases
249 {"empty", "", true, "cannot be empty", ""},
250 {"path traversal", "../etc/passwd", true, "path traversal", ""},
251 {"hidden traversal", "dir/../../../etc", true, "path traversal", ""},
252 {"absolute path", "/etc/passwd", true, "absolute paths not allowed", ""},
253 }
254
255 for _, tt := range tests {
256 t.Run(tt.name, func(t *testing.T) {
257 got, err := ValidatePath(tt.input)
258 if tt.wantErr {
259 if err == nil {
260 t.Errorf("ValidatePath(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
261 } else if !strings.Contains(err.Error(), tt.errMsg) {
262 t.Errorf("ValidatePath(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
263 }
264 } else {
265 if err != nil {
266 t.Errorf("ValidatePath(%q) unexpected error: %v", tt.input, err)
267 }
268 if got != tt.wantPath {
269 t.Errorf("ValidatePath(%q) = %q, want %q", tt.input, got, tt.wantPath)
270 }
271 }
272 })
273 }
274}
275
276func TestValidatePathWithinRoot(t *testing.T) {
277 tests := []struct {
278 name string
279 rootDir string
280 fullPath string
281 wantErr bool
282 }{
283 {"within root", "/data", "/data/file.txt", false},
284 {"nested within root", "/data", "/data/subdir/file.txt", false},
285 {"exact root", "/data", "/data", false},
286 {"escapes root", "/data", "/etc/passwd", true},
287 {"traversal escape", "/data", "/data/../etc/passwd", true},
288 {"sibling dir", "/data/registry", "/data/secrets/file", true},
289 }
290
291 for _, tt := range tests {
292 t.Run(tt.name, func(t *testing.T) {
293 err := ValidatePathWithinRoot(tt.rootDir, tt.fullPath)
294 if tt.wantErr && err == nil {
295 t.Errorf("ValidatePathWithinRoot(%q, %q) expected error, got nil", tt.rootDir, tt.fullPath)
296 } else if !tt.wantErr && err != nil {
297 t.Errorf("ValidatePathWithinRoot(%q, %q) unexpected error: %v", tt.rootDir, tt.fullPath, err)
298 }
299 })
300 }
301}