v2.31.0-dev18 7b9923f

path_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close