@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,354 @@
|
|
|
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 fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const http_1 = __importDefault(require("http"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
22
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
23
|
+
const typedi_1 = require("typedi");
|
|
24
|
+
const plugin_1 = require("../../src/plugin");
|
|
25
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
26
|
+
const defaultGateway_1 = require("../../src/gateway/defaultGateway");
|
|
27
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
28
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
29
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
30
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
31
|
+
const device_managers_1 = require("../../src/device-managers");
|
|
32
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
33
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
34
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
35
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
36
|
+
const config_1 = require("../../src/config");
|
|
37
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
38
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
39
|
+
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
40
|
+
/**
|
|
41
|
+
* A Xenon node's side of a create the hub forwards, in Appium 3's own
|
|
42
|
+
* server(): Appium's real umbrella with Xenon's real plugin, the production
|
|
43
|
+
* node wiring (nodeGatewayOptions), the real SessionLifecycleService and a
|
|
44
|
+
* scratch SQLite database of the node's own.
|
|
45
|
+
*
|
|
46
|
+
* Each instance has its own database, so the node cannot check the client's
|
|
47
|
+
* key: the hub did. The hub's create carries a token it signed instead
|
|
48
|
+
* (x-xenon-hub-token, audience xenon-node-create), naming the verified owner,
|
|
49
|
+
* the phone and its node. The node checks it against the hub's JWKS, and it is
|
|
50
|
+
* the session's credential: it attributes the session and passes
|
|
51
|
+
* XENON_REQUIRE_SESSION_TOKEN.
|
|
52
|
+
*
|
|
53
|
+
* The hub here is only what the node talks to: an Appium server serving the
|
|
54
|
+
* hub's public keys. What the hub sends is hub-gateway-create-appium-server.spec.ts.
|
|
55
|
+
*/
|
|
56
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
57
|
+
const NODE_ID = 'node-1';
|
|
58
|
+
describe('a create the hub forwards, on the node, in Appium 3’s own server()', function () {
|
|
59
|
+
this.timeout(60000);
|
|
60
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
61
|
+
const servers = [];
|
|
62
|
+
let dirs;
|
|
63
|
+
let hubKeys;
|
|
64
|
+
let appiumLogs;
|
|
65
|
+
let restore;
|
|
66
|
+
let saved;
|
|
67
|
+
let authDisabledBefore;
|
|
68
|
+
let gateBefore;
|
|
69
|
+
let umbrella;
|
|
70
|
+
let hubUrl;
|
|
71
|
+
let nodeOrigin;
|
|
72
|
+
let nodeAuthDisabled;
|
|
73
|
+
function keyService() {
|
|
74
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-node-create-keys-'));
|
|
76
|
+
dirs.push(dir);
|
|
77
|
+
const keys = new JwtKeyService_1.JwtKeyService();
|
|
78
|
+
yield keys.init(dir);
|
|
79
|
+
return keys;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
84
|
+
dirs = [];
|
|
85
|
+
hubKeys = yield keyService();
|
|
86
|
+
}));
|
|
87
|
+
after(() => {
|
|
88
|
+
appiumLogs.restore();
|
|
89
|
+
for (const dir of dirs)
|
|
90
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager);
|
|
94
|
+
// The hub's keys sign here only because the test mints what the hub would send.
|
|
95
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
96
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
97
|
+
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
98
|
+
getMaxSessionCount: () => undefined,
|
|
99
|
+
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
100
|
+
});
|
|
101
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
102
|
+
saved = {
|
|
103
|
+
store: device_store_1.DeviceStoreFactory._deviceStore,
|
|
104
|
+
pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
|
|
105
|
+
context: Object.assign({}, context),
|
|
106
|
+
};
|
|
107
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
108
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
|
|
109
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
110
|
+
config_1.config.authDisabled = false;
|
|
111
|
+
nodeAuthDisabled = false;
|
|
112
|
+
gateBefore = process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
113
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
114
|
+
yield scratch.db.pendingSession.deleteMany({});
|
|
115
|
+
yield scratch.db.device.deleteMany({});
|
|
116
|
+
yield boot();
|
|
117
|
+
}));
|
|
118
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
120
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
121
|
+
device_store_1.DeviceStoreFactory._deviceStore = saved.store;
|
|
122
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
|
|
123
|
+
Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
|
|
124
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
125
|
+
if (gateBefore === undefined)
|
|
126
|
+
delete process.env.XENON_REQUIRE_SESSION_TOKEN;
|
|
127
|
+
else
|
|
128
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = gateBefore;
|
|
129
|
+
sinon_1.default.restore();
|
|
130
|
+
restore();
|
|
131
|
+
for (const s of servers.splice(0)) {
|
|
132
|
+
s.closeAllConnections();
|
|
133
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
const port = (s) => s.address().port;
|
|
137
|
+
function boot() {
|
|
138
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
139
|
+
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
140
|
+
// The hub: only its public keys matter to the node.
|
|
141
|
+
const hubServer = yield baseDriver.server({
|
|
142
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
|
|
143
|
+
port: 0,
|
|
144
|
+
hostname: '127.0.0.1',
|
|
145
|
+
basePath: '/wd/hub',
|
|
146
|
+
cliArgs: { basePath: '/wd/hub' },
|
|
147
|
+
serverUpdaters: [
|
|
148
|
+
(app) => {
|
|
149
|
+
app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
servers.push(hubServer);
|
|
154
|
+
hubUrl = `http://127.0.0.1:${port(hubServer)}`;
|
|
155
|
+
umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
|
|
156
|
+
const nodeServer = yield baseDriver.server({
|
|
157
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
|
|
158
|
+
port: 0,
|
|
159
|
+
hostname: '127.0.0.1',
|
|
160
|
+
basePath: '/node',
|
|
161
|
+
cliArgs: { basePath: '/node' },
|
|
162
|
+
serverUpdaters: [
|
|
163
|
+
(app) => {
|
|
164
|
+
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({
|
|
165
|
+
enabled: () => false,
|
|
166
|
+
authDisabled: () => nodeAuthDisabled,
|
|
167
|
+
logger: quiet,
|
|
168
|
+
}), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
servers.push(nodeServer);
|
|
173
|
+
nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
|
|
174
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1',
|
|
175
|
+
// Registers the node's LocalSession in SESSION_MANAGER, where its owner is read.
|
|
176
|
+
enableDashboard: true, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), port(nodeServer), NODE_ID, '/node');
|
|
177
|
+
for (const udid of ['phone-1', 'phone-2']) {
|
|
178
|
+
yield scratch.db.device.create({
|
|
179
|
+
data: {
|
|
180
|
+
udid,
|
|
181
|
+
host: nodeOrigin,
|
|
182
|
+
nodeId: NODE_ID,
|
|
183
|
+
platform: 'android',
|
|
184
|
+
name: udid,
|
|
185
|
+
sdk: '14',
|
|
186
|
+
busy: false,
|
|
187
|
+
userBlocked: false,
|
|
188
|
+
offline: false,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** What the hub mints for alice's create on phone-1. */
|
|
195
|
+
const hubToken = (grant = {}) => typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor(Object.assign({ userId: 'alice', udid: 'phone-1', host: nodeOrigin }, grant));
|
|
196
|
+
/** The capabilities as the hub forwards them: no credentials, the phone pinned. */
|
|
197
|
+
const forwarded = (alwaysMatch = {}) => ({
|
|
198
|
+
capabilities: {
|
|
199
|
+
alwaysMatch: Object.assign({ platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION, 'appium:newCommandTimeout': 0, 'appium:udid': 'phone-1',
|
|
200
|
+
// No video: it would start Xenon's real capture pipeline for the phone.
|
|
201
|
+
'xe:options': { recordVideo: false } }, alwaysMatch),
|
|
202
|
+
firstMatch: [{}],
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const create = (token_1, ...args_1) => __awaiter(this, [token_1, ...args_1], void 0, function* (token, body = forwarded()) {
|
|
206
|
+
const req = (0, loopbackRequest_1.default)(nodeOrigin).post('/node/session');
|
|
207
|
+
if (token !== undefined)
|
|
208
|
+
req.set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
|
|
209
|
+
return req.send(body);
|
|
210
|
+
});
|
|
211
|
+
const row = (udid) => scratch.db.device.findFirst({ where: { udid } });
|
|
212
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
|
|
213
|
+
function expectNothingCreated() {
|
|
214
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
215
|
+
yield settle();
|
|
216
|
+
(0, chai_1.expect)(Object.keys(umbrella.sessions), 'driver sessions').to.deep.equal([]);
|
|
217
|
+
(0, chai_1.expect)(yield row('phone-1')).to.include({ busy: false, session_id: null });
|
|
218
|
+
(0, chai_1.expect)(yield row('phone-2')).to.include({ busy: false, session_id: null });
|
|
219
|
+
(0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
it('attributes the session to the owner the hub’s token names', () => __awaiter(this, void 0, void 0, function* () {
|
|
223
|
+
const res = yield create(yield hubToken());
|
|
224
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
225
|
+
const sessionId = res.body.value.sessionId;
|
|
226
|
+
(0, chai_1.expect)(Object.keys(umbrella.sessions)).to.deep.equal([sessionId]);
|
|
227
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(sessionId);
|
|
228
|
+
(0, chai_1.expect)(session.userId).to.equal('alice');
|
|
229
|
+
(0, chai_1.expect)(session.apiKeyId).to.equal(null);
|
|
230
|
+
(0, chai_1.expect)(yield row('phone-1')).to.include({ busy: true, session_id: sessionId });
|
|
231
|
+
(0, chai_1.expect)(yield scratch.db.pendingSession.count()).to.equal(0);
|
|
232
|
+
}));
|
|
233
|
+
it('an unattributed create from the hub stays unattributed', () => __awaiter(this, void 0, void 0, function* () {
|
|
234
|
+
const res = yield create(yield hubToken({ userId: null }));
|
|
235
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
236
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(res.body.value.sessionId).userId).to.equal(null);
|
|
237
|
+
}));
|
|
238
|
+
describe('with XENON_REQUIRE_SESSION_TOKEN on', () => {
|
|
239
|
+
beforeEach(() => {
|
|
240
|
+
process.env.XENON_REQUIRE_SESSION_TOKEN = '1';
|
|
241
|
+
});
|
|
242
|
+
it('the hub’s token admits the session', () => __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
const res = yield create(yield hubToken());
|
|
244
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
245
|
+
}));
|
|
246
|
+
it('a create with no credentials at all is refused, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
|
|
247
|
+
// Before the gate is asked: on a node, only the hub's creates are taken.
|
|
248
|
+
const res = yield create(undefined);
|
|
249
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
250
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('session not created');
|
|
251
|
+
(0, chai_1.expect)(res.body.value.message).to.include('through the hub');
|
|
252
|
+
yield expectNothingCreated();
|
|
253
|
+
}));
|
|
254
|
+
it('client credentials the node cannot check are neither needed nor passed on', () => __awaiter(this, void 0, void 0, function* () {
|
|
255
|
+
const res = yield create(yield hubToken(), forwarded({
|
|
256
|
+
'xe:options': { accessKey: 'ak-unknown', token: 'CLIENT-SECRET', recordVideo: false },
|
|
257
|
+
}));
|
|
258
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
259
|
+
const session = SessionManager_1.SESSION_MANAGER.getSession(res.body.value.sessionId);
|
|
260
|
+
(0, chai_1.expect)(session.userId).to.equal('alice');
|
|
261
|
+
const inner = umbrella.sessions[res.body.value.sessionId];
|
|
262
|
+
(0, chai_1.expect)(JSON.stringify(inner.originalCaps)).to.not.include('CLIENT-SECRET');
|
|
263
|
+
}));
|
|
264
|
+
});
|
|
265
|
+
describe('a token that is not the hub’s grant for this create', () => {
|
|
266
|
+
it('is refused when forged, expired or a command token, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
const forger = yield keyService();
|
|
268
|
+
const claims = { sub: 'alice', udid: 'phone-1', host: nodeOrigin };
|
|
269
|
+
const tokens = [
|
|
270
|
+
yield forger.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: 120 }),
|
|
271
|
+
yield hubKeys.sign(claims, { audience: hubSessionToken_1.HUB_CREATE_AUDIENCE, ttlSeconds: -3600 }),
|
|
272
|
+
yield hubKeys.sign({ sid: 's-1' }, { audience: hubSessionToken_1.HUB_TOKEN_AUDIENCE, ttlSeconds: 300 }),
|
|
273
|
+
'not-a-jwt',
|
|
274
|
+
];
|
|
275
|
+
for (const token of tokens) {
|
|
276
|
+
const res = yield create(token);
|
|
277
|
+
(0, chai_1.expect)(res.status, token).to.equal(400);
|
|
278
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('invalid argument');
|
|
279
|
+
(0, chai_1.expect)(res.body.value.message).to.include('session rejected');
|
|
280
|
+
}
|
|
281
|
+
yield expectNothingCreated();
|
|
282
|
+
}));
|
|
283
|
+
it('is refused the second time: a create token is taken once', () => __awaiter(this, void 0, void 0, function* () {
|
|
284
|
+
const token = yield hubToken();
|
|
285
|
+
const first = yield create(token);
|
|
286
|
+
(0, chai_1.expect)(first.status, JSON.stringify(first.body)).to.equal(200);
|
|
287
|
+
const ended = yield (0, loopbackRequest_1.default)(nodeOrigin).delete(`/node/session/${first.body.value.sessionId}`);
|
|
288
|
+
(0, chai_1.expect)(ended.status, JSON.stringify(ended.body)).to.equal(200);
|
|
289
|
+
const again = yield create(token);
|
|
290
|
+
(0, chai_1.expect)(again.status).to.equal(400);
|
|
291
|
+
(0, chai_1.expect)(again.body.value.error).to.equal('invalid argument');
|
|
292
|
+
(0, chai_1.expect)(again.body.value.message).to.include('session rejected');
|
|
293
|
+
yield expectNothingCreated();
|
|
294
|
+
}));
|
|
295
|
+
it('is refused when the capabilities name another phone than the grant', () => __awaiter(this, void 0, void 0, function* () {
|
|
296
|
+
const res = yield create(yield hubToken(), forwarded({ 'appium:udid': 'phone-2' }));
|
|
297
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
298
|
+
(0, chai_1.expect)(res.body.value.message).to.include('session rejected');
|
|
299
|
+
yield expectNothingCreated();
|
|
300
|
+
}));
|
|
301
|
+
it('is refused, and the phone given back, when it names another node’s phone', () => __awaiter(this, void 0, void 0, function* () {
|
|
302
|
+
const res = yield create(yield hubToken({ host: 'http://10.9.9.9:4725' }));
|
|
303
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
304
|
+
(0, chai_1.expect)(res.body.value.message).to.include('session rejected');
|
|
305
|
+
yield expectNothingCreated();
|
|
306
|
+
}));
|
|
307
|
+
it('cannot be checked while the hub’s keys cannot be fetched: 503, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
|
|
308
|
+
const token = yield hubToken();
|
|
309
|
+
const hub = servers[0];
|
|
310
|
+
hub.closeAllConnections();
|
|
311
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(hub, () => resolve()));
|
|
312
|
+
const res = yield create(token);
|
|
313
|
+
(0, chai_1.expect)(res.status).to.equal(503);
|
|
314
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
|
|
315
|
+
yield expectNothingCreated();
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
describe('a create that did not come from the hub', () => {
|
|
319
|
+
const direct = () => create(undefined, forwarded({
|
|
320
|
+
'xe:options': { accessKey: 'ak-node', token: 'node-secret', recordVideo: false },
|
|
321
|
+
}));
|
|
322
|
+
it('is refused, saying to create sessions through the hub, and nothing is allocated', () => __awaiter(this, void 0, void 0, function* () {
|
|
323
|
+
const res = yield direct();
|
|
324
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
325
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('session not created');
|
|
326
|
+
(0, chai_1.expect)(res.body.value.message).to.include('through the hub');
|
|
327
|
+
(0, chai_1.expect)(res.body.value.message).to.include(hubUrl);
|
|
328
|
+
yield expectNothingCreated();
|
|
329
|
+
}));
|
|
330
|
+
it('is refused when it reaches the plugin without the gateway too', () => __awaiter(this, void 0, void 0, function* () {
|
|
331
|
+
const lifecycle = new SessionLifecycleService_1.SessionLifecycleService();
|
|
332
|
+
const caps = forwarded().capabilities;
|
|
333
|
+
let refused;
|
|
334
|
+
yield lifecycle
|
|
335
|
+
.createSession(() => ({}), umbrella, caps, { localOnly: true })
|
|
336
|
+
.catch((error) => (refused = error));
|
|
337
|
+
(0, chai_1.expect)(refused === null || refused === void 0 ? void 0 : refused.message).to.include('through the hub');
|
|
338
|
+
yield expectNothingCreated();
|
|
339
|
+
}));
|
|
340
|
+
it('still works on a node with auth disabled, as it did (local development)', () => __awaiter(this, void 0, void 0, function* () {
|
|
341
|
+
nodeAuthDisabled = true;
|
|
342
|
+
config_1.config.authDisabled = true;
|
|
343
|
+
const res = yield direct();
|
|
344
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
345
|
+
}));
|
|
346
|
+
});
|
|
347
|
+
it('with the node’s auth disabled, a token is not checked and the session is unattributed', () => __awaiter(this, void 0, void 0, function* () {
|
|
348
|
+
nodeAuthDisabled = true;
|
|
349
|
+
config_1.config.authDisabled = true;
|
|
350
|
+
const res = yield create('not-a-jwt');
|
|
351
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
352
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession(res.body.value.sessionId).userId).to.equal(null);
|
|
353
|
+
}));
|
|
354
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
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 fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const http_1 = __importDefault(require("http"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
23
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
24
|
+
const typedi_1 = require("typedi");
|
|
25
|
+
const plugin_1 = require("../../src/plugin");
|
|
26
|
+
const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
|
|
27
|
+
const defaultGateway_1 = require("../../src/gateway/defaultGateway");
|
|
28
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
29
|
+
const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
|
|
30
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
31
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
32
|
+
const device_managers_1 = require("../../src/device-managers");
|
|
33
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
34
|
+
const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
35
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
36
|
+
const SessionOwnerResolver_1 = require("../../src/services/device-access/SessionOwnerResolver");
|
|
37
|
+
const LiveSessionOwners_1 = require("../../src/services/device-access/LiveSessionOwners");
|
|
38
|
+
const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
|
|
39
|
+
const config_1 = require("../../src/config");
|
|
40
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
41
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
42
|
+
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
43
|
+
/**
|
|
44
|
+
* Who owns a session the hub created on a node, as the node's own checks see
|
|
45
|
+
* it: SessionOwnerResolver, behind the /control ownership guard, the logcat
|
|
46
|
+
* WebSocket and the session listing.
|
|
47
|
+
*
|
|
48
|
+
* The node writes no Session row for a hub's session (the hub keeps the
|
|
49
|
+
* record), so the resolver, which read only rows, found no owner, and the
|
|
50
|
+
* fail-closed rule refused the device to everyone but admins, its owner
|
|
51
|
+
* included. The owner the hub's create token named is now known for as long
|
|
52
|
+
* as the session lives.
|
|
53
|
+
*/
|
|
54
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
55
|
+
const NODE_ID = 'node-1';
|
|
56
|
+
describe('the owner of a hub’s session, on the node', function () {
|
|
57
|
+
this.timeout(60000);
|
|
58
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
59
|
+
const servers = [];
|
|
60
|
+
let dirs;
|
|
61
|
+
let hubKeys;
|
|
62
|
+
let appiumLogs;
|
|
63
|
+
let restore;
|
|
64
|
+
let saved;
|
|
65
|
+
let authDisabledBefore;
|
|
66
|
+
let nodeOrigin;
|
|
67
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
|
|
69
|
+
dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-node-owner-keys-'))];
|
|
70
|
+
hubKeys = new JwtKeyService_1.JwtKeyService();
|
|
71
|
+
yield hubKeys.init(dirs[0]);
|
|
72
|
+
}));
|
|
73
|
+
after(() => {
|
|
74
|
+
appiumLogs.restore();
|
|
75
|
+
for (const dir of dirs)
|
|
76
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners);
|
|
80
|
+
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
81
|
+
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
82
|
+
typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
|
|
83
|
+
typedi_1.Container.set(LiveSessionOwners_1.LiveSessionOwners, new LiveSessionOwners_1.LiveSessionOwners());
|
|
84
|
+
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
85
|
+
getMaxSessionCount: () => undefined,
|
|
86
|
+
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
87
|
+
});
|
|
88
|
+
const context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
89
|
+
saved = {
|
|
90
|
+
store: device_store_1.DeviceStoreFactory._deviceStore,
|
|
91
|
+
pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
|
|
92
|
+
context: Object.assign({}, context),
|
|
93
|
+
};
|
|
94
|
+
device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
|
|
95
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
|
|
96
|
+
authDisabledBefore = config_1.config.authDisabled;
|
|
97
|
+
config_1.config.authDisabled = false;
|
|
98
|
+
yield scratch.db.pendingSession.deleteMany({});
|
|
99
|
+
yield scratch.db.session.deleteMany({});
|
|
100
|
+
yield scratch.db.device.deleteMany({});
|
|
101
|
+
yield boot();
|
|
102
|
+
}));
|
|
103
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
|
|
105
|
+
SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
|
|
106
|
+
device_store_1.DeviceStoreFactory._deviceStore = saved.store;
|
|
107
|
+
device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
|
|
108
|
+
Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
|
|
109
|
+
config_1.config.authDisabled = authDisabledBefore;
|
|
110
|
+
sinon_1.default.restore();
|
|
111
|
+
restore();
|
|
112
|
+
for (const s of servers.splice(0)) {
|
|
113
|
+
s.closeAllConnections();
|
|
114
|
+
yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
|
|
115
|
+
}
|
|
116
|
+
}));
|
|
117
|
+
const port = (s) => s.address().port;
|
|
118
|
+
function boot() {
|
|
119
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
|
|
121
|
+
const hubServer = yield baseDriver.server({
|
|
122
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
|
|
123
|
+
port: 0,
|
|
124
|
+
hostname: '127.0.0.1',
|
|
125
|
+
basePath: '/wd/hub',
|
|
126
|
+
cliArgs: { basePath: '/wd/hub' },
|
|
127
|
+
serverUpdaters: [
|
|
128
|
+
(app) => {
|
|
129
|
+
app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
servers.push(hubServer);
|
|
134
|
+
const hubUrl = `http://127.0.0.1:${port(hubServer)}`;
|
|
135
|
+
const nodeServer = yield baseDriver.server({
|
|
136
|
+
routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']])),
|
|
137
|
+
port: 0,
|
|
138
|
+
hostname: '127.0.0.1',
|
|
139
|
+
basePath: '/node',
|
|
140
|
+
cliArgs: { basePath: '/node' },
|
|
141
|
+
serverUpdaters: [
|
|
142
|
+
(app) => {
|
|
143
|
+
(0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => false, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
|
|
144
|
+
// The node's /control ownership guard, with a caller named by a header.
|
|
145
|
+
const control = express_1.default.Router();
|
|
146
|
+
control.use((0, deviceAccessGuard_1.deviceAccessGuard)());
|
|
147
|
+
control.post('/:udid/tap', (_req, res) => res.json({ success: true }));
|
|
148
|
+
app.use('/control', (req, _res, next) => {
|
|
149
|
+
req.auth = { userId: req.headers['x-test-user'], role: 'MEMBER', scopes: 'devices' };
|
|
150
|
+
next();
|
|
151
|
+
}, control);
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
servers.push(nodeServer);
|
|
156
|
+
nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
|
|
157
|
+
// The node's dashboard is off, as it usually is.
|
|
158
|
+
typedi_1.Container.get(PluginContext_1.PluginContext).setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), port(nodeServer), NODE_ID, '/node');
|
|
159
|
+
yield scratch.db.device.create({
|
|
160
|
+
data: {
|
|
161
|
+
udid: 'phone-1',
|
|
162
|
+
host: nodeOrigin,
|
|
163
|
+
nodeId: NODE_ID,
|
|
164
|
+
platform: 'android',
|
|
165
|
+
name: 'phone-1',
|
|
166
|
+
sdk: '14',
|
|
167
|
+
busy: false,
|
|
168
|
+
userBlocked: false,
|
|
169
|
+
offline: false,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** alice's session on phone-1, created as the hub creates it. */
|
|
175
|
+
function hubSession() {
|
|
176
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
|
|
178
|
+
userId: 'alice',
|
|
179
|
+
udid: 'phone-1',
|
|
180
|
+
host: nodeOrigin,
|
|
181
|
+
});
|
|
182
|
+
const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
|
|
183
|
+
.post('/node/session')
|
|
184
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
|
|
185
|
+
.send({
|
|
186
|
+
capabilities: {
|
|
187
|
+
alwaysMatch: {
|
|
188
|
+
platformName: 'Android',
|
|
189
|
+
'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION,
|
|
190
|
+
'appium:newCommandTimeout': 0,
|
|
191
|
+
'appium:udid': 'phone-1',
|
|
192
|
+
'xe:options': { recordVideo: false },
|
|
193
|
+
},
|
|
194
|
+
firstMatch: [{}],
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
198
|
+
return res.body.value.sessionId;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const tap = (user) => (0, loopbackRequest_1.default)(nodeOrigin).post('/control/phone-1/tap').set('x-test-user', user).send({ x: 1, y: 1 });
|
|
202
|
+
it('is the owner the hub’s create token named, while the session lives', () => __awaiter(this, void 0, void 0, function* () {
|
|
203
|
+
const sessionId = yield hubSession();
|
|
204
|
+
(0, chai_1.expect)(yield scratch.db.session.count(), 'Session rows on the node').to.equal(0);
|
|
205
|
+
const owners = typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver);
|
|
206
|
+
(0, chai_1.expect)(yield owners.ownerOf(sessionId)).to.equal('alice');
|
|
207
|
+
(0, chai_1.expect)((yield owners.ownersOf([sessionId])).get(sessionId)).to.equal('alice');
|
|
208
|
+
}));
|
|
209
|
+
it('lets its owner use the phone on the node, and nobody else', () => __awaiter(this, void 0, void 0, function* () {
|
|
210
|
+
yield hubSession();
|
|
211
|
+
const alice = yield tap('alice');
|
|
212
|
+
(0, chai_1.expect)(alice.status, JSON.stringify(alice.body)).to.equal(200);
|
|
213
|
+
const bob = yield tap('bob');
|
|
214
|
+
(0, chai_1.expect)(bob.status).to.equal(409);
|
|
215
|
+
(0, chai_1.expect)(bob.body.error).to.equal('device_in_use_by_session');
|
|
216
|
+
}));
|
|
217
|
+
it('is forgotten when the session ends', () => __awaiter(this, void 0, void 0, function* () {
|
|
218
|
+
const sessionId = yield hubSession();
|
|
219
|
+
(0, chai_1.expect)(typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).ownerOf(sessionId)).to.equal('alice');
|
|
220
|
+
yield (0, loopbackRequest_1.default)(nodeOrigin).delete(`/node/session/${sessionId}`).expect(200);
|
|
221
|
+
(0, chai_1.expect)(typedi_1.Container.get(LiveSessionOwners_1.LiveSessionOwners).ownerOf(sessionId)).to.equal(undefined);
|
|
222
|
+
}));
|
|
223
|
+
});
|