@xenon-device-management/xenon 2.0.0 → 2.1.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 +1 -2
- package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
- package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
- package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
- package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
- package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
- package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
- package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
- package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
- package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
- package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
- package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
- package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
- package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
- package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
- package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
- package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
- package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
- package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
- package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
- package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
- package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
- package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
- package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
- package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
- package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
- package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
- package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
- package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
- package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
- package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
- package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
- package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
- package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
- package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
- package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
- package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
- package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
- package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
- package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
- package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
- package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
- package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
- package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
- package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
- package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
- package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
- package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
- package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
- package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
- package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
- package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
- package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
- package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
- package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
- package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
- package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
- package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
- package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
- package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
- package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
- package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
- package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
- package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
- package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
- package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.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 +57 -13
- package/lib/src/app/routers/grid.js +28 -8
- package/lib/src/app/ws/h264StreamWs.js +13 -8
- package/lib/src/app/ws/logcatWs.js +12 -7
- 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 +81 -10
- package/lib/src/data-service/deviceClaims.js +98 -0
- package/lib/src/data-service/deviceFieldOwners.js +32 -10
- package/lib/src/data-service/prisma-store.js +94 -31
- package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
- 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 +3 -3
- package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
- 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 +158 -38
- 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/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 +50 -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/lease/LeaseOrphanSweeper.js +32 -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/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/unit/AndroidDeviceManager.spec.js +8 -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 +6 -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 +18 -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 +28 -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 +291 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
- package/lib/test/unit/hub-node-race.spec.js +401 -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 +2 -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 +92 -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 +4 -1
- package/lib/test/unit/loopback-request.spec.js +81 -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/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 +15 -7
- package/lib/test/unit/stream-own-device-row.spec.js +164 -0
- package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
- package/lib/test/unit/stream-stop-recording.spec.js +13 -5
- package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
- 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 +1 -2
- 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
|
@@ -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 InternalHttpClient_1 = require("../../src/InternalHttpClient");
|
|
25
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
26
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
27
|
+
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
28
|
+
const container_registration_1 = require("../helpers/container-registration");
|
|
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
|
+
let manager;
|
|
68
|
+
let post;
|
|
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
|
+
post = sinon_1.default.stub(InternalHttpClient_1.InternalHttpClient, 'post').resolves({ data: {} });
|
|
84
|
+
});
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
Object.assign(context, savedContext);
|
|
87
|
+
sinon_1.default.restore();
|
|
88
|
+
restore();
|
|
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
|
+
(0, chai_1.expect)(post.called, 'never forwarded to itself').to.equal(false);
|
|
100
|
+
}));
|
|
101
|
+
it('forwards a node’s phone once, never retried', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
+
device = {
|
|
103
|
+
udid: 'node-phone',
|
|
104
|
+
host: 'http://10.0.0.9:4725',
|
|
105
|
+
nodeId: 'node-9',
|
|
106
|
+
platform: 'android',
|
|
107
|
+
};
|
|
108
|
+
const res = yield tap('node-phone', '192.168.0.104:4723');
|
|
109
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
110
|
+
(0, chai_1.expect)(manager.taps).to.deep.equal([]);
|
|
111
|
+
(0, chai_1.expect)(post.calledOnce).to.equal(true);
|
|
112
|
+
(0, chai_1.expect)(post.firstCall.args[0]).to.equal('http://10.0.0.9:4725/xenon/api/control/node-phone/tap');
|
|
113
|
+
(0, chai_1.expect)(post.firstCall.args[2]).to.include({ retry: false });
|
|
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']);
|
|
@@ -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 deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
|
|
20
20
|
const UDID = 'DEV-1';
|
|
21
21
|
const ALICE = 'usr_alice';
|
|
@@ -66,19 +66,19 @@ function appWith(opts) {
|
|
|
66
66
|
}
|
|
67
67
|
describe('deviceAccessGuard', () => {
|
|
68
68
|
it('lets the lock owner through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
69
|
-
const res = yield (0,
|
|
69
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: ALICE })).post(`/control/${UDID}/tap`);
|
|
70
70
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
71
71
|
(0, chai_1.expect)(res.body.reached).to.equal(true);
|
|
72
72
|
}));
|
|
73
73
|
it('denies a foreign holder with 409 and names them', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
-
const res = yield (0,
|
|
74
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB })).post(`/control/${UDID}/tap`);
|
|
75
75
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
76
76
|
(0, chai_1.expect)(res.body.error).to.equal('device_held_by_another_user');
|
|
77
77
|
(0, chai_1.expect)(res.body.message).to.contain('alice@example.com');
|
|
78
78
|
(0, chai_1.expect)(res.body.holder).to.deep.equal({ userId: ALICE, name: 'alice@example.com' });
|
|
79
79
|
}));
|
|
80
80
|
it('never blocks a GET', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
-
const res = yield (0,
|
|
81
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/screenshot`);
|
|
82
82
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
83
83
|
}));
|
|
84
84
|
// The clipboard is the one read that is credential-shaped: it routinely holds
|
|
@@ -88,26 +88,26 @@ describe('deviceAccessGuard', () => {
|
|
|
88
88
|
// reading a stranger's clipboard, so it takes the ownership check despite
|
|
89
89
|
// being a GET.
|
|
90
90
|
it('blocks reading the clipboard of a device another user holds', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
-
const res = yield (0,
|
|
91
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/clipboard`);
|
|
92
92
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
93
93
|
(0, chai_1.expect)(res.body.error).to.equal('device_held_by_another_user');
|
|
94
94
|
(0, chai_1.expect)(JSON.stringify(res.body)).to.not.contain('hunter2');
|
|
95
95
|
}));
|
|
96
96
|
it('lets the holder read their own clipboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
-
const res = yield (0,
|
|
97
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: ALICE })).get(`/control/${UDID}/clipboard`);
|
|
98
98
|
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
99
99
|
(0, chai_1.expect)(res.body.content).to.equal('hunter2');
|
|
100
100
|
}));
|
|
101
101
|
it('lets an admin read the clipboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
-
const res = yield (0,
|
|
102
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, scopes: 'admin' })).get(`/control/${UDID}/clipboard`);
|
|
103
103
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
104
104
|
}));
|
|
105
105
|
it('allows the clipboard read when the device is free', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
106
|
-
const res = yield (0,
|
|
106
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, busy: false })).get(`/control/${UDID}/clipboard`);
|
|
107
107
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
108
108
|
}));
|
|
109
109
|
it('blocks the clipboard of a device running another user’s Appium session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
-
const res = yield (0,
|
|
110
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, sessionId: 'appium-1', sessionOwner: ALICE })).get(`/control/${UDID}/clipboard`);
|
|
111
111
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
112
112
|
(0, chai_1.expect)(res.body.error).to.equal('device_in_use_by_session');
|
|
113
113
|
}));
|
|
@@ -118,28 +118,28 @@ describe('deviceAccessGuard', () => {
|
|
|
118
118
|
// made that check decorative, since a reader refused at the socket could GET
|
|
119
119
|
// the identical bytes here.
|
|
120
120
|
it('blocks reading the logs of a device another user holds', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
-
const res = yield (0,
|
|
121
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/logs`);
|
|
122
122
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
123
123
|
(0, chai_1.expect)(res.body.error).to.equal('device_held_by_another_user');
|
|
124
124
|
(0, chai_1.expect)(JSON.stringify(res.body)).to.not.contain('SECRETLOGLINE');
|
|
125
125
|
}));
|
|
126
126
|
it('blocks the logs of a device running another user’s Appium session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
127
|
-
const res = yield (0,
|
|
127
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, sessionId: 'appium-1', sessionOwner: ALICE })).get(`/control/${UDID}/logs`);
|
|
128
128
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
129
129
|
(0, chai_1.expect)(res.body.error).to.equal('device_in_use_by_session');
|
|
130
130
|
(0, chai_1.expect)(JSON.stringify(res.body)).to.not.contain('SECRETLOGLINE');
|
|
131
131
|
}));
|
|
132
132
|
it('lets the holder read their own logs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
133
|
-
const res = yield (0,
|
|
133
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: ALICE })).get(`/control/${UDID}/logs`);
|
|
134
134
|
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
|
|
135
135
|
(0, chai_1.expect)(res.body.logs[0]).to.contain('SECRETLOGLINE');
|
|
136
136
|
}));
|
|
137
137
|
it('lets an admin read the logs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
-
const res = yield (0,
|
|
138
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, scopes: 'admin' })).get(`/control/${UDID}/logs`);
|
|
139
139
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
140
140
|
}));
|
|
141
141
|
it('allows the logs read when the device is free', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
142
|
-
const res = yield (0,
|
|
142
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, busy: false })).get(`/control/${UDID}/logs`);
|
|
143
143
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
144
144
|
}));
|
|
145
145
|
// Deliberately NOT extended to the other reads: screenshot and the MJPEG
|
|
@@ -147,39 +147,39 @@ describe('deviceAccessGuard', () => {
|
|
|
147
147
|
// decision for this guard was mutations-only. Clipboard and logs are the
|
|
148
148
|
// documented exceptions, so this pins the boundary.
|
|
149
149
|
it('does not extend the ownership check to other reads', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
-
const res = yield (0,
|
|
150
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/screenshot`);
|
|
151
151
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
152
152
|
}));
|
|
153
153
|
it('skips stream/start, stream/stop, stream/leave and stream/ticket', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
154
|
const app = appWith({ actorUserId: BOB });
|
|
155
155
|
for (const p of ['stream/start', 'stream/stop', 'stream/leave', 'stream/ticket']) {
|
|
156
|
-
const res = yield (0,
|
|
156
|
+
const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${UDID}/${p}`);
|
|
157
157
|
(0, chai_1.expect)(res.status, p).to.equal(200);
|
|
158
158
|
}
|
|
159
159
|
}));
|
|
160
160
|
it('lets an admin-scoped key through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
-
const res = yield (0,
|
|
161
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, scopes: 'admin' })).post(`/control/${UDID}/tap`);
|
|
162
162
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
163
163
|
}));
|
|
164
164
|
it('lets a SUPER_ADMIN through', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
165
|
-
const res = yield (0,
|
|
165
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, role: 'SUPER_ADMIN' })).post(`/control/${UDID}/tap`);
|
|
166
166
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
167
167
|
}));
|
|
168
168
|
it('does not treat a scope merely containing "admin" as admin', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
169
|
-
const res = yield (0,
|
|
169
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, scopes: 'nonadmin,devices' })).post(`/control/${UDID}/tap`);
|
|
170
170
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
171
171
|
}));
|
|
172
172
|
it('401s when there is no actor', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
173
|
-
const res = yield (0,
|
|
173
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({})).post(`/control/${UDID}/tap`);
|
|
174
174
|
(0, chai_1.expect)(res.status).to.equal(401);
|
|
175
175
|
}));
|
|
176
176
|
it('denies a foreign Appium session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
177
|
-
const res = yield (0,
|
|
177
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB, sessionId: 'appium-1', sessionOwner: ALICE })).post(`/control/${UDID}/tap`);
|
|
178
178
|
(0, chai_1.expect)(res.status).to.equal(409);
|
|
179
179
|
(0, chai_1.expect)(res.body.error).to.equal('device_in_use_by_session');
|
|
180
180
|
}));
|
|
181
181
|
it('allows the owner of the running Appium session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
-
const res = yield (0,
|
|
182
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: ALICE, sessionId: 'appium-1', sessionOwner: ALICE })).post(`/control/${UDID}/tap`);
|
|
183
183
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
184
184
|
}));
|
|
185
185
|
it('falls through to the handler when the device is unknown', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -198,7 +198,7 @@ describe('deviceAccessGuard', () => {
|
|
|
198
198
|
router.use((0, deviceAccessGuard_1.deviceAccessGuard)({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return null; }) }));
|
|
199
199
|
router.post('/:udid/tap', (_req, res) => res.status(404).send('Device not found'));
|
|
200
200
|
app.use('/control', router);
|
|
201
|
-
const res = yield (0,
|
|
201
|
+
const res = yield (0, loopbackRequest_1.default)(app).post(`/control/${UDID}/tap`);
|
|
202
202
|
(0, chai_1.expect)(res.status).to.equal(404);
|
|
203
203
|
}));
|
|
204
204
|
it('responds instead of hanging on a malformed-percent udid', function () {
|
|
@@ -209,13 +209,13 @@ describe('deviceAccessGuard', () => {
|
|
|
209
209
|
// this test's timeout well under the file default so a regression fails
|
|
210
210
|
// fast instead of silently eating the full 60s.
|
|
211
211
|
this.timeout(5000);
|
|
212
|
-
const res = yield (0,
|
|
212
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: ALICE })).post('/control/100%/tap');
|
|
213
213
|
(0, chai_1.expect)(res.status).to.equal(400);
|
|
214
214
|
(0, chai_1.expect)(res.body.success).to.equal(false);
|
|
215
215
|
});
|
|
216
216
|
});
|
|
217
217
|
it('fails closed with 503 when the device lookup throws, never reaching the handler', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
-
const res = yield (0,
|
|
218
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({
|
|
219
219
|
actorUserId: ALICE,
|
|
220
220
|
findDeviceImpl: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
221
221
|
throw new Error('store hiccup');
|
|
@@ -230,7 +230,7 @@ describe('deviceAccessGuard', () => {
|
|
|
230
230
|
(0, chai_1.expect)(res.body.reached).to.equal(undefined);
|
|
231
231
|
}));
|
|
232
232
|
it('fails closed with 503 when resolveSessionOwner throws, never reaching the handler', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
233
|
-
const res = yield (0,
|
|
233
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({
|
|
234
234
|
actorUserId: BOB,
|
|
235
235
|
sessionId: 'appium-1',
|
|
236
236
|
resolveSessionOwnerImpl: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -242,7 +242,7 @@ describe('deviceAccessGuard', () => {
|
|
|
242
242
|
(0, chai_1.expect)(res.body.reached).to.equal(undefined);
|
|
243
243
|
}));
|
|
244
244
|
it('still denies with 409 (no holder name) when describeHolder throws', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
245
|
-
const res = yield (0,
|
|
245
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({
|
|
246
246
|
actorUserId: BOB,
|
|
247
247
|
describeHolderImpl: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
248
248
|
throw new Error('lookup hiccup');
|
|
@@ -256,7 +256,7 @@ describe('deviceAccessGuard', () => {
|
|
|
256
256
|
(0, chai_1.expect)(res.body.holder.name).to.equal(undefined);
|
|
257
257
|
}));
|
|
258
258
|
it('skips a mixed-case allowlisted action like Stream/Start', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
259
|
-
const res = yield (0,
|
|
259
|
+
const res = yield (0, loopbackRequest_1.default)(appWith({ actorUserId: BOB })).post(`/control/${UDID}/Stream/Start`);
|
|
260
260
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
261
261
|
(0, chai_1.expect)(res.body.reached).to.equal(true);
|
|
262
262
|
}));
|
|
@@ -25,7 +25,10 @@ const db_1 = require("../../src/data-service/db");
|
|
|
25
25
|
const chai_1 = require("chai");
|
|
26
26
|
const semver_1 = __importDefault(require("semver"));
|
|
27
27
|
const test_container_1 = require("../helpers/test-container");
|
|
28
|
+
const loki_stores_1 = require("../helpers/loki-stores");
|
|
28
29
|
describe('Get device', () => {
|
|
30
|
+
// The in-memory store, also when this file runs on its own.
|
|
31
|
+
(0, loki_stores_1.useLokiStores)();
|
|
29
32
|
before('Set devices in memory', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
30
33
|
var _a, e_1, _b, _c;
|
|
31
34
|
yield (0, test_container_1.resetTestContainer)();
|
|
@@ -146,6 +149,7 @@ describe('Get device', () => {
|
|
|
146
149
|
finally { if (e_1) throw e_1.error; }
|
|
147
150
|
}
|
|
148
151
|
}));
|
|
152
|
+
after(() => (0, test_container_1.resetTestContainer)());
|
|
149
153
|
it('Get android device based on filter with minSDK', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
154
|
const filterOptions = {
|
|
151
155
|
platform: 'android',
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
require("reflect-metadata");
|
|
13
|
+
const chai_1 = require("chai");
|
|
14
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
15
|
+
/**
|
|
16
|
+
* clearStorage(onlyHosts) on the in-memory (Loki) store, the one the unit
|
|
17
|
+
* suite runs: a hub starting up forgets its own phones and keeps its nodes'.
|
|
18
|
+
* The Prisma store's side is in session-recovery-routing.spec.ts.
|
|
19
|
+
*/
|
|
20
|
+
describe('LokiDeviceStore.clearStorage(onlyHosts)', () => {
|
|
21
|
+
const HUB = 'http://clear-at-boot-hub:4724';
|
|
22
|
+
const NODE = 'http://clear-at-boot-hub:4725';
|
|
23
|
+
let savedStore;
|
|
24
|
+
let store;
|
|
25
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
savedStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
27
|
+
device_store_1.DeviceStoreFactory._deviceStore = undefined;
|
|
28
|
+
const env = process.env.NODE_ENV;
|
|
29
|
+
process.env.NODE_ENV = 'test';
|
|
30
|
+
store = device_store_1.DeviceStoreFactory.getStore();
|
|
31
|
+
process.env.NODE_ENV = env;
|
|
32
|
+
(0, chai_1.expect)(store.constructor.name).to.equal('LokiDeviceStore');
|
|
33
|
+
yield store.addDevices([
|
|
34
|
+
{ udid: 'clear-hub-phone', host: HUB, platform: 'android' },
|
|
35
|
+
{ udid: 'clear-node-phone', host: NODE, platform: 'android' },
|
|
36
|
+
]);
|
|
37
|
+
}));
|
|
38
|
+
afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
yield store.removeDevices({ udid: 'clear-hub-phone' });
|
|
40
|
+
yield store.removeDevices({ udid: 'clear-node-phone' });
|
|
41
|
+
device_store_1.DeviceStoreFactory._deviceStore = savedStore;
|
|
42
|
+
}));
|
|
43
|
+
it('deletes only the phones filed under the given hosts', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
yield store.clearStorage([HUB]);
|
|
45
|
+
(0, chai_1.expect)(yield store.findDevice({ udid: 'clear-hub-phone' })).to.equal(null);
|
|
46
|
+
(0, chai_1.expect)(yield store.findDevice({ udid: 'clear-node-phone' })).to.include({ host: NODE });
|
|
47
|
+
}));
|
|
48
|
+
});
|