@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
|
@@ -16,7 +16,7 @@ const chai_1 = require("chai");
|
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
17
|
const sinon_1 = __importDefault(require("sinon"));
|
|
18
18
|
const express_1 = __importDefault(require("express"));
|
|
19
|
-
const
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
20
|
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
21
21
|
const commandCaller_1 = require("../../src/middleware/commandCaller");
|
|
22
22
|
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
@@ -199,14 +199,14 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
199
199
|
describe('who may drive a session', () => {
|
|
200
200
|
it("lets the owner's key through to the route", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
201
201
|
const { app, ran } = yield lab();
|
|
202
|
-
const res = yield (0,
|
|
202
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
|
|
203
203
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
204
204
|
(0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
|
|
205
205
|
(0, chai_1.expect)(warnings).to.deep.equal([]);
|
|
206
206
|
}));
|
|
207
207
|
it("lets the owner's Bearer token through", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
208
208
|
const { app, ran } = yield lab();
|
|
209
|
-
yield (0,
|
|
209
|
+
yield (0, loopbackRequest_1.default)(app)
|
|
210
210
|
.get('/wd/hub/session/s-alice/url')
|
|
211
211
|
.set(bearer('alice-jwt'))
|
|
212
212
|
.expect(200);
|
|
@@ -214,13 +214,13 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
214
214
|
}));
|
|
215
215
|
it('refuses another user with the unknown-session answer, before the route runs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
216
|
const { app, ran } = yield lab();
|
|
217
|
-
const res = yield (0,
|
|
217
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token'));
|
|
218
218
|
yield expectRefused(res);
|
|
219
219
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
220
220
|
}));
|
|
221
221
|
it('logs the refusal with the session and the caller, never the credential', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
222
222
|
const { app } = yield lab();
|
|
223
|
-
yield (0,
|
|
223
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token'));
|
|
224
224
|
(0, chai_1.expect)(warnings).to.have.length(1);
|
|
225
225
|
(0, chai_1.expect)(warnings[0]).to.include('s-alice');
|
|
226
226
|
(0, chai_1.expect)(warnings[0]).to.include('bob');
|
|
@@ -229,7 +229,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
229
229
|
}));
|
|
230
230
|
it('refuses a request with no credentials, without looking anything up', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
231
|
const { app, ran } = yield lab();
|
|
232
|
-
yield expectRefused(yield (0,
|
|
232
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url'));
|
|
233
233
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
234
234
|
(0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called).to.equal(false);
|
|
235
235
|
(0, chai_1.expect)(ownerOf.called).to.equal(false);
|
|
@@ -238,7 +238,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
238
238
|
}));
|
|
239
239
|
it('refuses credentials that do not verify, without looking up the owner', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
240
240
|
const { app } = yield lab();
|
|
241
|
-
const res = yield (0,
|
|
241
|
+
const res = yield (0, loopbackRequest_1.default)(app)
|
|
242
242
|
.get('/wd/hub/session/s-alice/url')
|
|
243
243
|
.set({ 'x-xenon-access-key': 'ak-alice', 'x-xenon-token': 'guessed' });
|
|
244
244
|
yield expectRefused(res);
|
|
@@ -248,34 +248,34 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
248
248
|
}));
|
|
249
249
|
it('lets an admin-scoped key drive a session it does not own', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
250
250
|
const { app, ran } = yield lab();
|
|
251
|
-
yield (0,
|
|
251
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('carol-admin')).expect(200);
|
|
252
252
|
(0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
|
|
253
253
|
}));
|
|
254
254
|
it('lets a SUPER_ADMIN drive a session they do not own', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
255
255
|
const { app, ran } = yield lab();
|
|
256
|
-
yield (0,
|
|
256
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(bearer('root-jwt')).expect(200);
|
|
257
257
|
(0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
|
|
258
258
|
}));
|
|
259
259
|
it("refuses an ADMIN's ordinary key on someone else's session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
260
260
|
const { app, ran } = yield lab();
|
|
261
|
-
yield expectRefused(yield (0,
|
|
261
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('carol-ordinary')));
|
|
262
262
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
263
263
|
}));
|
|
264
264
|
it('refuses everyone but override admins on a session nobody owns', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
265
265
|
const { app, ran } = yield lab();
|
|
266
|
-
yield expectRefused(yield (0,
|
|
267
|
-
yield expectRefused(yield (0,
|
|
266
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('alice-token')));
|
|
267
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('carol-ordinary')));
|
|
268
268
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
269
|
-
yield (0,
|
|
270
|
-
yield (0,
|
|
269
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(as('carol-admin')).expect(200);
|
|
270
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-anon/url').set(bearer('root-jwt')).expect(200);
|
|
271
271
|
(0, chai_1.expect)(ran).to.deep.equal(['getUrl', 'getUrl']);
|
|
272
272
|
}));
|
|
273
273
|
it('answers a session the caller may not use exactly as one that does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
274
274
|
const { app } = yield lab();
|
|
275
|
-
const refused = yield (0,
|
|
275
|
+
const refused = yield (0, loopbackRequest_1.default)(app)
|
|
276
276
|
.get('/wd/hub/session/s-alice/url')
|
|
277
277
|
.set(as('bob-token'));
|
|
278
|
-
const missing = yield (0,
|
|
278
|
+
const missing = yield (0, loopbackRequest_1.default)(app)
|
|
279
279
|
.get('/wd/hub/session/no-such-session/url')
|
|
280
280
|
.set(as('bob-token'));
|
|
281
281
|
(0, chai_1.expect)(refused.status).to.equal(missing.status);
|
|
@@ -293,38 +293,38 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
293
293
|
for (const [method, url, name] of commands) {
|
|
294
294
|
it(`guards ${method.toUpperCase()} ${url.replace('/wd/hub', '')}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
295
295
|
const { app, ran } = yield lab();
|
|
296
|
-
yield expectRefused(yield (0,
|
|
296
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app)[method](url).set(as('bob-token')).send({}));
|
|
297
297
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
298
|
-
yield (0,
|
|
298
|
+
yield (0, loopbackRequest_1.default)(app)[method](url).set(as('alice-token')).send({}).expect(200);
|
|
299
299
|
(0, chai_1.expect)(ran).to.deep.equal([name]);
|
|
300
300
|
}));
|
|
301
301
|
}
|
|
302
302
|
it('guards a path in another letter case, which the routes also match', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
303
303
|
const { app, ran } = yield lab();
|
|
304
|
-
yield expectRefused(yield (0,
|
|
304
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/WD/HUB/SESSION/s-alice/url').set(as('bob-token')));
|
|
305
305
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
306
306
|
}));
|
|
307
307
|
});
|
|
308
308
|
describe('what it leaves alone', () => {
|
|
309
309
|
it('lets createSession through; it has its own credential checks', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
310
310
|
const { app, ran } = yield lab();
|
|
311
|
-
yield (0,
|
|
311
|
+
yield (0, loopbackRequest_1.default)(app).post('/wd/hub/session').send({ capabilities: {} }).expect(200);
|
|
312
312
|
(0, chai_1.expect)(ran).to.deep.equal(['createSession']);
|
|
313
313
|
(0, chai_1.expect)(ownerOf.called).to.equal(false);
|
|
314
314
|
}));
|
|
315
315
|
it('lets requests that are not session commands through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
316
316
|
const { app, ran } = yield lab();
|
|
317
|
-
yield (0,
|
|
318
|
-
yield (0,
|
|
319
|
-
yield (0,
|
|
317
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/status').expect(200);
|
|
318
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/appium/sessions').expect(200);
|
|
319
|
+
yield (0, loopbackRequest_1.default)(app).get('/xenon/api/devices').expect(200);
|
|
320
320
|
(0, chai_1.expect)(ran).to.deep.equal(['getStatus', 'getAppiumSessions', 'xenonApi']);
|
|
321
321
|
(0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called || ownerOf.called).to.equal(false);
|
|
322
322
|
}));
|
|
323
323
|
it('does nothing at all when the setting is off (the default)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
324
324
|
enabled = false;
|
|
325
325
|
const { app, ran } = yield lab();
|
|
326
|
-
yield (0,
|
|
327
|
-
yield (0,
|
|
326
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').expect(200);
|
|
327
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token')).expect(200);
|
|
328
328
|
(0, chai_1.expect)(ran).to.deep.equal(['getUrl', 'getUrl']);
|
|
329
329
|
(0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called || ownerOf.called).to.equal(false);
|
|
330
330
|
(0, chai_1.expect)(warnings).to.deep.equal([]);
|
|
@@ -332,7 +332,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
332
332
|
it('does nothing when auth is disabled, even with the setting on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
333
333
|
authDisabled = true;
|
|
334
334
|
const { app, ran } = yield lab();
|
|
335
|
-
yield (0,
|
|
335
|
+
yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').expect(200);
|
|
336
336
|
(0, chai_1.expect)(ran).to.deep.equal(['getUrl']);
|
|
337
337
|
(0, chai_1.expect)(verifyKeyPair.called || verifyBearer.called || ownerOf.called).to.equal(false);
|
|
338
338
|
}));
|
|
@@ -341,7 +341,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
341
341
|
it('answers 503 when the owner lookup fails, never letting the command through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
342
342
|
ownerOf.rejects(new Error('database is locked'));
|
|
343
343
|
const { app, ran } = yield lab();
|
|
344
|
-
const res = yield (0,
|
|
344
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
|
|
345
345
|
(0, chai_1.expect)(res.status).to.equal(503);
|
|
346
346
|
(0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
347
347
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
@@ -351,7 +351,7 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
351
351
|
it('answers 503 when the credential check fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
352
352
|
verifyKeyPair.rejects(new Error('database is locked'));
|
|
353
353
|
const { app, ran } = yield lab();
|
|
354
|
-
const res = yield (0,
|
|
354
|
+
const res = yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('alice-token'));
|
|
355
355
|
(0, chai_1.expect)(res.status).to.equal(503);
|
|
356
356
|
(0, chai_1.expect)(res.body).to.deep.equal(commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY);
|
|
357
357
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
@@ -360,13 +360,13 @@ describe('per-command auth (XENON_REQUIRE_COMMAND_AUTH)', () => {
|
|
|
360
360
|
describe('base path', () => {
|
|
361
361
|
it("normalises the base path the way Appium does ('wd/hub/' → '/wd/hub')", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
362
362
|
const { app, ran } = yield lab({ basePath: 'wd/hub/' });
|
|
363
|
-
yield expectRefused(yield (0,
|
|
363
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/wd/hub/session/s-alice/url').set(as('bob-token')));
|
|
364
364
|
(0, chai_1.expect)(ran).to.deep.equal([]);
|
|
365
365
|
}));
|
|
366
366
|
it("guards /session/:id under Appium 3's default empty base path", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
367
367
|
const { app, ran } = yield lab({ basePath: '', routePrefix: '' });
|
|
368
|
-
yield expectRefused(yield (0,
|
|
369
|
-
yield (0,
|
|
368
|
+
yield expectRefused(yield (0, loopbackRequest_1.default)(app).get('/session/s-alice/url').set(as('bob-token')));
|
|
369
|
+
yield (0, loopbackRequest_1.default)(app).post('/session').send({}).expect(200);
|
|
370
370
|
(0, chai_1.expect)(ran).to.deep.equal(['createSession']);
|
|
371
371
|
}));
|
|
372
372
|
});
|
|
@@ -15,7 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
require("reflect-metadata");
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const express_1 = __importDefault(require("express"));
|
|
18
|
-
const
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
19
|
const deviceTeamGuard_1 = require("../../src/middleware/deviceTeamGuard");
|
|
20
20
|
const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
|
|
21
21
|
/**
|
|
@@ -71,41 +71,41 @@ function appWith(caller) {
|
|
|
71
71
|
describe('the /control guards share one device lookup', () => {
|
|
72
72
|
it("costs a member's tap one lookup, not one per guard", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
73
|
const { app, lookups } = appWith(ALICE);
|
|
74
|
-
const res = yield (0,
|
|
74
|
+
const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_A}/tap`);
|
|
75
75
|
(0, chai_1.expect)(res.status, res.text).to.equal(200);
|
|
76
76
|
(0, chai_1.expect)(lookups).to.deep.equal([PHONE_A]);
|
|
77
77
|
}));
|
|
78
78
|
it("costs a member's clipboard read, which both guards check, one lookup", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
79
|
const { app, lookups } = appWith(ALICE);
|
|
80
|
-
const res = yield (0,
|
|
80
|
+
const res = yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/clipboard`);
|
|
81
81
|
(0, chai_1.expect)(res.status, res.text).to.equal(200);
|
|
82
82
|
(0, chai_1.expect)(lookups).to.deep.equal([PHONE_A]);
|
|
83
83
|
}));
|
|
84
84
|
it("costs a member's screenshot one lookup", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
85
|
const { app, lookups } = appWith(ALICE);
|
|
86
|
-
yield (0,
|
|
86
|
+
yield (0, loopbackRequest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
|
|
87
87
|
(0, chai_1.expect)(lookups).to.deep.equal([PHONE_A]);
|
|
88
88
|
}));
|
|
89
89
|
// The same count either way, so the time an answer takes can't tell a
|
|
90
90
|
// hidden phone from an unknown udid either.
|
|
91
91
|
it('costs a hidden phone and an unknown udid the same single lookup', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
92
|
const hidden = appWith(ALICE);
|
|
93
|
-
yield (0,
|
|
93
|
+
yield (0, loopbackRequest_1.default)(hidden.app).post(`/control/${PHONE_B}/tap`);
|
|
94
94
|
const unknown = appWith(ALICE);
|
|
95
|
-
yield (0,
|
|
95
|
+
yield (0, loopbackRequest_1.default)(unknown.app).post(`/control/${UNKNOWN}/tap`);
|
|
96
96
|
(0, chai_1.expect)(hidden.lookups).to.deep.equal([PHONE_B]);
|
|
97
97
|
(0, chai_1.expect)(unknown.lookups).to.deep.equal([UNKNOWN]);
|
|
98
98
|
}));
|
|
99
99
|
it("costs an admin's tap one lookup (the team guard skips admins)", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
100
100
|
const { app, lookups } = appWith(ADMIN);
|
|
101
|
-
const res = yield (0,
|
|
101
|
+
const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_B}/tap`);
|
|
102
102
|
(0, chai_1.expect)(res.status, res.text).to.equal(200);
|
|
103
103
|
(0, chai_1.expect)(lookups).to.deep.equal([PHONE_B]);
|
|
104
104
|
}));
|
|
105
105
|
it('does not share a lookup between two requests', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
106
|
const { app, lookups } = appWith(ALICE);
|
|
107
|
-
yield (0,
|
|
108
|
-
yield (0,
|
|
107
|
+
yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_A}/tap`);
|
|
108
|
+
yield (0, loopbackRequest_1.default)(app).post(`/control/${PHONE_A}/tap`);
|
|
109
109
|
(0, chai_1.expect)(lookups).to.deep.equal([PHONE_A, PHONE_A]);
|
|
110
110
|
}));
|
|
111
111
|
});
|
|
@@ -0,0 +1,115 @@
|
|
|
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 control_1 = __importDefault(require("../../src/app/routers/control"));
|
|
22
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
23
|
+
const device_managers_1 = require("../../src/device-managers");
|
|
24
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
25
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
26
|
+
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
27
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
28
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
29
|
+
/**
|
|
30
|
+
* Whether a /control action is forwarded to another server is decided by the
|
|
31
|
+
* phone's own row (its nodeId, else its exact host against this server's own
|
|
32
|
+
* hosts), never by the Host header the caller happened to use. It was a
|
|
33
|
+
* substring match on that header: a lab reached as `localhost` forwarded its
|
|
34
|
+
* own phone's tap back to itself with no login, and the tap failed. A
|
|
35
|
+
* forwarded tap is also sent once: InternalHttpClient's retry on a 5xx or a
|
|
36
|
+
* timeout could land the same tap twice on a slow node.
|
|
37
|
+
*/
|
|
38
|
+
// The route finds the manager by constructor name.
|
|
39
|
+
class AndroidDeviceManager {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.taps = [];
|
|
42
|
+
this.tap = (udid) => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
this.taps.push(udid);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const OWN_HOST = 'http://192.168.0.104:4723';
|
|
48
|
+
function buildApp() {
|
|
49
|
+
const app = (0, express_1.default)();
|
|
50
|
+
app.use(express_1.default.json());
|
|
51
|
+
const api = express_1.default.Router();
|
|
52
|
+
api.use((req, _res, next) => {
|
|
53
|
+
req.auth = {
|
|
54
|
+
kind: 'user-session',
|
|
55
|
+
userId: 'usr_me',
|
|
56
|
+
role: 'MEMBER',
|
|
57
|
+
scopes: (0, authMiddleware_1.scopesForRole)('MEMBER'),
|
|
58
|
+
rateLimit: 100,
|
|
59
|
+
};
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
control_1.default.register(api);
|
|
63
|
+
app.use('/xenon/api', api);
|
|
64
|
+
return app;
|
|
65
|
+
}
|
|
66
|
+
describe('/control forwards only another server’s phone', () => {
|
|
67
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
68
|
+
let manager;
|
|
69
|
+
let restore;
|
|
70
|
+
let context;
|
|
71
|
+
let savedContext;
|
|
72
|
+
let device;
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
restore = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager);
|
|
75
|
+
manager = new AndroidDeviceManager();
|
|
76
|
+
typedi_1.Container.set(device_managers_1.XenonManager, { deviceInstances: () => __awaiter(void 0, void 0, void 0, function* () { return [manager]; }) });
|
|
77
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
78
|
+
savedContext = Object.assign({}, context);
|
|
79
|
+
context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '192.168.0.104' }), 4723, 'lab-1', '');
|
|
80
|
+
sinon_1.default
|
|
81
|
+
.stub(device_store_1.DeviceStoreFactory, 'getStore')
|
|
82
|
+
.returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign({}, device)); }) });
|
|
83
|
+
});
|
|
84
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
Object.assign(context, savedContext);
|
|
86
|
+
sinon_1.default.restore();
|
|
87
|
+
restore();
|
|
88
|
+
yield loopback.closeAll();
|
|
89
|
+
}));
|
|
90
|
+
const tap = (udid, hostHeader) => (0, loopbackRequest_1.default)(buildApp())
|
|
91
|
+
.post(`/xenon/api/control/${udid}/tap`)
|
|
92
|
+
.set('Host', hostHeader)
|
|
93
|
+
.send({ x: 1, y: 2 });
|
|
94
|
+
it('taps this server’s own phone here, whatever name the caller used for the server', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
device = { udid: 'own-1', host: OWN_HOST, nodeId: 'lab-1', platform: 'android' };
|
|
96
|
+
const res = yield tap('own-1', 'localhost:4723');
|
|
97
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
98
|
+
(0, chai_1.expect)(manager.taps).to.deep.equal(['own-1']);
|
|
99
|
+
}));
|
|
100
|
+
it('forwards a node’s phone once, never retried, and relays its answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
const node = (0, express_1.default)();
|
|
102
|
+
const hits = [];
|
|
103
|
+
node.use((req, res) => {
|
|
104
|
+
hits.push(req.originalUrl);
|
|
105
|
+
res.status(503).json({ error: 'node busy' });
|
|
106
|
+
});
|
|
107
|
+
const nodeOrigin = `http://127.0.0.1:${(yield loopback.serve(node)).address().port}`;
|
|
108
|
+
device = { udid: 'node-phone', host: nodeOrigin, nodeId: 'node-9', platform: 'android' };
|
|
109
|
+
const res = yield tap('node-phone', '192.168.0.104:4723');
|
|
110
|
+
(0, chai_1.expect)(res.status).to.equal(503);
|
|
111
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ error: 'node busy' });
|
|
112
|
+
(0, chai_1.expect)(manager.taps).to.deep.equal([]);
|
|
113
|
+
(0, chai_1.expect)(hits).to.deep.equal(['/xenon/api/control/node-phone/tap']);
|
|
114
|
+
}));
|
|
115
|
+
});
|
|
@@ -16,7 +16,7 @@ require("reflect-metadata");
|
|
|
16
16
|
const chai_1 = require("chai");
|
|
17
17
|
const sinon_1 = __importDefault(require("sinon"));
|
|
18
18
|
const express_1 = __importDefault(require("express"));
|
|
19
|
-
const
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
20
|
const typedi_1 = require("typedi");
|
|
21
21
|
const control_1 = __importDefault(require("../../src/app/routers/control"));
|
|
22
22
|
const device_store_1 = require("../../src/data-service/device-store");
|
|
@@ -71,7 +71,7 @@ describe('stream/ticket and inspector/snapshot answer a failed device lookup', (
|
|
|
71
71
|
it('POST stream/ticket gets 503, not a hung request', function () {
|
|
72
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
73
73
|
this.timeout(5000);
|
|
74
|
-
const res = yield (0,
|
|
74
|
+
const res = yield (0, loopbackRequest_1.default)(buildApp())
|
|
75
75
|
.post('/xenon/api/control/DEV-1/stream/ticket')
|
|
76
76
|
.timeout(2000);
|
|
77
77
|
(0, chai_1.expect)(res.status).to.equal(503);
|
|
@@ -81,7 +81,7 @@ describe('stream/ticket and inspector/snapshot answer a failed device lookup', (
|
|
|
81
81
|
it('GET inspector/snapshot gets 503, not a hung request', function () {
|
|
82
82
|
return __awaiter(this, void 0, void 0, function* () {
|
|
83
83
|
this.timeout(5000);
|
|
84
|
-
const res = yield (0,
|
|
84
|
+
const res = yield (0, loopbackRequest_1.default)(buildApp())
|
|
85
85
|
.get('/xenon/api/control/DEV-1/inspector/snapshot')
|
|
86
86
|
.timeout(2000);
|
|
87
87
|
(0, chai_1.expect)(res.status).to.equal(503);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("reflect-metadata");
|
|
4
|
+
const chai_1 = require("chai");
|
|
5
|
+
const base_driver_1 = require("@appium/base-driver");
|
|
6
|
+
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
7
|
+
/**
|
|
8
|
+
* Appium's createSession answers a failure as `{ error: <Error> }`. The old
|
|
9
|
+
* message JSON.stringify'd that Error, which is `{}`, so a create Appium
|
|
10
|
+
* refused (no driver for the automationName, bad capabilities) reached the
|
|
11
|
+
* client as "Failed to create session on node <host>. Error: {}" with a 500,
|
|
12
|
+
* instead of Appium's own reason and status.
|
|
13
|
+
*/
|
|
14
|
+
describe('SessionLifecycleService.throwProperError', () => {
|
|
15
|
+
const throwProperError = (session) => SessionLifecycleService_1.SessionLifecycleService.prototype.throwProperError.call({}, session, 'http://node:4723');
|
|
16
|
+
function thrown(session) {
|
|
17
|
+
try {
|
|
18
|
+
throwProperError(session);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
throw new Error('expected throwProperError to throw');
|
|
24
|
+
}
|
|
25
|
+
it("rethrows the Error in Appium's `{ error }` answer, keeping its type and reason", () => {
|
|
26
|
+
const cause = new base_driver_1.errors.SessionNotCreatedError("Could not find a driver for automationName 'NoSuchDriver'");
|
|
27
|
+
const error = thrown({ error: cause });
|
|
28
|
+
(0, chai_1.expect)(error).to.equal(cause);
|
|
29
|
+
(0, chai_1.expect)(error.message).to.include("automationName 'NoSuchDriver'");
|
|
30
|
+
});
|
|
31
|
+
it('keeps a string reason, naming the node', () => {
|
|
32
|
+
const error = thrown({ error: 'no device' });
|
|
33
|
+
(0, chai_1.expect)(error.message).to.equal('Failed to create session on node http://node:4723. Error: no device');
|
|
34
|
+
});
|
|
35
|
+
it('keeps a plain object reason as JSON', () => {
|
|
36
|
+
const error = thrown({ error: { code: 'X' } });
|
|
37
|
+
(0, chai_1.expect)(error.message).to.include('{"code":"X"}');
|
|
38
|
+
});
|
|
39
|
+
it('rethrows an Error it is given directly', () => {
|
|
40
|
+
const cause = new Error('boom');
|
|
41
|
+
(0, chai_1.expect)(thrown(cause)).to.equal(cause);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
const chai_1 = require("chai");
|
|
16
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
17
|
+
const createHandoff_1 = require("../../src/gateway/createHandoff");
|
|
18
|
+
/**
|
|
19
|
+
* The allocation the session gateway made for one `POST /session`, on its way
|
|
20
|
+
* through Appium's route to the plugin's createSession. Taken once by the
|
|
21
|
+
* plugin, or given back once if the plugin never takes it.
|
|
22
|
+
*/
|
|
23
|
+
describe('CreateHandoff', () => {
|
|
24
|
+
afterEach(() => sinon_1.default.restore());
|
|
25
|
+
it('hands its allocation over exactly once', () => {
|
|
26
|
+
const release = sinon_1.default.spy();
|
|
27
|
+
const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, release);
|
|
28
|
+
(0, chai_1.expect)(handoff.take()).to.deep.equal({ udid: 'u1' });
|
|
29
|
+
(0, chai_1.expect)(() => handoff.take()).to.throw(/already/);
|
|
30
|
+
handoff.abandon();
|
|
31
|
+
(0, chai_1.expect)(release.called, 'a taken allocation is the taker’s to release').to.equal(false);
|
|
32
|
+
});
|
|
33
|
+
it('gives an allocation nobody took back once, and cannot be taken after', () => {
|
|
34
|
+
const release = sinon_1.default.spy();
|
|
35
|
+
const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, release);
|
|
36
|
+
handoff.abandon();
|
|
37
|
+
handoff.abandon();
|
|
38
|
+
(0, chai_1.expect)(release.callCount).to.equal(1);
|
|
39
|
+
(0, chai_1.expect)(release.firstCall.args[0]).to.deep.equal({ udid: 'u1' });
|
|
40
|
+
(0, chai_1.expect)(() => handoff.take()).to.throw(/closed/);
|
|
41
|
+
});
|
|
42
|
+
it('keeps no reference to the allocation once it is taken or released', () => {
|
|
43
|
+
const taken = new createHandoff_1.CreateHandoff({ udid: 'u1' }, () => undefined);
|
|
44
|
+
taken.take();
|
|
45
|
+
(0, chai_1.expect)(taken.allocation).to.equal(undefined);
|
|
46
|
+
const released = new createHandoff_1.CreateHandoff({ udid: 'u2' }, () => undefined);
|
|
47
|
+
released.abandon();
|
|
48
|
+
(0, chai_1.expect)(released.allocation).to.equal(undefined);
|
|
49
|
+
});
|
|
50
|
+
it('a release that throws is contained', () => {
|
|
51
|
+
const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, () => {
|
|
52
|
+
throw new Error('store down');
|
|
53
|
+
});
|
|
54
|
+
(0, chai_1.expect)(() => handoff.abandon()).to.not.throw();
|
|
55
|
+
});
|
|
56
|
+
describe('the request context', () => {
|
|
57
|
+
it('is none outside a request', () => {
|
|
58
|
+
(0, chai_1.expect)((0, createHandoff_1.currentCreateHandoff)()).to.equal(undefined);
|
|
59
|
+
});
|
|
60
|
+
it('follows the request through awaits, timers and callbacks', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
const handoff = new createHandoff_1.CreateHandoff({ udid: 'u1' }, () => undefined);
|
|
62
|
+
const seen = yield (0, createHandoff_1.runWithCreateHandoff)(handoff, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
yield Promise.resolve();
|
|
64
|
+
yield new Promise((resolve) => setTimeout(resolve, 5));
|
|
65
|
+
return new Promise((resolve) => setImmediate(() => resolve((0, createHandoff_1.currentCreateHandoff)())));
|
|
66
|
+
}));
|
|
67
|
+
(0, chai_1.expect)(seen).to.equal(handoff);
|
|
68
|
+
}));
|
|
69
|
+
it('keeps two requests apart', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
70
|
+
const a = new createHandoff_1.CreateHandoff({ udid: 'a' }, () => undefined);
|
|
71
|
+
const b = new createHandoff_1.CreateHandoff({ udid: 'b' }, () => undefined);
|
|
72
|
+
const [seenA, seenB] = yield Promise.all([
|
|
73
|
+
(0, createHandoff_1.runWithCreateHandoff)(a, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
yield new Promise((resolve) => setTimeout(resolve, 10));
|
|
75
|
+
return (0, createHandoff_1.currentCreateHandoff)();
|
|
76
|
+
})),
|
|
77
|
+
(0, createHandoff_1.runWithCreateHandoff)(b, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
yield new Promise((resolve) => setTimeout(resolve, 1));
|
|
79
|
+
return (0, createHandoff_1.currentCreateHandoff)();
|
|
80
|
+
})),
|
|
81
|
+
]);
|
|
82
|
+
(0, chai_1.expect)(seenA).to.equal(a);
|
|
83
|
+
(0, chai_1.expect)(seenB).to.equal(b);
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -52,7 +52,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
52
52
|
const os_1 = __importDefault(require("os"));
|
|
53
53
|
const path_1 = __importDefault(require("path"));
|
|
54
54
|
const express_1 = __importDefault(require("express"));
|
|
55
|
-
const
|
|
55
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
56
56
|
const typedi_1 = require("typedi");
|
|
57
57
|
const SocketServer_1 = require("../../src/services/SocketServer");
|
|
58
58
|
const SocketEvents_1 = require("../../src/enums/SocketEvents");
|
|
@@ -370,6 +370,9 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
|
|
|
370
370
|
entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
|
|
371
371
|
cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
|
|
372
372
|
});
|
|
373
|
+
// The route looks the session up first. No row: it assembles as before.
|
|
374
|
+
// Unstubbed, that read the developer's database, and hung without it.
|
|
375
|
+
sinon_1.default.stub(prisma_1.prisma.session, 'findUnique').resolves(null);
|
|
373
376
|
const app = (0, express_1.default)();
|
|
374
377
|
app.use((req, _res, next) => {
|
|
375
378
|
req.auth = {
|
|
@@ -382,7 +385,7 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
|
|
|
382
385
|
next();
|
|
383
386
|
});
|
|
384
387
|
bug_report_1.default.register(app);
|
|
385
|
-
yield (0,
|
|
388
|
+
yield (0, loopbackRequest_1.default)(app)
|
|
386
389
|
.post('/sessions/s5/bug-report?mode=full')
|
|
387
390
|
.buffer(true)
|
|
388
391
|
.parse((r, cb) => {
|
|
@@ -419,7 +422,7 @@ describe('Dashboard events name their phone (team-scoped call sites)', () => {
|
|
|
419
422
|
const router = express_1.default.Router();
|
|
420
423
|
grid_1.default.register(router, {});
|
|
421
424
|
app.use(router);
|
|
422
|
-
const res = yield (0,
|
|
425
|
+
const res = yield (0, loopbackRequest_1.default)(app).put('/device/phone-a/team').send({ teamId: 'team-c' });
|
|
423
426
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
424
427
|
(0, chai_1.expect)(yield resolver.resolve('phone-a')).to.deep.equal({ known: true, teamId: 'team-c' });
|
|
425
428
|
(0, chai_1.expect)(lookups).to.deep.equal(['phone-a']);
|