1package cef
2
3import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "testing"
8
9 purecef "github.com/bnema/purego-cef/cef"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/application/usecase"
14)
15
16type stubDownloadItem struct {
17 id uint32
18 url string
19 suggested string
20 mimeType string
21 fullPath string
22 complete bool
23 canceled bool
24 interrupted bool
25 reason purecef.DownloadInterruptReason
26 percentComplete int32
27 totalBytes int64
28 receivedBytes int64
29}
30
31func (s stubDownloadItem) IsValid() bool { return true }
32func (s stubDownloadItem) IsInProgress() bool { return !s.complete && !s.canceled && !s.interrupted }
33func (s stubDownloadItem) IsComplete() bool { return s.complete }
34func (s stubDownloadItem) IsCanceled() bool { return s.canceled }
35func (s stubDownloadItem) IsInterrupted() bool { return s.interrupted }
36func (s stubDownloadItem) GetInterruptReason() purecef.DownloadInterruptReason {
37 return s.reason
38}
39func (s stubDownloadItem) GetCurrentSpeed() int64 { return 0 }
40func (s stubDownloadItem) GetPercentComplete() int32 { return s.percentComplete }
41func (s stubDownloadItem) GetTotalBytes() int64 { return s.totalBytes }
42func (s stubDownloadItem) GetReceivedBytes() int64 { return s.receivedBytes }
43func (s stubDownloadItem) GetStartTime() uintptr { return 0 }
44func (s stubDownloadItem) GetEndTime() uintptr { return 0 }
45func (s stubDownloadItem) GetFullPath() string { return s.fullPath }
46func (s stubDownloadItem) GetID() uint32 { return s.id }
47func (s stubDownloadItem) GetURL() string { return s.url }
48func (s stubDownloadItem) GetOriginalURL() string { return s.url }
49func (s stubDownloadItem) GetSuggestedFileName() string { return s.suggested }
50func (s stubDownloadItem) GetContentDisposition() string { return "" }
51func (s stubDownloadItem) GetMimeType() string { return s.mimeType }
52func (s stubDownloadItem) IsPaused() bool { return false }
53
54type stubBeforeDownloadCallback struct {
55 path string
56 showDialog int32
57}
58
59func (s *stubBeforeDownloadCallback) Cont(downloadPath string, showDialog int32) {
60 s.path = downloadPath
61 s.showDialog = showDialog
62}
63
64type captureDownloadEvents struct {
65 events []port.DownloadEvent
66}
67
68func (c *captureDownloadEvents) OnDownloadEvent(_ context.Context, event port.DownloadEvent) {
69 c.events = append(c.events, event)
70}
71
72func TestCEFDownloadHandlerLifecycle(t *testing.T) {
73 ctx := context.Background()
74 preparer := usecase.NewPrepareDownloadUseCase(nil)
75 events := &captureDownloadEvents{}
76 downloadDir := t.TempDir()
77 handler := newDownloadHandler(downloadDir, events, preparer)
78 callback := &stubBeforeDownloadCallback{}
79
80 item := stubDownloadItem{
81 id: 7,
82 url: "https://example.com/image.iso",
83 suggested: "image.iso",
84 mimeType: "application/x-iso9660-image",
85 }
86
87 ok := handler.onBeforeDownload(ctx, nil, item, item.suggested, callback)
88
89 require.True(t, ok)
90 require.Equal(t, filepath.Join(downloadDir, "image.iso"), callback.path)
91 require.Len(t, events.events, 1)
92 require.Equal(t, port.DownloadEventStarted, events.events[0].Type)
93
94 item.complete = true
95 item.fullPath = callback.path
96 handler.onDownloadUpdated(ctx, item, nil)
97 // Repeated completion updates should not emit duplicate finished events.
98 handler.onDownloadUpdated(ctx, item, nil)
99
100 require.Len(t, events.events, 2)
101 require.Equal(t, port.DownloadEventFinished, events.events[1].Type)
102 require.Equal(t, "image.iso", events.events[1].Filename)
103
104 // Active map should be cleaned up after terminal event.
105 handler.mu.Lock()
106 _, inActive := handler.active[7]
107 _, inFinished := handler.finished[7]
108 handler.mu.Unlock()
109 require.False(t, inActive, "completed download should be removed from active map")
110 require.True(t, inFinished, "completed download should be in finished set")
111}
112
113func TestCanDownload_AllowsAllMethods(t *testing.T) {
114 preparer := usecase.NewPrepareDownloadUseCase(nil)
115 handler := newDownloadHandler("/tmp/downloads", nil, preparer)
116
117 require.True(t, handler.canDownload(nil, "https://example.com/file.zip", "GET"))
118 require.True(t, handler.canDownload(nil, "https://example.com/file.zip", "POST"))
119 require.True(t, handler.canDownload(nil, "https://example.com/file.zip", "PUT"))
120 require.True(t, handler.canDownload(nil, "https://example.com/file.zip", ""))
121}
122
123func TestMarkFinished_SuppressesDuplicatesAfterCleanup(t *testing.T) {
124 preparer := usecase.NewPrepareDownloadUseCase(nil)
125 handler := newDownloadHandler("/tmp/downloads", nil, preparer)
126
127 // Simulate a download that was tracked through onBeforeDownload.
128 handler.mu.Lock()
129 handler.active[42] = cefDownloadState{filename: "test.bin", destination: "/tmp/test.bin", lastProgressPercent: -1}
130 handler.mu.Unlock()
131
132 require.True(t, handler.markFinished(42), "first terminal event should succeed")
133
134 // Simulate a concurrent update re-populating active before a duplicate terminal update.
135 handler.mu.Lock()
136 handler.active[42] = cefDownloadState{filename: "test.bin", destination: "/tmp/test.bin", lastProgressPercent: -1}
137 handler.mu.Unlock()
138
139 require.False(t, handler.markFinished(42), "second terminal event should be suppressed")
140
141 // Active map should not contain the entry.
142 handler.mu.Lock()
143 _, inActive := handler.active[42]
144 handler.mu.Unlock()
145 require.False(t, inActive)
146}
147
148func TestCEFDownloadHandlerEmitsProgressOncePerPercent(t *testing.T) {
149 ctx := context.Background()
150 preparer := usecase.NewPrepareDownloadUseCase(nil)
151 events := &captureDownloadEvents{}
152 handler := newDownloadHandler("/tmp/downloads", events, preparer)
153 callback := &stubBeforeDownloadCallback{}
154
155 item := stubDownloadItem{
156 id: 11,
157 url: "https://example.com/archive.zip",
158 suggested: "archive.zip",
159 mimeType: "application/zip",
160 }
161
162 require.True(t, handler.onBeforeDownload(ctx, nil, item, item.suggested, callback))
163
164 item.fullPath = callback.path
165 item.percentComplete = 12
166 item.receivedBytes = 12
167 item.totalBytes = 100
168 handler.onDownloadUpdated(ctx, item, nil)
169 handler.onDownloadUpdated(ctx, item, nil)
170
171 item.percentComplete = 13
172 item.receivedBytes = 13
173 handler.onDownloadUpdated(ctx, item, nil)
174
175 require.Len(t, events.events, 3)
176 require.Equal(t, port.DownloadEventStarted, events.events[0].Type)
177 require.Equal(t, port.DownloadEventProgress, events.events[1].Type)
178 require.InEpsilon(t, 0.12, events.events[1].Progress, 0.0001)
179 require.Equal(t, port.DownloadEventProgress, events.events[2].Type)
180 require.InEpsilon(t, 0.13, events.events[2].Progress, 0.0001)
181}
182
183func TestCEFDownloadInterruptedErrorIncludesCodeAndReason(t *testing.T) {
184 ctx := context.Background()
185 preparer := usecase.NewPrepareDownloadUseCase(nil)
186 events := &captureDownloadEvents{}
187 handler := newDownloadHandler("/tmp/downloads", events, preparer)
188 callback := &stubBeforeDownloadCallback{}
189
190 item := stubDownloadItem{
191 id: 9,
192 url: "https://example.com/ubuntu.iso",
193 suggested: "ubuntu.iso",
194 mimeType: "application/x-iso9660-image",
195 }
196
197 require.True(t, handler.onBeforeDownload(ctx, nil, item, item.suggested, callback))
198
199 item.fullPath = callback.path
200 item.interrupted = true
201 item.reason = purecef.DownloadInterruptReasonServerBadContent
202 handler.onDownloadUpdated(ctx, item, nil)
203
204 require.Len(t, events.events, 2)
205 require.Error(t, events.events[1].Error)
206 expectedCode := fmt.Sprintf("code=%d", purecef.DownloadInterruptReasonServerBadContent)
207 require.ErrorContains(t, events.events[1].Error, expectedCode)
208 require.ErrorContains(t, events.events[1].Error, "reason=server_bad_content")
209}