filename_test.go RawHistoryFolder

1package download
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/assert"
7)
8
9func TestSanitizeFilename(t *testing.T) {
10	tests := []struct {
11		name     string
12		input    string
13		expected string
14	}{
15		{
16			name:     "normal filename",
17			input:    "document.pdf",
18			expected: "document.pdf",
19		},
20		{
21			name:     "filename with spaces",
22			input:    "my document.pdf",
23			expected: "my document.pdf",
24		},
25		{
26			name:     "path traversal with parent dirs",
27			input:    "../../../etc/passwd",
28			expected: "passwd",
29		},
30		{
31			name:     "path traversal hidden file",
32			input:    "../.ssh/id_rsa",
33			expected: "id_rsa",
34		},
35		{
36			name:     "nested path",
37			input:    "foo/bar/baz.txt",
38			expected: "baz.txt",
39		},
40		{
41			name:     "absolute path",
42			input:    "/etc/passwd",
43			expected: "passwd",
44		},
45		{
46			name:     "dot only",
47			input:    ".",
48			expected: "download",
49		},
50		{
51			name:     "double dot only",
52			input:    "..",
53			expected: "download",
54		},
55		{
56			name:     "empty string",
57			input:    "",
58			expected: "download",
59		},
60		{
61			name:     "hidden file",
62			input:    ".bashrc",
63			expected: ".bashrc",
64		},
65		{
66			name:     "windows style path",
67			input:    "..\\..\\Windows\\System32\\config",
68			expected: "config",
69		},
70	}
71
72	for _, tt := range tests {
73		t.Run(tt.name, func(t *testing.T) {
74			result := SanitizeFilename(tt.input)
75			assert.Equal(t, tt.expected, result)
76		})
77	}
78}
79
80func TestSanitizeFilenameWithExtension(t *testing.T) {
81	tests := []struct {
82		name     string
83		input    string
84		mimeType string
85		expected string
86	}{
87		{
88			name:     "no extension adds inferred",
89			input:    "download",
90			mimeType: "application/pdf",
91			expected: "download.pdf",
92		},
93		{
94			name:     "existing extension preserved",
95			input:    "report.pdf",
96			mimeType: "application/pdf",
97			expected: "report.pdf",
98		},
99		{
100			name:     "path traversal with inferred extension",
101			input:    "../report",
102			mimeType: "application/pdf",
103			expected: "report.pdf",
104		},
105		{
106			name:     "no mime type returns sanitized",
107			input:    "report",
108			mimeType: "",
109			expected: "report",
110		},
111	}
112
113	for _, tt := range tests {
114		t.Run(tt.name, func(t *testing.T) {
115			result := SanitizeFilenameWithExtension(tt.input, tt.mimeType)
116			assert.Equal(t, tt.expected, result)
117		})
118	}
119}
120
121func TestGetExtensionFromMimeType(t *testing.T) {
122	tests := []struct {
123		name     string
124		input    string
125		expected string
126	}{
127		{
128			name:     "empty mime type",
129			input:    "",
130			expected: "",
131		},
132		{
133			name:     "unknown mime type",
134			input:    "application/unknown",
135			expected: "",
136		},
137		{
138			name:     "pdf mime type",
139			input:    "application/pdf",
140			expected: ".pdf",
141		},
142		{
143			name:     "pdf mime type with charset param",
144			input:    "application/pdf; charset=binary",
145			expected: ".pdf",
146		},
147		{
148			name:     "text html with charset",
149			input:    "text/html; charset=utf-8",
150			expected: ".html",
151		},
152		{
153			name:     "mime type with multiple params",
154			input:    "application/json; charset=utf-8; boundary=something",
155			expected: ".json",
156		},
157		{
158			name:     "invalid mime type",
159			input:    "not-a-valid-mime",
160			expected: "",
161		},
162	}
163
164	for _, tt := range tests {
165		t.Run(tt.name, func(t *testing.T) {
166			result := GetExtensionFromMimeType(tt.input)
167			assert.Equal(t, tt.expected, result)
168		})
169	}
170}
171
172func TestExtractFilenameFromDestination(t *testing.T) {
173	tests := []struct {
174		name     string
175		input    string
176		expected string
177	}{
178		{
179			name:     "file URI with path",
180			input:    "file:///home/user/Downloads/document.pdf",
181			expected: "document.pdf",
182		},
183		{
184			name:     "file URI root",
185			input:    "file:///document.pdf",
186			expected: "document.pdf",
187		},
188		{
189			name:     "plain path",
190			input:    "/home/user/Downloads/image.png",
191			expected: "image.png",
192		},
193		{
194			name:     "filename only",
195			input:    "video.mp4",
196			expected: "video.mp4",
197		},
198		{
199			name:     "empty string",
200			input:    "",
201			expected: "download",
202		},
203		{
204			name:     "file URI with spaces",
205			input:    "file:///home/user/My Documents/file.txt",
206			expected: "file.txt",
207		},
208	}
209
210	for _, tt := range tests {
211		t.Run(tt.name, func(t *testing.T) {
212			result := ExtractFilenameFromDestination(tt.input)
213			assert.Equal(t, tt.expected, result)
214		})
215	}
216}
217
218func TestExtractFilenameFromURI(t *testing.T) {
219	tests := []struct {
220		name     string
221		input    string
222		expected string
223	}{
224		{
225			name:     "http URL with filename",
226			input:    "https://example.com/files/document.pdf",
227			expected: "document.pdf",
228		},
229		{
230			name:     "http URL with query params",
231			input:    "https://example.com/files/document.pdf?token=abc",
232			expected: "document.pdf",
233		},
234		{
235			name:     "http URL without filename",
236			input:    "https://example.com/",
237			expected: "download",
238		},
239		{
240			name:     "empty string",
241			input:    "",
242			expected: "download",
243		},
244		{
245			name:     "plain path",
246			input:    "/path/to/file.txt",
247			expected: "file.txt",
248		},
249	}
250
251	for _, tt := range tests {
252		t.Run(tt.name, func(t *testing.T) {
253			result := ExtractFilenameFromURI(tt.input)
254			assert.Equal(t, tt.expected, result)
255		})
256	}
257}
258
259func TestMakeUniqueFilename(t *testing.T) {
260	tests := []struct {
261		name          string
262		dir           string
263		filename      string
264		existingFiles map[string]bool
265		expected      string
266	}{
267		{
268			name:          "file does not exist",
269			dir:           "/tmp",
270			filename:      "document.pdf",
271			existingFiles: map[string]bool{},
272			expected:      "document.pdf",
273		},
274		{
275			name:     "file exists, adds _(1)",
276			dir:      "/tmp",
277			filename: "document.pdf",
278			existingFiles: map[string]bool{
279				"/tmp/document.pdf": true,
280			},
281			expected: "document_(1).pdf",
282		},
283		{
284			name:     "file and _(1) exist, adds _(2)",
285			dir:      "/tmp",
286			filename: "document.pdf",
287			existingFiles: map[string]bool{
288				"/tmp/document.pdf":     true,
289				"/tmp/document_(1).pdf": true,
290			},
291			expected: "document_(2).pdf",
292		},
293		{
294			name:     "no extension",
295			dir:      "/tmp",
296			filename: "download",
297			existingFiles: map[string]bool{
298				"/tmp/download": true,
299			},
300			expected: "download_(1)",
301		},
302		{
303			name:     "hidden file with extension",
304			dir:      "/tmp",
305			filename: ".config.bak",
306			existingFiles: map[string]bool{
307				"/tmp/.config.bak": true,
308			},
309			expected: ".config_(1).bak",
310		},
311	}
312
313	for _, tt := range tests {
314		t.Run(tt.name, func(t *testing.T) {
315			exists := func(path string) bool {
316				return tt.existingFiles[path]
317			}
318			result := MakeUniqueFilename(tt.dir, tt.filename, exists)
319			assert.Equal(t, tt.expected, result)
320		})
321	}
322}

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 8

Tags 42

↑↓ moveenter openesc close