detached 75e8c77

fix(cef): harden permission handler origin, type mapping and tests

- derive the origin from the main frame URL and deny cross-origin requesters
  or an unavailable main frame
- return 0 (CEF default) from OnShowPermissionPrompt for empty/unsupported
  masks; drop camera pan/tilt/zoom from the supported mask
- deny desktop audio capture without desktop video (loopback audio)
- deny in the GTK dispatch closure when the WebView was destroyed
- mapping functions return only []string; media allow continues with the
  requested mask
- document that an open dialog is not closed on prompt dismissal
- tests: table once-only test, pre-resolved/destroyed delivery, duplicate key,
  cross-origin, desktop-audio-only, unsupported prompt returns 0
commit
75e8c773540200e7325bd06742e1398f38d0872a
author
brice · · 2026-10-03 06:22 UTC
committer
brice · · 2026-10-03 07:50 UTC
parent
ef83d2a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +313 -144
M internal/infrastructure/cef/permission_handler.go +101 -49
@@ -8,6 +8,7 @@ import (
88 purecef "github.com/bnema/purego-cef/cef"
99 "github.com/rs/zerolog"
1010
11+ "github.com/bnema/dumber/internal/application/port"
1112 "github.com/bnema/dumber/internal/domain/entity"
1213 urlutil "github.com/bnema/dumber/internal/domain/url"
1314 "github.com/bnema/dumber/internal/logging"
@@ -21,46 +22,50 @@ const supportedMediaAccessMask = uint32(
2122 purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
2223
2324 // mediaPermissionTypes maps a CEF media access bitmask to Dumber permission
24-// types. The returned allowedMask contains only the requested bits that Dumber
25-// understands; it is what must be granted when the user allows the request.
26-// An empty request or one containing unknown bits yields no types (deny).
27-func mediaPermissionTypes(requested uint32) (types []string, allowedMask uint32) {
25+// types. It returns nil (deny) for an empty mask, a mask containing unknown
26+// bits, or desktop audio without desktop video: Chromium would grant a
27+// "loopback" system audio device for it without going through any portal.
28+func mediaPermissionTypes(requested uint32) []string {
29+ const (
30+ deviceAudio = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceAudioCapture)
31+ deviceVideo = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture)
32+ desktopAudio = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture)
33+ desktopVideo = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
34+ )
2835 if requested == 0 || requested&^supportedMediaAccessMask != 0 {
29- return nil, 0
36+ return nil
37+ }
38+ if requested&desktopAudio != 0 && requested&desktopVideo == 0 {
39+ return nil
3040 }
31- allowedMask = requested
32- if allowedMask&uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceAudioCapture) != 0 {
41+ var types []string
42+ if requested&deviceAudio != 0 {
3343 types = append(types, string(entity.PermissionTypeMicrophone))
3444 }
35- if allowedMask&uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture) != 0 {
45+ if requested&deviceVideo != 0 {
3646 types = append(types, string(entity.PermissionTypeCamera))
3747 }
38- const desktop = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture |
39- purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
40- if allowedMask&desktop != 0 {
48+ if requested&desktopVideo != 0 {
4149 types = append(types, string(entity.PermissionTypeDisplay))
4250 }
43- return types, allowedMask
51+ return types
4452 }
4553
4654 // promptPermissionTypes maps a CEF permission prompt bitmask to Dumber
47-// permission types. ok is false when the mask is empty or contains any bit that
48-// Dumber does not support, in which case the whole request must be denied.
49-func promptPermissionTypes(requested uint32) (types []string, ok bool) {
50- if requested == 0 {
51- return nil, false
52- }
55+// permission types. It returns nil when the mask is empty or contains any bit
56+// Dumber does not support.
57+func promptPermissionTypes(requested uint32) []string {
5358 const (
54- camera = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream |
55- purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
59+ camera = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
5660 mic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
5761 geolocation = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
5862 notifications = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
5963 supported = camera | mic | geolocation | notifications
6064 )
61- if requested&^supported != 0 {
62- return nil, false
65+ if requested == 0 || requested&^supported != 0 {
66+ return nil
6367 }
68+ var types []string
6469 if requested&mic != 0 {
6570 types = append(types, string(entity.PermissionTypeMicrophone))
6671 }
@@ -73,7 +78,7 @@ func promptPermissionTypes(requested uint32) (types []string, ok bool) {
7378 if requested&notifications != 0 {
7479 types = append(types, string(entity.PermissionTypeNotification))
7580 }
76- return types, true
81+ return types
7782 }
7883
7984 // cefPermissionRequest guarantees that a CEF permission callback is resolved
@@ -169,7 +174,7 @@ func (t *permissionTracker) drain() []*cefPermissionRequest {
169174
170175 // OnRequestMediaAccessPermission handles getUserMedia/getDisplayMedia requests.
171176 func (h *handlerSet) OnRequestMediaAccessPermission(
172- _ purecef.Browser,
177+ browser purecef.Browser,
173178 _ purecef.Frame,
174179 requestingOrigin string,
175180 requestedPermissions uint32,
@@ -178,21 +183,24 @@ func (h *handlerSet) OnRequestMediaAccessPermission(
178183 if h == nil || h.wv == nil || callback == nil {
179184 return 0
180185 }
181- types, allowedMask := mediaPermissionTypes(requestedPermissions)
182- h.beginPermissionRequest(h.permissions.nextMediaKey(), requestingOrigin, types, func(allow bool) {
183- if allow && allowedMask != 0 {
184- callback.Cont(allowedMask)
185- return
186- }
187- callback.Cancel()
188- })
186+ h.beginPermissionRequest(
187+ h.permissions.nextMediaKey(), browser, requestingOrigin,
188+ mediaPermissionTypes(requestedPermissions),
189+ func(allow bool) {
190+ if allow {
191+ callback.Cont(requestedPermissions)
192+ return
193+ }
194+ callback.Cancel()
195+ })
189196 return 1
190197 }
191198
192199 // OnShowPermissionPrompt handles non-media permission requests
193-// (geolocation, notifications, ...).
200+// (geolocation, notifications). Unsupported requests are left to CEF's default
201+// handling by returning 0.
194202 func (h *handlerSet) OnShowPermissionPrompt(
195- _ purecef.Browser,
203+ browser purecef.Browser,
196204 promptID uint64,
197205 requestingOrigin string,
198206 requestedPermissions uint32,
@@ -201,8 +209,11 @@ func (h *handlerSet) OnShowPermissionPrompt(
201209 if h == nil || h.wv == nil || callback == nil {
202210 return 0
203211 }
204- types, _ := promptPermissionTypes(requestedPermissions)
205- h.beginPermissionRequest(permissionKey{prompt: true, id: promptID}, requestingOrigin, types, func(allow bool) {
212+ types := promptPermissionTypes(requestedPermissions)
213+ if len(types) == 0 {
214+ return 0
215+ }
216+ h.beginPermissionRequest(permissionKey{prompt: true, id: promptID}, browser, requestingOrigin, types, func(allow bool) {
206217 if allow {
207218 callback.Cont(purecef.PermissionRequestResultPermissionResultAccept)
208219 return
@@ -214,6 +225,10 @@ func (h *handlerSet) OnShowPermissionPrompt(
214225
215226 // OnDismissPermissionPrompt is called when CEF dismisses a prompt we handled.
216227 // The callback is no longer valid, so it must not be invoked anymore.
228+//
229+// Known limitation: a Dumber permission dialog that is already open is not
230+// closed on dismissal, because the dialog port has no cancel API. Its eventual
231+// answer is ignored by the once-only guard.
217232 func (h *handlerSet) OnDismissPermissionPrompt(
218233 _ purecef.Browser,
219234 promptID uint64,
@@ -246,10 +261,12 @@ func (h *handlerSet) denyPendingPermissions() {
246261 // beginPermissionRequest registers a request, forwards it to the WebView's
247262 // OnPermissionRequest callback on the GTK thread, and guarantees finish is
248263 // invoked exactly once on the CEF UI thread. It denies when the request cannot
249-// be mapped, the origin is invalid, no callback is available, or the WebView
250-// is destroyed.
264+// be mapped, the top-level origin is unavailable or differs from the requesting
265+// origin (cross-origin frames are unsupported), no callback is available, or
266+// the WebView is destroyed.
251267 func (h *handlerSet) beginPermissionRequest(
252268 key permissionKey,
269+ browser purecef.Browser,
253270 requestingOrigin string,
254271 types []string,
255272 finish func(allow bool),
@@ -270,9 +287,9 @@ func (h *handlerSet) beginPermissionRequest(
270287 req.resolve(false)
271288 return
272289 }
273- origin, err := urlutil.ExtractOrigin(requestingOrigin)
274- if err != nil {
275- log.Debug().Err(err).Msg("cef: denying permission request with invalid origin")
290+ origin, ok := topLevelPermissionOrigin(browser, requestingOrigin)
291+ if !ok {
292+ log.Debug().Msg("cef: denying permission request; requester is not the top-level origin")
276293 req.resolve(false)
277294 return
278295 }
@@ -286,16 +303,51 @@ func (h *handlerSet) beginPermissionRequest(
286303 return
287304 }
288305
306+ wv.runOnGTK(func() { h.deliverPermissionRequest(req, cb, origin, types) })
307+}
308+
309+// deliverPermissionRequest runs on the GTK thread. It skips requests that were
310+// already resolved (dismissed, closed) and denies when the WebView was destroyed
311+// or the callback does not handle the request.
312+func (h *handlerSet) deliverPermissionRequest(
313+ req *cefPermissionRequest,
314+ cb *port.WebViewCallbacks,
315+ origin string,
316+ types []string,
317+) {
318+ if req.isDone() {
319+ return
320+ }
321+ if h.wv.destroyed.Load() {
322+ req.resolve(false)
323+ return
324+ }
289325 allow := func() { req.resolve(true) }
290326 deny := func() { req.resolve(false) }
291- wv.runOnGTK(func() {
292- if req.isDone() {
293- return
294- }
295- if !cb.OnPermissionRequest(origin, types, map[string]string{}, allow, deny) {
296- req.resolve(false)
297- }
298- })
327+ if !cb.OnPermissionRequest(origin, types, map[string]string{}, allow, deny) {
328+ req.resolve(false)
329+ }
330+}
331+
332+// topLevelPermissionOrigin returns the canonical top-level (main frame) origin
333+// and whether the requesting origin matches it.
334+func topLevelPermissionOrigin(browser purecef.Browser, requestingOrigin string) (string, bool) {
335+ if browser == nil {
336+ return "", false
337+ }
338+ frame := browser.GetMainFrame()
339+ if frame == nil {
340+ return "", false
341+ }
342+ top, err := urlutil.ExtractOrigin(frame.GetURL())
343+ if err != nil {
344+ return "", false
345+ }
346+ requester, err := urlutil.ExtractOrigin(requestingOrigin)
347+ if err != nil || requester != top {
348+ return "", false
349+ }
350+ return top, true
299351 }
300352
301353 // dispatchPermissionResult runs fn on the CEF UI thread, as CEF permission
@@ -8,6 +8,7 @@ import (
8 purecef "github.com/bnema/purego-cef/cef"8 purecef "github.com/bnema/purego-cef/cef"
9 "github.com/rs/zerolog"9 "github.com/rs/zerolog"
10 10
11+ "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/domain/entity"12 "github.com/bnema/dumber/internal/domain/entity"
12 urlutil "github.com/bnema/dumber/internal/domain/url"13 urlutil "github.com/bnema/dumber/internal/domain/url"
13 "github.com/bnema/dumber/internal/logging"14 "github.com/bnema/dumber/internal/logging"
@@ -21,46 +22,50 @@ const supportedMediaAccessMask = uint32(
21 purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)22 purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
22 23
23 // mediaPermissionTypes maps a CEF media access bitmask to Dumber permission24 // mediaPermissionTypes maps a CEF media access bitmask to Dumber permission
24-// types. The returned allowedMask contains only the requested bits that Dumber25+// types. It returns nil (deny) for an empty mask, a mask containing unknown
25-// understands; it is what must be granted when the user allows the request.26+// bits, or desktop audio without desktop video: Chromium would grant a
26-// An empty request or one containing unknown bits yields no types (deny).27+// "loopback" system audio device for it without going through any portal.
27-func mediaPermissionTypes(requested uint32) (types []string, allowedMask uint32) {28+func mediaPermissionTypes(requested uint32) []string {
29+ const (
30+ deviceAudio = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceAudioCapture)
31+ deviceVideo = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture)
32+ desktopAudio = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture)
33+ desktopVideo = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
34+ )
28 if requested == 0 || requested&^supportedMediaAccessMask != 0 {35 if requested == 0 || requested&^supportedMediaAccessMask != 0 {
29- return nil, 036+ return nil
37+ }
38+ if requested&desktopAudio != 0 && requested&desktopVideo == 0 {
39+ return nil
30 }40 }
31- allowedMask = requested41+ var types []string
32- if allowedMask&uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceAudioCapture) != 0 {42+ if requested&deviceAudio != 0 {
33 types = append(types, string(entity.PermissionTypeMicrophone))43 types = append(types, string(entity.PermissionTypeMicrophone))
34 }44 }
35- if allowedMask&uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture) != 0 {45+ if requested&deviceVideo != 0 {
36 types = append(types, string(entity.PermissionTypeCamera))46 types = append(types, string(entity.PermissionTypeCamera))
37 }47 }
38- const desktop = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture |48+ if requested&desktopVideo != 0 {
39- purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
40- if allowedMask&desktop != 0 {
41 types = append(types, string(entity.PermissionTypeDisplay))49 types = append(types, string(entity.PermissionTypeDisplay))
42 }50 }
43- return types, allowedMask51+ return types
44 }52 }
45 53
46 // promptPermissionTypes maps a CEF permission prompt bitmask to Dumber54 // promptPermissionTypes maps a CEF permission prompt bitmask to Dumber
47-// permission types. ok is false when the mask is empty or contains any bit that55+// permission types. It returns nil when the mask is empty or contains any bit
48-// Dumber does not support, in which case the whole request must be denied.56+// Dumber does not support.
49-func promptPermissionTypes(requested uint32) (types []string, ok bool) {57+func promptPermissionTypes(requested uint32) []string {
50- if requested == 0 {
51- return nil, false
52- }
53 const (58 const (
54- camera = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream |59+ camera = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
55- purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
56 mic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)60 mic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
57 geolocation = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)61 geolocation = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
58 notifications = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)62 notifications = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
59 supported = camera | mic | geolocation | notifications63 supported = camera | mic | geolocation | notifications
60 )64 )
61- if requested&^supported != 0 {65+ if requested == 0 || requested&^supported != 0 {
62- return nil, false66+ return nil
63 }67 }
68+ var types []string
64 if requested&mic != 0 {69 if requested&mic != 0 {
65 types = append(types, string(entity.PermissionTypeMicrophone))70 types = append(types, string(entity.PermissionTypeMicrophone))
66 }71 }
@@ -73,7 +78,7 @@ func promptPermissionTypes(requested uint32) (types []string, ok bool) {
73 if requested&notifications != 0 {78 if requested&notifications != 0 {
74 types = append(types, string(entity.PermissionTypeNotification))79 types = append(types, string(entity.PermissionTypeNotification))
75 }80 }
76- return types, true81+ return types
77 }82 }
78 83
79 // cefPermissionRequest guarantees that a CEF permission callback is resolved84 // cefPermissionRequest guarantees that a CEF permission callback is resolved
@@ -169,7 +174,7 @@ func (t *permissionTracker) drain() []*cefPermissionRequest {
169 174
170 // OnRequestMediaAccessPermission handles getUserMedia/getDisplayMedia requests.175 // OnRequestMediaAccessPermission handles getUserMedia/getDisplayMedia requests.
171 func (h *handlerSet) OnRequestMediaAccessPermission(176 func (h *handlerSet) OnRequestMediaAccessPermission(
172- _ purecef.Browser,177+ browser purecef.Browser,
173 _ purecef.Frame,178 _ purecef.Frame,
174 requestingOrigin string,179 requestingOrigin string,
175 requestedPermissions uint32,180 requestedPermissions uint32,
@@ -178,21 +183,24 @@ func (h *handlerSet) OnRequestMediaAccessPermission(
178 if h == nil || h.wv == nil || callback == nil {183 if h == nil || h.wv == nil || callback == nil {
179 return 0184 return 0
180 }185 }
181- types, allowedMask := mediaPermissionTypes(requestedPermissions)186+ h.beginPermissionRequest(
182- h.beginPermissionRequest(h.permissions.nextMediaKey(), requestingOrigin, types, func(allow bool) {187+ h.permissions.nextMediaKey(), browser, requestingOrigin,
183- if allow && allowedMask != 0 {188+ mediaPermissionTypes(requestedPermissions),
184- callback.Cont(allowedMask)189+ func(allow bool) {
185- return190+ if allow {
186- }191+ callback.Cont(requestedPermissions)
187- callback.Cancel()192+ return
188- })193+ }
194+ callback.Cancel()
195+ })
189 return 1196 return 1
190 }197 }
191 198
192 // OnShowPermissionPrompt handles non-media permission requests199 // OnShowPermissionPrompt handles non-media permission requests
193-// (geolocation, notifications, ...).200+// (geolocation, notifications). Unsupported requests are left to CEF's default
201+// handling by returning 0.
194 func (h *handlerSet) OnShowPermissionPrompt(202 func (h *handlerSet) OnShowPermissionPrompt(
195- _ purecef.Browser,203+ browser purecef.Browser,
196 promptID uint64,204 promptID uint64,
197 requestingOrigin string,205 requestingOrigin string,
198 requestedPermissions uint32,206 requestedPermissions uint32,
@@ -201,8 +209,11 @@ func (h *handlerSet) OnShowPermissionPrompt(
201 if h == nil || h.wv == nil || callback == nil {209 if h == nil || h.wv == nil || callback == nil {
202 return 0210 return 0
203 }211 }
204- types, _ := promptPermissionTypes(requestedPermissions)212+ types := promptPermissionTypes(requestedPermissions)
205- h.beginPermissionRequest(permissionKey{prompt: true, id: promptID}, requestingOrigin, types, func(allow bool) {213+ if len(types) == 0 {
214+ return 0
215+ }
216+ h.beginPermissionRequest(permissionKey{prompt: true, id: promptID}, browser, requestingOrigin, types, func(allow bool) {
206 if allow {217 if allow {
207 callback.Cont(purecef.PermissionRequestResultPermissionResultAccept)218 callback.Cont(purecef.PermissionRequestResultPermissionResultAccept)
208 return219 return
@@ -214,6 +225,10 @@ func (h *handlerSet) OnShowPermissionPrompt(
214 225
215 // OnDismissPermissionPrompt is called when CEF dismisses a prompt we handled.226 // OnDismissPermissionPrompt is called when CEF dismisses a prompt we handled.
216 // The callback is no longer valid, so it must not be invoked anymore.227 // The callback is no longer valid, so it must not be invoked anymore.
228+//
229+// Known limitation: a Dumber permission dialog that is already open is not
230+// closed on dismissal, because the dialog port has no cancel API. Its eventual
231+// answer is ignored by the once-only guard.
217 func (h *handlerSet) OnDismissPermissionPrompt(232 func (h *handlerSet) OnDismissPermissionPrompt(
218 _ purecef.Browser,233 _ purecef.Browser,
219 promptID uint64,234 promptID uint64,
@@ -246,10 +261,12 @@ func (h *handlerSet) denyPendingPermissions() {
246 // beginPermissionRequest registers a request, forwards it to the WebView's261 // beginPermissionRequest registers a request, forwards it to the WebView's
247 // OnPermissionRequest callback on the GTK thread, and guarantees finish is262 // OnPermissionRequest callback on the GTK thread, and guarantees finish is
248 // invoked exactly once on the CEF UI thread. It denies when the request cannot263 // invoked exactly once on the CEF UI thread. It denies when the request cannot
249-// be mapped, the origin is invalid, no callback is available, or the WebView264+// be mapped, the top-level origin is unavailable or differs from the requesting
250-// is destroyed.265+// origin (cross-origin frames are unsupported), no callback is available, or
266+// the WebView is destroyed.
251 func (h *handlerSet) beginPermissionRequest(267 func (h *handlerSet) beginPermissionRequest(
252 key permissionKey,268 key permissionKey,
269+ browser purecef.Browser,
253 requestingOrigin string,270 requestingOrigin string,
254 types []string,271 types []string,
255 finish func(allow bool),272 finish func(allow bool),
@@ -270,9 +287,9 @@ func (h *handlerSet) beginPermissionRequest(
270 req.resolve(false)287 req.resolve(false)
271 return288 return
272 }289 }
273- origin, err := urlutil.ExtractOrigin(requestingOrigin)290+ origin, ok := topLevelPermissionOrigin(browser, requestingOrigin)
274- if err != nil {291+ if !ok {
275- log.Debug().Err(err).Msg("cef: denying permission request with invalid origin")292+ log.Debug().Msg("cef: denying permission request; requester is not the top-level origin")
276 req.resolve(false)293 req.resolve(false)
277 return294 return
278 }295 }
@@ -286,16 +303,51 @@ func (h *handlerSet) beginPermissionRequest(
286 return303 return
287 }304 }
288 305
306+ wv.runOnGTK(func() { h.deliverPermissionRequest(req, cb, origin, types) })
307+}
308+
309+// deliverPermissionRequest runs on the GTK thread. It skips requests that were
310+// already resolved (dismissed, closed) and denies when the WebView was destroyed
311+// or the callback does not handle the request.
312+func (h *handlerSet) deliverPermissionRequest(
313+ req *cefPermissionRequest,
314+ cb *port.WebViewCallbacks,
315+ origin string,
316+ types []string,
317+) {
318+ if req.isDone() {
319+ return
320+ }
321+ if h.wv.destroyed.Load() {
322+ req.resolve(false)
323+ return
324+ }
289 allow := func() { req.resolve(true) }325 allow := func() { req.resolve(true) }
290 deny := func() { req.resolve(false) }326 deny := func() { req.resolve(false) }
291- wv.runOnGTK(func() {327+ if !cb.OnPermissionRequest(origin, types, map[string]string{}, allow, deny) {
292- if req.isDone() {328+ req.resolve(false)
293- return329+ }
294- }330+}
295- if !cb.OnPermissionRequest(origin, types, map[string]string{}, allow, deny) {331+
296- req.resolve(false)332+// topLevelPermissionOrigin returns the canonical top-level (main frame) origin
297- }333+// and whether the requesting origin matches it.
298- })334+func topLevelPermissionOrigin(browser purecef.Browser, requestingOrigin string) (string, bool) {
335+ if browser == nil {
336+ return "", false
337+ }
338+ frame := browser.GetMainFrame()
339+ if frame == nil {
340+ return "", false
341+ }
342+ top, err := urlutil.ExtractOrigin(frame.GetURL())
343+ if err != nil {
344+ return "", false
345+ }
346+ requester, err := urlutil.ExtractOrigin(requestingOrigin)
347+ if err != nil || requester != top {
348+ return "", false
349+ }
350+ return top, true
299 }351 }
300 352
301 // dispatchPermissionResult runs fn on the CEF UI thread, as CEF permission353 // dispatchPermissionResult runs fn on the CEF UI thread, as CEF permission
M internal/infrastructure/cef/permission_handler_test.go +212 -95
@@ -5,6 +5,7 @@ import (
55 "testing"
66
77 purecef "github.com/bnema/purego-cef/cef"
8+ cefmocks "github.com/bnema/purego-cef/cef/mocks"
89 "github.com/stretchr/testify/assert"
910 "github.com/stretchr/testify/require"
1011
@@ -35,7 +36,12 @@ type permissionCapture struct {
3536 calls int
3637 }
3738
38-func newPermissionTestHandler(handled bool) (*handlerSet, *permissionCapture) {
39+const testTopURL = "https://meet.example.com/room"
40+
41+// newPermissionTestHandler builds a handlerSet whose OnPermissionRequest
42+// captures the request, and a browser whose main frame is at testTopURL.
43+func newPermissionTestHandler(t *testing.T, handled bool) (*handlerSet, *permissionCapture, purecef.Browser) {
44+ t.Helper()
3945 capture := &permissionCapture{}
4046 wv := &WebView{
4147 ctx: context.Background(),
@@ -47,7 +53,11 @@ func newPermissionTestHandler(handled bool) (*handlerSet, *permissionCapture) {
4753 },
4854 },
4955 }
50- return &handlerSet{wv: wv}, capture
56+ browser := cefmocks.NewMockBrowser(t)
57+ frame := cefmocks.NewMockFrame(t)
58+ browser.EXPECT().GetMainFrame().Return(frame).Maybe()
59+ frame.EXPECT().GetURL().Return(testTopURL).Maybe()
60+ return &handlerSet{wv: wv}, capture, browser
5161 }
5262
5363 const (
@@ -55,6 +65,18 @@ const (
5565 mediaCamera = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture)
5666 mediaDeskA = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture)
5767 mediaDeskV = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
68+
69+ promptGeo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
70+ promptNotif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
71+ promptMic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
72+ promptCam = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
73+ promptPTZ = uint32(purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
74+ promptClip = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)
75+)
76+
77+const (
78+ promptAccept = purecef.PermissionRequestResultPermissionResultAccept
79+ promptDeny = purecef.PermissionRequestResultPermissionResultDeny
5880 )
5981
6082 func TestMediaPermissionTypes(t *testing.T) {
@@ -62,63 +84,51 @@ func TestMediaPermissionTypes(t *testing.T) {
6284 name string
6385 requested uint32
6486 types []string
65- allowed uint32
6687 }{
67- {"none", 0, nil, 0},
68- {"microphone", mediaMic, []string{"microphone"}, mediaMic},
69- {"camera", mediaCamera, []string{"camera"}, mediaCamera},
70- {"microphone and camera", mediaMic | mediaCamera, []string{"microphone", "camera"}, mediaMic | mediaCamera},
71- {"desktop video", mediaDeskV, []string{"display"}, mediaDeskV},
72- {"desktop audio and video", mediaDeskA | mediaDeskV, []string{"display"}, mediaDeskA | mediaDeskV},
73- {"camera and desktop", mediaCamera | mediaDeskV, []string{"camera", "display"}, mediaCamera | mediaDeskV},
74- {"unknown bit denies everything", mediaMic | 1<<10, nil, 0},
88+ {"none", 0, nil},
89+ {"microphone", mediaMic, []string{"microphone"}},
90+ {"camera", mediaCamera, []string{"camera"}},
91+ {"microphone and camera", mediaMic | mediaCamera, []string{"microphone", "camera"}},
92+ {"desktop video", mediaDeskV, []string{"display"}},
93+ {"desktop audio and video", mediaDeskA | mediaDeskV, []string{"display"}},
94+ {"camera and desktop", mediaCamera | mediaDeskV, []string{"camera", "display"}},
95+ {"desktop audio only is denied", mediaDeskA, nil},
96+ {"mic and desktop audio only is denied", mediaMic | mediaDeskA, nil},
97+ {"unknown bit denies everything", mediaMic | 1<<10, nil},
7598 }
7699 for _, tt := range tests {
77100 t.Run(tt.name, func(t *testing.T) {
78- types, allowed := mediaPermissionTypes(tt.requested)
79- assert.Equal(t, tt.types, types)
80- assert.Equal(t, tt.allowed, allowed)
101+ assert.Equal(t, tt.types, mediaPermissionTypes(tt.requested))
81102 })
82103 }
83104 }
84105
85106 func TestPromptPermissionTypes(t *testing.T) {
86- const (
87- geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
88- notif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
89- mic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
90- cam = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
91- ptz = uint32(purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
92- clip = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)
93- )
94107 tests := []struct {
95108 name string
96109 requested uint32
97110 types []string
98- ok bool
99111 }{
100- {"none", 0, nil, false},
101- {"geolocation", geo, []string{"geolocation"}, true},
102- {"notifications", notif, []string{"notification"}, true},
103- {"mic and camera", mic | cam, []string{"microphone", "camera"}, true},
104- {"camera pan tilt zoom", ptz, []string{"camera"}, true},
105- {"unsupported", clip, nil, false},
106- {"supported plus unsupported denies all", geo | clip, nil, false},
112+ {"none", 0, nil},
113+ {"geolocation", promptGeo, []string{"geolocation"}},
114+ {"notifications", promptNotif, []string{"notification"}},
115+ {"mic and camera", promptMic | promptCam, []string{"microphone", "camera"}},
116+ {"camera pan tilt zoom unsupported", promptPTZ, nil},
117+ {"unsupported", promptClip, nil},
118+ {"supported plus unsupported denies all", promptGeo | promptClip, nil},
107119 }
108120 for _, tt := range tests {
109121 t.Run(tt.name, func(t *testing.T) {
110- types, ok := promptPermissionTypes(tt.requested)
111- assert.Equal(t, tt.ok, ok)
112- assert.Equal(t, tt.types, types)
122+ assert.Equal(t, tt.types, promptPermissionTypes(tt.requested))
113123 })
114124 }
115125 }
116126
117127 func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) {
118- h, capture := newPermissionTestHandler(true)
128+ h, capture, browser := newPermissionTestHandler(t, true)
119129 cb := &stubMediaAccessCallback{}
120130
121- handled := h.OnRequestMediaAccessPermission(nil, nil, "https://meet.example.com/room", mediaMic|mediaCamera, cb)
131+ handled := h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic|mediaCamera, cb)
122132
123133 require.Equal(t, int32(1), handled)
124134 require.Equal(t, "https://meet.example.com", capture.origin)
@@ -126,46 +136,98 @@ func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) {
126136 require.Empty(t, cb.contCalls)
127137
128138 capture.allow()
129- capture.allow()
130- capture.deny()
131-
132139 assert.Equal(t, []uint32{mediaMic | mediaCamera}, cb.contCalls)
133140 assert.Zero(t, cb.cancelCalls)
134141 assert.Empty(t, h.permissions.pending)
135142 }
136143
137-func TestOnRequestMediaAccessPermissionDenyOnlyOnce(t *testing.T) {
138- h, capture := newPermissionTestHandler(true)
139- cb := &stubMediaAccessCallback{}
140-
141- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))
142- capture.deny()
143- capture.allow()
144- capture.deny()
144+func TestPermissionRequestResolvesOnlyOnce(t *testing.T) {
145+ steps := map[string]func(c *permissionCapture){
146+ "allow first": func(c *permissionCapture) { c.allow(); c.deny(); c.allow() },
147+ "deny first": func(c *permissionCapture) { c.deny(); c.allow(); c.deny() },
148+ }
149+ for name, step := range steps {
150+ wantAllow := name == "allow first"
151+ t.Run("media "+name, func(t *testing.T) {
152+ h, capture, browser := newPermissionTestHandler(t, true)
153+ cb := &stubMediaAccessCallback{}
154+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
155+ step(capture)
156+ if wantAllow {
157+ assert.Equal(t, []uint32{mediaMic}, cb.contCalls)
158+ assert.Zero(t, cb.cancelCalls)
159+ } else {
160+ assert.Empty(t, cb.contCalls)
161+ assert.Equal(t, 1, cb.cancelCalls)
162+ }
163+ })
164+ t.Run("prompt "+name, func(t *testing.T) {
165+ h, capture, browser := newPermissionTestHandler(t, true)
166+ cb := &stubPermissionPromptCallback{}
167+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 7, "https://meet.example.com", promptGeo, cb))
168+ require.Equal(t, []string{"geolocation"}, capture.types)
169+ step(capture)
170+ want := promptDeny
171+ if wantAllow {
172+ want = promptAccept
173+ }
174+ assert.Equal(t, []purecef.PermissionRequestResult{want}, cb.results)
175+ })
176+ }
177+}
145178
146- assert.Empty(t, cb.contCalls)
147- assert.Equal(t, 1, cb.cancelCalls)
179+func TestOnRequestMediaAccessPermissionDenies(t *testing.T) {
180+ tests := []struct {
181+ name string
182+ handled bool
183+ origin string
184+ requested uint32
185+ wantCalls int
186+ }{
187+ {"handler returns false", false, "https://meet.example.com", mediaMic, 1},
188+ {"unknown bits", true, "https://meet.example.com", mediaMic | 1<<10, 0},
189+ {"desktop audio only", true, "https://meet.example.com", mediaDeskA, 0},
190+ {"invalid origin", true, "", mediaMic, 0},
191+ {"cross-origin requester", true, "https://evil.example.org", mediaMic, 0},
192+ {"different port", true, "https://meet.example.com:8443", mediaMic, 0},
193+ }
194+ for _, tt := range tests {
195+ t.Run(tt.name, func(t *testing.T) {
196+ h, capture, browser := newPermissionTestHandler(t, tt.handled)
197+ cb := &stubMediaAccessCallback{}
198+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, tt.origin, tt.requested, cb))
199+ assert.Equal(t, 1, cb.cancelCalls)
200+ assert.Empty(t, cb.contCalls)
201+ assert.Equal(t, tt.wantCalls, capture.calls)
202+ })
203+ }
148204 }
149205
150-func TestOnRequestMediaAccessPermissionDeniesWhenNotHandledOrUnsupported(t *testing.T) {
151- t.Run("handler returns false", func(t *testing.T) {
152- h, _ := newPermissionTestHandler(false)
206+func TestOnRequestMediaAccessPermissionDeniesWithoutTopLevelOrigin(t *testing.T) {
207+ t.Run("nil browser", func(t *testing.T) {
208+ h, capture, _ := newPermissionTestHandler(t, true)
153209 cb := &stubMediaAccessCallback{}
154- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))
210+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://meet.example.com", mediaMic, cb))
155211 assert.Equal(t, 1, cb.cancelCalls)
156- assert.Empty(t, cb.contCalls)
212+ assert.Zero(t, capture.calls)
157213 })
158- t.Run("unknown bits", func(t *testing.T) {
159- h, capture := newPermissionTestHandler(true)
214+ t.Run("nil main frame", func(t *testing.T) {
215+ h, capture, _ := newPermissionTestHandler(t, true)
216+ browser := cefmocks.NewMockBrowser(t)
217+ browser.EXPECT().GetMainFrame().Return(nil)
160218 cb := &stubMediaAccessCallback{}
161- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic|1<<10, cb))
219+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
162220 assert.Equal(t, 1, cb.cancelCalls)
163221 assert.Zero(t, capture.calls)
164222 })
165- t.Run("invalid origin", func(t *testing.T) {
166- h, capture := newPermissionTestHandler(true)
223+ t.Run("invalid main frame url", func(t *testing.T) {
224+ h, capture, _ := newPermissionTestHandler(t, true)
225+ browser := cefmocks.NewMockBrowser(t)
226+ frame := cefmocks.NewMockFrame(t)
227+ browser.EXPECT().GetMainFrame().Return(frame)
228+ frame.EXPECT().GetURL().Return("about:blank")
167229 cb := &stubMediaAccessCallback{}
168- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "", mediaMic, cb))
230+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
169231 assert.Equal(t, 1, cb.cancelCalls)
170232 assert.Zero(t, capture.calls)
171233 })
@@ -177,9 +239,10 @@ func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) {
177239 "nil OnPermissionRequest fn": {},
178240 } {
179241 t.Run(name, func(t *testing.T) {
180- h := &handlerSet{wv: &WebView{ctx: context.Background(), callbacks: cbs}}
242+ h, _, browser := newPermissionTestHandler(t, true)
243+ h.wv.callbacks = cbs
181244 cb := &stubMediaAccessCallback{}
182- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))
245+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
183246 assert.Equal(t, 1, cb.cancelCalls)
184247 assert.Empty(t, cb.contCalls)
185248 })
@@ -187,46 +250,106 @@ func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) {
187250 }
188251
189252 func TestOnRequestMediaAccessPermissionDeniesWhenDestroyed(t *testing.T) {
190- h, capture := newPermissionTestHandler(true)
253+ h, capture, browser := newPermissionTestHandler(t, true)
191254 h.wv.destroyed.Store(true)
192255 cb := &stubMediaAccessCallback{}
193- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))
256+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
194257 assert.Equal(t, 1, cb.cancelCalls)
195258 assert.Zero(t, capture.calls)
196259 }
197260
198-func TestOnShowPermissionPromptAllowAndDeny(t *testing.T) {
199- const geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
261+func TestDeliverPermissionRequest(t *testing.T) {
262+ newReq := func(allowed *[]bool) *cefPermissionRequest {
263+ return &cefPermissionRequest{finish: func(allow bool) { *allowed = append(*allowed, allow) }}
264+ }
265+ handlerCalls := 0
266+ cb := &port.WebViewCallbacks{
267+ OnPermissionRequest: func(string, []string, map[string]string, func(), func()) bool {
268+ handlerCalls++
269+ return true
270+ },
271+ }
200272
201- h, capture := newPermissionTestHandler(true)
202- allowCb := &stubPermissionPromptCallback{}
203- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 7, "https://maps.example", geo, allowCb))
204- require.Equal(t, []string{"geolocation"}, capture.types)
205- capture.allow()
206- capture.deny()
207- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultAccept}, allowCb.results)
273+ t.Run("already resolved is skipped", func(t *testing.T) {
274+ h := &handlerSet{wv: &WebView{ctx: context.Background()}}
275+ var results []bool
276+ req := newReq(&results)
277+ req.resolve(false)
278+ handlerCalls = 0
279+ h.deliverPermissionRequest(req, cb, "https://a.example", []string{"camera"})
280+ assert.Zero(t, handlerCalls)
281+ assert.Equal(t, []bool{false}, results)
282+ })
283+ t.Run("destroyed before delivery denies", func(t *testing.T) {
284+ h := &handlerSet{wv: &WebView{ctx: context.Background()}}
285+ h.wv.destroyed.Store(true)
286+ var results []bool
287+ req := newReq(&results)
288+ handlerCalls = 0
289+ h.deliverPermissionRequest(req, cb, "https://a.example", []string{"camera"})
290+ assert.Zero(t, handlerCalls)
291+ assert.Equal(t, []bool{false}, results)
292+ })
293+}
208294
209- denyCb := &stubPermissionPromptCallback{}
210- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 8, "https://maps.example", geo, denyCb))
211- capture.deny()
212- capture.allow()
213- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultDeny}, denyCb.results)
295+func TestPermissionTrackerAddDuplicateKeyReturnsPrevious(t *testing.T) {
296+ var tracker permissionTracker
297+ var results []string
298+ mk := func(name string) *cefPermissionRequest {
299+ return &cefPermissionRequest{finish: func(bool) { results = append(results, name) }}
300+ }
301+ key := permissionKey{prompt: true, id: 1}
302+ first, second := mk("first"), mk("second")
303+
304+ assert.Nil(t, tracker.add(key, first))
305+ prev := tracker.add(key, second)
306+ require.Same(t, first, prev)
307+ prev.resolve(false)
308+
309+ assert.Equal(t, []string{"first"}, results)
310+ assert.Same(t, second, tracker.get(key), "resolving the replaced request must not untrack the new one")
311+}
312+
313+func TestOnShowPermissionPromptDuplicateIDDeniesPrevious(t *testing.T) {
314+ h, _, browser := newPermissionTestHandler(t, true)
315+ first, second := &stubPermissionPromptCallback{}, &stubPermissionPromptCallback{}
316+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 4, "https://meet.example.com", promptGeo, first))
317+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 4, "https://meet.example.com", promptGeo, second))
318+ assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, first.results)
319+ assert.Empty(t, second.results)
320+}
321+
322+func TestOnShowPermissionPromptUnsupportedReturnsZero(t *testing.T) {
323+ for name, mask := range map[string]uint32{
324+ "empty": 0,
325+ "clipboard": promptClip,
326+ "ptz": promptPTZ,
327+ "mixed": promptGeo | promptClip,
328+ "unknown bit": 1 << 30,
329+ } {
330+ t.Run(name, func(t *testing.T) {
331+ h, capture, browser := newPermissionTestHandler(t, true)
332+ cb := &stubPermissionPromptCallback{}
333+ assert.Equal(t, int32(0), h.OnShowPermissionPrompt(browser, 1, "https://meet.example.com", mask, cb))
334+ assert.Empty(t, cb.results)
335+ assert.Zero(t, capture.calls)
336+ assert.Empty(t, h.permissions.pending)
337+ })
338+ }
214339 }
215340
216-func TestOnShowPermissionPromptDeniesUnsupportedType(t *testing.T) {
217- const clip = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)
218- h, capture := newPermissionTestHandler(true)
341+func TestOnShowPermissionPromptDeniesCrossOrigin(t *testing.T) {
342+ h, capture, browser := newPermissionTestHandler(t, true)
219343 cb := &stubPermissionPromptCallback{}
220- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 1, "https://a.example", clip, cb))
221- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultDeny}, cb.results)
344+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 1, "https://evil.example.org", promptNotif, cb))
345+ assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, cb.results)
222346 assert.Zero(t, capture.calls)
223347 }
224348
225349 func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) {
226- const notif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
227- h, capture := newPermissionTestHandler(true)
350+ h, capture, browser := newPermissionTestHandler(t, true)
228351 cb := &stubPermissionPromptCallback{}
229- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 3, "https://a.example", notif, cb))
352+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 3, "https://meet.example.com", promptNotif, cb))
230353
231354 h.OnDismissPermissionPrompt(nil, 3, purecef.PermissionRequestResultPermissionResultDismiss)
232355 capture.allow()
@@ -241,13 +364,12 @@ func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) {
241364 }
242365
243366 func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) {
244- const geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
245- h, capture := newPermissionTestHandler(true)
367+ h, capture, browser := newPermissionTestHandler(t, true)
246368 media := &stubMediaAccessCallback{}
247369 prompt := &stubPermissionPromptCallback{}
248- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, media))
370+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, media))
249371 mediaAllow := capture.allow
250- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 5, "https://a.example", geo, prompt))
372+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 5, "https://meet.example.com", promptGeo, prompt))
251373 promptAllow := capture.allow
252374
253375 h.denyPendingPermissions()
@@ -257,7 +379,7 @@ func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) {
257379
258380 assert.Equal(t, 1, media.cancelCalls)
259381 assert.Empty(t, media.contCalls)
260- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultDeny}, prompt.results)
382+ assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, prompt.results)
261383 assert.Empty(t, h.permissions.pending)
262384 }
263385
@@ -291,8 +413,3 @@ func TestDispatchPermissionResultPostsToUIThread(t *testing.T) {
291413 posted.Execute()
292414 assert.Equal(t, 1, ran)
293415 }
294-
295-func TestGetPermissionHandlerEnabled(t *testing.T) {
296- h := &handlerSet{wv: &WebView{ctx: context.Background()}}
297- assert.NotNil(t, h.GetPermissionHandler())
298-}
@@ -5,6 +5,7 @@ import (
5 "testing"5 "testing"
6 6
7 purecef "github.com/bnema/purego-cef/cef"7 purecef "github.com/bnema/purego-cef/cef"
8+ cefmocks "github.com/bnema/purego-cef/cef/mocks"
8 "github.com/stretchr/testify/assert"9 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
10 11
@@ -35,7 +36,12 @@ type permissionCapture struct {
35 calls int36 calls int
36 }37 }
37 38
38-func newPermissionTestHandler(handled bool) (*handlerSet, *permissionCapture) {39+const testTopURL = "https://meet.example.com/room"
40+
41+// newPermissionTestHandler builds a handlerSet whose OnPermissionRequest
42+// captures the request, and a browser whose main frame is at testTopURL.
43+func newPermissionTestHandler(t *testing.T, handled bool) (*handlerSet, *permissionCapture, purecef.Browser) {
44+ t.Helper()
39 capture := &permissionCapture{}45 capture := &permissionCapture{}
40 wv := &WebView{46 wv := &WebView{
41 ctx: context.Background(),47 ctx: context.Background(),
@@ -47,7 +53,11 @@ func newPermissionTestHandler(handled bool) (*handlerSet, *permissionCapture) {
47 },53 },
48 },54 },
49 }55 }
50- return &handlerSet{wv: wv}, capture56+ browser := cefmocks.NewMockBrowser(t)
57+ frame := cefmocks.NewMockFrame(t)
58+ browser.EXPECT().GetMainFrame().Return(frame).Maybe()
59+ frame.EXPECT().GetURL().Return(testTopURL).Maybe()
60+ return &handlerSet{wv: wv}, capture, browser
51 }61 }
52 62
53 const (63 const (
@@ -55,6 +65,18 @@ const (
55 mediaCamera = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture)65 mediaCamera = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture)
56 mediaDeskA = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture)66 mediaDeskA = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture)
57 mediaDeskV = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)67 mediaDeskV = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture)
68+
69+ promptGeo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
70+ promptNotif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
71+ promptMic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
72+ promptCam = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
73+ promptPTZ = uint32(purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
74+ promptClip = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)
75+)
76+
77+const (
78+ promptAccept = purecef.PermissionRequestResultPermissionResultAccept
79+ promptDeny = purecef.PermissionRequestResultPermissionResultDeny
58 )80 )
59 81
60 func TestMediaPermissionTypes(t *testing.T) {82 func TestMediaPermissionTypes(t *testing.T) {
@@ -62,63 +84,51 @@ func TestMediaPermissionTypes(t *testing.T) {
62 name string84 name string
63 requested uint3285 requested uint32
64 types []string86 types []string
65- allowed uint32
66 }{87 }{
67- {"none", 0, nil, 0},88+ {"none", 0, nil},
68- {"microphone", mediaMic, []string{"microphone"}, mediaMic},89+ {"microphone", mediaMic, []string{"microphone"}},
69- {"camera", mediaCamera, []string{"camera"}, mediaCamera},90+ {"camera", mediaCamera, []string{"camera"}},
70- {"microphone and camera", mediaMic | mediaCamera, []string{"microphone", "camera"}, mediaMic | mediaCamera},91+ {"microphone and camera", mediaMic | mediaCamera, []string{"microphone", "camera"}},
71- {"desktop video", mediaDeskV, []string{"display"}, mediaDeskV},92+ {"desktop video", mediaDeskV, []string{"display"}},
72- {"desktop audio and video", mediaDeskA | mediaDeskV, []string{"display"}, mediaDeskA | mediaDeskV},93+ {"desktop audio and video", mediaDeskA | mediaDeskV, []string{"display"}},
73- {"camera and desktop", mediaCamera | mediaDeskV, []string{"camera", "display"}, mediaCamera | mediaDeskV},94+ {"camera and desktop", mediaCamera | mediaDeskV, []string{"camera", "display"}},
74- {"unknown bit denies everything", mediaMic | 1<<10, nil, 0},95+ {"desktop audio only is denied", mediaDeskA, nil},
96+ {"mic and desktop audio only is denied", mediaMic | mediaDeskA, nil},
97+ {"unknown bit denies everything", mediaMic | 1<<10, nil},
75 }98 }
76 for _, tt := range tests {99 for _, tt := range tests {
77 t.Run(tt.name, func(t *testing.T) {100 t.Run(tt.name, func(t *testing.T) {
78- types, allowed := mediaPermissionTypes(tt.requested)101+ assert.Equal(t, tt.types, mediaPermissionTypes(tt.requested))
79- assert.Equal(t, tt.types, types)
80- assert.Equal(t, tt.allowed, allowed)
81 })102 })
82 }103 }
83 }104 }
84 105
85 func TestPromptPermissionTypes(t *testing.T) {106 func TestPromptPermissionTypes(t *testing.T) {
86- const (
87- geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)
88- notif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)
89- mic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream)
90- cam = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream)
91- ptz = uint32(purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom)
92- clip = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)
93- )
94 tests := []struct {107 tests := []struct {
95 name string108 name string
96 requested uint32109 requested uint32
97 types []string110 types []string
98- ok bool
99 }{111 }{
100- {"none", 0, nil, false},112+ {"none", 0, nil},
101- {"geolocation", geo, []string{"geolocation"}, true},113+ {"geolocation", promptGeo, []string{"geolocation"}},
102- {"notifications", notif, []string{"notification"}, true},114+ {"notifications", promptNotif, []string{"notification"}},
103- {"mic and camera", mic | cam, []string{"microphone", "camera"}, true},115+ {"mic and camera", promptMic | promptCam, []string{"microphone", "camera"}},
104- {"camera pan tilt zoom", ptz, []string{"camera"}, true},116+ {"camera pan tilt zoom unsupported", promptPTZ, nil},
105- {"unsupported", clip, nil, false},117+ {"unsupported", promptClip, nil},
106- {"supported plus unsupported denies all", geo | clip, nil, false},118+ {"supported plus unsupported denies all", promptGeo | promptClip, nil},
107 }119 }
108 for _, tt := range tests {120 for _, tt := range tests {
109 t.Run(tt.name, func(t *testing.T) {121 t.Run(tt.name, func(t *testing.T) {
110- types, ok := promptPermissionTypes(tt.requested)122+ assert.Equal(t, tt.types, promptPermissionTypes(tt.requested))
111- assert.Equal(t, tt.ok, ok)
112- assert.Equal(t, tt.types, types)
113 })123 })
114 }124 }
115 }125 }
116 126
117 func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) {127 func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) {
118- h, capture := newPermissionTestHandler(true)128+ h, capture, browser := newPermissionTestHandler(t, true)
119 cb := &stubMediaAccessCallback{}129 cb := &stubMediaAccessCallback{}
120 130
121- handled := h.OnRequestMediaAccessPermission(nil, nil, "https://meet.example.com/room", mediaMic|mediaCamera, cb)131+ handled := h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic|mediaCamera, cb)
122 132
123 require.Equal(t, int32(1), handled)133 require.Equal(t, int32(1), handled)
124 require.Equal(t, "https://meet.example.com", capture.origin)134 require.Equal(t, "https://meet.example.com", capture.origin)
@@ -126,46 +136,98 @@ func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) {
126 require.Empty(t, cb.contCalls)136 require.Empty(t, cb.contCalls)
127 137
128 capture.allow()138 capture.allow()
129- capture.allow()
130- capture.deny()
131-
132 assert.Equal(t, []uint32{mediaMic | mediaCamera}, cb.contCalls)139 assert.Equal(t, []uint32{mediaMic | mediaCamera}, cb.contCalls)
133 assert.Zero(t, cb.cancelCalls)140 assert.Zero(t, cb.cancelCalls)
134 assert.Empty(t, h.permissions.pending)141 assert.Empty(t, h.permissions.pending)
135 }142 }
136 143
137-func TestOnRequestMediaAccessPermissionDenyOnlyOnce(t *testing.T) {144+func TestPermissionRequestResolvesOnlyOnce(t *testing.T) {
138- h, capture := newPermissionTestHandler(true)145+ steps := map[string]func(c *permissionCapture){
139- cb := &stubMediaAccessCallback{}146+ "allow first": func(c *permissionCapture) { c.allow(); c.deny(); c.allow() },
140-147+ "deny first": func(c *permissionCapture) { c.deny(); c.allow(); c.deny() },
141- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))148+ }
142- capture.deny()149+ for name, step := range steps {
143- capture.allow()150+ wantAllow := name == "allow first"
144- capture.deny()151+ t.Run("media "+name, func(t *testing.T) {
152+ h, capture, browser := newPermissionTestHandler(t, true)
153+ cb := &stubMediaAccessCallback{}
154+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
155+ step(capture)
156+ if wantAllow {
157+ assert.Equal(t, []uint32{mediaMic}, cb.contCalls)
158+ assert.Zero(t, cb.cancelCalls)
159+ } else {
160+ assert.Empty(t, cb.contCalls)
161+ assert.Equal(t, 1, cb.cancelCalls)
162+ }
163+ })
164+ t.Run("prompt "+name, func(t *testing.T) {
165+ h, capture, browser := newPermissionTestHandler(t, true)
166+ cb := &stubPermissionPromptCallback{}
167+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 7, "https://meet.example.com", promptGeo, cb))
168+ require.Equal(t, []string{"geolocation"}, capture.types)
169+ step(capture)
170+ want := promptDeny
171+ if wantAllow {
172+ want = promptAccept
173+ }
174+ assert.Equal(t, []purecef.PermissionRequestResult{want}, cb.results)
175+ })
176+ }
177+}
145 178
146- assert.Empty(t, cb.contCalls)179+func TestOnRequestMediaAccessPermissionDenies(t *testing.T) {
147- assert.Equal(t, 1, cb.cancelCalls)180+ tests := []struct {
181+ name string
182+ handled bool
183+ origin string
184+ requested uint32
185+ wantCalls int
186+ }{
187+ {"handler returns false", false, "https://meet.example.com", mediaMic, 1},
188+ {"unknown bits", true, "https://meet.example.com", mediaMic | 1<<10, 0},
189+ {"desktop audio only", true, "https://meet.example.com", mediaDeskA, 0},
190+ {"invalid origin", true, "", mediaMic, 0},
191+ {"cross-origin requester", true, "https://evil.example.org", mediaMic, 0},
192+ {"different port", true, "https://meet.example.com:8443", mediaMic, 0},
193+ }
194+ for _, tt := range tests {
195+ t.Run(tt.name, func(t *testing.T) {
196+ h, capture, browser := newPermissionTestHandler(t, tt.handled)
197+ cb := &stubMediaAccessCallback{}
198+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, tt.origin, tt.requested, cb))
199+ assert.Equal(t, 1, cb.cancelCalls)
200+ assert.Empty(t, cb.contCalls)
201+ assert.Equal(t, tt.wantCalls, capture.calls)
202+ })
203+ }
148 }204 }
149 205
150-func TestOnRequestMediaAccessPermissionDeniesWhenNotHandledOrUnsupported(t *testing.T) {206+func TestOnRequestMediaAccessPermissionDeniesWithoutTopLevelOrigin(t *testing.T) {
151- t.Run("handler returns false", func(t *testing.T) {207+ t.Run("nil browser", func(t *testing.T) {
152- h, _ := newPermissionTestHandler(false)208+ h, capture, _ := newPermissionTestHandler(t, true)
153 cb := &stubMediaAccessCallback{}209 cb := &stubMediaAccessCallback{}
154- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))210+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://meet.example.com", mediaMic, cb))
155 assert.Equal(t, 1, cb.cancelCalls)211 assert.Equal(t, 1, cb.cancelCalls)
156- assert.Empty(t, cb.contCalls)212+ assert.Zero(t, capture.calls)
157 })213 })
158- t.Run("unknown bits", func(t *testing.T) {214+ t.Run("nil main frame", func(t *testing.T) {
159- h, capture := newPermissionTestHandler(true)215+ h, capture, _ := newPermissionTestHandler(t, true)
216+ browser := cefmocks.NewMockBrowser(t)
217+ browser.EXPECT().GetMainFrame().Return(nil)
160 cb := &stubMediaAccessCallback{}218 cb := &stubMediaAccessCallback{}
161- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic|1<<10, cb))219+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
162 assert.Equal(t, 1, cb.cancelCalls)220 assert.Equal(t, 1, cb.cancelCalls)
163 assert.Zero(t, capture.calls)221 assert.Zero(t, capture.calls)
164 })222 })
165- t.Run("invalid origin", func(t *testing.T) {223+ t.Run("invalid main frame url", func(t *testing.T) {
166- h, capture := newPermissionTestHandler(true)224+ h, capture, _ := newPermissionTestHandler(t, true)
225+ browser := cefmocks.NewMockBrowser(t)
226+ frame := cefmocks.NewMockFrame(t)
227+ browser.EXPECT().GetMainFrame().Return(frame)
228+ frame.EXPECT().GetURL().Return("about:blank")
167 cb := &stubMediaAccessCallback{}229 cb := &stubMediaAccessCallback{}
168- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "", mediaMic, cb))230+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
169 assert.Equal(t, 1, cb.cancelCalls)231 assert.Equal(t, 1, cb.cancelCalls)
170 assert.Zero(t, capture.calls)232 assert.Zero(t, capture.calls)
171 })233 })
@@ -177,9 +239,10 @@ func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) {
177 "nil OnPermissionRequest fn": {},239 "nil OnPermissionRequest fn": {},
178 } {240 } {
179 t.Run(name, func(t *testing.T) {241 t.Run(name, func(t *testing.T) {
180- h := &handlerSet{wv: &WebView{ctx: context.Background(), callbacks: cbs}}242+ h, _, browser := newPermissionTestHandler(t, true)
243+ h.wv.callbacks = cbs
181 cb := &stubMediaAccessCallback{}244 cb := &stubMediaAccessCallback{}
182- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))245+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
183 assert.Equal(t, 1, cb.cancelCalls)246 assert.Equal(t, 1, cb.cancelCalls)
184 assert.Empty(t, cb.contCalls)247 assert.Empty(t, cb.contCalls)
185 })248 })
@@ -187,46 +250,106 @@ func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) {
187 }250 }
188 251
189 func TestOnRequestMediaAccessPermissionDeniesWhenDestroyed(t *testing.T) {252 func TestOnRequestMediaAccessPermissionDeniesWhenDestroyed(t *testing.T) {
190- h, capture := newPermissionTestHandler(true)253+ h, capture, browser := newPermissionTestHandler(t, true)
191 h.wv.destroyed.Store(true)254 h.wv.destroyed.Store(true)
192 cb := &stubMediaAccessCallback{}255 cb := &stubMediaAccessCallback{}
193- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, cb))256+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, cb))
194 assert.Equal(t, 1, cb.cancelCalls)257 assert.Equal(t, 1, cb.cancelCalls)
195 assert.Zero(t, capture.calls)258 assert.Zero(t, capture.calls)
196 }259 }
197 260
198-func TestOnShowPermissionPromptAllowAndDeny(t *testing.T) {261+func TestDeliverPermissionRequest(t *testing.T) {
199- const geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)262+ newReq := func(allowed *[]bool) *cefPermissionRequest {
263+ return &cefPermissionRequest{finish: func(allow bool) { *allowed = append(*allowed, allow) }}
264+ }
265+ handlerCalls := 0
266+ cb := &port.WebViewCallbacks{
267+ OnPermissionRequest: func(string, []string, map[string]string, func(), func()) bool {
268+ handlerCalls++
269+ return true
270+ },
271+ }
200 272
201- h, capture := newPermissionTestHandler(true)273+ t.Run("already resolved is skipped", func(t *testing.T) {
202- allowCb := &stubPermissionPromptCallback{}274+ h := &handlerSet{wv: &WebView{ctx: context.Background()}}
203- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 7, "https://maps.example", geo, allowCb))275+ var results []bool
204- require.Equal(t, []string{"geolocation"}, capture.types)276+ req := newReq(&results)
205- capture.allow()277+ req.resolve(false)
206- capture.deny()278+ handlerCalls = 0
207- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultAccept}, allowCb.results)279+ h.deliverPermissionRequest(req, cb, "https://a.example", []string{"camera"})
280+ assert.Zero(t, handlerCalls)
281+ assert.Equal(t, []bool{false}, results)
282+ })
283+ t.Run("destroyed before delivery denies", func(t *testing.T) {
284+ h := &handlerSet{wv: &WebView{ctx: context.Background()}}
285+ h.wv.destroyed.Store(true)
286+ var results []bool
287+ req := newReq(&results)
288+ handlerCalls = 0
289+ h.deliverPermissionRequest(req, cb, "https://a.example", []string{"camera"})
290+ assert.Zero(t, handlerCalls)
291+ assert.Equal(t, []bool{false}, results)
292+ })
293+}
208 294
209- denyCb := &stubPermissionPromptCallback{}295+func TestPermissionTrackerAddDuplicateKeyReturnsPrevious(t *testing.T) {
210- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 8, "https://maps.example", geo, denyCb))296+ var tracker permissionTracker
211- capture.deny()297+ var results []string
212- capture.allow()298+ mk := func(name string) *cefPermissionRequest {
213- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultDeny}, denyCb.results)299+ return &cefPermissionRequest{finish: func(bool) { results = append(results, name) }}
300+ }
301+ key := permissionKey{prompt: true, id: 1}
302+ first, second := mk("first"), mk("second")
303+
304+ assert.Nil(t, tracker.add(key, first))
305+ prev := tracker.add(key, second)
306+ require.Same(t, first, prev)
307+ prev.resolve(false)
308+
309+ assert.Equal(t, []string{"first"}, results)
310+ assert.Same(t, second, tracker.get(key), "resolving the replaced request must not untrack the new one")
311+}
312+
313+func TestOnShowPermissionPromptDuplicateIDDeniesPrevious(t *testing.T) {
314+ h, _, browser := newPermissionTestHandler(t, true)
315+ first, second := &stubPermissionPromptCallback{}, &stubPermissionPromptCallback{}
316+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 4, "https://meet.example.com", promptGeo, first))
317+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 4, "https://meet.example.com", promptGeo, second))
318+ assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, first.results)
319+ assert.Empty(t, second.results)
320+}
321+
322+func TestOnShowPermissionPromptUnsupportedReturnsZero(t *testing.T) {
323+ for name, mask := range map[string]uint32{
324+ "empty": 0,
325+ "clipboard": promptClip,
326+ "ptz": promptPTZ,
327+ "mixed": promptGeo | promptClip,
328+ "unknown bit": 1 << 30,
329+ } {
330+ t.Run(name, func(t *testing.T) {
331+ h, capture, browser := newPermissionTestHandler(t, true)
332+ cb := &stubPermissionPromptCallback{}
333+ assert.Equal(t, int32(0), h.OnShowPermissionPrompt(browser, 1, "https://meet.example.com", mask, cb))
334+ assert.Empty(t, cb.results)
335+ assert.Zero(t, capture.calls)
336+ assert.Empty(t, h.permissions.pending)
337+ })
338+ }
214 }339 }
215 340
216-func TestOnShowPermissionPromptDeniesUnsupportedType(t *testing.T) {341+func TestOnShowPermissionPromptDeniesCrossOrigin(t *testing.T) {
217- const clip = uint32(purecef.PermissionRequestTypesPermissionTypeClipboard)342+ h, capture, browser := newPermissionTestHandler(t, true)
218- h, capture := newPermissionTestHandler(true)
219 cb := &stubPermissionPromptCallback{}343 cb := &stubPermissionPromptCallback{}
220- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 1, "https://a.example", clip, cb))344+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 1, "https://evil.example.org", promptNotif, cb))
221- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultDeny}, cb.results)345+ assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, cb.results)
222 assert.Zero(t, capture.calls)346 assert.Zero(t, capture.calls)
223 }347 }
224 348
225 func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) {349 func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) {
226- const notif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications)350+ h, capture, browser := newPermissionTestHandler(t, true)
227- h, capture := newPermissionTestHandler(true)
228 cb := &stubPermissionPromptCallback{}351 cb := &stubPermissionPromptCallback{}
229- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 3, "https://a.example", notif, cb))352+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 3, "https://meet.example.com", promptNotif, cb))
230 353
231 h.OnDismissPermissionPrompt(nil, 3, purecef.PermissionRequestResultPermissionResultDismiss)354 h.OnDismissPermissionPrompt(nil, 3, purecef.PermissionRequestResultPermissionResultDismiss)
232 capture.allow()355 capture.allow()
@@ -241,13 +364,12 @@ func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) {
241 }364 }
242 365
243 func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) {366 func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) {
244- const geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation)367+ h, capture, browser := newPermissionTestHandler(t, true)
245- h, capture := newPermissionTestHandler(true)
246 media := &stubMediaAccessCallback{}368 media := &stubMediaAccessCallback{}
247 prompt := &stubPermissionPromptCallback{}369 prompt := &stubPermissionPromptCallback{}
248- require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(nil, nil, "https://a.example", mediaMic, media))370+ require.Equal(t, int32(1), h.OnRequestMediaAccessPermission(browser, nil, "https://meet.example.com", mediaMic, media))
249 mediaAllow := capture.allow371 mediaAllow := capture.allow
250- require.Equal(t, int32(1), h.OnShowPermissionPrompt(nil, 5, "https://a.example", geo, prompt))372+ require.Equal(t, int32(1), h.OnShowPermissionPrompt(browser, 5, "https://meet.example.com", promptGeo, prompt))
251 promptAllow := capture.allow373 promptAllow := capture.allow
252 374
253 h.denyPendingPermissions()375 h.denyPendingPermissions()
@@ -257,7 +379,7 @@ func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) {
257 379
258 assert.Equal(t, 1, media.cancelCalls)380 assert.Equal(t, 1, media.cancelCalls)
259 assert.Empty(t, media.contCalls)381 assert.Empty(t, media.contCalls)
260- assert.Equal(t, []purecef.PermissionRequestResult{purecef.PermissionRequestResultPermissionResultDeny}, prompt.results)382+ assert.Equal(t, []purecef.PermissionRequestResult{promptDeny}, prompt.results)
261 assert.Empty(t, h.permissions.pending)383 assert.Empty(t, h.permissions.pending)
262 }384 }
263 385
@@ -291,8 +413,3 @@ func TestDispatchPermissionResultPostsToUIThread(t *testing.T) {
291 posted.Execute()413 posted.Execute()
292 assert.Equal(t, 1, ran)414 assert.Equal(t, 1, ran)
293 }415 }
294-
295-func TestGetPermissionHandlerEnabled(t *testing.T) {
296- h := &handlerSet{wv: &WebView{ctx: context.Background()}}
297- assert.NotNil(t, h.GetPermissionHandler())
298-}

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