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