v2.12.0 657aee7

path_test.go RawHistoryFolder

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}

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