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
167 {"valid uuid", "1234567890-myapp", false, ""},
168 {"with underscore", "1234567890-my_app", false, ""},
169 {"with hyphen in name", "1234567890-my-app", false, ""},
170 {"nested repo uuid", "1234567890-myorg_myapp", false, ""},
171
172 // Invalid cases - path traversal
173 {"path traversal", "1234567890-../etc/passwd", true, "path traversal"},
174 {"traversal in middle", "1234567890-app/../etc", true, "path traversal"},
175
176 // Invalid cases - format
177 {"empty", "", true, "cannot be empty"},
178 {"no timestamp", "myapp", true, "invalid UUID format"},
179 {"no hyphen", "1234567890myapp", true, "invalid UUID format"},
180 {"special chars", "1234567890-my@app", true, "invalid UUID format"},
181 {"spaces", "1234567890-my app", true, "invalid UUID format"},
182 {"slash not replaced", "1234567890-myorg/myapp", true, "invalid UUID format"},
183 }
184
185 for _, tt := range tests {
186 t.Run(tt.name, func(t *testing.T) {
187 err := ValidateUUID(tt.input)
188 if tt.wantErr {
189 if err == nil {
190 t.Errorf("ValidateUUID(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
191 } else if !strings.Contains(err.Error(), tt.errMsg) {
192 t.Errorf("ValidateUUID(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
193 }
194 } else if err != nil {
195 t.Errorf("ValidateUUID(%q) unexpected error: %v", tt.input, err)
196 }
197 })
198 }
199}
200
201func TestValidatePath(t *testing.T) {
202 tests := []struct {
203 name string
204 input string
205 wantErr bool
206 errMsg string
207 wantPath string
208 }{
209 // Valid cases
210 {"simple", "myfile", false, "", "myfile"},
211 {"nested", "dir/file", false, "", "dir/file"},
212 {"with dots in name", "file.txt", false, "", "file.txt"},
213
214 // Invalid cases
215 {"empty", "", true, "cannot be empty", ""},
216 {"path traversal", "../etc/passwd", true, "path traversal", ""},
217 {"hidden traversal", "dir/../../../etc", true, "path traversal", ""},
218 {"absolute path", "/etc/passwd", true, "absolute paths not allowed", ""},
219 }
220
221 for _, tt := range tests {
222 t.Run(tt.name, func(t *testing.T) {
223 got, err := ValidatePath(tt.input)
224 if tt.wantErr {
225 if err == nil {
226 t.Errorf("ValidatePath(%q) expected error containing %q, got nil", tt.input, tt.errMsg)
227 } else if !strings.Contains(err.Error(), tt.errMsg) {
228 t.Errorf("ValidatePath(%q) error = %q, want error containing %q", tt.input, err.Error(), tt.errMsg)
229 }
230 } else {
231 if err != nil {
232 t.Errorf("ValidatePath(%q) unexpected error: %v", tt.input, err)
233 }
234 if got != tt.wantPath {
235 t.Errorf("ValidatePath(%q) = %q, want %q", tt.input, got, tt.wantPath)
236 }
237 }
238 })
239 }
240}
241
242func TestValidatePathWithinRoot(t *testing.T) {
243 tests := []struct {
244 name string
245 rootDir string
246 fullPath string
247 wantErr bool
248 }{
249 {"within root", "/data", "/data/file.txt", false},
250 {"nested within root", "/data", "/data/subdir/file.txt", false},
251 {"exact root", "/data", "/data", false},
252 {"escapes root", "/data", "/etc/passwd", true},
253 {"traversal escape", "/data", "/data/../etc/passwd", true},
254 {"sibling dir", "/data/registry", "/data/secrets/file", true},
255 }
256
257 for _, tt := range tests {
258 t.Run(tt.name, func(t *testing.T) {
259 err := ValidatePathWithinRoot(tt.rootDir, tt.fullPath)
260 if tt.wantErr && err == nil {
261 t.Errorf("ValidatePathWithinRoot(%q, %q) expected error, got nil", tt.rootDir, tt.fullPath)
262 } else if !tt.wantErr && err != nil {
263 t.Errorf("ValidatePathWithinRoot(%q, %q) unexpected error: %v", tt.rootDir, tt.fullPath, err)
264 }
265 })
266 }
267}