v2.2.3 133ff88

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
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}

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