@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 @@ 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 users_1 = require("../../src/app/routers/users");
|
|
22
22
|
const auth_1 = require("../../src/app/routers/auth");
|
|
@@ -71,7 +71,7 @@ describe('password-reset links', () => {
|
|
|
71
71
|
logged.push(args.map(String).join(' '));
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
const r = yield (0,
|
|
74
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/m1/reset-link');
|
|
75
75
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
76
76
|
(0, chai_1.expect)(r.body.emailed).to.equal(false);
|
|
77
77
|
(0, chai_1.expect)(r.body.link).to.match(new RegExp(`/xenon/reset-password#${RAW}$`));
|
|
@@ -87,7 +87,7 @@ describe('password-reset links', () => {
|
|
|
87
87
|
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
|
|
88
88
|
.resolves({ raw: RAW, id: 't1' });
|
|
89
89
|
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
90
|
-
const r = yield (0,
|
|
90
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/m1/reset-link');
|
|
91
91
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
92
92
|
(0, chai_1.expect)(r.body).to.include({ emailed: true });
|
|
93
93
|
(0, chai_1.expect)(r.body).to.not.have.property('link');
|
|
@@ -99,20 +99,20 @@ describe('password-reset links', () => {
|
|
|
99
99
|
.stub(prisma_1.prisma.user, 'findUnique')
|
|
100
100
|
.resolves(Object.assign(Object.assign({}, member), { id: 's1', role: 'SUPER_ADMIN' }));
|
|
101
101
|
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
102
|
-
const r = yield (0,
|
|
102
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/s1/reset-link');
|
|
103
103
|
(0, chai_1.expect)(r.status).to.equal(403);
|
|
104
104
|
(0, chai_1.expect)(create.called).to.equal(false);
|
|
105
105
|
}));
|
|
106
106
|
it('refuses an ADMIN issuing a link for another ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
107
|
sinon_1.default.stub(prisma_1.prisma.user, 'findUnique').resolves(Object.assign(Object.assign({}, member), { id: 'a2', role: 'ADMIN' }));
|
|
108
108
|
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
109
|
-
const r = yield (0,
|
|
109
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'admin1', role: 'ADMIN' })).post('/users/a2/reset-link');
|
|
110
110
|
(0, chai_1.expect)(r.status).to.equal(403);
|
|
111
111
|
(0, chai_1.expect)(create.called).to.equal(false);
|
|
112
112
|
}));
|
|
113
113
|
it('refuses a link for yourself', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
114
|
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
115
|
-
const r = yield (0,
|
|
115
|
+
const r = yield (0, loopbackRequest_1.default)(usersApp({ userId: 'me', role: 'SUPER_ADMIN' })).post('/users/me/reset-link');
|
|
116
116
|
(0, chai_1.expect)(r.status).to.equal(400);
|
|
117
117
|
(0, chai_1.expect)(create.called).to.equal(false);
|
|
118
118
|
}));
|
|
@@ -121,9 +121,9 @@ describe('password-reset links', () => {
|
|
|
121
121
|
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
122
122
|
const app = usersApp({ userId: 'root', role: 'SUPER_ADMIN' });
|
|
123
123
|
find.resolves(null);
|
|
124
|
-
(0, chai_1.expect)((yield (0,
|
|
124
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/users/nope/reset-link')).status).to.equal(404);
|
|
125
125
|
find.resolves(Object.assign(Object.assign({}, member), { status: 'DISABLED' }));
|
|
126
|
-
(0, chai_1.expect)((yield (0,
|
|
126
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/users/m1/reset-link')).status).to.equal(409);
|
|
127
127
|
(0, chai_1.expect)(create.called).to.equal(false);
|
|
128
128
|
}));
|
|
129
129
|
});
|
|
@@ -154,7 +154,7 @@ describe('password-reset links', () => {
|
|
|
154
154
|
.resolves({ id: 'u1', email: 'u@x.local', name: 'U', status: 'ACTIVE' });
|
|
155
155
|
const find = typedi_1.Container.get(UserService_1.UserService).findByEmail;
|
|
156
156
|
const create = sinon_1.default.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken');
|
|
157
|
-
const r = yield (0,
|
|
157
|
+
const r = yield (0, loopbackRequest_1.default)(authApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
|
|
158
158
|
// The mint-or-skip decision runs in the same continuation as the user
|
|
159
159
|
// lookup, so once the lookup has resolved and a macrotask has passed,
|
|
160
160
|
// createToken has either been called or never will be.
|
|
@@ -172,7 +172,7 @@ describe('password-reset links', () => {
|
|
|
172
172
|
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'createToken')
|
|
173
173
|
.resolves({ raw: RAW, id: 't1' });
|
|
174
174
|
const send = sinon_1.default.stub(typedi_1.Container.get(EmailService_1.EmailService), 'send').resolves();
|
|
175
|
-
yield (0,
|
|
175
|
+
yield (0, loopbackRequest_1.default)(authApp()).post('/auth/forgot-password').send({ email: 'u@x.local' });
|
|
176
176
|
yield until(() => send.called);
|
|
177
177
|
(0, chai_1.expect)(send.calledOnce).to.equal(true);
|
|
178
178
|
(0, chai_1.expect)(send.firstCall.args[0].text).to.include(`/xenon/reset-password#${RAW}`);
|
|
@@ -191,10 +191,10 @@ describe('password-reset links', () => {
|
|
|
191
191
|
const verify = sinon_1.default
|
|
192
192
|
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'verifyToken')
|
|
193
193
|
.resolves({ id: 't1', userId: 'u1' });
|
|
194
|
-
const ok = yield (0,
|
|
194
|
+
const ok = yield (0, loopbackRequest_1.default)(app).post('/auth/reset-password/check').send({ token: RAW });
|
|
195
195
|
(0, chai_1.expect)(ok.status).to.equal(200);
|
|
196
196
|
(0, chai_1.expect)(verify.firstCall.args[0]).to.equal(RAW);
|
|
197
|
-
const old = yield (0,
|
|
197
|
+
const old = yield (0, loopbackRequest_1.default)(app).get(`/auth/reset-password/check/${RAW}`);
|
|
198
198
|
(0, chai_1.expect)(old.status).to.equal(404);
|
|
199
199
|
}));
|
|
200
200
|
});
|
|
@@ -210,7 +210,7 @@ describe('password-reset links', () => {
|
|
|
210
210
|
sinon_1.default.stub(reset, 'consume').resolves();
|
|
211
211
|
sinon_1.default.stub(typedi_1.Container.get(UserSessionService_1.UserSessionService), 'revokeAllForUser').resolves();
|
|
212
212
|
const revoke = sinon_1.default.stub(reset, 'revokeAllForUser').resolves(2);
|
|
213
|
-
const r = yield (0,
|
|
213
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
214
214
|
.post('/auth/reset-password')
|
|
215
215
|
.send({ token: RAW, newPassword: 'a-new-password' });
|
|
216
216
|
(0, chai_1.expect)(r.status).to.equal(204);
|
|
@@ -229,7 +229,7 @@ describe('password-reset links', () => {
|
|
|
229
229
|
const revoke = sinon_1.default
|
|
230
230
|
.stub(typedi_1.Container.get(PasswordResetService_1.PasswordResetService), 'revokeAllForUser')
|
|
231
231
|
.resolves(1);
|
|
232
|
-
const r = yield (0,
|
|
232
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
233
233
|
.post('/auth/change-password')
|
|
234
234
|
.send({ oldPassword: 'old-password', newPassword: 'new-password' });
|
|
235
235
|
(0, chai_1.expect)(r.status).to.equal(204);
|
|
@@ -51,8 +51,10 @@ const sinon_1 = __importDefault(require("sinon"));
|
|
|
51
51
|
const fs_1 = __importDefault(require("fs"));
|
|
52
52
|
const os_1 = __importDefault(require("os"));
|
|
53
53
|
const path_1 = __importDefault(require("path"));
|
|
54
|
+
const jose = __importStar(require("jose"));
|
|
54
55
|
const typedi_1 = require("typedi");
|
|
55
56
|
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
57
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
56
58
|
const PluginContext_1 = require("../../src/PluginContext");
|
|
57
59
|
const ApiKeyService_1 = require("../../src/services/ApiKeyService");
|
|
58
60
|
const UserService_1 = require("../../src/services/UserService");
|
|
@@ -111,7 +113,7 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
111
113
|
let restore;
|
|
112
114
|
let pendingCopy;
|
|
113
115
|
let finalCaps;
|
|
114
|
-
let
|
|
116
|
+
let release;
|
|
115
117
|
const create = (c) => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
118
|
const next = sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
119
|
driverCaps = JSON.parse(JSON.stringify(c));
|
|
@@ -152,8 +154,8 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
152
154
|
}));
|
|
153
155
|
sinon_1.default.stub(pendingSessions, 'removePendingSession').resolves();
|
|
154
156
|
sinon_1.default.stub(deviceService, 'updateDeviceProgress').resolves();
|
|
155
|
-
sinon_1.default.stub(deviceService, '
|
|
156
|
-
|
|
157
|
+
sinon_1.default.stub(deviceService, 'claimDeviceForSession').resolves(true);
|
|
158
|
+
release = sinon_1.default.stub(deviceService, 'releasePendingClaim').resolves(true);
|
|
157
159
|
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
158
160
|
sinon_1.default.stub(svc, 'createSessionInstance').callsFake((...args) => {
|
|
159
161
|
finalCaps = JSON.parse(JSON.stringify(args[2]));
|
|
@@ -286,7 +288,7 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
286
288
|
(0, chai_1.expect)(text, where).to.not.match(/"token"|"accessKey"/);
|
|
287
289
|
}
|
|
288
290
|
}));
|
|
289
|
-
it('a peer node gets the ticket and the credentials; the hub keeps neither', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
291
|
+
it('a peer node gets the ticket and the hub’s token, never the credentials; the hub keeps neither', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
290
292
|
allocate.resolves({
|
|
291
293
|
udid: 'u2',
|
|
292
294
|
host: 'http://10.0.0.2:4723',
|
|
@@ -294,18 +296,19 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
294
296
|
nodeId: 'node-2',
|
|
295
297
|
});
|
|
296
298
|
let forwarded;
|
|
297
|
-
|
|
299
|
+
let hubToken;
|
|
300
|
+
sinon_1.default.stub(svc, 'forwardSessionRequest').callsFake((_d, fc, t) => __awaiter(void 0, void 0, void 0, function* () {
|
|
298
301
|
forwarded = JSON.parse(JSON.stringify(fc));
|
|
302
|
+
hubToken = t;
|
|
299
303
|
return { value: ['sess-2', { platformName: 'Android' }] };
|
|
300
304
|
}));
|
|
301
305
|
sinon_1.default.stub(svc, 'finalizeSession').resolves();
|
|
302
306
|
const c = caps('app-a', 'ak_team_a');
|
|
303
307
|
yield svc.createSession(sinon_1.default.stub(), {}, c);
|
|
304
308
|
(0, chai_1.expect)(parseAppUrl(forwarded.alwaysMatch['appium:app']).appId).to.equal('app-a');
|
|
305
|
-
(0, chai_1.expect)(forwarded.alwaysMatch['xe:options']).to.deep.equal({
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
});
|
|
309
|
+
(0, chai_1.expect)(forwarded.alwaysMatch['xe:options']).to.deep.equal({});
|
|
310
|
+
(0, chai_1.expect)(JSON.stringify(forwarded)).to.not.match(/"token"|"accessKey"/);
|
|
311
|
+
(0, chai_1.expect)(jose.decodeJwt(hubToken)).to.include({ aud: hubSessionToken_1.HUB_CREATE_AUDIENCE, udid: 'u2' });
|
|
309
312
|
for (const [where, seen] of Object.entries({ pendingCopy, sent: c })) {
|
|
310
313
|
const text = JSON.stringify(seen);
|
|
311
314
|
(0, chai_1.expect)(text, where).to.include(PLAIN);
|
|
@@ -326,6 +329,7 @@ describe('createSession — an app named by id follows the team rule', () => {
|
|
|
326
329
|
}
|
|
327
330
|
(0, chai_1.expect)(String(err === null || err === void 0 ? void 0 : err.message)).to.match(/app-a.*JWT key service not initialized/);
|
|
328
331
|
(0, chai_1.expect)(driverCaps, 'the driver was never started').to.equal(undefined);
|
|
329
|
-
(0, chai_1.expect)(
|
|
332
|
+
(0, chai_1.expect)(release.calledOnce).to.equal(true);
|
|
333
|
+
(0, chai_1.expect)(release.firstCall.args[0]).to.include({ udid: 'u1', host: 'h1' });
|
|
330
334
|
}));
|
|
331
335
|
});
|
|
@@ -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 fs_1 = __importDefault(require("fs"));
|
|
21
21
|
const os_1 = __importDefault(require("os"));
|
|
22
22
|
const path_1 = __importDefault(require("path"));
|
|
@@ -99,12 +99,12 @@ describe('session files', () => {
|
|
|
99
99
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
100
100
|
});
|
|
101
101
|
it("serves a visible session's screenshot to a member", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
-
const r = yield (0,
|
|
102
|
+
const r = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-shared/asset/screenshots/shot.png');
|
|
103
103
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
104
104
|
(0, chai_1.expect)(String(r.body)).to.equal('png of sess-shared');
|
|
105
105
|
}));
|
|
106
106
|
it('serves a video with Range, for seeking', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
|
-
const r = yield (0,
|
|
107
|
+
const r = yield (0, loopbackRequest_1.default)(app(member))
|
|
108
108
|
.get('/session/sess-shared/asset/video/sess-shared.mp4')
|
|
109
109
|
.set('Range', 'bytes=2-4');
|
|
110
110
|
(0, chai_1.expect)(r.status).to.equal(206);
|
|
@@ -116,15 +116,15 @@ describe('session files', () => {
|
|
|
116
116
|
error: true,
|
|
117
117
|
message: `Session with id ${id} not found`,
|
|
118
118
|
});
|
|
119
|
-
const hidden = yield (0,
|
|
120
|
-
const unknown = yield (0,
|
|
119
|
+
const hidden = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-b/asset/screenshots/shot.png');
|
|
120
|
+
const unknown = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-nope/asset/screenshots/shot.png');
|
|
121
121
|
(0, chai_1.expect)(unknown.status).to.equal(404);
|
|
122
122
|
(0, chai_1.expect)(unknown.body).to.deep.equal(unknownBody('sess-nope'));
|
|
123
123
|
(0, chai_1.expect)(hidden.status).to.equal(404);
|
|
124
124
|
(0, chai_1.expect)(hidden.body).to.deep.equal(unknownBody('sess-b'));
|
|
125
125
|
}));
|
|
126
126
|
it("an admin gets another team's session file", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
|
-
const r = yield (0,
|
|
127
|
+
const r = yield (0, loopbackRequest_1.default)(app(admin)).get('/session/sess-b/asset/screenshots/shot.png');
|
|
128
128
|
(0, chai_1.expect)(r.status).to.equal(200);
|
|
129
129
|
(0, chai_1.expect)(String(r.body)).to.equal('png of sess-b');
|
|
130
130
|
}));
|
|
@@ -136,14 +136,14 @@ describe('session files', () => {
|
|
|
136
136
|
'/session/sess-shared/asset/screenshots/%2e%2e%2finterceptor%2fsession.har',
|
|
137
137
|
'/session/sess-shared/asset/video/..%2f..%2frecordings%2fgrp%2fcomposite.mp4',
|
|
138
138
|
]) {
|
|
139
|
-
const r = yield (0,
|
|
139
|
+
const r = yield (0, loopbackRequest_1.default)(app(admin)).get(url);
|
|
140
140
|
(0, chai_1.expect)(r.status, url).to.equal(404);
|
|
141
141
|
(0, chai_1.expect)(r.text, url).to.not.include('HAR-SECRET');
|
|
142
142
|
(0, chai_1.expect)(r.text, url).to.not.include('COMPOSITE-SECRET');
|
|
143
143
|
}
|
|
144
144
|
}));
|
|
145
145
|
it('a missing file in a visible session is a JSON 404', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
-
const r = yield (0,
|
|
146
|
+
const r = yield (0, loopbackRequest_1.default)(app(member)).get('/session/sess-shared/asset/screenshots/none.png');
|
|
147
147
|
(0, chai_1.expect)(r.status).to.equal(404);
|
|
148
148
|
(0, chai_1.expect)(r.body).to.have.property('error');
|
|
149
149
|
}));
|
|
@@ -160,7 +160,7 @@ describe('session files', () => {
|
|
|
160
160
|
it('no longer serves session files to a caller with no login', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
161
|
const a = (0, express_1.default)();
|
|
162
162
|
a.use('/xenon', (0, index_1.createRouter)({ bindHostOrIp: '127.0.0.1', enableDashboard: true }));
|
|
163
|
-
const r = yield (0,
|
|
163
|
+
const r = yield (0, loopbackRequest_1.default)(a).get(`/xenon/session-recordings/${path_1.default.basename(probeDir)}/secret.txt`);
|
|
164
164
|
(0, chai_1.expect)(r.text).to.not.include(secret);
|
|
165
165
|
}));
|
|
166
166
|
});
|
|
@@ -36,15 +36,22 @@ const capsWith = (obj) => ({
|
|
|
36
36
|
describe('authorizeSessionRequest — identity', () => {
|
|
37
37
|
let svc;
|
|
38
38
|
let authDisabledBefore;
|
|
39
|
+
let restore;
|
|
39
40
|
beforeEach(() => {
|
|
40
41
|
authDisabledBefore = config_1.config.authDisabled;
|
|
41
42
|
config_1.config.authDisabled = false;
|
|
42
|
-
|
|
43
|
+
restore = (0, container_registration_1.saveRegistrations)(ApiKeyService_1.ApiKeyService, JwtKeyService_1.JwtKeyService, UserService_1.UserService);
|
|
44
|
+
// No such user and no teams, as the developer's database answered for
|
|
45
|
+
// these ids: the real lookups read that database. Nothing here is about
|
|
46
|
+
// the owner's teams.
|
|
47
|
+
typedi_1.Container.set(UserService_1.UserService, { findById: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) });
|
|
48
|
+
sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
49
|
+
svc = new SessionLifecycleService_1.SessionLifecycleService();
|
|
43
50
|
});
|
|
44
51
|
afterEach(() => {
|
|
45
52
|
config_1.config.authDisabled = authDisabledBefore;
|
|
46
53
|
sinon_1.default.restore();
|
|
47
|
-
|
|
54
|
+
restore();
|
|
48
55
|
});
|
|
49
56
|
it('returns both ids for a valid xe:options pair', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
57
|
typedi_1.Container.set(ApiKeyService_1.ApiKeyService, {
|
|
@@ -304,6 +311,26 @@ describe('authorizeSessionRequest — leaseAccess', () => {
|
|
|
304
311
|
}
|
|
305
312
|
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.match(/not allowed/);
|
|
306
313
|
}));
|
|
314
|
+
it('reads the team from xe:options.team, over a flat cap, and still checks it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
315
|
+
withKey(keyRow('sessions'), user('MEMBER'));
|
|
316
|
+
teamRows.resolves([{ teamId: 'team_a' }, { teamId: 'team_b' }]);
|
|
317
|
+
const caps = (team) => capsWith({
|
|
318
|
+
'xe:options': { accessKey: 'ak', token: 'tk', team },
|
|
319
|
+
'xenon:team': 'team_a',
|
|
320
|
+
});
|
|
321
|
+
(0, chai_1.expect)(yield scope(caps('team_b'))).to.deep.equal({
|
|
322
|
+
callerTeamIds: ['team_b'],
|
|
323
|
+
scoped: true,
|
|
324
|
+
});
|
|
325
|
+
let error;
|
|
326
|
+
try {
|
|
327
|
+
yield invoke(svc, caps('team_c'));
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
error = e;
|
|
331
|
+
}
|
|
332
|
+
(0, chai_1.expect)(error === null || error === void 0 ? void 0 : error.message).to.equal("xe:options.team 'team_c' is not allowed for this API key");
|
|
333
|
+
}));
|
|
307
334
|
it('falls back to the shared pool when the lookup fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
308
335
|
withKey(keyRow('sessions'), null);
|
|
309
336
|
typedi_1.Container.set(UserService_1.UserService, { findById: sinon_1.default.stub().rejects(new Error('db down')) });
|
|
@@ -0,0 +1,255 @@
|
|
|
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 express_1 = __importDefault(require("express"));
|
|
17
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
18
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
|
+
const base_driver_1 = require("@appium/base-driver");
|
|
20
|
+
const sessionCreate_1 = require("../../src/gateway/sessionCreate");
|
|
21
|
+
const createHandoff_1 = require("../../src/gateway/createHandoff");
|
|
22
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
23
|
+
const loopbackServer_1 = require("../helpers/loopbackServer");
|
|
24
|
+
/**
|
|
25
|
+
* `POST <basePath>/session` in the session gateway: allocation happens here,
|
|
26
|
+
* before Appium's route. A remote phone's session is created on its node and
|
|
27
|
+
* answered here; a local phone's allocation goes on to the plugin through the
|
|
28
|
+
* request's async context, and is given back if nothing takes it.
|
|
29
|
+
*
|
|
30
|
+
* Appium's route is stood in for by a plain route, and the plugin by a
|
|
31
|
+
* function that takes the handoff, the way XenonPlugin.createSession does.
|
|
32
|
+
*/
|
|
33
|
+
describe('the session gateway’s create layer', () => {
|
|
34
|
+
const loopback = (0, loopbackServer_1.loopbackServers)();
|
|
35
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
36
|
+
let lifecycle;
|
|
37
|
+
let routeHits;
|
|
38
|
+
let pluginTakes;
|
|
39
|
+
let base;
|
|
40
|
+
const grant = { userId: 'alice', udid: 'phone-1', host: 'http://node:4725' };
|
|
41
|
+
const caps = () => ({
|
|
42
|
+
capabilities: { alwaysMatch: { platformName: 'Android' }, firstMatch: [{}] },
|
|
43
|
+
});
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
routeHits = [];
|
|
46
|
+
pluginTakes = true;
|
|
47
|
+
lifecycle = {
|
|
48
|
+
prepareSession: sinon_1.default.stub().resolves({ remote: false, device: { udid: 'phone-1' } }),
|
|
49
|
+
completeRemoteSession: sinon_1.default
|
|
50
|
+
.stub()
|
|
51
|
+
.resolves({ protocol: 'W3C', value: ['node-s1', { platformName: 'Android' }, 'W3C'] }),
|
|
52
|
+
releaseAllocation: sinon_1.default.stub().resolves(),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
sinon_1.default.restore();
|
|
57
|
+
yield loopback.closeAll();
|
|
58
|
+
}));
|
|
59
|
+
function serve() {
|
|
60
|
+
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
61
|
+
var _a;
|
|
62
|
+
const basePath = (_a = opts.basePath) !== null && _a !== void 0 ? _a : '/wd/hub';
|
|
63
|
+
const app = (0, express_1.default)();
|
|
64
|
+
app.use(express_1.default.json());
|
|
65
|
+
app.use(`${basePath}/session`, (0, sessionCreate_1.createSessionCreateLayer)({
|
|
66
|
+
lifecycle: () => lifecycle,
|
|
67
|
+
hubGrants: opts.hubGrants,
|
|
68
|
+
authDisabled: () => !!opts.authDisabled,
|
|
69
|
+
logger: quiet,
|
|
70
|
+
}));
|
|
71
|
+
if (opts.beforeRoute)
|
|
72
|
+
app.use(opts.beforeRoute);
|
|
73
|
+
// Appium's route, with the plugin inside it.
|
|
74
|
+
app.post(`${basePath}/session`, (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
75
|
+
var _a;
|
|
76
|
+
const hit = { headers: Object.assign({}, req.headers), body: req.body };
|
|
77
|
+
routeHits.push(hit);
|
|
78
|
+
yield new Promise((resolve) => setTimeout(resolve, 1));
|
|
79
|
+
if (pluginTakes)
|
|
80
|
+
hit.took = (_a = (0, createHandoff_1.currentCreateHandoff)()) === null || _a === void 0 ? void 0 : _a.take();
|
|
81
|
+
res.json({ value: { sessionId: 'local-s1', capabilities: {} } });
|
|
82
|
+
}));
|
|
83
|
+
app.all('*', (_req, res) => res.status(404).json({ value: { error: 'unknown command' } }));
|
|
84
|
+
const server = yield loopback.serve(app);
|
|
85
|
+
base = `http://127.0.0.1:${server.address().port}`;
|
|
86
|
+
return base;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
describe('what it handles', () => {
|
|
90
|
+
it('only a POST of exactly the create path', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
yield serve();
|
|
92
|
+
yield (0, loopbackRequest_1.default)(base).get('/wd/hub/session').expect(404);
|
|
93
|
+
yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session/abc/url').send({}).expect(404);
|
|
94
|
+
yield (0, loopbackRequest_1.default)(base).post('/wd/hub/sessionx').send(caps()).expect(404);
|
|
95
|
+
(0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
|
|
96
|
+
}));
|
|
97
|
+
it('a body without a capabilities object goes straight on, for Appium to answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
98
|
+
yield serve();
|
|
99
|
+
yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send({ desiredCapabilities: {} }).expect(200);
|
|
100
|
+
yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send({ capabilities: 'no' }).expect(200);
|
|
101
|
+
(0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
|
|
102
|
+
}));
|
|
103
|
+
it('matches the path as the router does: letter case, a trailing slash, any base path', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
yield serve({ basePath: '' });
|
|
105
|
+
yield (0, loopbackRequest_1.default)(base).post('/SESSION/').send(caps()).expect(200);
|
|
106
|
+
(0, chai_1.expect)(lifecycle.prepareSession.calledOnce).to.equal(true);
|
|
107
|
+
}));
|
|
108
|
+
it('fills in an omitted firstMatch, as the plugin used to', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
109
|
+
yield serve();
|
|
110
|
+
yield (0, loopbackRequest_1.default)(base)
|
|
111
|
+
.post('/wd/hub/session')
|
|
112
|
+
.send({ capabilities: { alwaysMatch: { platformName: 'Android' } } })
|
|
113
|
+
.expect(200);
|
|
114
|
+
(0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[0].firstMatch).to.deep.equal([{}]);
|
|
115
|
+
(0, chai_1.expect)(routeHits[0].body.capabilities.firstMatch).to.deep.equal([{}]);
|
|
116
|
+
}));
|
|
117
|
+
});
|
|
118
|
+
describe('a remote phone', () => {
|
|
119
|
+
beforeEach(() => lifecycle.prepareSession.resolves({ remote: true }));
|
|
120
|
+
it('is created on its node and answered here, as Appium answers a new session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
+
yield serve();
|
|
122
|
+
const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
|
|
123
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
124
|
+
(0, chai_1.expect)(res.headers['content-type']).to.equal('application/json; charset=utf-8');
|
|
125
|
+
(0, chai_1.expect)(res.body).to.deep.equal({
|
|
126
|
+
value: { capabilities: { platformName: 'Android' }, sessionId: 'node-s1' },
|
|
127
|
+
});
|
|
128
|
+
(0, chai_1.expect)(routeHits, 'Appium’s route').to.deep.equal([]);
|
|
129
|
+
(0, chai_1.expect)(lifecycle.completeRemoteSession.calledOnce).to.equal(true);
|
|
130
|
+
}));
|
|
131
|
+
it('a failure is answered as Appium answers the error', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
132
|
+
lifecycle.completeRemoteSession.rejects(new base_driver_1.errors.SessionNotCreatedError('node said no'));
|
|
133
|
+
yield serve();
|
|
134
|
+
const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
|
|
135
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
136
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('session not created');
|
|
137
|
+
(0, chai_1.expect)(res.body.value.message).to.include('node said no');
|
|
138
|
+
(0, chai_1.expect)(routeHits).to.deep.equal([]);
|
|
139
|
+
}));
|
|
140
|
+
});
|
|
141
|
+
describe('a refusal before any phone is allocated', () => {
|
|
142
|
+
it('is answered as Appium answers the error, and Appium never sees the request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
143
|
+
lifecycle.prepareSession.rejects(new base_driver_1.errors.InvalidArgumentError('session rejected: no'));
|
|
144
|
+
yield serve();
|
|
145
|
+
const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
|
|
146
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
147
|
+
(0, chai_1.expect)(res.body.value).to.include({ error: 'invalid argument' });
|
|
148
|
+
(0, chai_1.expect)(res.body.value.message).to.include('session rejected: no');
|
|
149
|
+
(0, chai_1.expect)(routeHits).to.deep.equal([]);
|
|
150
|
+
}));
|
|
151
|
+
it('a plain error is an unknown error', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
152
|
+
lifecycle.prepareSession.rejects(new Error('No device matching request.'));
|
|
153
|
+
yield serve();
|
|
154
|
+
const res = yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps());
|
|
155
|
+
(0, chai_1.expect)(res.status).to.equal(500);
|
|
156
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
159
|
+
describe('a local phone', () => {
|
|
160
|
+
it('its allocation reaches the plugin through Appium’s route, and is not given back', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
+
yield serve();
|
|
162
|
+
yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps()).expect(200);
|
|
163
|
+
(0, chai_1.expect)(routeHits[0].took).to.deep.equal({ remote: false, device: { udid: 'phone-1' } });
|
|
164
|
+
(0, chai_1.expect)(lifecycle.prepareSession.calledOnce).to.equal(true);
|
|
165
|
+
(0, chai_1.expect)(lifecycle.releaseAllocation.called).to.equal(false);
|
|
166
|
+
}));
|
|
167
|
+
it('is given back once when Appium answers without the plugin taking it', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
168
|
+
pluginTakes = false;
|
|
169
|
+
yield serve();
|
|
170
|
+
yield (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps()).expect(200);
|
|
171
|
+
yield new Promise((resolve) => setImmediate(resolve));
|
|
172
|
+
(0, chai_1.expect)(lifecycle.releaseAllocation.callCount).to.equal(1);
|
|
173
|
+
(0, chai_1.expect)(lifecycle.releaseAllocation.firstCall.args[0].device.udid).to.equal('phone-1');
|
|
174
|
+
}));
|
|
175
|
+
it('is given back, and Appium never called, when the client left while it waited', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
176
|
+
let allocated;
|
|
177
|
+
lifecycle.prepareSession.callsFake(() => new Promise((resolve) => {
|
|
178
|
+
allocated = () => resolve({ remote: false, device: { udid: 'phone-1' } });
|
|
179
|
+
}));
|
|
180
|
+
yield serve();
|
|
181
|
+
const req = (0, loopbackRequest_1.default)(base).post('/wd/hub/session').send(caps()).timeout(50);
|
|
182
|
+
yield req.then(() => undefined, () => undefined);
|
|
183
|
+
// The server sees the connection close, then the phone turns up.
|
|
184
|
+
yield new Promise((resolve) => setTimeout(resolve, 20));
|
|
185
|
+
allocated();
|
|
186
|
+
yield new Promise((resolve) => setTimeout(resolve, 20));
|
|
187
|
+
(0, chai_1.expect)(lifecycle.releaseAllocation.callCount).to.equal(1);
|
|
188
|
+
(0, chai_1.expect)(routeHits).to.deep.equal([]);
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
describe('on a node: the hub’s token', () => {
|
|
192
|
+
it('a valid one is the grant the node allocates by; the header goes no further', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
193
|
+
const verifyCreate = sinon_1.default.stub().resolves(grant);
|
|
194
|
+
yield serve({ hubGrants: { verifyCreate } });
|
|
195
|
+
yield (0, loopbackRequest_1.default)(base)
|
|
196
|
+
.post('/wd/hub/session')
|
|
197
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good-token')
|
|
198
|
+
.send(caps())
|
|
199
|
+
.expect(200);
|
|
200
|
+
(0, chai_1.expect)(verifyCreate.calledOnceWith('good-token')).to.equal(true);
|
|
201
|
+
(0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[1]).to.deep.equal({ hubGrant: grant });
|
|
202
|
+
(0, chai_1.expect)(routeHits[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
|
|
203
|
+
}));
|
|
204
|
+
it('an invalid one is refused, and nothing is allocated', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
yield serve({ hubGrants: { verifyCreate: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) } });
|
|
206
|
+
const res = yield (0, loopbackRequest_1.default)(base)
|
|
207
|
+
.post('/wd/hub/session')
|
|
208
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, 'forged')
|
|
209
|
+
.send(caps());
|
|
210
|
+
(0, chai_1.expect)(res.status).to.equal(400);
|
|
211
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('invalid argument');
|
|
212
|
+
(0, chai_1.expect)(res.body.value.message).to.include('session rejected');
|
|
213
|
+
(0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
|
|
214
|
+
(0, chai_1.expect)(routeHits).to.deep.equal([]);
|
|
215
|
+
}));
|
|
216
|
+
it('one that cannot be checked is a 503, and nothing is allocated', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
217
|
+
yield serve({
|
|
218
|
+
hubGrants: {
|
|
219
|
+
verifyCreate: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
220
|
+
throw new hubSessionToken_1.HubTokenUnavailableError('hub down');
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const res = yield (0, loopbackRequest_1.default)(base)
|
|
225
|
+
.post('/wd/hub/session')
|
|
226
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, 'token')
|
|
227
|
+
.send(caps());
|
|
228
|
+
(0, chai_1.expect)(res.status).to.equal(503);
|
|
229
|
+
(0, chai_1.expect)(res.body.value.error).to.equal('unknown error');
|
|
230
|
+
(0, chai_1.expect)(lifecycle.prepareSession.called).to.equal(false);
|
|
231
|
+
}));
|
|
232
|
+
it('with auth disabled it is not checked, only removed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
233
|
+
const verifyCreate = sinon_1.default.stub().resolves(null);
|
|
234
|
+
yield serve({ hubGrants: { verifyCreate }, authDisabled: true });
|
|
235
|
+
yield (0, loopbackRequest_1.default)(base)
|
|
236
|
+
.post('/wd/hub/session')
|
|
237
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, 'anything')
|
|
238
|
+
.send(caps())
|
|
239
|
+
.expect(200);
|
|
240
|
+
(0, chai_1.expect)(verifyCreate.called).to.equal(false);
|
|
241
|
+
(0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[1]).to.deep.equal({});
|
|
242
|
+
(0, chai_1.expect)(routeHits[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
|
|
243
|
+
}));
|
|
244
|
+
it('a hub, which checks no hub tokens, ignores and removes one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
245
|
+
yield serve();
|
|
246
|
+
yield (0, loopbackRequest_1.default)(base)
|
|
247
|
+
.post('/wd/hub/session')
|
|
248
|
+
.set(hubSessionToken_1.HUB_TOKEN_HEADER, 'anything')
|
|
249
|
+
.send(caps())
|
|
250
|
+
.expect(200);
|
|
251
|
+
(0, chai_1.expect)(lifecycle.prepareSession.firstCall.args[1]).to.deep.equal({});
|
|
252
|
+
(0, chai_1.expect)(routeHits[0].headers[hubSessionToken_1.HUB_TOKEN_HEADER]).to.equal(undefined);
|
|
253
|
+
}));
|
|
254
|
+
});
|
|
255
|
+
});
|