v0.33.1 9577fee

prepare_download_test.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"errors"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9)
10
11// mockDownloadResponse implements port.DownloadResponse for testing.
12type mockDownloadResponse struct {
13	mimeType          string
14	suggestedFilename string
15	uri               string
16}
17
18func (m *mockDownloadResponse) GetMimeType() string          { return m.mimeType }
19func (m *mockDownloadResponse) GetSuggestedFilename() string { return m.suggestedFilename }
20func (m *mockDownloadResponse) GetUri() string               { return m.uri }
21
22// mockFileSystem implements port.FileSystem for testing.
23type mockFileSystem struct {
24	existingFiles map[string]bool
25	existsErr     error
26}
27
28func (m *mockFileSystem) Exists(_ context.Context, path string) (bool, error) {
29	if m.existsErr != nil {
30		return false, m.existsErr
31	}
32	return m.existingFiles[path], nil
33}
34
35func (*mockFileSystem) IsDirectory(_ context.Context, _ string) (bool, error) {
36	return false, nil
37}
38
39func (*mockFileSystem) GetSize(_ context.Context, _ string) (int64, error) {
40	return 0, nil
41}
42
43func (*mockFileSystem) RemoveAll(_ context.Context, _ string) error {
44	return nil
45}
46
47func TestPrepareDownloadUseCase_Execute(t *testing.T) {
48	ctx := context.Background()
49	uc := NewPrepareDownloadUseCase(nil) // nil FileSystem disables deduplication
50
51	tests := []struct {
52		name             string
53		input            PrepareDownloadInput
54		expectedFilename string
55		expectedPath     string
56	}{
57		{
58			name: "uses suggested filename directly",
59			input: PrepareDownloadInput{
60				SuggestedFilename: "document.pdf",
61				DownloadDir:       "/tmp/downloads",
62			},
63			expectedFilename: "document.pdf",
64			expectedPath:     "/tmp/downloads/document.pdf",
65		},
66		{
67			name: "sanitizes path traversal in suggested filename",
68			input: PrepareDownloadInput{
69				SuggestedFilename: "../../../etc/passwd",
70				DownloadDir:       "/tmp/downloads",
71			},
72			expectedFilename: "passwd",
73			expectedPath:     "/tmp/downloads/passwd",
74		},
75		{
76			name: "falls back to response suggested filename",
77			input: PrepareDownloadInput{
78				SuggestedFilename: "",
79				Response: &mockDownloadResponse{
80					suggestedFilename: "from-header.pdf",
81				},
82				DownloadDir: "/tmp/downloads",
83			},
84			expectedFilename: "from-header.pdf",
85			expectedPath:     "/tmp/downloads/from-header.pdf",
86		},
87		{
88			name: "falls back to URI path",
89			input: PrepareDownloadInput{
90				SuggestedFilename: "",
91				Response: &mockDownloadResponse{
92					suggestedFilename: "",
93					uri:               "https://example.com/files/report.pdf",
94				},
95				DownloadDir: "/tmp/downloads",
96			},
97			expectedFilename: "report.pdf",
98			expectedPath:     "/tmp/downloads/report.pdf",
99		},
100		{
101			name: "adds extension from MIME type when missing",
102			input: PrepareDownloadInput{
103				SuggestedFilename: "report",
104				Response: &mockDownloadResponse{
105					mimeType: "application/pdf",
106				},
107				DownloadDir: "/tmp/downloads",
108			},
109			expectedFilename: "report.pdf",
110			expectedPath:     "/tmp/downloads/report.pdf",
111		},
112		{
113			name: "preserves existing extension even with MIME type",
114			input: PrepareDownloadInput{
115				SuggestedFilename: "report.doc",
116				Response: &mockDownloadResponse{
117					mimeType: "application/pdf",
118				},
119				DownloadDir: "/tmp/downloads",
120			},
121			expectedFilename: "report.doc",
122			expectedPath:     "/tmp/downloads/report.doc",
123		},
124		{
125			name: "handles nil response",
126			input: PrepareDownloadInput{
127				SuggestedFilename: "file.txt",
128				Response:          nil,
129				DownloadDir:       "/tmp/downloads",
130			},
131			expectedFilename: "file.txt",
132			expectedPath:     "/tmp/downloads/file.txt",
133		},
134		{
135			name: "uses default filename for empty inputs",
136			input: PrepareDownloadInput{
137				SuggestedFilename: "",
138				Response:          nil,
139				DownloadDir:       "/tmp/downloads",
140			},
141			expectedFilename: "download",
142			expectedPath:     "/tmp/downloads/download",
143		},
144		{
145			name: "sanitizes windows path traversal",
146			input: PrepareDownloadInput{
147				SuggestedFilename: "..\\..\\Windows\\System32\\config",
148				DownloadDir:       "/tmp/downloads",
149			},
150			expectedFilename: "config",
151			expectedPath:     "/tmp/downloads/config",
152		},
153	}
154
155	for _, tt := range tests {
156		t.Run(tt.name, func(t *testing.T) {
157			result := uc.Execute(ctx, tt.input)
158
159			assert.NotNil(t, result)
160			assert.Equal(t, tt.expectedFilename, result.Filename)
161			assert.Equal(t, tt.expectedPath, result.DestinationPath)
162		})
163	}
164}
165
166func TestPrepareDownloadUseCase_ResolvePriority(t *testing.T) {
167	ctx := context.Background()
168	uc := NewPrepareDownloadUseCase(nil)
169
170	t.Run("suggested filename takes priority over response", func(t *testing.T) {
171		input := PrepareDownloadInput{
172			SuggestedFilename: "priority.pdf",
173			Response: &mockDownloadResponse{
174				suggestedFilename: "response.pdf",
175				uri:               "https://example.com/uri.pdf",
176			},
177			DownloadDir: "/tmp",
178		}
179
180		result := uc.Execute(ctx, input)
181		assert.Equal(t, "priority.pdf", result.Filename)
182	})
183
184	t.Run("response suggested takes priority over URI", func(t *testing.T) {
185		input := PrepareDownloadInput{
186			SuggestedFilename: "",
187			Response: &mockDownloadResponse{
188				suggestedFilename: "response.pdf",
189				uri:               "https://example.com/uri.pdf",
190			},
191			DownloadDir: "/tmp",
192		}
193
194		result := uc.Execute(ctx, input)
195		assert.Equal(t, "response.pdf", result.Filename)
196	})
197}
198
199func TestPrepareDownloadUseCase_Deduplication(t *testing.T) {
200	ctx := context.Background()
201
202	t.Run("existing file triggers _(1) suffix", func(t *testing.T) {
203		fs := &mockFileSystem{
204			existingFiles: map[string]bool{
205				"/tmp/downloads/document.pdf": true,
206			},
207		}
208		uc := NewPrepareDownloadUseCase(fs)
209
210		input := PrepareDownloadInput{
211			SuggestedFilename: "document.pdf",
212			DownloadDir:       "/tmp/downloads",
213		}
214
215		result := uc.Execute(ctx, input)
216		assert.Equal(t, "document_(1).pdf", result.Filename)
217		assert.Equal(t, "/tmp/downloads/document_(1).pdf", result.DestinationPath)
218	})
219
220	t.Run("multiple existing files increment suffix", func(t *testing.T) {
221		fs := &mockFileSystem{
222			existingFiles: map[string]bool{
223				"/tmp/downloads/document.pdf":     true,
224				"/tmp/downloads/document_(1).pdf": true,
225			},
226		}
227		uc := NewPrepareDownloadUseCase(fs)
228
229		input := PrepareDownloadInput{
230			SuggestedFilename: "document.pdf",
231			DownloadDir:       "/tmp/downloads",
232		}
233
234		result := uc.Execute(ctx, input)
235		assert.Equal(t, "document_(2).pdf", result.Filename)
236		assert.Equal(t, "/tmp/downloads/document_(2).pdf", result.DestinationPath)
237	})
238
239	t.Run("fs.Exists error assumes file exists (conservative)", func(t *testing.T) {
240		fs := &mockFileSystem{
241			existingFiles: map[string]bool{},
242			existsErr:     errors.New("permission denied"),
243		}
244		uc := NewPrepareDownloadUseCase(fs)
245
246		input := PrepareDownloadInput{
247			SuggestedFilename: "document.pdf",
248			DownloadDir:       "/tmp/downloads",
249		}
250
251		result := uc.Execute(ctx, input)
252		// When fs.Exists returns an error, we conservatively assume file exists
253		// This triggers deduplication to use timestamp fallback (after 1000 attempts)
254		assert.NotEqual(t, "document.pdf", result.Filename)
255		assert.Contains(t, result.Filename, "document_")
256	})
257
258	t.Run("non-existing file uses original name", func(t *testing.T) {
259		fs := &mockFileSystem{
260			existingFiles: map[string]bool{},
261		}
262		uc := NewPrepareDownloadUseCase(fs)
263
264		input := PrepareDownloadInput{
265			SuggestedFilename: "document.pdf",
266			DownloadDir:       "/tmp/downloads",
267		}
268
269		result := uc.Execute(ctx, input)
270		assert.Equal(t, "document.pdf", result.Filename)
271		assert.Equal(t, "/tmp/downloads/document.pdf", result.DestinationPath)
272	})
273}

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