detached 5041353

permission_handler_test.go RawHistoryFolder

1package cef
2
3import (
4	"context"
5	"testing"
6
7	purecef "github.com/bnema/purego-cef/cef"
8	cefmocks "github.com/bnema/purego-cef/cef/mocks"
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11
12	"github.com/bnema/dumber/internal/application/port"
13	"github.com/bnema/dumber/internal/domain/entity"
14)
15
16type stubMediaAccessCallback struct {
17	contCalls   []uint32
18	cancelCalls int
19}
20
21func (s *stubMediaAccessCallback) Cont(allowed uint32) { s.contCalls = append(s.contCalls, allowed) }
22func (s *stubMediaAccessCallback) Cancel()             { s.cancelCalls++ }
23
24type stubPermissionPromptCallback struct {
25	results []purecef.PermissionRequestResult
26}
27
28func (s *stubPermissionPromptCallback) Cont(result purecef.PermissionRequestResult) {
29	s.results = append(s.results, result)
30}
31
32type permissionCapture struct {
33	origin string
34	types  []string
35	allow  func()
36	deny   func()
37	meta   map[string]string
38	calls  int
39}
40
41const testTopURL = "https://meet.example.com/room"
42
43// newPermissionTestHandler builds a handlerSet whose OnPermissionRequest
44// captures the request, and a browser whose main frame is at testTopURL.
45func newPermissionTestHandler(t *testing.T, handled bool) (*handlerSet, *permissionCapture, purecef.Browser) {
46	t.Helper()
47	capture := &permissionCapture{}
48	wv := &WebView{
49		ctx: context.Background(),
50		callbacks: &port.WebViewCallbacks{
51			OnPermissionRequest: func(origin string, types []string, _ map[string]string, allow, deny func()) bool {
52				capture.calls++
53				capture.origin, capture.types, capture.allow, capture.deny = origin, types, allow, deny
54				return handled
55			},
56		},
57	}
58	browser := cefmocks.NewMockBrowser(t)
59	frame := cefmocks.NewMockFrame(t)
60	browser.EXPECT().GetMainFrame().Return(frame).Maybe()
61	frame.EXPECT().GetURL().Return(testTopURL).Maybe()
62	return &handlerSet{wv: wv}, capture, browser
63}
64
65const (
66	mediaMic    = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceAudioCapture)
67	mediaCamera = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture)
68	mediaDeskA  = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture)
69	mediaDeskV  = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
70
71	promptGeo   = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
72	promptNotif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
73	promptMic   = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
74	promptCam   = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
75	promptPTZ   = uint32(purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
76	promptClip  = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)
77)
78
79const (
80	promptAccept = purecef.PermissionRequestResultPermissionResultAccept
81	promptDeny   = purecef.PermissionRequestResultPermissionResultDeny
82)
83
84func TestMediaPermissionTypes(t *testing.T) {
85	tests := []struct {
86		name      string
87		requested uint32
88		types     []string
89	}{
90		{"none", 0, nil},
91		{"microphone", mediaMic, []string{"microphone"}},
92		{"camera", mediaCamera, []string{"camera"}},
93		{"microphone and camera", mediaMic | mediaCamera, []string{"microphone", "camera"}},
94		{"desktop video", mediaDeskV, []string{"display"}},
95		{"desktop audio and video", mediaDeskA | mediaDeskV, []string{"display"}},
96		{"camera and desktop", mediaCamera | mediaDeskV, []string{"camera", "display"}},
97		{"desktop audio only is denied", mediaDeskA, nil},
98		{"mic and desktop audio only is denied", mediaMic | mediaDeskA, nil},
99		{"unknown bit denies everything", mediaMic | 1<<10, nil},
100	}
101	for _, tt := range tests {
102		t.Run(tt.name, func(t *testing.T) {
103			assert.Equal(t, tt.types, mediaPermissionTypes(tt.requested))
104		})
105	}
106}
107
108func TestPromptPermissionTypes(t *testing.T) {
109	tests := []struct {
110		name      string
111		requested uint32
112		types     []string
113	}{
114		{"none", 0, nil},
115		{"geolocation", promptGeo, []string{"geolocation"}},
116		{"notifications", promptNotif, []string{"notification"}},
117		{"mic and camera", promptMic | promptCam, []string{"microphone", "camera"}},
118		{"camera pan tilt zoom unsupported", promptPTZ, nil},
119		{"unsupported", promptClip, nil},
120		{"supported plus unsupported denies all", promptGeo | promptClip, nil},
121	}
122	for _, tt := range tests {
123		t.Run(tt.name, func(t *testing.T) {
124			assert.Equal(t, tt.types, promptPermissionTypes(tt.requested))
125		})
126	}
127}
128
129func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) {
130	h, capture, browser := newPermissionTestHandler(t, true)
131	cb := &stubMediaAccessCallback{}
132
133	handled := h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic|mediaCamera, cb)
134
135	require.Equal(t, int32(1), handled)
136	require.Equal(t, "https://meet.example.com", capture.origin)
137	require.Equal(t, []string{"microphone", "camera"}, capture.types)
138	require.Empty(t, cb.contCalls)
139
140	capture.allow()
141	assert.Equal(t, []uint32{mediaMic | mediaCamera}, cb.contCalls)
142	assert.Zero(t, cb.cancelCalls)
143	assert.Empty(t, h.permissions.pending)
144}
145
146func TestPermissionRequestResolvesOnlyOnce(t *testing.T) {
147	steps := map[string]func(c *permissionCapture){
148		"allow first": func(c *permissionCapture) { c.allow(); c.deny(); c.allow() },
149		"deny first":  func(c *permissionCapture) { c.deny(); c.allow(); c.deny() },
150	}
151	for name, step := range steps {
152		wantAllow := name == "allow first"
153		t.Run("media "+name, func(t *testing.T) {
154			h, capture, browser := newPermissionTestHandler(t, true)
155			cb := &stubMediaAccessCallback{}
156			require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
157			step(capture)
158			if wantAllow {
159				assert.Equal(t, []uint32{mediaMic}, cb.contCalls)
160				assert.Zero(t, cb.cancelCalls)
161			} else {
162				assert.Empty(t, cb.contCalls)
163				assert.Equal(t, 1, cb.cancelCalls)
164			}
165		})
166		t.Run("prompt "+name, func(t *testing.T) {
167			h, capture, browser := newPermissionTestHandler(t, true)
168			cb := &stubPermissionPromptCallback{}
169			require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 7, "https://meet.example.com", promptGeo, cb))
170			require.Equal(t, []string{"geolocation"}, capture.types)
171			step(capture)
172			want := promptDeny
173			if wantAllow {
174				want = promptAccept
175			}
176			assert.Equal(t, []purecef.PermissionRequestResult{want}, cb.results)
177		})
178	}
179}
180
181func TestOnRequestMediaAccessPermissionDenies(t *testing.T) {
182	tests := []struct {
183		name      string
184		handled   bool
185		origin    string
186		requested uint32
187		wantCalls int
188	}{
189		{"handler returns false", false, "https://meet.example.com", mediaMic, 1},
190		{"unknown bits", true, "https://meet.example.com", mediaMic | 1<<10, 0},
191		{"desktop audio only", true, "https://meet.example.com", mediaDeskA, 0},
192		{"invalid origin", true, "", mediaMic, 0},
193		{"cross-origin requester", true, "https://evil.example.org", mediaMic, 0},
194		{"different port", true, "https://meet.example.com:8443", mediaMic, 0},
195	}
196	for _, tt := range tests {
197		t.Run(tt.name, func(t *testing.T) {
198			h, capture, browser := newPermissionTestHandler(t, tt.handled)
199			cb := &stubMediaAccessCallback{}
200			require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, tt.origin, tt.requested, cb))
201			assert.Equal(t, 1, cb.cancelCalls)
202			assert.Empty(t, cb.contCalls)
203			assert.Equal(t, tt.wantCalls, capture.calls)
204		})
205	}
206}
207
208func TestOnRequestMediaAccessPermissionDeniesWithoutTopLevelOrigin(t *testing.T) {
209	t.Run("nil browser", func(t *testing.T) {
210		h, capture, _ := newPermissionTestHandler(t, true)
211		cb := &stubMediaAccessCallback{}
212		require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://meet.example.com", mediaMic, cb))
213		assert.Equal(t, 1, cb.cancelCalls)
214		assert.Zero(t, capture.calls)
215	})
216	t.Run("nil main frame", func(t *testing.T) {
217		h, capture, _ := newPermissionTestHandler(t, true)
218		browser := cefmocks.NewMockBrowser(t)
219		browser.EXPECT().GetMainFrame().Return(nil)
220		cb := &stubMediaAccessCallback{}
221		require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
222		assert.Equal(t, 1, cb.cancelCalls)
223		assert.Zero(t, capture.calls)
224	})
225	t.Run("invalid main frame url", func(t *testing.T) {
226		h, capture, _ := newPermissionTestHandler(t, true)
227		browser := cefmocks.NewMockBrowser(t)
228		frame := cefmocks.NewMockFrame(t)
229		browser.EXPECT().GetMainFrame().Return(frame)
230		frame.EXPECT().GetURL().Return("about:blank")
231		cb := &stubMediaAccessCallback{}
232		require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
233		assert.Equal(t, 1, cb.cancelCalls)
234		assert.Zero(t, capture.calls)
235	})
236}
237
238func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) {
239	for name, cbs := range map[string]*port.WebViewCallbacks{
240		"nil callbacks":              nil,
241		"nil OnPermissionRequest fn": {},
242	} {
243		t.Run(name, func(t *testing.T) {
244			h, _, browser := newPermissionTestHandler(t, true)
245			h.wv.callbacks = cbs
246			cb := &stubMediaAccessCallback{}
247			require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
248			assert.Equal(t, 1, cb.cancelCalls)
249			assert.Empty(t, cb.contCalls)
250		})
251	}
252}
253
254func TestOnRequestMediaAccessPermissionDeniesWhenDestroyed(t *testing.T) {
255	h, capture, browser := newPermissionTestHandler(t, true)
256	h.wv.destroyed.Store(true)
257	cb := &stubMediaAccessCallback{}
258	require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
259	assert.Equal(t, 1, cb.cancelCalls)
260	assert.Zero(t, capture.calls)
261}
262
263func TestDeliverPermissionRequest(t *testing.T) {
264	newReq := func(allowed *[]bool) *cefPermissionRequest {
265		return &cefPermissionRequest{finish: func(allow bool) { *allowed = append(*allowed, allow) }}
266	}
267	handlerCalls := 0
268	cb := &port.WebViewCallbacks{
269		OnPermissionRequest: func(string, []string, map[string]string, func(), func()) bool {
270			handlerCalls++
271			return true
272		},
273	}
274
275	t.Run("already resolved is skipped", func(t *testing.T) {
276		h := &handlerSet{wv: &WebView{ctx: context.Background()}}
277		var results []bool
278		req := newReq(&results)
279		req.resolve(false)
280		handlerCalls = 0
281		h.deliverPermissionRequest(req, cb, "https://a.example", []string{"camera"}, nil)
282		assert.Zero(t, handlerCalls)
283		assert.Equal(t, []bool{false}, results)
284	})
285	t.Run("destroyed before delivery denies", func(t *testing.T) {
286		h := &handlerSet{wv: &WebView{ctx: context.Background()}}
287		h.wv.destroyed.Store(true)
288		var results []bool
289		req := newReq(&results)
290		handlerCalls = 0
291		h.deliverPermissionRequest(req, cb, "https://a.example", []string{"camera"}, nil)
292		assert.Zero(t, handlerCalls)
293		assert.Equal(t, []bool{false}, results)
294	})
295}
296
297func TestPermissionTrackerAddDuplicateKeyReturnsPrevious(t *testing.T) {
298	var tracker permissionTracker
299	var results []string
300	mk := func(name string) *cefPermissionRequest {
301		return &cefPermissionRequest{finish: func(bool) { results = append(results, name) }}
302	}
303	key := permissionKey{prompt: true, id: 1}
304	first, second := mk("first"), mk("second")
305
306	assert.Nil(t, tracker.add(key, first))
307	prev := tracker.add(key, second)
308	require.Same(t, first, prev)
309	prev.resolve(false)
310
311	assert.Equal(t, []string{"first"}, results)
312	assert.Same(t, second, tracker.get(key), "resolving the replaced request must not untrack the new one")
313}
314
315func TestOnShowPermissionPromptDuplicateIDDeniesPrevious(t *testing.T) {
316	h, _, browser := newPermissionTestHandler(t, true)
317	first, second := &stubPermissionPromptCallback{}, &stubPermissionPromptCallback{}
318	require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 4, "https://meet.example.com", promptGeo, first))
319	require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 4, "https://meet.example.com", promptGeo, second))
320	assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, first.results)
321	assert.Empty(t, second.results)
322}
323
324func TestOnShowPermissionPromptUnsupportedReturnsZero(t *testing.T) {
325	for name, mask := range map[string]uint32{
326		"empty":       0,
327		"clipboard":   promptClip,
328		"ptz":         promptPTZ,
329		"mixed":       promptGeo | promptClip,
330		"unknown bit": 1 << 30,
331	} {
332		t.Run(name, func(t *testing.T) {
333			h, capture, browser := newPermissionTestHandler(t, true)
334			cb := &stubPermissionPromptCallback{}
335			assert.Equal(t, int32(0), h.OnShowPermissionPrompt(browser, 1, "https://meet.example.com", mask, cb))
336			assert.Empty(t, cb.results)
337			assert.Zero(t, capture.calls)
338			assert.Empty(t, h.permissions.pending)
339		})
340	}
341}
342
343func TestOnShowPermissionPromptDeniesCrossOrigin(t *testing.T) {
344	h, capture, browser := newPermissionTestHandler(t, true)
345	cb := &stubPermissionPromptCallback{}
346	require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 1, "https://evil.example.org", promptNotif, cb))
347	assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, cb.results)
348	assert.Zero(t, capture.calls)
349}
350
351func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) {
352	h, capture, browser := newPermissionTestHandler(t, true)
353	cb := &stubPermissionPromptCallback{}
354	require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 3, "https://meet.example.com", promptNotif, cb))
355
356	h.OnDismissPermissionPrompt(nil, 3, purecef.PermissionRequestResultPermissionResultDismiss)
357	capture.allow()
358	capture.deny()
359
360	assert.Empty(t, cb.results, "a dismissed prompt callback must not be continued")
361	assert.Empty(t, h.permissions.pending)
362
363	// Dismissing an unknown or already-resolved prompt is a no-op.
364	h.OnDismissPermissionPrompt(nil, 3, purecef.PermissionRequestResultPermissionResultDismiss)
365	h.OnDismissPermissionPrompt(nil, 99, purecef.PermissionRequestResultPermissionResultDismiss)
366}
367
368func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) {
369	h, capture, browser := newPermissionTestHandler(t, true)
370	media := &stubMediaAccessCallback{}
371	prompt := &stubPermissionPromptCallback{}
372	require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, media))
373	mediaAllow := capture.allow
374	require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 5, "https://meet.example.com", promptGeo, prompt))
375	promptAllow := capture.allow
376
377	h.denyPendingPermissions()
378	h.denyPendingPermissions()
379	mediaAllow()
380	promptAllow()
381
382	assert.Equal(t, 1, media.cancelCalls)
383	assert.Empty(t, media.contCalls)
384	assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, prompt.results)
385	assert.Empty(t, h.permissions.pending)
386}
387
388func TestDispatchPermissionResultRunsInlineWhenPostFails(t *testing.T) {
389	prevNew, prevPost := cefNewTask, cefPostTask
390	cefNewTask = func(task purecef.Task) purecef.Task { return task }
391	cefPostTask = func(purecef.ThreadID, purecef.Task) int32 { return 0 }
392	defer func() { cefNewTask, cefPostTask = prevNew, prevPost }()
393
394	ran := 0
395	dispatchPermissionResult(&WebView{ctx: context.Background(), engine: &Engine{}}, func() { ran++ })
396	assert.Equal(t, 1, ran)
397}
398
399func TestDispatchPermissionResultPostsToUIThread(t *testing.T) {
400	prevNew, prevPost := cefNewTask, cefPostTask
401	var posted purecef.Task
402	var gotThread purecef.ThreadID
403	cefNewTask = func(task purecef.Task) purecef.Task { return task }
404	cefPostTask = func(id purecef.ThreadID, task purecef.Task) int32 {
405		gotThread, posted = id, task
406		return 1
407	}
408	defer func() { cefNewTask, cefPostTask = prevNew, prevPost }()
409
410	ran := 0
411	dispatchPermissionResult(&WebView{ctx: context.Background(), engine: &Engine{}}, func() { ran++ })
412	require.NotNil(t, posted)
413	assert.Zero(t, ran)
414	assert.Equal(t, purecef.ThreadIDTidUi, gotThread)
415	posted.Execute()
416	assert.Equal(t, 1, ran)
417}
418
419func TestOnRequestMediaAccessPermissionFlagsDesktopVideoAsUnmediated(t *testing.T) {
420	tests := []struct {
421		name      string
422		requested uint32
423		flagged   bool
424		audio     bool
425	}{
426		{"desktop video", mediaDeskV, true, false},
427		{"desktop audio and video", mediaDeskA | mediaDeskV, true, true},
428		{"camera and desktop video", mediaCamera | mediaDeskV, true, false},
429		{"camera only", mediaCamera, false, false},
430		{"microphone and camera", mediaMic | mediaCamera, false, false},
431	}
432	for _, tt := range tests {
433		t.Run(tt.name, func(t *testing.T) {
434			capture := &permissionCapture{}
435			h, _, browser := newPermissionTestHandler(t, true)
436			h.wv.callbacks.OnPermissionRequest = func(_ string, types []string, meta map[string]string, _, _ func()) bool {
437				capture.types, capture.meta = types, meta
438				return true
439			}
440			cb := &stubMediaAccessCallback{}
441			require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", tt.requested, cb))
442			require.NotNil(t, capture.types)
443			assert.Equal(t, tt.flagged, entity.PermissionMetadata(capture.meta).IsUnmediatedCapture())
444			assert.Equal(t, tt.audio, entity.PermissionMetadata(capture.meta).IsUnmediatedAudio())
445		})
446	}
447}

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