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}