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}