v0.31.0 bf0756b

download_handler_test.go RawHistoryFolder

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}

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