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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 8 | 8 | purecef "github.com/bnema/purego-cef/cef" | |
| 9 | 9 | "github.com/rs/zerolog" | |
| 10 | 10 | ||
| 11 | + | "github.com/bnema/dumber/internal/application/port" | |
| 11 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | 13 | urlutil "github.com/bnema/dumber/internal/domain/url" | |
| 13 | 14 | "github.com/bnema/dumber/internal/logging" | |
| @@ -21,46 +22,50 @@ const supportedMediaAccessMask = uint32( | |||
| 21 | 22 | purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture) | |
| 22 | 23 | ||
| 23 | 24 | // 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 | + | ) | |
| 28 | 35 | 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 | |
| 30 | 40 | } | |
| 31 | - | allowedMask = requested | |
| 32 | - | if allowedMask&uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceAudioCapture) != 0 { | |
| 41 | + | var types []string | |
| 42 | + | if requested&deviceAudio != 0 { | |
| 33 | 43 | types = append(types, string(entity.PermissionTypeMicrophone)) | |
| 34 | 44 | } | |
| 35 | - | if allowedMask&uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture) != 0 { | |
| 45 | + | if requested&deviceVideo != 0 { | |
| 36 | 46 | types = append(types, string(entity.PermissionTypeCamera)) | |
| 37 | 47 | } | |
| 38 | - | const desktop = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture | | |
| 39 | - | purecef.MediaAccessPermissionTypesMediaPermissionDesktopVideoCapture) | |
| 40 | - | if allowedMask&desktop != 0 { | |
| 48 | + | if requested&desktopVideo != 0 { | |
| 41 | 49 | types = append(types, string(entity.PermissionTypeDisplay)) | |
| 42 | 50 | } | |
| 43 | - | return types, allowedMask | |
| 51 | + | return types | |
| 44 | 52 | } | |
| 45 | 53 | ||
| 46 | 54 | // 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 { | |
| 53 | 58 | const ( | |
| 54 | - | camera = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream | | |
| 55 | - | purecef.PermissionRequestTypesPermissionTypeCameraPanTiltZoom) | |
| 59 | + | camera = uint32(purecef.PermissionRequestTypesPermissionTypeCameraStream) | |
| 56 | 60 | mic = uint32(purecef.PermissionRequestTypesPermissionTypeMicStream) | |
| 57 | 61 | geolocation = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation) | |
| 58 | 62 | notifications = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications) | |
| 59 | 63 | supported = camera | mic | geolocation | notifications | |
| 60 | 64 | ) | |
| 61 | - | if requested&^supported != 0 { | |
| 62 | - | return nil, false | |
| 65 | + | if requested == 0 || requested&^supported != 0 { | |
| 66 | + | return nil | |
| 63 | 67 | } | |
| 68 | + | var types []string | |
| 64 | 69 | if requested&mic != 0 { | |
| 65 | 70 | types = append(types, string(entity.PermissionTypeMicrophone)) | |
| 66 | 71 | } | |
| @@ -73,7 +78,7 @@ func promptPermissionTypes(requested uint32) (types []string, ok bool) { | |||
| 73 | 78 | if requested¬ifications != 0 { | |
| 74 | 79 | types = append(types, string(entity.PermissionTypeNotification)) | |
| 75 | 80 | } | |
| 76 | - | return types, true | |
| 81 | + | return types | |
| 77 | 82 | } | |
| 78 | 83 | ||
| 79 | 84 | // cefPermissionRequest guarantees that a CEF permission callback is resolved | |
| @@ -169,7 +174,7 @@ func (t *permissionTracker) drain() []*cefPermissionRequest { | |||
| 169 | 174 | ||
| 170 | 175 | // OnRequestMediaAccessPermission handles getUserMedia/getDisplayMedia requests. | |
| 171 | 176 | func (h *handlerSet) OnRequestMediaAccessPermission( | |
| 172 | - | _ purecef.Browser, | |
| 177 | + | browser purecef.Browser, | |
| 173 | 178 | _ purecef.Frame, | |
| 174 | 179 | requestingOrigin string, | |
| 175 | 180 | requestedPermissions uint32, | |
| @@ -178,21 +183,24 @@ func (h *handlerSet) OnRequestMediaAccessPermission( | |||
| 178 | 183 | if h == nil || h.wv == nil || callback == nil { | |
| 179 | 184 | return 0 | |
| 180 | 185 | } | |
| 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 | + | }) | |
| 189 | 196 | return 1 | |
| 190 | 197 | } | |
| 191 | 198 | ||
| 192 | 199 | // 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 | 202 | func (h *handlerSet) OnShowPermissionPrompt( | |
| 195 | - | _ purecef.Browser, | |
| 203 | + | browser purecef.Browser, | |
| 196 | 204 | promptID uint64, | |
| 197 | 205 | requestingOrigin string, | |
| 198 | 206 | requestedPermissions uint32, | |
| @@ -201,8 +209,11 @@ func (h *handlerSet) OnShowPermissionPrompt( | |||
| 201 | 209 | if h == nil || h.wv == nil || callback == nil { | |
| 202 | 210 | return 0 | |
| 203 | 211 | } | |
| 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) { | |
| 206 | 217 | if allow { | |
| 207 | 218 | callback.Cont(purecef.PermissionRequestResultPermissionResultAccept) | |
| 208 | 219 | return | |
| @@ -214,6 +225,10 @@ func (h *handlerSet) OnShowPermissionPrompt( | |||
| 214 | 225 | ||
| 215 | 226 | // OnDismissPermissionPrompt is called when CEF dismisses a prompt we handled. | |
| 216 | 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 | 232 | func (h *handlerSet) OnDismissPermissionPrompt( | |
| 218 | 233 | _ purecef.Browser, | |
| 219 | 234 | promptID uint64, | |
| @@ -246,10 +261,12 @@ func (h *handlerSet) denyPendingPermissions() { | |||
| 246 | 261 | // beginPermissionRequest registers a request, forwards it to the WebView's | |
| 247 | 262 | // OnPermissionRequest callback on the GTK thread, and guarantees finish is | |
| 248 | 263 | // 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. | |
| 251 | 267 | func (h *handlerSet) beginPermissionRequest( | |
| 252 | 268 | key permissionKey, | |
| 269 | + | browser purecef.Browser, | |
| 253 | 270 | requestingOrigin string, | |
| 254 | 271 | types []string, | |
| 255 | 272 | finish func(allow bool), | |
| @@ -270,9 +287,9 @@ func (h *handlerSet) beginPermissionRequest( | |||
| 270 | 287 | req.resolve(false) | |
| 271 | 288 | return | |
| 272 | 289 | } | |
| 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") | |
| 276 | 293 | req.resolve(false) | |
| 277 | 294 | return | |
| 278 | 295 | } | |
| @@ -286,16 +303,51 @@ func (h *handlerSet) beginPermissionRequest( | |||
| 286 | 303 | 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 | 325 | allow := func() { req.resolve(true) } | |
| 290 | 326 | 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 | |
| 299 | 351 | } | |
| 300 | 352 | ||
| 301 | 353 | // 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 permission | 24 | // mediaPermissionTypes maps a CEF media access bitmask to Dumber permission | ||
| 24 | - | // types. The returned allowedMask contains only the requested bits that Dumber | 25 | + | // 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, 0 | 36 | + | return nil |
| 37 | + | } | |||
| 38 | + | if requested&desktopAudio != 0 && requested&desktopVideo == 0 { | |||
| 39 | + | return nil | |||
| 30 | } | 40 | } | ||
| 31 | - | allowedMask = requested | 41 | + | 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, allowedMask | 51 | + | return types |
| 44 | } | 52 | } | ||
| 45 | 53 | ||||
| 46 | // promptPermissionTypes maps a CEF permission prompt bitmask to Dumber | 54 | // 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 | 55 | + | // 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 | notifications | 63 | supported = camera | mic | geolocation | notifications | ||
| 60 | ) | 64 | ) | ||
| 61 | - | if requested&^supported != 0 { | 65 | + | if requested == 0 || requested&^supported != 0 { |
| 62 | - | return nil, false | 66 | + | 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¬ifications != 0 { | 78 | if requested¬ifications != 0 { | ||
| 74 | types = append(types, string(entity.PermissionTypeNotification)) | 79 | types = append(types, string(entity.PermissionTypeNotification)) | ||
| 75 | } | 80 | } | ||
| 76 | - | return types, true | 81 | + | return types |
| 77 | } | 82 | } | ||
| 78 | 83 | ||||
| 79 | // cefPermissionRequest guarantees that a CEF permission callback is resolved | 84 | // 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 0 | 184 | 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 | - | return | 190 | + | if allow { |
| 186 | - | } | 191 | + | callback.Cont(requestedPermissions) |
| 187 | - | callback.Cancel() | 192 | + | return |
| 188 | - | }) | 193 | + | } |
| 194 | + | callback.Cancel() | |||
| 195 | + | }) | |||
| 189 | return 1 | 196 | return 1 | ||
| 190 | } | 197 | } | ||
| 191 | 198 | ||||
| 192 | // OnShowPermissionPrompt handles non-media permission requests | 199 | // 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 0 | 210 | 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 | return | 219 | 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's | 261 | // beginPermissionRequest registers a request, forwards it to the WebView's | ||
| 247 | // OnPermissionRequest callback on the GTK thread, and guarantees finish is | 262 | // OnPermissionRequest callback on the GTK thread, and guarantees finish is | ||
| 248 | // invoked exactly once on the CEF UI thread. It denies when the request cannot | 263 | // 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 | 264 | + | // 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 | return | 288 | 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 | return | 294 | return | ||
| 278 | } | 295 | } | ||
| @@ -286,16 +303,51 @@ func (h *handlerSet) beginPermissionRequest( | |||||
| 286 | return | 303 | 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 | - | return | 329 | + | } |
| 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 permission | 353 | // 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 ( | |||
| 5 | 5 | "testing" | |
| 6 | 6 | ||
| 7 | 7 | purecef "github.com/bnema/purego-cef/cef" | |
| 8 | + | cefmocks "github.com/bnema/purego-cef/cef/mocks" | |
| 8 | 9 | "github.com/stretchr/testify/assert" | |
| 9 | 10 | "github.com/stretchr/testify/require" | |
| 10 | 11 | ||
| @@ -35,7 +36,12 @@ type permissionCapture struct { | |||
| 35 | 36 | 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 | 45 | capture := &permissionCapture{} | |
| 40 | 46 | wv := &WebView{ | |
| 41 | 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}, 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 | |
| 51 | 61 | } | |
| 52 | 62 | ||
| 53 | 63 | const ( | |
| @@ -55,6 +65,18 @@ const ( | |||
| 55 | 65 | mediaCamera = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDeviceVideoCapture) | |
| 56 | 66 | mediaDeskA = uint32(purecef.MediaAccessPermissionTypesMediaPermissionDesktopAudioCapture) | |
| 57 | 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 | 82 | func TestMediaPermissionTypes(t *testing.T) { | |
| @@ -62,63 +84,51 @@ func TestMediaPermissionTypes(t *testing.T) { | |||
| 62 | 84 | name string | |
| 63 | 85 | requested uint32 | |
| 64 | 86 | types []string | |
| 65 | - | allowed uint32 | |
| 66 | 87 | }{ | |
| 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}, | |
| 75 | 98 | } | |
| 76 | 99 | for _, tt := range tests { | |
| 77 | 100 | 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)) | |
| 81 | 102 | }) | |
| 82 | 103 | } | |
| 83 | 104 | } | |
| 84 | 105 | ||
| 85 | 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 | 107 | tests := []struct { | |
| 95 | 108 | name string | |
| 96 | 109 | requested uint32 | |
| 97 | 110 | types []string | |
| 98 | - | ok bool | |
| 99 | 111 | }{ | |
| 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}, | |
| 107 | 119 | } | |
| 108 | 120 | for _, tt := range tests { | |
| 109 | 121 | 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)) | |
| 113 | 123 | }) | |
| 114 | 124 | } | |
| 115 | 125 | } | |
| 116 | 126 | ||
| 117 | 127 | func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) { | |
| 118 | - | h, capture := newPermissionTestHandler(true) | |
| 128 | + | h, capture, browser := newPermissionTestHandler(t, true) | |
| 119 | 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 | 133 | require.Equal(t, int32(1), handled) | |
| 124 | 134 | require.Equal(t, "https://meet.example.com", capture.origin) | |
| @@ -126,46 +136,98 @@ func TestOnRequestMediaAccessPermissionAllowGrantsRequestedMask(t *testing.T) { | |||
| 126 | 136 | require.Empty(t, cb.contCalls) | |
| 127 | 137 | ||
| 128 | 138 | capture.allow() | |
| 129 | - | capture.allow() | |
| 130 | - | capture.deny() | |
| 131 | - | ||
| 132 | 139 | assert.Equal(t, []uint32{mediaMic | mediaCamera}, cb.contCalls) | |
| 133 | 140 | assert.Zero(t, cb.cancelCalls) | |
| 134 | 141 | assert.Empty(t, h.permissions.pending) | |
| 135 | 142 | } | |
| 136 | 143 | ||
| 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 | + | } | |
| 145 | 178 | ||
| 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 | + | } | |
| 148 | 204 | } | |
| 149 | 205 | ||
| 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) | |
| 153 | 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 | 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) { | |
| 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) | |
| 160 | 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 | 220 | assert.Equal(t, 1, cb.cancelCalls) | |
| 163 | 221 | assert.Zero(t, capture.calls) | |
| 164 | 222 | }) | |
| 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") | |
| 167 | 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 | 231 | assert.Equal(t, 1, cb.cancelCalls) | |
| 170 | 232 | assert.Zero(t, capture.calls) | |
| 171 | 233 | }) | |
| @@ -177,9 +239,10 @@ func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) { | |||
| 177 | 239 | "nil OnPermissionRequest fn": {}, | |
| 178 | 240 | } { | |
| 179 | 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 | 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 | 246 | assert.Equal(t, 1, cb.cancelCalls) | |
| 184 | 247 | assert.Empty(t, cb.contCalls) | |
| 185 | 248 | }) | |
| @@ -187,46 +250,106 @@ func TestOnRequestMediaAccessPermissionDeniesWithoutCallback(t *testing.T) { | |||
| 187 | 250 | } | |
| 188 | 251 | ||
| 189 | 252 | func TestOnRequestMediaAccessPermissionDeniesWhenDestroyed(t *testing.T) { | |
| 190 | - | h, capture := newPermissionTestHandler(true) | |
| 253 | + | h, capture, browser := newPermissionTestHandler(t, true) | |
| 191 | 254 | h.wv.destroyed.Store(true) | |
| 192 | 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 | 257 | assert.Equal(t, 1, cb.cancelCalls) | |
| 195 | 258 | assert.Zero(t, capture.calls) | |
| 196 | 259 | } | |
| 197 | 260 | ||
| 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 | + | } | |
| 200 | 272 | ||
| 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 | + | } | |
| 208 | 294 | ||
| 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 | + | } | |
| 214 | 339 | } | |
| 215 | 340 | ||
| 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) | |
| 219 | 343 | 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) | |
| 222 | 346 | assert.Zero(t, capture.calls) | |
| 223 | 347 | } | |
| 224 | 348 | ||
| 225 | 349 | func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) { | |
| 226 | - | const notif = uint32(purecef.PermissionRequestTypesPermissionTypeNotifications) | |
| 227 | - | h, capture := newPermissionTestHandler(true) | |
| 350 | + | h, capture, browser := newPermissionTestHandler(t, true) | |
| 228 | 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 | 354 | h.OnDismissPermissionPrompt(nil, 3, purecef.PermissionRequestResultPermissionResultDismiss) | |
| 232 | 355 | capture.allow() | |
| @@ -241,13 +364,12 @@ func TestOnDismissPermissionPromptDropsPendingWithoutCallingCEF(t *testing.T) { | |||
| 241 | 364 | } | |
| 242 | 365 | ||
| 243 | 366 | func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) { | |
| 244 | - | const geo = uint32(purecef.PermissionRequestTypesPermissionTypeGeolocation) | |
| 245 | - | h, capture := newPermissionTestHandler(true) | |
| 367 | + | h, capture, browser := newPermissionTestHandler(t, true) | |
| 246 | 368 | media := &stubMediaAccessCallback{} | |
| 247 | 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 | 371 | 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 | 373 | promptAllow := capture.allow | |
| 252 | 374 | ||
| 253 | 375 | h.denyPendingPermissions() | |
| @@ -257,7 +379,7 @@ func TestDenyPendingPermissionsResolvesAllOnce(t *testing.T) { | |||
| 257 | 379 | ||
| 258 | 380 | assert.Equal(t, 1, media.cancelCalls) | |
| 259 | 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 | 383 | assert.Empty(t, h.permissions.pending) | |
| 262 | 384 | } | |
| 263 | 385 | ||
| @@ -291,8 +413,3 @@ func TestDispatchPermissionResultPostsToUIThread(t *testing.T) { | |||
| 291 | 413 | posted.Execute() | |
| 292 | 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 | - | } | |
| @@ -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 int | 36 | 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}, 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 | |||
| 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 string | 84 | name string | ||
| 63 | requested uint32 | 85 | requested uint32 | ||
| 64 | types []string | 86 | 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 string | 108 | name string | ||
| 96 | requested uint32 | 109 | requested uint32 | ||
| 97 | types []string | 110 | 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.allow | 371 | 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.allow | 373 | 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 | - | } | |||