@xenon-device-management/xenon 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/lib/package.json +3 -4
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-XpewX5nn.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-BQDv3ZCC.js} +1 -1
- package/lib/public/assets/BugReportButton-C7OmS61B.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-wl3l2Zlt.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-fPxujqqE.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-5bCutWrs.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-owsBiDT1.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-DFhqzPSt.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-Bw-a-E-e.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-WtByf5bC.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-FpWWFNgT.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-Jzn9c7o8.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-BR7nOhpT.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-B_Va3V05.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-DxRFWvkN.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-CQf-9_V6.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-SoE1Kd7N.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-CgGXTlgn.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-uQ6WIwiC.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-C9RyfzXS.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-DFHi_8yG.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-Bii1t4qc.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-Bnv8Z9hi.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-_Y78wXiP.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-D4MqSOc2.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-uGHCXe-L.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-Dy1OqpzB.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-BUsldEli.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-D3Qq0tKp.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-B4IOD1DE.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-BfYfzvSg.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-DZwYE5Va.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CRG4Utt5.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-CCDYo_TN.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-DuLCmVFb.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-CtKNPqzb.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-3uxl3o7K.js} +43 -43
- package/lib/public/assets/{index-BvH_ekRy.js → index-Zy1Op5C1.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-CzTWvUXZ.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CI-nYDFM.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-YZZGFyTX.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-L8tzWgDo.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-DBbX-fu8.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-AJwkOIsE.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-Di_66F2m.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-Cnab2zdg.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-DiSumY-1.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-CwE4gXl5.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CfR4Sm9V.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-DOS8P9CQ.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-BYpHwQIH.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BRVJU_qV.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Bi3JvAFZ.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-Cqx7pEou.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C0DsncKj.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CAhpdilF.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CrCVeW2m.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-F_hwYqEt.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-C-s5pypL.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-DY5uzv6A.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-Dli7gwyg.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-ui4IhHNC.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV4hSmXJ.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-DkGy_J-L.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-BFtMqQOh.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BU0nXIVe.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-Nb_rzRmF.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-CUNzD2kH.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-Ca3UFXZe.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-B2bH2vKX.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/InternalHttpClient.js +5 -1
- package/lib/src/XenonCapabilityManager.js +15 -1
- package/lib/src/app/appiumBasePath.js +32 -0
- package/lib/src/app/index.js +12 -0
- package/lib/src/app/insertBeforeRoutes.js +24 -4
- package/lib/src/app/registerCommandAuth.js +152 -33
- package/lib/src/app/routers/control.js +11 -104
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/routers/nodePhoneControl.js +301 -0
- package/lib/src/app/ws/h264StreamWs.js +43 -12
- package/lib/src/app/ws/logcatWs.js +48 -14
- package/lib/src/app/ws/nodeSocketRelay.js +167 -0
- package/lib/src/app/ws/upgradeRouter.js +215 -0
- package/lib/src/dashboard/event-manager.js +7 -6
- package/lib/src/data-service/device-service.js +152 -49
- package/lib/src/data-service/device-store.js +92 -10
- package/lib/src/data-service/deviceClaims.js +116 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +102 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +40 -42
- package/lib/src/device-managers/HealthMonitorService.js +17 -2
- package/lib/src/device-managers/IOSDeviceManager.js +5 -1
- package/lib/src/device-managers/NodeDevices.js +7 -2
- package/lib/src/device-managers/android/AndroidH264StreamService.js +13 -5
- package/lib/src/device-managers/android/AndroidStreamService.js +9 -6
- package/lib/src/device-managers/android/previewHold.js +2 -2
- package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
- package/lib/src/device-managers/ios/IOSStreamService.js +162 -40
- package/lib/src/device-managers/localDeviceHosts.js +86 -0
- package/lib/src/device-managers/ownDeviceRow.js +38 -0
- package/lib/src/device-utils.js +78 -24
- package/lib/src/gateway/createHandoff.js +73 -0
- package/lib/src/gateway/defaultGateway.js +80 -0
- package/lib/src/gateway/forwardToNode.js +147 -0
- package/lib/src/gateway/hubSessionToken.js +356 -0
- package/lib/src/gateway/internalCall.js +90 -0
- package/lib/src/gateway/nodeSessionStatus.js +140 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
- package/lib/src/gateway/sessionCreate.js +136 -0
- package/lib/src/gateway/sessionGateway.js +201 -0
- package/lib/src/gateway/sessionLocator.js +100 -0
- package/lib/src/generated/client/edge.js +7 -4
- package/lib/src/generated/client/index-browser.js +4 -1
- package/lib/src/generated/client/index.d.ts +112 -1
- package/lib/src/generated/client/index.js +7 -4
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +9 -0
- package/lib/src/generated/client/wasm.js +4 -1
- package/lib/src/helpers/index.js +5 -1
- package/lib/src/logger.js +5 -0
- package/lib/src/middleware/authMiddleware.js +80 -0
- package/lib/src/middleware/commandAuth.js +31 -17
- package/lib/src/middleware/csrfMiddleware.js +3 -2
- package/lib/src/middleware/deviceAccessGuard.js +4 -1
- package/lib/src/middleware/sessionListingFilter.js +153 -0
- package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
- package/lib/src/plugin.js +27 -1
- package/lib/src/prisma.js +2 -1
- package/lib/src/services/DeviceReconciler.js +5 -2
- package/lib/src/services/OrphanSweeper.js +12 -19
- package/lib/src/services/PortAllocator.js +75 -16
- package/lib/src/services/ServerManager.js +63 -30
- package/lib/src/services/SessionLifecycleService.js +335 -76
- package/lib/src/services/ShutdownCoordinator.js +7 -1
- package/lib/src/services/SocketServer.js +11 -2
- package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
- package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
- package/lib/src/services/device-access/ticketActorAccess.js +3 -1
- package/lib/src/services/lease/LeaseOrphanSweeper.js +34 -6
- package/lib/src/services/lease/LeaseService.js +19 -5
- package/lib/src/services/session/nodeCreateCaps.js +55 -0
- package/lib/src/services/session/sessionCredentials.js +7 -59
- package/lib/src/sessions/CloudSession.js +12 -0
- package/lib/src/sessions/LocalSession.js +52 -26
- package/lib/src/sessions/RemoteSession.js +116 -9
- package/lib/src/sessions/SessionManager.js +49 -6
- package/lib/src/sessions/appiumUmbrella.js +32 -0
- package/lib/test/helpers/appium-umbrella.js +84 -0
- package/lib/test/helpers/fake-device-table.js +97 -0
- package/lib/test/helpers/loki-stores.js +41 -0
- package/lib/test/helpers/loopbackRequest.js +77 -0
- package/lib/test/helpers/own-node-id.js +29 -0
- package/lib/test/helpers/scratch-database.js +100 -0
- package/lib/test/helpers/scratch-port-leases.js +119 -0
- package/lib/test/helpers/test-container.js +35 -27
- package/lib/test/integration/androidDevices.spec.js +5 -4
- package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
- package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
- package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
- package/lib/test/setup/scratch-default-database.js +32 -0
- package/lib/test/unit/AndroidDeviceManager.spec.js +56 -1
- package/lib/test/unit/DeviceModel.spec.js +9 -9
- package/lib/test/unit/HealingCascade.test.js +5 -1
- package/lib/test/unit/OrphanSweeper.test.js +17 -13
- package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
- package/lib/test/unit/StreamTicketService.spec.js +4 -1
- package/lib/test/unit/TokenEndpoint.spec.js +17 -3
- package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
- package/lib/test/unit/android-h264-service.spec.js +24 -0
- package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
- package/lib/test/unit/apps-team-visibility.spec.js +16 -16
- package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
- package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
- package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
- package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
- package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
- package/lib/test/unit/authMiddleware.test.js +3 -0
- package/lib/test/unit/bug-report/route.spec.js +9 -9
- package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
- package/lib/test/unit/clipboard-route.spec.js +22 -8
- package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
- package/lib/test/unit/command-auth.spec.js +31 -31
- package/lib/test/unit/control-device-lookup.spec.js +9 -9
- package/lib/test/unit/control-forward-decision.spec.js +115 -0
- package/lib/test/unit/control-lookup-failure.spec.js +3 -3
- package/lib/test/unit/create-error-message.spec.js +43 -0
- package/lib/test/unit/create-handoff.spec.js +86 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/device-access-guard.spec.js +66 -28
- package/lib/test/unit/device-service.spec.js +4 -0
- package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
- package/lib/test/unit/device-team-guard.spec.js +20 -20
- package/lib/test/unit/device-utils.spec.js +8 -0
- package/lib/test/unit/extract-team-cap.spec.js +65 -0
- package/lib/test/unit/grid-admin-only.spec.js +3 -3
- package/lib/test/unit/healing-tier-gate.spec.js +5 -1
- package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
- package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
- package/lib/test/unit/helpers.spec.js +27 -0
- package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
- package/lib/test/unit/hub-create-once.spec.js +190 -0
- package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
- package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
- package/lib/test/unit/hub-node-control-forward.spec.js +608 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +426 -0
- package/lib/test/unit/hub-node-same-host.spec.js +356 -0
- package/lib/test/unit/hub-register-removal.spec.js +165 -0
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
- package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
- package/lib/test/unit/hub-session-token.spec.js +265 -0
- package/lib/test/unit/insert-before-routes.spec.js +5 -5
- package/lib/test/unit/install-repository-app-team.spec.js +6 -2
- package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
- package/lib/test/unit/internal-call.spec.js +181 -0
- package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
- package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
- package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
- package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
- package/lib/test/unit/is-xenon-running.spec.js +69 -0
- package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
- package/lib/test/unit/lease/LeaseService.spec.js +28 -1
- package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
- package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +107 -0
- package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
- package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
- package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
- package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
- package/lib/test/unit/logcat-ws.spec.js +12 -1
- package/lib/test/unit/loopback-request.spec.js +81 -0
- package/lib/test/unit/mark-node-busy-loki.spec.js +53 -0
- package/lib/test/unit/node-create-caps.spec.js +54 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
- package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
- package/lib/test/unit/node-phone-control.spec.js +131 -0
- package/lib/test/unit/node-socket-relay.spec.js +357 -0
- package/lib/test/unit/omni-vision.spec.js +7 -3
- package/lib/test/unit/plugin.spec.js +45 -4
- package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
- package/lib/test/unit/profile-router.test.js +11 -11
- package/lib/test/unit/queue-requester.spec.js +5 -0
- package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
- package/lib/test/unit/queue-visibility.spec.js +1 -1
- package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
- package/lib/test/unit/recording-instrumentation.spec.js +5 -0
- package/lib/test/unit/recording-source-ended.spec.js +5 -0
- package/lib/test/unit/recording-store.spec.js +9 -3
- package/lib/test/unit/recordings-library-routes.spec.js +51 -51
- package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
- package/lib/test/unit/remote-session-create.spec.js +130 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
- package/lib/test/unit/reset-link.test.js +14 -14
- package/lib/test/unit/session-app-resolution.spec.js +14 -10
- package/lib/test/unit/session-assets.spec.js +9 -9
- package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
- package/lib/test/unit/session-create-layer.spec.js +255 -0
- package/lib/test/unit/session-credential-strip.spec.js +25 -9
- package/lib/test/unit/session-credentials.spec.js +0 -47
- package/lib/test/unit/session-listing-auth.spec.js +345 -0
- package/lib/test/unit/session-locator.spec.js +250 -0
- package/lib/test/unit/session-node-calls.spec.js +174 -0
- package/lib/test/unit/session-owner-resolver.spec.js +107 -0
- package/lib/test/unit/session-recovery-routing.spec.js +118 -0
- package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
- package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
- package/lib/test/unit/socketServer.bearer.spec.js +3 -1
- package/lib/test/unit/stream-leave-route.spec.js +19 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +17 -5
- package/lib/test/unit/stream-stop-recording.spec.js +17 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +26 -6
- package/lib/test/unit/stream-watchdog-timers.spec.js +46 -0
- package/lib/test/unit/upgrade-router.spec.js +446 -0
- package/lib/test/unit/users-router.test.js +11 -8
- package/lib/test/unit/verify-credential.spec.js +4 -1
- package/lib/test/unit/webdriver-info-route.spec.js +64 -0
- package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
- package/prisma/schema.prisma +9 -0
- package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
- package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
- package/lib/src/proxy/wd-command-proxy.js +0 -221
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const fs_1 = __importDefault(require("fs"));
|
|
51
|
+
const http_1 = __importDefault(require("http"));
|
|
52
|
+
const os_1 = __importDefault(require("os"));
|
|
53
|
+
const path_1 = __importDefault(require("path"));
|
|
54
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
55
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
56
|
+
const jose = __importStar(require("jose"));
|
|
57
|
+
const typedi_1 = require("typedi");
|
|
58
|
+
const commandCaller_1 = require("../../src/middleware/commandCaller");
|
|
59
|
+
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
60
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
61
|
+
const insertBeforeRoutes_1 = require("../../src/app/insertBeforeRoutes");
|
|
62
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
63
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
64
|
+
const SessionOwnerResolver_1 = require("../../src/services/device-access/SessionOwnerResolver");
|
|
65
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
66
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
67
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
68
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
69
|
+
const event_manager_1 = require("../../src/dashboard/event-manager");
|
|
70
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
71
|
+
const internalCall_1 = require("../../src/gateway/internalCall");
|
|
72
|
+
const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
|
|
73
|
+
const defaultGateway_1 = require("../../src/gateway/defaultGateway");
|
|
74
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
75
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
76
|
+
/**
|
|
77
|
+
* A Xenon hub and a Xenon node, each in Appium 3's own server() (Express 5,
|
|
78
|
+
* Appium's middleware, its WebDriver routes, the plugin updaters, then its
|
|
79
|
+
* catch-all 404), with the session gateway installed by the production wiring
|
|
80
|
+
* (hubGatewayOptions / nodeGatewayOptions), over a scratch SQLite database.
|
|
81
|
+
*
|
|
82
|
+
* Each server's driver is a fake that records the commands that reach it,
|
|
83
|
+
* which is how these tests show where a command went: a remote session's
|
|
84
|
+
* command must reach the node's driver and never the hub's.
|
|
85
|
+
*/
|
|
86
|
+
function appiumDir() {
|
|
87
|
+
return path_1.default.dirname(require.resolve('appium/package.json'));
|
|
88
|
+
}
|
|
89
|
+
function appiumBaseDriver() {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
91
|
+
return require(require.resolve('@appium/base-driver', { paths: [appiumDir()] }));
|
|
92
|
+
}
|
|
93
|
+
const quiet = {
|
|
94
|
+
debug: () => undefined,
|
|
95
|
+
info: () => undefined,
|
|
96
|
+
warn: () => undefined,
|
|
97
|
+
error: () => undefined,
|
|
98
|
+
};
|
|
99
|
+
const EXACT_UNKNOWN_SESSION = JSON.stringify(commandAuth_1.UNKNOWN_SESSION_BODY);
|
|
100
|
+
// A route only a driver defines (UiAutomator2 does): the node has it, the hub
|
|
101
|
+
// has no driver and does not. The gateway must not need the hub to have it.
|
|
102
|
+
const DRIVER_ROUTES = {
|
|
103
|
+
'/session/:sessionId/appium/stop_recording_screen': {
|
|
104
|
+
POST: { command: 'stopRecordingScreen', payloadParams: { optional: ['options'] } },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
function fakeDriver(name) {
|
|
108
|
+
const { errors } = appiumBaseDriver();
|
|
109
|
+
const commands = [];
|
|
110
|
+
const sessions = new Set();
|
|
111
|
+
const driver = {
|
|
112
|
+
log: quiet,
|
|
113
|
+
protocol: 'W3C',
|
|
114
|
+
sessionExists: (id) => sessions.has(id),
|
|
115
|
+
proxyActive: () => false,
|
|
116
|
+
canProxy: () => false,
|
|
117
|
+
proxyRouteIsAvoided: () => false,
|
|
118
|
+
executeCommand: (command, ...args) => __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
commands.push(command);
|
|
120
|
+
if (command === 'getUrl')
|
|
121
|
+
return `https://${name}.example/`;
|
|
122
|
+
if (command === 'findElement') {
|
|
123
|
+
if (args[1] === 'missing')
|
|
124
|
+
throw new errors.NoSuchElementError();
|
|
125
|
+
return { 'element-6066-11e4-a52e-4f735466cecf': `${name}-el` };
|
|
126
|
+
}
|
|
127
|
+
if (command === 'stopRecordingScreen')
|
|
128
|
+
return 'node-video.mp4';
|
|
129
|
+
if (command === 'deleteSession') {
|
|
130
|
+
sessions.delete(args[0]);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
return { commands, sessions, driver };
|
|
137
|
+
}
|
|
138
|
+
describe('the session gateway between a hub and a node, in Appium 3’s own server()', function () {
|
|
139
|
+
this.timeout(60000);
|
|
140
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
141
|
+
const servers = [];
|
|
142
|
+
let keyDir;
|
|
143
|
+
let hubKeys;
|
|
144
|
+
let restore;
|
|
145
|
+
let savedStore;
|
|
146
|
+
let hub;
|
|
147
|
+
let node;
|
|
148
|
+
let hubUrl;
|
|
149
|
+
let nodeOrigin;
|
|
150
|
+
let nodeRequests;
|
|
151
|
+
let refusals;
|
|
152
|
+
let stamp = 0;
|
|
153
|
+
// Appium's HTTP logger writes every request to stdout; quiet it here only.
|
|
154
|
+
let appiumLog;
|
|
155
|
+
let appiumLogLevel;
|
|
156
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
appiumBaseDriver();
|
|
158
|
+
const support = require.resolve('@appium/support', { paths: [appiumDir()] });
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
160
|
+
const log = require(require.resolve('@appium/logger', { paths: [support] })).default;
|
|
161
|
+
appiumLog = log;
|
|
162
|
+
appiumLogLevel = log.level;
|
|
163
|
+
log.level = 'silent';
|
|
164
|
+
keyDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-gateway-keys-'));
|
|
165
|
+
hubKeys = new JwtKeyService_1.JwtKeyService();
|
|
166
|
+
yield hubKeys.init(keyDir);
|
|
167
|
+
}));
|
|
168
|
+
after(() => {
|
|
169
|
+
if (appiumLog && appiumLogLevel)
|
|
170
|
+
appiumLog.level = appiumLogLevel;
|
|
171
|
+
fs_1.default.rmSync(keyDir, { recursive: true, force: true });
|
|
172
|
+
});
|
|
173
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, nodeWebDriverUrl_1.NodeBasePathResolver, SessionOwnerResolver_1.SessionOwnerResolver);
|
|
175
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
176
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
177
|
+
typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
|
|
178
|
+
typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
|
|
179
|
+
savedStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
180
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
181
|
+
yield scratch.db.session.deleteMany({});
|
|
182
|
+
yield scratch.db.device.deleteMany({});
|
|
183
|
+
nodeRequests = [];
|
|
184
|
+
refusals = [];
|
|
185
|
+
stamp++;
|
|
186
|
+
}));
|
|
187
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
188
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
189
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
190
|
+
device_store_1.DeviceStoreFactory._deviceStore = savedStore;
|
|
191
|
+
sinon_1.default.restore();
|
|
192
|
+
restore();
|
|
193
|
+
for (const s of servers.splice(0)) {
|
|
194
|
+
s.closeAllConnections();
|
|
195
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
196
|
+
}
|
|
197
|
+
}));
|
|
198
|
+
const port = (s) => s.address().port;
|
|
199
|
+
/** Credentials a client may present: alice's and bob's keys. */
|
|
200
|
+
function verifier() {
|
|
201
|
+
const users = { 'alice-token': 'alice', 'bob-token': 'bob' };
|
|
202
|
+
return new commandCaller_1.CommandCallerVerifier({
|
|
203
|
+
verifyKeyPair: (_ak, token) => __awaiter(this, void 0, void 0, function* () {
|
|
204
|
+
return users[token]
|
|
205
|
+
? {
|
|
206
|
+
row: {
|
|
207
|
+
id: `key-${users[token]}`,
|
|
208
|
+
userId: users[token],
|
|
209
|
+
scopes: 'sessions',
|
|
210
|
+
expiresAt: null,
|
|
211
|
+
},
|
|
212
|
+
user: { id: users[token], role: 'MEMBER', status: 'ACTIVE' },
|
|
213
|
+
}
|
|
214
|
+
: null;
|
|
215
|
+
}),
|
|
216
|
+
verifyBearer: () => __awaiter(this, void 0, void 0, function* () { return null; }),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const as = (who) => ({
|
|
220
|
+
'x-xenon-access-key': `ak-${who}`,
|
|
221
|
+
'x-xenon-token': `${who}-token`,
|
|
222
|
+
});
|
|
223
|
+
function boot() {
|
|
224
|
+
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
225
|
+
var _a;
|
|
226
|
+
const baseDriver = appiumBaseDriver();
|
|
227
|
+
const nodeBase = (_a = opts.nodeBase) !== null && _a !== void 0 ? _a : '/node';
|
|
228
|
+
const authLogger = {
|
|
229
|
+
info: () => undefined,
|
|
230
|
+
warn: (m) => refusals.push(m),
|
|
231
|
+
error: () => undefined,
|
|
232
|
+
};
|
|
233
|
+
hub = fakeDriver('hub');
|
|
234
|
+
const hubServer = yield baseDriver.server({
|
|
235
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(hub.driver),
|
|
236
|
+
port: 0,
|
|
237
|
+
hostname: '127.0.0.1',
|
|
238
|
+
basePath: '/wd/hub',
|
|
239
|
+
cliArgs: { basePath: '/wd/hub' },
|
|
240
|
+
serverUpdaters: [
|
|
241
|
+
(app) => {
|
|
242
|
+
app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
243
|
+
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/wd/hub' }, (0, registerCommandAuth_1.commandAuthDeps)({
|
|
244
|
+
enabled: () => !!opts.hubAuth,
|
|
245
|
+
authDisabled: () => false,
|
|
246
|
+
verifier: verifier(),
|
|
247
|
+
logger: authLogger,
|
|
248
|
+
}), (0, defaultGateway_1.hubGatewayOptions)({
|
|
249
|
+
basePath: '/wd/hub',
|
|
250
|
+
isLocalHost: (host) => host === hubUrl,
|
|
251
|
+
nodeId: () => 'hub-node-id',
|
|
252
|
+
dashboard: () => !!opts.dashboard,
|
|
253
|
+
}));
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
servers.push(hubServer);
|
|
258
|
+
hubUrl = `http://127.0.0.1:${port(hubServer)}`;
|
|
259
|
+
node = fakeDriver('node');
|
|
260
|
+
const nodeServer = yield baseDriver.server({
|
|
261
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(node.driver),
|
|
262
|
+
port: 0,
|
|
263
|
+
hostname: '127.0.0.1',
|
|
264
|
+
basePath: nodeBase,
|
|
265
|
+
extraMethodMap: DRIVER_ROUTES,
|
|
266
|
+
cliArgs: { basePath: nodeBase },
|
|
267
|
+
serverUpdaters: [
|
|
268
|
+
(app) => {
|
|
269
|
+
// What reaches the node, headers and all (placed first).
|
|
270
|
+
(0, insertBeforeRoutes_1.insertBeforeRoutes)(app, '/', (req, _res, next) => {
|
|
271
|
+
if (!req.url.startsWith('/xenon/'))
|
|
272
|
+
nodeRequests.push({ url: req.url, headers: Object.assign({}, req.headers) });
|
|
273
|
+
next();
|
|
274
|
+
});
|
|
275
|
+
if (opts.advertise !== false) {
|
|
276
|
+
app.get(nodeWebDriverUrl_1.WEBDRIVER_INFO_PATH, (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => nodeBase));
|
|
277
|
+
}
|
|
278
|
+
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: nodeBase }, (0, registerCommandAuth_1.commandAuthDeps)({
|
|
279
|
+
enabled: () => !!opts.nodeAuth,
|
|
280
|
+
authDisabled: () => false,
|
|
281
|
+
verifier: verifier(),
|
|
282
|
+
ownerOf: () => __awaiter(this, void 0, void 0, function* () { return null; }), // the node's own database knows no owner
|
|
283
|
+
logger: authLogger,
|
|
284
|
+
}), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
servers.push(nodeServer);
|
|
289
|
+
nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
|
|
290
|
+
return { hub: hubServer, node: nodeServer, nodeBase };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/** A session running on the node, as the hub records it. */
|
|
294
|
+
function remoteSession() {
|
|
295
|
+
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
296
|
+
var _a;
|
|
297
|
+
const id = `node-session-${stamp}`;
|
|
298
|
+
node.sessions.add(id);
|
|
299
|
+
yield scratch.db.device.create({
|
|
300
|
+
data: {
|
|
301
|
+
udid: `phone-${stamp}`,
|
|
302
|
+
host: nodeOrigin,
|
|
303
|
+
nodeId: 'node-1',
|
|
304
|
+
platform: 'android',
|
|
305
|
+
busy: true,
|
|
306
|
+
session_id: id,
|
|
307
|
+
sessionStartTime: Date.now(),
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
yield scratch.db.session.create({
|
|
311
|
+
data: {
|
|
312
|
+
id,
|
|
313
|
+
desired_capabilities: '{}',
|
|
314
|
+
session_capabilities: '{}',
|
|
315
|
+
node_id: 'node-1',
|
|
316
|
+
has_live_video: false,
|
|
317
|
+
device_udid: `phone-${stamp}`,
|
|
318
|
+
device_platform: 'android',
|
|
319
|
+
device_version: '14',
|
|
320
|
+
user_id: (_a = opts.owner) !== null && _a !== void 0 ? _a : 'alice',
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
let session;
|
|
324
|
+
if (opts.registered !== false) {
|
|
325
|
+
const resolver = typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver);
|
|
326
|
+
const base = yield resolver.resolve(nodeOrigin, '/wd/hub');
|
|
327
|
+
session = new RemoteSession_1.RemoteSession({
|
|
328
|
+
sessionId: id,
|
|
329
|
+
device: {
|
|
330
|
+
udid: `phone-${stamp}`,
|
|
331
|
+
host: nodeOrigin,
|
|
332
|
+
nodeId: 'node-1',
|
|
333
|
+
platform: 'android',
|
|
334
|
+
},
|
|
335
|
+
sessionResponse: {},
|
|
336
|
+
xenonOption: {},
|
|
337
|
+
baseUrl: `${nodeOrigin}${base}`,
|
|
338
|
+
});
|
|
339
|
+
SessionManager_1.SESSION_MANAGER.addSession(id, session);
|
|
340
|
+
}
|
|
341
|
+
return { id, session };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
describe('forwarding', () => {
|
|
345
|
+
it("a remote session's command reaches the node and its answer comes back; the hub's driver never sees it", () => __awaiter(this, void 0, void 0, function* () {
|
|
346
|
+
yield boot({ nodeAuth: true });
|
|
347
|
+
const { id } = yield remoteSession();
|
|
348
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).set(as('alice'));
|
|
349
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
350
|
+
(0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
|
|
351
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: 'https://node.example/' });
|
|
352
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['getUrl']);
|
|
353
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
354
|
+
// Under the node's own base path, with the hub's token and not the client's key.
|
|
355
|
+
(0, chai_1.expect)(nodeRequests.map((r) => r.url)).to.deep.equal([`/node/session/${id}/url`]);
|
|
356
|
+
const sent = nodeRequests[0].headers;
|
|
357
|
+
(0, chai_1.expect)(sent['x-xenon-access-key']).to.equal(undefined);
|
|
358
|
+
(0, chai_1.expect)(sent['x-xenon-token']).to.equal(undefined);
|
|
359
|
+
const claims = jose.decodeJwt(String(sent[hubSessionToken_1.HUB_TOKEN_HEADER]));
|
|
360
|
+
(0, chai_1.expect)(claims).to.include({ aud: hubSessionToken_1.HUB_TOKEN_AUDIENCE, sid: id });
|
|
361
|
+
}));
|
|
362
|
+
it("relays a body, the node's error status and its error body unchanged", () => __awaiter(this, void 0, void 0, function* () {
|
|
363
|
+
yield boot({ nodeAuth: true });
|
|
364
|
+
const { id } = yield remoteSession();
|
|
365
|
+
const found = yield (0, loopbackRequest_1.default)(hubUrl)
|
|
366
|
+
.post(`/wd/hub/session/${id}/element`)
|
|
367
|
+
.send({ using: 'id', value: 'login' });
|
|
368
|
+
(0, chai_1.expect)(found.status).to.equal(200);
|
|
369
|
+
(0, chai_1.expect)(found.body.value).to.deep.include({
|
|
370
|
+
'element-6066-11e4-a52e-4f735466cecf': 'node-el',
|
|
371
|
+
});
|
|
372
|
+
const missing = yield (0, loopbackRequest_1.default)(hubUrl)
|
|
373
|
+
.post(`/wd/hub/session/${id}/element`)
|
|
374
|
+
.send({ using: 'id', value: 'missing' });
|
|
375
|
+
(0, chai_1.expect)(missing.status).to.equal(404);
|
|
376
|
+
(0, chai_1.expect)(missing.body.value.error).to.equal('no such element');
|
|
377
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['findElement', 'findElement']);
|
|
378
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
379
|
+
}));
|
|
380
|
+
it('forwards a driver-only route the hub has no route for', () => __awaiter(this, void 0, void 0, function* () {
|
|
381
|
+
yield boot({ nodeAuth: true });
|
|
382
|
+
const { id } = yield remoteSession();
|
|
383
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl)
|
|
384
|
+
.post(`/wd/hub/session/${id}/appium/stop_recording_screen`)
|
|
385
|
+
.send({});
|
|
386
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
387
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: 'node-video.mp4' });
|
|
388
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['stopRecordingScreen']);
|
|
389
|
+
}));
|
|
390
|
+
it('matches the path as the routes do, in any letter case', () => __awaiter(this, void 0, void 0, function* () {
|
|
391
|
+
yield boot({ nodeAuth: true });
|
|
392
|
+
const { id } = yield remoteSession();
|
|
393
|
+
yield (0, loopbackRequest_1.default)(hubUrl).get(`/WD/HUB/SESSION/${id}/url`).expect(200);
|
|
394
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['getUrl']);
|
|
395
|
+
(0, chai_1.expect)(nodeRequests.map((r) => r.url)).to.deep.equal([`/node/session/${id}/url`]);
|
|
396
|
+
}));
|
|
397
|
+
it('keeps the dashboard’s command hooks for a registered remote session', () => __awaiter(this, void 0, void 0, function* () {
|
|
398
|
+
yield boot({ dashboard: true });
|
|
399
|
+
const { id } = yield remoteSession();
|
|
400
|
+
const before = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'beforeSessionCommand').resolves(true);
|
|
401
|
+
const after = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
|
|
402
|
+
yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).expect(200);
|
|
403
|
+
(0, chai_1.expect)(before.firstCall.args.slice(0, 2)).to.deep.equal([id, 'getUrl']);
|
|
404
|
+
(0, chai_1.expect)(after.firstCall.args[1]).to.equal('getUrl');
|
|
405
|
+
(0, chai_1.expect)(JSON.parse(after.firstCall.args[5])).to.deep.equal({ value: 'https://node.example/' });
|
|
406
|
+
}));
|
|
407
|
+
it('answers a node that cannot be reached with a WebDriver error', () => __awaiter(this, void 0, void 0, function* () {
|
|
408
|
+
yield boot();
|
|
409
|
+
const { id } = yield remoteSession();
|
|
410
|
+
yield new Promise((resolve) => {
|
|
411
|
+
const s = servers[1];
|
|
412
|
+
s.closeAllConnections();
|
|
413
|
+
http_1.default.Server.prototype.close.call(s, () => resolve());
|
|
414
|
+
});
|
|
415
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`);
|
|
416
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
417
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
|
|
418
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
419
|
+
}));
|
|
420
|
+
});
|
|
421
|
+
describe('DELETE of a remote session', () => {
|
|
422
|
+
it('runs the session lifecycle with the forward as its driver step', () => __awaiter(this, void 0, void 0, function* () {
|
|
423
|
+
yield boot({ nodeAuth: true });
|
|
424
|
+
const { id, session } = yield remoteSession();
|
|
425
|
+
session.isVideoAvailable = true; // recording, as startVideoRecording leaves it
|
|
426
|
+
const lifecycle = sinon_1.default.spy(typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService), 'deleteSession');
|
|
427
|
+
const stopped = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
428
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl).delete(`/wd/hub/session/${id}`);
|
|
429
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
430
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: null });
|
|
431
|
+
(0, chai_1.expect)(lifecycle.calledOnce).to.equal(true);
|
|
432
|
+
(0, chai_1.expect)(lifecycle.firstCall.args[1]).to.equal(id);
|
|
433
|
+
// The node's video is fetched while the session still exists, then it is deleted.
|
|
434
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['stopRecordingScreen', 'deleteSession']);
|
|
435
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
436
|
+
(0, chai_1.expect)(stopped.calledWith(id)).to.equal(true);
|
|
437
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(id)).to.equal(undefined);
|
|
438
|
+
const phone = yield scratch.db.device.findFirst({ where: { udid: `phone-${stamp}` } });
|
|
439
|
+
(0, chai_1.expect)(phone).to.include({ busy: false, session_id: null });
|
|
440
|
+
const row = yield scratch.db.session.findUnique({ where: { id } });
|
|
441
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.video_recording).to.equal('node-video.mp4');
|
|
442
|
+
}));
|
|
443
|
+
it('treats DELETE of a sub-resource as an ordinary command', () => __awaiter(this, void 0, void 0, function* () {
|
|
444
|
+
yield boot();
|
|
445
|
+
const { id } = yield remoteSession();
|
|
446
|
+
const lifecycle = sinon_1.default.spy(typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService), 'deleteSession');
|
|
447
|
+
yield (0, loopbackRequest_1.default)(hubUrl).delete(`/wd/hub/session/${id}/cookie`);
|
|
448
|
+
(0, chai_1.expect)(lifecycle.called).to.equal(false);
|
|
449
|
+
(0, chai_1.expect)(nodeRequests.map((r) => r.url)).to.deep.equal([`/node/session/${id}/cookie`]);
|
|
450
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(id)).to.not.equal(undefined);
|
|
451
|
+
}));
|
|
452
|
+
});
|
|
453
|
+
describe('after a hub restart (the session only in the database)', () => {
|
|
454
|
+
it('routes from the Session row and the phone’s row, under the node’s own base path', () => __awaiter(this, void 0, void 0, function* () {
|
|
455
|
+
yield boot({ nodeAuth: true, nodeBase: '' });
|
|
456
|
+
const { id } = yield remoteSession({ registered: false });
|
|
457
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(id)).to.equal(undefined);
|
|
458
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`);
|
|
459
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
460
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: 'https://node.example/' });
|
|
461
|
+
(0, chai_1.expect)(nodeRequests.map((r) => r.url)).to.deep.equal([`/session/${id}/url`]);
|
|
462
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
463
|
+
}));
|
|
464
|
+
it('and its DELETE still forwards and frees the phone', () => __awaiter(this, void 0, void 0, function* () {
|
|
465
|
+
yield boot({ nodeAuth: true });
|
|
466
|
+
const { id } = yield remoteSession({ registered: false });
|
|
467
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
468
|
+
yield (0, loopbackRequest_1.default)(hubUrl).delete(`/wd/hub/session/${id}`).expect(200);
|
|
469
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['deleteSession']);
|
|
470
|
+
const phone = yield scratch.db.device.findFirst({ where: { udid: `phone-${stamp}` } });
|
|
471
|
+
(0, chai_1.expect)(phone).to.include({ busy: false, session_id: null });
|
|
472
|
+
}));
|
|
473
|
+
it('a closed session is not routed', () => __awaiter(this, void 0, void 0, function* () {
|
|
474
|
+
yield boot();
|
|
475
|
+
const { id } = yield remoteSession({ registered: false });
|
|
476
|
+
yield scratch.db.session.update({ where: { id }, data: { endTime: new Date() } });
|
|
477
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`);
|
|
478
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
479
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('invalid session id');
|
|
480
|
+
(0, chai_1.expect)(nodeRequests).to.deep.equal([]);
|
|
481
|
+
}));
|
|
482
|
+
});
|
|
483
|
+
describe('base paths', () => {
|
|
484
|
+
it('a node that does not say its base path is taken to share the hub’s', () => __awaiter(this, void 0, void 0, function* () {
|
|
485
|
+
yield boot({ nodeBase: '/wd/hub', advertise: false });
|
|
486
|
+
const { id } = yield remoteSession({ registered: false });
|
|
487
|
+
yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).expect(200);
|
|
488
|
+
(0, chai_1.expect)(nodeRequests.map((r) => r.url)).to.deep.equal([`/wd/hub/session/${id}/url`]);
|
|
489
|
+
}));
|
|
490
|
+
});
|
|
491
|
+
describe('/wd-internal on the hub', () => {
|
|
492
|
+
it('without the secret, answers exactly as an unknown route does', () => __awaiter(this, void 0, void 0, function* () {
|
|
493
|
+
yield boot({ hubAuth: true });
|
|
494
|
+
hub.sessions.add('hub-local');
|
|
495
|
+
const internal = yield (0, loopbackRequest_1.default)(hubUrl).get('/wd/hub/wd-internal/session/hub-local/timeouts');
|
|
496
|
+
const unknown = yield (0, loopbackRequest_1.default)(hubUrl).get('/wd/hub/not-a-route/session/hub-local/timeouts');
|
|
497
|
+
(0, chai_1.expect)(internal.status).to.equal(404);
|
|
498
|
+
(0, chai_1.expect)(internal.status).to.equal(unknown.status);
|
|
499
|
+
(0, chai_1.expect)(internal.headers['content-type']).to.equal(unknown.headers['content-type']);
|
|
500
|
+
(0, chai_1.expect)(internal.body).to.deep.equal(unknown.body);
|
|
501
|
+
(0, chai_1.expect)(internal.body.value.error).to.equal('unknown command');
|
|
502
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
503
|
+
}));
|
|
504
|
+
it('with the secret, reaches the route without credentials though command auth is on', () => __awaiter(this, void 0, void 0, function* () {
|
|
505
|
+
yield boot({ hubAuth: true });
|
|
506
|
+
hub.sessions.add('hub-local');
|
|
507
|
+
yield (0, loopbackRequest_1.default)(hubUrl).get('/wd/hub/session/hub-local/timeouts').expect(404);
|
|
508
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal([]);
|
|
509
|
+
const res = yield (0, loopbackRequest_1.default)(hubUrl)
|
|
510
|
+
.get('/wd/hub/wd-internal/session/hub-local/timeouts')
|
|
511
|
+
.set((0, internalCall_1.internalCallHeaders)());
|
|
512
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
513
|
+
(0, chai_1.expect)(hub.commands).to.deep.equal(['getTimeouts']);
|
|
514
|
+
}));
|
|
515
|
+
it('never forwards the secret to a node', () => __awaiter(this, void 0, void 0, function* () {
|
|
516
|
+
yield boot();
|
|
517
|
+
const { id } = yield remoteSession();
|
|
518
|
+
yield (0, loopbackRequest_1.default)(hubUrl)
|
|
519
|
+
.get(`/wd/hub/wd-internal/session/${id}/url`)
|
|
520
|
+
.set((0, internalCall_1.internalCallHeaders)())
|
|
521
|
+
.expect(200);
|
|
522
|
+
(0, chai_1.expect)(nodeRequests).to.have.length(1);
|
|
523
|
+
(0, chai_1.expect)(nodeRequests[0].headers[internalCall_1.INTERNAL_CALL_HEADER]).to.equal(undefined);
|
|
524
|
+
}));
|
|
525
|
+
});
|
|
526
|
+
describe('per-command auth on a remote session (at the hub)', () => {
|
|
527
|
+
it('lets the owner through to the node', () => __awaiter(this, void 0, void 0, function* () {
|
|
528
|
+
yield boot({ hubAuth: true, nodeAuth: true });
|
|
529
|
+
const { id } = yield remoteSession({ owner: 'alice' });
|
|
530
|
+
yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).set(as('alice')).expect(200);
|
|
531
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['getUrl']);
|
|
532
|
+
}));
|
|
533
|
+
it('answers another user, or no credentials, with the unknown-session answer; the node sees nothing', () => __awaiter(this, void 0, void 0, function* () {
|
|
534
|
+
yield boot({ hubAuth: true, nodeAuth: true });
|
|
535
|
+
const { id } = yield remoteSession({ owner: 'alice' });
|
|
536
|
+
const bob = yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).set(as('bob'));
|
|
537
|
+
const nobody = yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`);
|
|
538
|
+
for (const res of [bob, nobody]) {
|
|
539
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
540
|
+
(0, chai_1.expect)(res.text).to.equal(EXACT_UNKNOWN_SESSION);
|
|
541
|
+
}
|
|
542
|
+
(0, chai_1.expect)(nodeRequests).to.deep.equal([]);
|
|
543
|
+
(0, chai_1.expect)(node.commands).to.deep.equal([]);
|
|
544
|
+
}));
|
|
545
|
+
});
|
|
546
|
+
describe("the node's check of the hub's token (node command auth on)", () => {
|
|
547
|
+
function tokenFor(sessionId_1) {
|
|
548
|
+
return __awaiter(this, arguments, void 0, function* (sessionId, ttlSeconds = 300, keys = hubKeys) {
|
|
549
|
+
return keys.sign({ sid: sessionId }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds });
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
it("accepts the hub's token for the session", () => __awaiter(this, void 0, void 0, function* () {
|
|
553
|
+
const { nodeBase } = yield boot({ nodeAuth: true });
|
|
554
|
+
const { id } = yield remoteSession();
|
|
555
|
+
yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
556
|
+
.get(`${nodeBase}/session/${id}/url`)
|
|
557
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, yield tokenFor(id))
|
|
558
|
+
.expect(200);
|
|
559
|
+
(0, chai_1.expect)(node.commands).to.deep.equal(['getUrl']);
|
|
560
|
+
}));
|
|
561
|
+
it('refuses a forged, an expired, or another session’s token, and none at all', () => __awaiter(this, void 0, void 0, function* () {
|
|
562
|
+
const { nodeBase } = yield boot({ nodeAuth: true });
|
|
563
|
+
const { id } = yield remoteSession();
|
|
564
|
+
const otherDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-forger-'));
|
|
565
|
+
try {
|
|
566
|
+
const forger = new JwtKeyService_1.JwtKeyService();
|
|
567
|
+
yield forger.init(otherDir);
|
|
568
|
+
const tokens = [
|
|
569
|
+
yield tokenFor(id, 300, forger),
|
|
570
|
+
yield tokenFor(id, -3600),
|
|
571
|
+
yield tokenFor('another-session'),
|
|
572
|
+
];
|
|
573
|
+
for (const token of tokens) {
|
|
574
|
+
const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
575
|
+
.get(`${nodeBase}/session/${id}/url`)
|
|
576
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
|
|
577
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
578
|
+
(0, chai_1.expect)(res.text).to.equal(EXACT_UNKNOWN_SESSION);
|
|
579
|
+
}
|
|
580
|
+
const bare = yield (0, loopbackRequest_1.default)(nodeOrigin).get(`${nodeBase}/session/${id}/url`);
|
|
581
|
+
(0, chai_1.expect)(bare.text).to.equal(EXACT_UNKNOWN_SESSION);
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
fs_1.default.rmSync(otherDir, { recursive: true, force: true });
|
|
585
|
+
}
|
|
586
|
+
(0, chai_1.expect)(node.commands).to.deep.equal([]);
|
|
587
|
+
}));
|
|
588
|
+
it("with the node's command auth off, a hub token is not needed", () => __awaiter(this, void 0, void 0, function* () {
|
|
589
|
+
const { nodeBase } = yield boot({ nodeAuth: false });
|
|
590
|
+
const { id } = yield remoteSession();
|
|
591
|
+
yield (0, loopbackRequest_1.default)(nodeOrigin).get(`${nodeBase}/session/${id}/url`).expect(200);
|
|
592
|
+
}));
|
|
593
|
+
});
|
|
594
|
+
});
|