@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,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const typedi_1 = require("typedi");
|
|
21
|
+
const SessionType_1 = __importDefault(require("../../src/enums/SessionType"));
|
|
22
|
+
const sessionLocator_1 = require("../../src/gateway/sessionLocator");
|
|
23
|
+
const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
|
|
24
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
25
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
26
|
+
/**
|
|
27
|
+
* Where a session runs, as the hub's gateway asks it: SESSION_MANAGER for a
|
|
28
|
+
* live session, the database after a restart, and never a routing table of
|
|
29
|
+
* its own. And where a node's WebDriver API lives, which a node says itself.
|
|
30
|
+
*/
|
|
31
|
+
const NODE = 'http://10.0.0.7:4725';
|
|
32
|
+
function fakeSession(type, url, host = NODE) {
|
|
33
|
+
return {
|
|
34
|
+
getType: () => type,
|
|
35
|
+
getWebDriverUrl: () => url,
|
|
36
|
+
getDevice: () => ({ udid: 'u1', host }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
describe('SessionLocator', () => {
|
|
40
|
+
let live;
|
|
41
|
+
let rows;
|
|
42
|
+
let devices;
|
|
43
|
+
let findOpenSession;
|
|
44
|
+
let findDevice;
|
|
45
|
+
let webDriverUrlOf;
|
|
46
|
+
function locator(maxRemembered) {
|
|
47
|
+
return new sessionLocator_1.SessionLocator({
|
|
48
|
+
getSession: (id) => live[id],
|
|
49
|
+
findOpenSession,
|
|
50
|
+
findDevice,
|
|
51
|
+
isLocalDevice: (device) => device.host === 'http://10.0.0.1:4724',
|
|
52
|
+
webDriverUrlOf,
|
|
53
|
+
maxRemembered,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
live = {};
|
|
58
|
+
rows = {};
|
|
59
|
+
devices = {};
|
|
60
|
+
findOpenSession = sinon_1.default.spy((id) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = rows[id]) !== null && _a !== void 0 ? _a : null; }));
|
|
61
|
+
findDevice = sinon_1.default.spy((udid) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = devices[udid]) !== null && _a !== void 0 ? _a : null; }));
|
|
62
|
+
webDriverUrlOf = sinon_1.default.spy((device) => __awaiter(void 0, void 0, void 0, function* () { return `${device.host}/node-base`; }));
|
|
63
|
+
});
|
|
64
|
+
it('answers a live local session from SESSION_MANAGER, with no lookup', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
live.s = fakeSession(SessionType_1.default.LOCAL, 'http://10.0.0.1:4724/wd/hub/wd-internal');
|
|
66
|
+
(0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({ kind: 'local' });
|
|
67
|
+
(0, chai_1.expect)(findOpenSession.called).to.equal(false);
|
|
68
|
+
}));
|
|
69
|
+
it('answers a live remote session with its node URL and the session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
live.s = fakeSession(SessionType_1.default.REMOTE, `${NODE}/wd/hub`);
|
|
71
|
+
const location = yield locator().locate('s');
|
|
72
|
+
(0, chai_1.expect)(location).to.include({ kind: 'remote', url: `${NODE}/wd/hub`, cloud: false });
|
|
73
|
+
(0, chai_1.expect)(location.session).to.equal(live.s);
|
|
74
|
+
(0, chai_1.expect)(findOpenSession.called).to.equal(false);
|
|
75
|
+
}));
|
|
76
|
+
it('marks a cloud session, which gets no hub token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
live.s = fakeSession(SessionType_1.default.CLOUD, 'https://cloud.example/wd/hub', 'https://cloud.example');
|
|
78
|
+
(0, chai_1.expect)(yield locator().locate('s')).to.include({ kind: 'remote', cloud: true });
|
|
79
|
+
}));
|
|
80
|
+
it('never routes a session back to this server’s own phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
live.s = fakeSession(SessionType_1.default.REMOTE, 'http://10.0.0.1:4724/wd/hub', 'http://10.0.0.1:4724');
|
|
82
|
+
(0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({ kind: 'local' });
|
|
83
|
+
}));
|
|
84
|
+
it('after a restart, routes an open session from its row and its phone’s row', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
rows.s = { device_udid: 'u1', node_id: 'node-1' };
|
|
86
|
+
devices.u1 = { udid: 'u1', host: NODE, nodeId: 'node-1' };
|
|
87
|
+
(0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({
|
|
88
|
+
kind: 'remote',
|
|
89
|
+
url: `${NODE}/node-base`,
|
|
90
|
+
cloud: false,
|
|
91
|
+
});
|
|
92
|
+
(0, chai_1.expect)(findDevice.firstCall.args).to.deep.equal(['u1', 'node-1']);
|
|
93
|
+
}));
|
|
94
|
+
it('remembers the database’s answer, which cannot change, until forgotten', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
rows.s = { device_udid: 'u1', node_id: null };
|
|
96
|
+
devices.u1 = { udid: 'u1', host: NODE };
|
|
97
|
+
const l = locator();
|
|
98
|
+
yield l.locate('s');
|
|
99
|
+
yield l.locate('s');
|
|
100
|
+
(0, chai_1.expect)(findOpenSession.callCount).to.equal(1);
|
|
101
|
+
l.forget('s');
|
|
102
|
+
yield l.locate('s');
|
|
103
|
+
(0, chai_1.expect)(findOpenSession.callCount).to.equal(2);
|
|
104
|
+
}));
|
|
105
|
+
it('answers local for a session nobody here knows, so Appium gives its own answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
+
const l = locator();
|
|
107
|
+
(0, chai_1.expect)(yield l.locate('nobody')).to.deep.equal({ kind: 'local' });
|
|
108
|
+
yield l.locate('nobody');
|
|
109
|
+
(0, chai_1.expect)(findOpenSession.callCount).to.equal(1);
|
|
110
|
+
}));
|
|
111
|
+
it('answers local for an open session on this server’s own phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
rows.s = { device_udid: 'u1', node_id: null };
|
|
113
|
+
devices.u1 = { udid: 'u1', host: 'http://10.0.0.1:4724' };
|
|
114
|
+
(0, chai_1.expect)(yield locator().locate('s')).to.deep.equal({ kind: 'local' });
|
|
115
|
+
(0, chai_1.expect)(webDriverUrlOf.called).to.equal(false);
|
|
116
|
+
}));
|
|
117
|
+
it('bounds what it remembers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
|
+
const l = locator(2);
|
|
119
|
+
yield l.locate('a');
|
|
120
|
+
yield l.locate('b');
|
|
121
|
+
yield l.locate('c');
|
|
122
|
+
yield l.locate('a');
|
|
123
|
+
(0, chai_1.expect)(findOpenSession.callCount).to.equal(4);
|
|
124
|
+
}));
|
|
125
|
+
it('lets a database error through for the gateway to answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const l = new sessionLocator_1.SessionLocator({
|
|
127
|
+
getSession: () => undefined,
|
|
128
|
+
findOpenSession: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
throw new Error('database is locked');
|
|
130
|
+
}),
|
|
131
|
+
findDevice,
|
|
132
|
+
isLocalDevice: () => false,
|
|
133
|
+
webDriverUrlOf,
|
|
134
|
+
});
|
|
135
|
+
let thrown;
|
|
136
|
+
try {
|
|
137
|
+
yield l.locate('s');
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
thrown = err;
|
|
141
|
+
}
|
|
142
|
+
(0, chai_1.expect)(thrown.message).to.equal('database is locked');
|
|
143
|
+
}));
|
|
144
|
+
describe('sessionRowsFrom (Prisma)', () => {
|
|
145
|
+
it('reads only an open session, and prefers the phone on the session’s node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
const db = {
|
|
147
|
+
session: { findFirst: sinon_1.default.stub().resolves({ device_udid: 'u1', node_id: 'n1' }) },
|
|
148
|
+
device: {
|
|
149
|
+
findFirst: sinon_1.default
|
|
150
|
+
.stub()
|
|
151
|
+
.callsFake((_a) => __awaiter(void 0, [_a], void 0, function* ({ where }) { return where.nodeId ? { udid: 'u1', host: NODE, nodeId: 'n1' } : { udid: 'u1', host: 'x' }; })),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const rowsFrom = (0, sessionLocator_1.sessionRowsFrom)(db);
|
|
155
|
+
(0, chai_1.expect)(yield rowsFrom.findOpenSession('s')).to.deep.equal({
|
|
156
|
+
device_udid: 'u1',
|
|
157
|
+
node_id: 'n1',
|
|
158
|
+
});
|
|
159
|
+
(0, chai_1.expect)(db.session.findFirst.firstCall.args[0].where).to.deep.equal({
|
|
160
|
+
id: 's',
|
|
161
|
+
endTime: null,
|
|
162
|
+
});
|
|
163
|
+
(0, chai_1.expect)(yield rowsFrom.findDevice('u1', 'n1')).to.include({ host: NODE });
|
|
164
|
+
(0, chai_1.expect)(yield rowsFrom.findDevice('u1', null)).to.include({ host: 'x' });
|
|
165
|
+
}));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('NodeBasePathResolver', () => {
|
|
169
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
170
|
+
let restore;
|
|
171
|
+
let hits;
|
|
172
|
+
beforeEach(() => {
|
|
173
|
+
restore = (0, container_registration_1.saveRegistrations)(nodeWebDriverUrl_1.NodeBasePathResolver);
|
|
174
|
+
typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
|
|
175
|
+
hits = 0;
|
|
176
|
+
});
|
|
177
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
restore();
|
|
179
|
+
yield loopback.closeAll();
|
|
180
|
+
}));
|
|
181
|
+
function node(basePath) {
|
|
182
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
const app = (0, express_1.default)();
|
|
184
|
+
if (basePath !== undefined) {
|
|
185
|
+
app.get(nodeWebDriverUrl_1.WEBDRIVER_INFO_PATH, (req, res, next) => {
|
|
186
|
+
hits++;
|
|
187
|
+
(0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => basePath)(req, res, next);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const server = yield loopback.serve(app);
|
|
191
|
+
return `http://127.0.0.1:${server.address().port}`;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
it('the node says its base path, normalised as Appium does', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
const origin = yield node('node/base/');
|
|
196
|
+
const res = yield (0, loopbackRequest_1.default)(origin).get(nodeWebDriverUrl_1.WEBDRIVER_INFO_PATH).expect(200);
|
|
197
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ basePath: '/node/base' });
|
|
198
|
+
}));
|
|
199
|
+
it('uses the base path the node says, however the hub’s differs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
200
|
+
const origin = yield node('');
|
|
201
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver).resolve(origin, '/wd/hub')).to.equal('');
|
|
202
|
+
}));
|
|
203
|
+
it('falls back to the hub’s own for a node that does not say (an older Xenon)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
204
|
+
const origin = yield node(undefined);
|
|
205
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver).resolve(origin, 'wd/hub')).to.equal('/wd/hub');
|
|
206
|
+
}));
|
|
207
|
+
it('falls back to the hub’s own for a node that cannot be reached', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
208
|
+
const origin = yield node('/x');
|
|
209
|
+
yield loopback.closeAll();
|
|
210
|
+
(0, chai_1.expect)(yield typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver).resolve(origin, '/wd/hub')).to.equal('/wd/hub');
|
|
211
|
+
}));
|
|
212
|
+
it('asks once for concurrent callers, and again only after the answer expires', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
213
|
+
const origin = yield node('/node');
|
|
214
|
+
const resolver = typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver);
|
|
215
|
+
let now = 1000;
|
|
216
|
+
resolver.now = () => now;
|
|
217
|
+
const answers = yield Promise.all([
|
|
218
|
+
resolver.resolve(origin, '/wd/hub'),
|
|
219
|
+
resolver.resolve(origin, '/wd/hub'),
|
|
220
|
+
]);
|
|
221
|
+
(0, chai_1.expect)(answers).to.deep.equal(['/node', '/node']);
|
|
222
|
+
(0, chai_1.expect)(hits).to.equal(1);
|
|
223
|
+
now += nodeWebDriverUrl_1.BASE_PATH_TTL_MS - 1;
|
|
224
|
+
yield resolver.resolve(origin, '/wd/hub');
|
|
225
|
+
(0, chai_1.expect)(hits).to.equal(1);
|
|
226
|
+
now += 2;
|
|
227
|
+
yield resolver.resolve(origin, '/wd/hub');
|
|
228
|
+
(0, chai_1.expect)(hits).to.equal(2);
|
|
229
|
+
}));
|
|
230
|
+
it('asks again soon after a fallback', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
const origin = yield node(undefined);
|
|
232
|
+
const resolver = typedi_1.Container.get(nodeWebDriverUrl_1.NodeBasePathResolver);
|
|
233
|
+
let now = 1000;
|
|
234
|
+
resolver.now = () => now;
|
|
235
|
+
yield resolver.resolve(origin, '/wd/hub');
|
|
236
|
+
now += nodeWebDriverUrl_1.FALLBACK_TTL_MS + 1;
|
|
237
|
+
const lookUp = sinon_1.default.spy(resolver, 'lookUp');
|
|
238
|
+
yield resolver.resolve(origin, '/wd/hub');
|
|
239
|
+
(0, chai_1.expect)(lookUp.callCount).to.equal(1);
|
|
240
|
+
}));
|
|
241
|
+
it('nodeWebDriverUrl: a node’s origin plus its own base path', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
242
|
+
const origin = yield node('/node');
|
|
243
|
+
(0, chai_1.expect)(yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)({ udid: 'u', host: `${origin}/` }, '/wd/hub')).to.equal(`${origin}/node`);
|
|
244
|
+
}));
|
|
245
|
+
it('nodeWebDriverUrl: a cloud provider’s own URL, with no lookup', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
246
|
+
const url = yield (0, nodeWebDriverUrl_1.nodeWebDriverUrl)({ udid: 'u', host: 'https://cloud.example', cloud: 'pCloudy' }, '/wd/hub');
|
|
247
|
+
(0, chai_1.expect)(url).to.equal('https://cloud.example/wd/hub');
|
|
248
|
+
(0, chai_1.expect)(hits).to.equal(0);
|
|
249
|
+
}));
|
|
250
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const express_1 = __importDefault(require("express"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
21
|
+
const CloudSession_1 = require("../../src/sessions/CloudSession");
|
|
22
|
+
const LocalSession_1 = require("../../src/sessions/LocalSession");
|
|
23
|
+
const XenonSession_1 = require("../../src/sessions/XenonSession");
|
|
24
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
25
|
+
const internalCall_1 = require("../../src/gateway/internalCall");
|
|
26
|
+
const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
|
|
27
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
28
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
29
|
+
/**
|
|
30
|
+
* The calls a session object makes about its session over HTTP.
|
|
31
|
+
*
|
|
32
|
+
* A RemoteSession's go to the node, with the hub's session token for that
|
|
33
|
+
* session, so a node with per-command auth on accepts them (the heartbeat's
|
|
34
|
+
* probe among them). A cloud provider is not a Xenon node and never gets one.
|
|
35
|
+
* A LocalSession's go to this server's own `/wd-internal` path with the
|
|
36
|
+
* per-process secret, never through an HTTP proxy, and its heartbeat asks the
|
|
37
|
+
* in-process driver instead of sending a command.
|
|
38
|
+
*/
|
|
39
|
+
describe('session calls to the server that runs the session', () => {
|
|
40
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
41
|
+
let seen;
|
|
42
|
+
let port;
|
|
43
|
+
let restore;
|
|
44
|
+
const env = Object.assign({}, process.env);
|
|
45
|
+
const device = { udid: 'u1', host: 'http://10.0.0.7:4725', platform: 'android' };
|
|
46
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
seen = [];
|
|
48
|
+
const app = (0, express_1.default)();
|
|
49
|
+
app.use(express_1.default.json());
|
|
50
|
+
app.all('*', (req, res) => {
|
|
51
|
+
seen.push({ method: req.method, url: req.originalUrl, headers: req.headers });
|
|
52
|
+
res.json({ value: req.path.endsWith('/source') ? '<hierarchy/>' : 'ok' });
|
|
53
|
+
});
|
|
54
|
+
const server = yield loopback.serve(app);
|
|
55
|
+
port = server.address().port;
|
|
56
|
+
restore = (0, container_registration_1.saveRegistrations)(hubSessionToken_1.HubSessionTokenIssuer, nodeSessionStatus_1.NodeSessionProbeSupport);
|
|
57
|
+
const probes = new nodeSessionStatus_1.NodeSessionProbeSupport();
|
|
58
|
+
probes.logger = { warn: () => undefined };
|
|
59
|
+
typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, probes);
|
|
60
|
+
const issuer = new hubSessionToken_1.HubSessionTokenIssuer();
|
|
61
|
+
issuer.useSigner({ sign: (claims) => __awaiter(void 0, void 0, void 0, function* () { return `hub-token-for-${claims.sid}`; }) });
|
|
62
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, issuer);
|
|
63
|
+
}));
|
|
64
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
process.env = Object.assign({}, env);
|
|
66
|
+
sinon_1.default.restore();
|
|
67
|
+
restore();
|
|
68
|
+
yield loopback.closeAll();
|
|
69
|
+
}));
|
|
70
|
+
function remote(Type = RemoteSession_1.RemoteSession) {
|
|
71
|
+
return new Type({
|
|
72
|
+
sessionId: 's-1',
|
|
73
|
+
device,
|
|
74
|
+
sessionResponse: {},
|
|
75
|
+
xenonOption: {},
|
|
76
|
+
baseUrl: `http://127.0.0.1:${port}/node-base`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
describe('RemoteSession (hub to node)', () => {
|
|
80
|
+
it('sends the hub’s session token on every call, the heartbeat probe included', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
const session = remote();
|
|
82
|
+
yield session.getScreenShot();
|
|
83
|
+
yield session.getPageSource();
|
|
84
|
+
yield session.startVideoRecording({ resolution: '720x1280' });
|
|
85
|
+
yield session.stopVideoRecording();
|
|
86
|
+
yield session.startPerformanceRecording();
|
|
87
|
+
yield session.stopPerformanceRecording();
|
|
88
|
+
const health = yield session.checkHealth();
|
|
89
|
+
(0, chai_1.expect)(health.isHealthy).to.equal(true);
|
|
90
|
+
(0, chai_1.expect)(seen.map((r) => `${r.method} ${r.url}`)).to.deep.equal([
|
|
91
|
+
'GET /node-base/session/s-1/screenshot',
|
|
92
|
+
'GET /node-base/session/s-1/source',
|
|
93
|
+
'POST /node-base/session/s-1/appium/start_recording_screen',
|
|
94
|
+
'POST /node-base/session/s-1/appium/stop_recording_screen',
|
|
95
|
+
'POST /node-base/session/s-1/execute/sync',
|
|
96
|
+
'POST /node-base/session/s-1/execute/sync',
|
|
97
|
+
// This node has no session-status route (no NODE_SESSION_STATUS_HEADER
|
|
98
|
+
// on its answer), so the heartbeat falls back to the WebDriver probe.
|
|
99
|
+
'GET /xenon/api/node/sessions/s-1',
|
|
100
|
+
'GET /node-base/session/s-1/timeouts',
|
|
101
|
+
]);
|
|
102
|
+
for (const r of seen)
|
|
103
|
+
(0, chai_1.expect)(r.headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal('hub-token-for-s-1');
|
|
104
|
+
for (const r of seen)
|
|
105
|
+
(0, chai_1.expect)(r.headers[internalCall_1.INTERNAL_CALL_HEADER]).to.equal(undefined);
|
|
106
|
+
}));
|
|
107
|
+
it('goes without a token when the hub cannot sign one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).useSigner({
|
|
109
|
+
sign: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
throw new Error('JWT key service not initialized');
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
sinon_1.default.stub(typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).logger, 'warn');
|
|
114
|
+
yield remote().getPageSource();
|
|
115
|
+
(0, chai_1.expect)(seen).to.have.length(1);
|
|
116
|
+
(0, chai_1.expect)(seen[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
|
|
117
|
+
}));
|
|
118
|
+
it('says the node’s WebDriver URL it was made with', () => {
|
|
119
|
+
(0, chai_1.expect)(remote().getWebDriverUrl()).to.equal(`http://127.0.0.1:${port}/node-base`);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('CloudSession (hub to a cloud provider)', () => {
|
|
123
|
+
it('never sends a hub token to a cloud provider', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
yield remote(CloudSession_1.CloudSession).getPageSource();
|
|
125
|
+
yield remote(CloudSession_1.CloudSession).checkHealth();
|
|
126
|
+
(0, chai_1.expect)(seen).to.have.length(2);
|
|
127
|
+
for (const r of seen)
|
|
128
|
+
(0, chai_1.expect)(r.headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
131
|
+
describe('LocalSession (this server)', () => {
|
|
132
|
+
function local(driver = {}) {
|
|
133
|
+
return new LocalSession_1.LocalSession({
|
|
134
|
+
sessionId: 's-1',
|
|
135
|
+
device: Object.assign(Object.assign({}, device), { host: `http://127.0.0.1:${port}` }),
|
|
136
|
+
sessionResponse: {},
|
|
137
|
+
xenonOption: {},
|
|
138
|
+
driver: Object.assign({ opts: { address: '0.0.0.0', port, basePath: '/wd/hub' }, sessions: {} }, driver),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
it('falls back to /wd-internal with the secret, and never through a proxy', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
142
|
+
// A proxy in the environment would receive the secret; it must be skipped.
|
|
143
|
+
process.env.HTTP_PROXY = 'http://127.0.0.1:1';
|
|
144
|
+
process.env.http_proxy = 'http://127.0.0.1:1';
|
|
145
|
+
delete process.env.NO_PROXY;
|
|
146
|
+
delete process.env.no_proxy;
|
|
147
|
+
const session = local();
|
|
148
|
+
(0, chai_1.expect)(yield session.getPageSource()).to.equal('<hierarchy/>');
|
|
149
|
+
yield session.stopVideoRecording();
|
|
150
|
+
(0, chai_1.expect)(seen.map((r) => `${r.method} ${r.url}`)).to.deep.equal([
|
|
151
|
+
'GET /wd/hub/wd-internal/session/s-1/source',
|
|
152
|
+
'POST /wd/hub/wd-internal/session/s-1/appium/stop_recording_screen',
|
|
153
|
+
]);
|
|
154
|
+
const secret = (0, internalCall_1.internalCallHeaders)()[internalCall_1.INTERNAL_CALL_HEADER];
|
|
155
|
+
for (const r of seen) {
|
|
156
|
+
(0, chai_1.expect)(r.headers[internalCall_1.INTERNAL_CALL_HEADER]).to.equal(secret);
|
|
157
|
+
(0, chai_1.expect)(r.headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
|
|
158
|
+
}
|
|
159
|
+
}));
|
|
160
|
+
it('checks its health in process: no command, so no new-command timeout is reset', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
+
const exists = sinon_1.default.stub().returns(true);
|
|
162
|
+
const healthy = yield local({ sessionExists: exists }).checkHealth();
|
|
163
|
+
(0, chai_1.expect)(healthy).to.deep.include({ isHealthy: true, errorType: XenonSession_1.HealthErrorType.NONE });
|
|
164
|
+
exists.returns(false);
|
|
165
|
+
const gone = yield local({ sessionExists: exists }).checkHealth();
|
|
166
|
+
(0, chai_1.expect)(gone).to.deep.include({
|
|
167
|
+
isHealthy: false,
|
|
168
|
+
errorType: XenonSession_1.HealthErrorType.SESSION_NOT_FOUND,
|
|
169
|
+
});
|
|
170
|
+
(0, chai_1.expect)(exists.alwaysCalledWith('s-1')).to.equal(true);
|
|
171
|
+
(0, chai_1.expect)(seen).to.deep.equal([]);
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -139,3 +139,110 @@ describe('SessionOwnerResolver.displayName', () => {
|
|
|
139
139
|
(0, chai_1.expect)(db.calls.user).to.equal(1);
|
|
140
140
|
}));
|
|
141
141
|
});
|
|
142
|
+
// Prisma-shaped findMany delegates over fixed tables, recording each query so
|
|
143
|
+
// "one query for the whole list" is observable.
|
|
144
|
+
function tableDb(opts) {
|
|
145
|
+
const queries = [];
|
|
146
|
+
const pick = (rows, where) => rows.filter((row) => where.id.in.includes(row.id));
|
|
147
|
+
return {
|
|
148
|
+
queries,
|
|
149
|
+
session: {
|
|
150
|
+
findUnique: () => __awaiter(this, void 0, void 0, function* () {
|
|
151
|
+
throw new Error('ownersOf must not look sessions up one by one');
|
|
152
|
+
}),
|
|
153
|
+
findMany: (_a) => __awaiter(this, [_a], void 0, function* ({ where }) {
|
|
154
|
+
queries.push({ table: 'session', ids: [...where.id.in] });
|
|
155
|
+
return pick(opts.sessions, where);
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
apiKey: {
|
|
159
|
+
findUnique: () => __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
throw new Error('ownersOf must not look keys up one by one');
|
|
161
|
+
}),
|
|
162
|
+
findMany: (_a) => __awaiter(this, [_a], void 0, function* ({ where }) {
|
|
163
|
+
var _b;
|
|
164
|
+
queries.push({ table: 'apiKey', ids: [...where.id.in] });
|
|
165
|
+
return pick((_b = opts.apiKeys) !== null && _b !== void 0 ? _b : [], where);
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
describe('SessionOwnerResolver.ownersOf', () => {
|
|
171
|
+
it("resolves every session's owner with one query, by ownerOf's rule", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
172
|
+
const db = tableDb({
|
|
173
|
+
sessions: [
|
|
174
|
+
{ id: 's1', api_key_id: 'k1', user_id: 'usr_direct' },
|
|
175
|
+
{ id: 's2', api_key_id: null, user_id: null },
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
const r = new SessionOwnerResolver_1.SessionOwnerResolver(db);
|
|
179
|
+
const owners = yield r.ownersOf(['s1', 's2', 's-missing']);
|
|
180
|
+
(0, chai_1.expect)([...owners.entries()]).to.deep.equal([
|
|
181
|
+
['s1', 'usr_direct'],
|
|
182
|
+
['s2', null],
|
|
183
|
+
['s-missing', null],
|
|
184
|
+
]);
|
|
185
|
+
(0, chai_1.expect)(db.queries).to.deep.equal([{ table: 'session', ids: ['s1', 's2', 's-missing'] }]);
|
|
186
|
+
}));
|
|
187
|
+
it('takes the ApiKey hop in one more query, only for rows written before user_id', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
188
|
+
const db = tableDb({
|
|
189
|
+
sessions: [
|
|
190
|
+
{ id: 's1', api_key_id: 'k1', user_id: null },
|
|
191
|
+
{ id: 's2', api_key_id: 'k2', user_id: null },
|
|
192
|
+
{ id: 's3', api_key_id: 'k1', user_id: 'usr_direct' },
|
|
193
|
+
],
|
|
194
|
+
apiKeys: [
|
|
195
|
+
{ id: 'k1', userId: 'usr_legacy_1' },
|
|
196
|
+
{ id: 'k2', userId: 'usr_legacy_2' },
|
|
197
|
+
],
|
|
198
|
+
});
|
|
199
|
+
const r = new SessionOwnerResolver_1.SessionOwnerResolver(db);
|
|
200
|
+
const owners = yield r.ownersOf(['s1', 's2', 's3']);
|
|
201
|
+
(0, chai_1.expect)(owners.get('s1')).to.equal('usr_legacy_1');
|
|
202
|
+
(0, chai_1.expect)(owners.get('s2')).to.equal('usr_legacy_2');
|
|
203
|
+
(0, chai_1.expect)(owners.get('s3')).to.equal('usr_direct');
|
|
204
|
+
(0, chai_1.expect)(db.queries).to.have.length(2);
|
|
205
|
+
(0, chai_1.expect)(db.queries[1].table).to.equal('apiKey');
|
|
206
|
+
(0, chai_1.expect)(db.queries[1].ids).to.have.members(['k1', 'k2']);
|
|
207
|
+
}));
|
|
208
|
+
it('agrees with ownerOf and shares its cache of resolved owners', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
209
|
+
const db = tableDb({ sessions: [{ id: 's1', api_key_id: null, user_id: 'usr_a' }] });
|
|
210
|
+
const r = new SessionOwnerResolver_1.SessionOwnerResolver(db);
|
|
211
|
+
(0, chai_1.expect)((yield r.ownersOf(['s1'])).get('s1')).to.equal('usr_a');
|
|
212
|
+
// Resolved above, so ownerOf answers from the cache; its findUnique would throw.
|
|
213
|
+
(0, chai_1.expect)(yield r.ownerOf('s1')).to.equal('usr_a');
|
|
214
|
+
// And a cached owner is not queried again.
|
|
215
|
+
yield r.ownersOf(['s1']);
|
|
216
|
+
(0, chai_1.expect)(db.queries).to.have.length(1);
|
|
217
|
+
}));
|
|
218
|
+
it('does not cache an unresolved owner', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
219
|
+
const db = tableDb({ sessions: [] });
|
|
220
|
+
const r = new SessionOwnerResolver_1.SessionOwnerResolver(db);
|
|
221
|
+
yield r.ownersOf(['s1']);
|
|
222
|
+
yield r.ownersOf(['s1']);
|
|
223
|
+
(0, chai_1.expect)(db.queries).to.have.length(2);
|
|
224
|
+
}));
|
|
225
|
+
it('makes no query for an empty list, and asks for a repeated id once', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
226
|
+
const db = tableDb({ sessions: [{ id: 's1', api_key_id: null, user_id: 'usr_a' }] });
|
|
227
|
+
const r = new SessionOwnerResolver_1.SessionOwnerResolver(db);
|
|
228
|
+
(0, chai_1.expect)((yield r.ownersOf([])).size).to.equal(0);
|
|
229
|
+
(0, chai_1.expect)(db.queries).to.deep.equal([]);
|
|
230
|
+
yield r.ownersOf(['s1', 's1']);
|
|
231
|
+
(0, chai_1.expect)(db.queries).to.deep.equal([{ table: 'session', ids: ['s1'] }]);
|
|
232
|
+
}));
|
|
233
|
+
it('lets a failed query reject, so the caller can fail closed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
234
|
+
const db = tableDb({ sessions: [] });
|
|
235
|
+
db.session.findMany = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
236
|
+
throw new Error('database is locked');
|
|
237
|
+
});
|
|
238
|
+
const r = new SessionOwnerResolver_1.SessionOwnerResolver(db);
|
|
239
|
+
let caught;
|
|
240
|
+
try {
|
|
241
|
+
yield r.ownersOf(['s1']);
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
caught = err;
|
|
245
|
+
}
|
|
246
|
+
(0, chai_1.expect)(caught === null || caught === void 0 ? void 0 : caught.message).to.equal('database is locked');
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const typedi_1 = require("typedi");
|
|
19
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
20
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
21
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
22
|
+
const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
|
|
23
|
+
const SessionType_1 = __importDefault(require("../../src/enums/SessionType"));
|
|
24
|
+
const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
|
|
25
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
26
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
27
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
28
|
+
/**
|
|
29
|
+
* After a hub restart, SessionManager rebuilds the sessions still running on
|
|
30
|
+
* nodes from the database, and the session gateway routes them from there.
|
|
31
|
+
* A recovered session must point at its node's own base path, and a session
|
|
32
|
+
* that ran on the hub's own phone (its driver died with the hub) must never be
|
|
33
|
+
* rebuilt as a remote session pointing back at the hub.
|
|
34
|
+
*/
|
|
35
|
+
describe('recovering remote sessions after a hub restart', function () {
|
|
36
|
+
this.timeout(60000);
|
|
37
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
38
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
39
|
+
let restore;
|
|
40
|
+
let savedStore;
|
|
41
|
+
let nodeOrigin;
|
|
42
|
+
const HUB = 'http://10.0.0.1:4724';
|
|
43
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
restore = (0, container_registration_1.saveRegistrations)(nodeWebDriverUrl_1.NodeBasePathResolver);
|
|
45
|
+
typedi_1.Container.set(nodeWebDriverUrl_1.NodeBasePathResolver, new nodeWebDriverUrl_1.NodeBasePathResolver());
|
|
46
|
+
savedStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
47
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
48
|
+
yield scratch.db.session.deleteMany({});
|
|
49
|
+
yield scratch.db.device.deleteMany({});
|
|
50
|
+
const node = (0, express_1.default)();
|
|
51
|
+
node.get('/xenon/api/webdriver', (0, nodeWebDriverUrl_1.webdriverInfoHandler)(() => '/node-base'));
|
|
52
|
+
const server = yield loopback.serve(node);
|
|
53
|
+
nodeOrigin = `http://127.0.0.1:${server.address().port}`;
|
|
54
|
+
}));
|
|
55
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
56
|
+
device_store_1.DeviceStoreFactory._deviceStore = savedStore;
|
|
57
|
+
restore();
|
|
58
|
+
yield loopback.closeAll();
|
|
59
|
+
}));
|
|
60
|
+
function running(id, udid, host, nodeId) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
yield scratch.db.device.create({
|
|
63
|
+
data: { udid, host, nodeId, platform: 'android', busy: true, session_id: id },
|
|
64
|
+
});
|
|
65
|
+
yield scratch.db.session.create({
|
|
66
|
+
data: {
|
|
67
|
+
id,
|
|
68
|
+
desired_capabilities: '{}',
|
|
69
|
+
session_capabilities: '{}',
|
|
70
|
+
node_id: nodeId,
|
|
71
|
+
has_live_video: false,
|
|
72
|
+
device_udid: udid,
|
|
73
|
+
device_platform: 'android',
|
|
74
|
+
device_version: '14',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
it("rebuilds a node's session under the node's own base path", () => __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
yield running('on-node', 'phone-n', nodeOrigin, 'node-1');
|
|
81
|
+
const manager = new SessionManager_1.SessionManager();
|
|
82
|
+
const recovered = yield manager.recoverActiveSessions('hub-new-id', '/wd/hub', (h) => h === HUB);
|
|
83
|
+
(0, chai_1.expect)(recovered).to.equal(1);
|
|
84
|
+
const session = manager.getSession('on-node');
|
|
85
|
+
(0, chai_1.expect)(session.getType()).to.equal(SessionType_1.default.REMOTE);
|
|
86
|
+
(0, chai_1.expect)(session.getWebDriverUrl()).to.equal(`${nodeOrigin}/node-base`);
|
|
87
|
+
}));
|
|
88
|
+
describe("the hub's boot", () => {
|
|
89
|
+
const hubHosts = { origin: HUB, hosts: new Set([HUB]) };
|
|
90
|
+
it("keeps a node's phone, so the session on it is recovered rather than failed", () => __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
yield running('on-node', 'phone-n', nodeOrigin, 'node-1');
|
|
92
|
+
yield running('on-hub', 'phone-h', HUB, 'hub-old-id');
|
|
93
|
+
// What ServerManager does at boot, in order: clear, then recover.
|
|
94
|
+
const cleared = (0, localDeviceHosts_1.devicesClearedAtBoot)({ hub: undefined }, hubHosts);
|
|
95
|
+
yield device_store_1.DeviceStoreFactory.getStore().clearStorage(cleared);
|
|
96
|
+
const left = yield scratch.db.device.findMany({ select: { udid: true } });
|
|
97
|
+
(0, chai_1.expect)(left.map((d) => d.udid)).to.deep.equal(['phone-n']);
|
|
98
|
+
const manager = new SessionManager_1.SessionManager();
|
|
99
|
+
const recovered = yield manager.recoverActiveSessions('hub-new-id', '/wd/hub', (h) => h === HUB);
|
|
100
|
+
(0, chai_1.expect)(recovered).to.equal(1);
|
|
101
|
+
(0, chai_1.expect)(manager.getSession('on-node')).to.not.equal(undefined);
|
|
102
|
+
const hubRow = yield scratch.db.session.findUnique({ where: { id: 'on-hub' } });
|
|
103
|
+
(0, chai_1.expect)(hubRow === null || hubRow === void 0 ? void 0 : hubRow.status).to.equal('failed');
|
|
104
|
+
}));
|
|
105
|
+
it('a node still clears every phone it has, as before', () => {
|
|
106
|
+
(0, chai_1.expect)((0, localDeviceHosts_1.devicesClearedAtBoot)({ hub: 'http://hub:4724' }, hubHosts)).to.equal(undefined);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it("marks a session on the hub's own phone failed, though its node id changed with the restart", () => __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
yield running('on-hub', 'phone-h', HUB, 'hub-old-id');
|
|
111
|
+
const manager = new SessionManager_1.SessionManager();
|
|
112
|
+
const recovered = yield manager.recoverActiveSessions('hub-new-id', '/wd/hub', (h) => h === HUB);
|
|
113
|
+
(0, chai_1.expect)(recovered).to.equal(0);
|
|
114
|
+
(0, chai_1.expect)(manager.getSession('on-hub')).to.equal(undefined);
|
|
115
|
+
const row = yield scratch.db.session.findUnique({ where: { id: 'on-hub' } });
|
|
116
|
+
(0, chai_1.expect)(row === null || row === void 0 ? void 0 : row.status).to.equal('failed');
|
|
117
|
+
}));
|
|
118
|
+
});
|