@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.
Files changed (279) hide show
  1. package/README.md +2 -1
  2. package/lib/package.json +1 -2
  3. package/lib/public/assets/{AnnotationOverlay-BmTPCAds.js → AnnotationOverlay-C-Pw_hvw.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-B5eB7f8B.js → ApiKeyGate-DmYQ0oSk.js} +1 -1
  5. package/lib/public/assets/BugReportButton-n8T1dFUp.js +10 -0
  6. package/lib/public/assets/{DeviceMosaicView-9DUoZnQi.js → DeviceMosaicView-D-rGOBbJ.js} +1 -1
  7. package/lib/public/assets/{EmptyState-lvyNiXev.js → EmptyState-DExETevp.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BduMPM9K.js → FieldGroup-CCdFlZm6.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-BSqX6I94.js → FilterMenu-DiUSOSL-.js} +1 -1
  10. package/lib/public/assets/{Menu-Ba5cT7jZ.js → Menu-ZOfpH6gh.js} +1 -1
  11. package/lib/public/assets/{Modal-CceoMY7D.js → Modal-CZNtoB-1.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-6T9dOidc.js → RecordingPage-BKp6h8Jl.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-qj6FdZXR.js → RecordingsPage-Bf881FX4.js} +1 -1
  14. package/lib/public/assets/{SegmentedControl-CSRuZ8TR.js → SegmentedControl-EUoyizvW.js} +1 -1
  15. package/lib/public/assets/{SettingCard-Bp1kz4By.js → SettingCard-ClT4hf4_.js} +1 -1
  16. package/lib/public/assets/{Table-LV7zFyZg.js → Table-ucR4bnmc.js} +1 -1
  17. package/lib/public/assets/{activity-D17TUloi.js → activity-K-hdylwq.js} +1 -1
  18. package/lib/public/assets/{ai-settings-qvbhY89P.js → ai-settings-BLhdwI3c.js} +1 -1
  19. package/lib/public/assets/{api-keys-BfpvdG7G.js → api-keys-BbiN7rBI.js} +1 -1
  20. package/lib/public/assets/{apps-Dbnzd0fW.js → apps-Dez1petz.js} +1 -1
  21. package/lib/public/assets/{arrow-left-DD1VjtC7.js → arrow-left-UjmFHLAJ.js} +1 -1
  22. package/lib/public/assets/{arrow-right-D0pnM3ma.js → arrow-right-BVyEigbu.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-BIC8JOV6.js → arrow-up-right-B-ZlBof3.js} +1 -1
  24. package/lib/public/assets/{auth-shell-DWHPXSnI.js → auth-shell-DAQjAgUv.js} +2 -2
  25. package/lib/public/assets/{builds-page-ClCNBUv_.js → builds-page-CmIPa5JK.js} +1 -1
  26. package/lib/public/assets/{button-xtAhYR-f.js → button-Dd-IhMC1.js} +1 -1
  27. package/lib/public/assets/{calendar-CupwJ7Bm.js → calendar-CAIeJwa5.js} +1 -1
  28. package/lib/public/assets/{check-CyuEoKkd.js → check-C4Jrjg8f.js} +1 -1
  29. package/lib/public/assets/{chevron-left-DgX-F9xK.js → chevron-left-BGzlf-4i.js} +1 -1
  30. package/lib/public/assets/{chevron-right-BKJgJnU9.js → chevron-right-_wYqDjDC.js} +1 -1
  31. package/lib/public/assets/{circle-check-CZdg8mMa.js → circle-check-Ckn00yMB.js} +1 -1
  32. package/lib/public/assets/{circle-x-CZH6t9z-.js → circle-x-pdVkomLa.js} +1 -1
  33. package/lib/public/assets/{clock-Cd-t7xpe.js → clock-B8vmwkHj.js} +1 -1
  34. package/lib/public/assets/{copy-C6zerX3I.js → copy-BAqnFI-F.js} +1 -1
  35. package/lib/public/assets/{device-explorer-B1pI6b_M.js → device-explorer-CYq_4F_N.js} +3 -3
  36. package/lib/public/assets/device-explorer-D2AqrrLN.css +1 -0
  37. package/lib/public/assets/{download-x9WP7mcL.js → download-B6UEAp_1.js} +1 -1
  38. package/lib/public/assets/{forgot-password-B0TEH4N-.js → forgot-password-CLqNvmrN.js} +1 -1
  39. package/lib/public/assets/{funnel-Cb40MfFN.js → funnel-Da90BBbT.js} +1 -1
  40. package/lib/public/assets/{index-BR1wTIPC.js → index-BuIIcmGp.js} +2 -2
  41. package/lib/public/assets/{index-BvH_ekRy.js → index-MX7IzqyW.js} +1 -1
  42. package/lib/public/assets/{input-DejcCiTI.js → input-Bq9aY-We.js} +1 -1
  43. package/lib/public/assets/{keycodes-Mavx5iGg.js → keycodes-CIhXIJyP.js} +1 -1
  44. package/lib/public/assets/{layers-BKqYLiHE.js → layers-bXjpswQI.js} +1 -1
  45. package/lib/public/assets/{lock-LFsFhc17.js → lock-pJ72y5Fy.js} +1 -1
  46. package/lib/public/assets/{login-x2pkzt-t.js → login-UOcHALV2.js} +1 -1
  47. package/lib/public/assets/{maintenance-settings-CYafcoV-.js → maintenance-settings-XnJgp5yb.js} +1 -1
  48. package/lib/public/assets/{monitor-_7_uJ8d9.js → monitor-QkAhI1Jx.js} +1 -1
  49. package/lib/public/assets/{mouse-pointer-2-BfLuURKN.js → mouse-pointer-2-B42Da0m4.js} +1 -1
  50. package/lib/public/assets/{network-Z_zBhpsD.js → network-CKVqcyp6.js} +1 -1
  51. package/lib/public/assets/{overview-BY5uv08S.js → overview-BORGkN3N.js} +1 -1
  52. package/lib/public/assets/{page-header-CP2UkykG.js → page-header-CKWFOjOI.js} +1 -1
  53. package/lib/public/assets/{plus-BTD0dnQ0.js → plus-BpFKVdC8.js} +1 -1
  54. package/lib/public/assets/{profile-page-BhJZuCS7.js → profile-page-CcbKBicY.js} +1 -1
  55. package/lib/public/assets/{reset-password-CMN0SO81.js → reset-password-BA0W9bxl.js} +1 -1
  56. package/lib/public/assets/{runbook-page-CBpPX9_5.js → runbook-page-Cp7ZBq3s.js} +1 -1
  57. package/lib/public/assets/{select-Uc9E5pye.js → select-DIZWIoVR.js} +1 -1
  58. package/lib/public/assets/{selector-detail-page-BSyBnRuU.js → selector-detail-page-C_4YuvAp.js} +1 -1
  59. package/lib/public/assets/{selector-health-DzqxNkzm.js → selector-health-CXjD_Xth.js} +1 -1
  60. package/lib/public/assets/{selector-health-page-CyPLjoD1.js → selector-health-page-CTqIHeoG.js} +1 -1
  61. package/lib/public/assets/{session-detail-page-BZQZN1P-.js → session-detail-page-B0KYkk8z.js} +1 -1
  62. package/lib/public/assets/{settings-CkSWiZq8.js → settings-BcRBKUXd.js} +1 -1
  63. package/lib/public/assets/{status-pill-outline-BFYni2qo.js → status-pill-outline-Dh2PXSf_.js} +1 -1
  64. package/lib/public/assets/{tablet-BSd-xSZK.js → tablet-_sXqZztt.js} +1 -1
  65. package/lib/public/assets/{teams-PpoEQz31.js → teams-DGHT8MKm.js} +1 -1
  66. package/lib/public/assets/{trash-2-BorJv5_u.js → trash-2-BV88cV3Z.js} +1 -1
  67. package/lib/public/assets/{upload-C3i7sAZl.js → upload-CIDH9wkj.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-CeZQ3EsH.js → use-password-reset-mode-DYGuSNwl.js} +1 -1
  69. package/lib/public/assets/{users-CEJ3fC78.js → users-7yviqflc.js} +1 -1
  70. package/lib/public/assets/{users-Drd7ifJx.js → users-BUE7MoTq.js} +1 -1
  71. package/lib/public/assets/{video-off-CNzn6bPE.js → video-off-BTRxtlGo.js} +1 -1
  72. package/lib/public/assets/{webhook-settings-BFqX6QXV.js → webhook-settings-DMKD9Pgm.js} +1 -1
  73. package/lib/public/assets/{zap-4yN8Jdfw.js → zap-CWE94qcK.js} +1 -1
  74. package/lib/public/index.html +1 -1
  75. package/lib/src/InternalHttpClient.js +5 -1
  76. package/lib/src/XenonCapabilityManager.js +15 -1
  77. package/lib/src/app/appiumBasePath.js +32 -0
  78. package/lib/src/app/index.js +12 -0
  79. package/lib/src/app/insertBeforeRoutes.js +24 -4
  80. package/lib/src/app/registerCommandAuth.js +152 -33
  81. package/lib/src/app/routers/control.js +57 -13
  82. package/lib/src/app/routers/grid.js +28 -8
  83. package/lib/src/app/ws/h264StreamWs.js +13 -8
  84. package/lib/src/app/ws/logcatWs.js +12 -7
  85. package/lib/src/app/ws/upgradeRouter.js +215 -0
  86. package/lib/src/dashboard/event-manager.js +7 -6
  87. package/lib/src/data-service/device-service.js +152 -49
  88. package/lib/src/data-service/device-store.js +81 -10
  89. package/lib/src/data-service/deviceClaims.js +98 -0
  90. package/lib/src/data-service/deviceFieldOwners.js +32 -10
  91. package/lib/src/data-service/prisma-store.js +94 -31
  92. package/lib/src/device-managers/AndroidDeviceManager.js +13 -15
  93. package/lib/src/device-managers/HealthMonitorService.js +17 -2
  94. package/lib/src/device-managers/IOSDeviceManager.js +5 -1
  95. package/lib/src/device-managers/NodeDevices.js +7 -2
  96. package/lib/src/device-managers/android/AndroidH264StreamService.js +3 -3
  97. package/lib/src/device-managers/android/AndroidStreamService.js +6 -5
  98. package/lib/src/device-managers/android/previewHold.js +2 -2
  99. package/lib/src/device-managers/ios/IOSDiscoveryService.js +19 -8
  100. package/lib/src/device-managers/ios/IOSStreamService.js +158 -38
  101. package/lib/src/device-managers/localDeviceHosts.js +86 -0
  102. package/lib/src/device-managers/ownDeviceRow.js +38 -0
  103. package/lib/src/device-utils.js +78 -24
  104. package/lib/src/gateway/createHandoff.js +73 -0
  105. package/lib/src/gateway/defaultGateway.js +80 -0
  106. package/lib/src/gateway/forwardToNode.js +147 -0
  107. package/lib/src/gateway/hubSessionToken.js +356 -0
  108. package/lib/src/gateway/internalCall.js +90 -0
  109. package/lib/src/gateway/nodeSessionStatus.js +140 -0
  110. package/lib/src/gateway/nodeWebDriverUrl.js +139 -0
  111. package/lib/src/gateway/sessionCreate.js +136 -0
  112. package/lib/src/gateway/sessionGateway.js +201 -0
  113. package/lib/src/gateway/sessionLocator.js +100 -0
  114. package/lib/src/generated/client/edge.js +7 -4
  115. package/lib/src/generated/client/index-browser.js +4 -1
  116. package/lib/src/generated/client/index.d.ts +112 -1
  117. package/lib/src/generated/client/index.js +7 -4
  118. package/lib/src/generated/client/package.json +1 -1
  119. package/lib/src/generated/client/schema.prisma +9 -0
  120. package/lib/src/generated/client/wasm.js +4 -1
  121. package/lib/src/helpers/index.js +5 -1
  122. package/lib/src/logger.js +5 -0
  123. package/lib/src/middleware/authMiddleware.js +80 -0
  124. package/lib/src/middleware/commandAuth.js +31 -17
  125. package/lib/src/middleware/csrfMiddleware.js +3 -2
  126. package/lib/src/middleware/sessionListingFilter.js +153 -0
  127. package/lib/src/middleware/sessionUpgradeGuard.js +203 -0
  128. package/lib/src/plugin.js +27 -1
  129. package/lib/src/prisma.js +2 -1
  130. package/lib/src/services/DeviceReconciler.js +5 -2
  131. package/lib/src/services/OrphanSweeper.js +12 -19
  132. package/lib/src/services/PortAllocator.js +75 -16
  133. package/lib/src/services/ServerManager.js +50 -30
  134. package/lib/src/services/SessionLifecycleService.js +335 -76
  135. package/lib/src/services/ShutdownCoordinator.js +7 -1
  136. package/lib/src/services/SocketServer.js +11 -2
  137. package/lib/src/services/device-access/LiveSessionOwners.js +53 -0
  138. package/lib/src/services/device-access/SessionOwnerResolver.js +74 -0
  139. package/lib/src/services/lease/LeaseOrphanSweeper.js +32 -6
  140. package/lib/src/services/lease/LeaseService.js +19 -5
  141. package/lib/src/services/session/nodeCreateCaps.js +55 -0
  142. package/lib/src/services/session/sessionCredentials.js +7 -59
  143. package/lib/src/sessions/CloudSession.js +12 -0
  144. package/lib/src/sessions/LocalSession.js +52 -26
  145. package/lib/src/sessions/RemoteSession.js +116 -9
  146. package/lib/src/sessions/SessionManager.js +49 -6
  147. package/lib/src/sessions/appiumUmbrella.js +32 -0
  148. package/lib/test/helpers/appium-umbrella.js +84 -0
  149. package/lib/test/helpers/fake-device-table.js +97 -0
  150. package/lib/test/helpers/loki-stores.js +41 -0
  151. package/lib/test/helpers/loopbackRequest.js +77 -0
  152. package/lib/test/helpers/scratch-database.js +100 -0
  153. package/lib/test/helpers/scratch-port-leases.js +119 -0
  154. package/lib/test/helpers/test-container.js +35 -27
  155. package/lib/test/integration/androidDevices.spec.js +5 -4
  156. package/lib/test/integration/ios/01iOSSimulator.spec.js +6 -5
  157. package/lib/test/integration/ios/02iOSDevices.spec.js +3 -2
  158. package/lib/test/integration/prisma-device-store-add-devices.spec.js +14 -4
  159. package/lib/test/unit/AndroidDeviceManager.spec.js +8 -1
  160. package/lib/test/unit/DeviceModel.spec.js +9 -9
  161. package/lib/test/unit/HealingCascade.test.js +5 -1
  162. package/lib/test/unit/OrphanSweeper.test.js +17 -13
  163. package/lib/test/unit/SessionHeartbeatWrite.test.js +6 -7
  164. package/lib/test/unit/StreamTicketService.spec.js +4 -1
  165. package/lib/test/unit/TokenEndpoint.spec.js +17 -3
  166. package/lib/test/unit/aggregate-hotspots-session-scope.spec.js +5 -5
  167. package/lib/test/unit/android-h264-service.spec.js +6 -0
  168. package/lib/test/unit/android-stream-stall-eof.spec.js +10 -0
  169. package/lib/test/unit/apps-team-visibility.spec.js +16 -16
  170. package/lib/test/unit/auth-me-auth-disabled.test.js +7 -4
  171. package/lib/test/unit/authMiddleware.appTicket.spec.js +5 -5
  172. package/lib/test/unit/authMiddleware.bearer.spec.js +4 -1
  173. package/lib/test/unit/authMiddleware.mcpAudience.spec.js +4 -1
  174. package/lib/test/unit/authMiddleware.streamTicket.spec.js +19 -7
  175. package/lib/test/unit/authMiddleware.test.js +3 -0
  176. package/lib/test/unit/bug-report/route.spec.js +9 -9
  177. package/lib/test/unit/cliargs-admin-only.spec.js +3 -3
  178. package/lib/test/unit/clipboard-route.spec.js +18 -8
  179. package/lib/test/unit/command-auth-appium-server.spec.js +187 -32
  180. package/lib/test/unit/command-auth.spec.js +31 -31
  181. package/lib/test/unit/control-device-lookup.spec.js +9 -9
  182. package/lib/test/unit/control-forward-decision.spec.js +115 -0
  183. package/lib/test/unit/control-lookup-failure.spec.js +3 -3
  184. package/lib/test/unit/create-error-message.spec.js +43 -0
  185. package/lib/test/unit/create-handoff.spec.js +86 -0
  186. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  187. package/lib/test/unit/device-access-guard.spec.js +28 -28
  188. package/lib/test/unit/device-service.spec.js +4 -0
  189. package/lib/test/unit/device-store-clear-at-boot.spec.js +48 -0
  190. package/lib/test/unit/device-sync-lost-update.spec.js +17 -89
  191. package/lib/test/unit/device-team-guard.spec.js +20 -20
  192. package/lib/test/unit/device-utils.spec.js +8 -0
  193. package/lib/test/unit/extract-team-cap.spec.js +65 -0
  194. package/lib/test/unit/grid-admin-only.spec.js +3 -3
  195. package/lib/test/unit/healing-tier-gate.spec.js +5 -1
  196. package/lib/test/unit/health-monitor-own-phones.spec.js +102 -0
  197. package/lib/test/unit/health-monitor-stream-lookup.spec.js +11 -1
  198. package/lib/test/unit/helpers.spec.js +27 -0
  199. package/lib/test/unit/hub-boot-sweep-recovery.spec.js +192 -0
  200. package/lib/test/unit/hub-create-once.spec.js +190 -0
  201. package/lib/test/unit/hub-create-session-umbrella.spec.js +160 -0
  202. package/lib/test/unit/hub-gateway-create-appium-server.spec.js +591 -0
  203. package/lib/test/unit/hub-node-control-forward.spec.js +291 -0
  204. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +594 -0
  205. package/lib/test/unit/hub-node-race.spec.js +401 -0
  206. package/lib/test/unit/hub-node-same-host.spec.js +356 -0
  207. package/lib/test/unit/hub-register-removal.spec.js +165 -0
  208. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +277 -0
  209. package/lib/test/unit/hub-routed-session-recovery.spec.js +151 -0
  210. package/lib/test/unit/hub-session-token.spec.js +265 -0
  211. package/lib/test/unit/insert-before-routes.spec.js +5 -5
  212. package/lib/test/unit/install-repository-app-team.spec.js +2 -2
  213. package/lib/test/unit/interceptor/MitmProxyHost.spec.js +23 -10
  214. package/lib/test/unit/internal-call.spec.js +181 -0
  215. package/lib/test/unit/ios-discovery-same-host.spec.js +193 -0
  216. package/lib/test/unit/ios-stream-appium-tunnel.spec.js +317 -0
  217. package/lib/test/unit/ios-stream-port-leases.spec.js +299 -0
  218. package/lib/test/unit/ios-wda-status-identity.spec.js +84 -0
  219. package/lib/test/unit/is-xenon-running.spec.js +69 -0
  220. package/lib/test/unit/lease/LeaseOrphanSweeper.spec.js +2 -2
  221. package/lib/test/unit/lease/LeaseService.spec.js +28 -1
  222. package/lib/test/unit/lease/lease-reap-held-phone.spec.js +154 -0
  223. package/lib/test/unit/lease/lease-sweeper-expiry.spec.js +92 -0
  224. package/lib/test/unit/lease/lease-team-visibility.spec.js +2 -2
  225. package/lib/test/unit/lease/lease-token-log-filter.spec.js +242 -64
  226. package/lib/test/unit/lease/lease-token-oracle.spec.js +20 -7
  227. package/lib/test/unit/lease/session-lease-proof.spec.js +8 -3
  228. package/lib/test/unit/logcat-ws.spec.js +4 -1
  229. package/lib/test/unit/loopback-request.spec.js +81 -0
  230. package/lib/test/unit/node-create-caps.spec.js +54 -0
  231. package/lib/test/unit/node-gateway-create-appium-server.spec.js +354 -0
  232. package/lib/test/unit/node-hub-session-owner.spec.js +223 -0
  233. package/lib/test/unit/omni-vision.spec.js +7 -3
  234. package/lib/test/unit/plugin.spec.js +45 -4
  235. package/lib/test/unit/port-allocator-os-probe.spec.js +151 -0
  236. package/lib/test/unit/profile-router.test.js +11 -11
  237. package/lib/test/unit/queue-requester.spec.js +5 -0
  238. package/lib/test/unit/queue-team-scope-routes.spec.js +17 -16
  239. package/lib/test/unit/queue-visibility.spec.js +1 -1
  240. package/lib/test/unit/recording-downloads-visibility.spec.js +2 -2
  241. package/lib/test/unit/recording-instrumentation.spec.js +5 -0
  242. package/lib/test/unit/recording-source-ended.spec.js +5 -0
  243. package/lib/test/unit/recording-store.spec.js +9 -3
  244. package/lib/test/unit/recordings-library-routes.spec.js +51 -51
  245. package/lib/test/unit/recordings-write-visibility.spec.js +2 -2
  246. package/lib/test/unit/remote-session-create.spec.js +130 -0
  247. package/lib/test/unit/reservation-team-visibility.spec.js +5 -5
  248. package/lib/test/unit/reset-link.test.js +14 -14
  249. package/lib/test/unit/session-app-resolution.spec.js +14 -10
  250. package/lib/test/unit/session-assets.spec.js +9 -9
  251. package/lib/test/unit/session-attribution-wiring.spec.js +29 -2
  252. package/lib/test/unit/session-create-layer.spec.js +255 -0
  253. package/lib/test/unit/session-credential-strip.spec.js +25 -9
  254. package/lib/test/unit/session-credentials.spec.js +0 -47
  255. package/lib/test/unit/session-listing-auth.spec.js +345 -0
  256. package/lib/test/unit/session-locator.spec.js +250 -0
  257. package/lib/test/unit/session-node-calls.spec.js +174 -0
  258. package/lib/test/unit/session-owner-resolver.spec.js +107 -0
  259. package/lib/test/unit/session-recovery-routing.spec.js +118 -0
  260. package/lib/test/unit/session-upgrade-guard.spec.js +571 -0
  261. package/lib/test/unit/shutdown-drain-remote.spec.js +58 -0
  262. package/lib/test/unit/socketServer.bearer.spec.js +3 -1
  263. package/lib/test/unit/stream-leave-route.spec.js +15 -7
  264. package/lib/test/unit/stream-own-device-row.spec.js +164 -0
  265. package/lib/test/unit/stream-stop-ownership.spec.js +13 -5
  266. package/lib/test/unit/stream-stop-recording.spec.js +13 -5
  267. package/lib/test/unit/stream-ticket-identity.spec.js +15 -5
  268. package/lib/test/unit/upgrade-router.spec.js +446 -0
  269. package/lib/test/unit/users-router.test.js +11 -8
  270. package/lib/test/unit/verify-credential.spec.js +4 -1
  271. package/lib/test/unit/webdriver-info-route.spec.js +64 -0
  272. package/lib/test/unit/ws-upgrade-routing-appium-server.spec.js +500 -0
  273. package/lib/tsconfig.tsbuildinfo +1 -1
  274. package/package.json +1 -2
  275. package/prisma/migrations/20260929120000_device_claims/migration.sql +8 -0
  276. package/prisma/schema.prisma +9 -0
  277. package/lib/public/assets/BugReportButton-DKYsOdGV.js +0 -10
  278. package/lib/public/assets/device-explorer-DV8rWRxL.css +0 -1
  279. 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 supertest_1 = __importDefault(require("supertest"));
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, supertest_1.default)(app).post(`/control/${PHONE_A}/tap`);
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, supertest_1.default)(app).get(`/control/${PHONE_A}/clipboard`);
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, supertest_1.default)(app).get(`/control/${PHONE_A}/screenshot`);
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, supertest_1.default)(hidden.app).post(`/control/${PHONE_B}/tap`);
93
+ yield (0, loopbackRequest_1.default)(hidden.app).post(`/control/${PHONE_B}/tap`);
94
94
  const unknown = appWith(ALICE);
95
- yield (0, supertest_1.default)(unknown.app).post(`/control/${UNKNOWN}/tap`);
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, supertest_1.default)(app).post(`/control/${PHONE_B}/tap`);
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, supertest_1.default)(app).post(`/control/${PHONE_A}/tap`);
108
- yield (0, supertest_1.default)(app).post(`/control/${PHONE_A}/tap`);
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 supertest_1 = __importDefault(require("supertest"));
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, supertest_1.default)(buildApp())
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, supertest_1.default)(buildApp())
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 supertest_1 = __importDefault(require("supertest"));
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, supertest_1.default)(app)
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, supertest_1.default)(app).put('/device/phone-a/team').send({ teamId: 'team-c' });
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 supertest_1 = __importDefault(require("supertest"));
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, supertest_1.default)(appWith({ actorUserId: ALICE })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: BOB })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/screenshot`);
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, supertest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/clipboard`);
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, supertest_1.default)(appWith({ actorUserId: ALICE })).get(`/control/${UDID}/clipboard`);
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, supertest_1.default)(appWith({ actorUserId: BOB, scopes: 'admin' })).get(`/control/${UDID}/clipboard`);
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, supertest_1.default)(appWith({ actorUserId: BOB, busy: false })).get(`/control/${UDID}/clipboard`);
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, supertest_1.default)(appWith({ actorUserId: BOB, sessionId: 'appium-1', sessionOwner: ALICE })).get(`/control/${UDID}/clipboard`);
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, supertest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/logs`);
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, supertest_1.default)(appWith({ actorUserId: BOB, sessionId: 'appium-1', sessionOwner: ALICE })).get(`/control/${UDID}/logs`);
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, supertest_1.default)(appWith({ actorUserId: ALICE })).get(`/control/${UDID}/logs`);
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, supertest_1.default)(appWith({ actorUserId: BOB, scopes: 'admin' })).get(`/control/${UDID}/logs`);
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, supertest_1.default)(appWith({ actorUserId: BOB, busy: false })).get(`/control/${UDID}/logs`);
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, supertest_1.default)(appWith({ actorUserId: BOB })).get(`/control/${UDID}/screenshot`);
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, supertest_1.default)(app).post(`/control/${UDID}/${p}`);
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, supertest_1.default)(appWith({ actorUserId: BOB, scopes: 'admin' })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: BOB, role: 'SUPER_ADMIN' })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: BOB, scopes: 'nonadmin,devices' })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({})).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: BOB, sessionId: 'appium-1', sessionOwner: ALICE })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: ALICE, sessionId: 'appium-1', sessionOwner: ALICE })).post(`/control/${UDID}/tap`);
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, supertest_1.default)(app).post(`/control/${UDID}/tap`);
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, supertest_1.default)(appWith({ actorUserId: ALICE })).post('/control/100%/tap');
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, supertest_1.default)(appWith({
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, supertest_1.default)(appWith({
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, supertest_1.default)(appWith({
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, supertest_1.default)(appWith({ actorUserId: BOB })).post(`/control/${UDID}/Stream/Start`);
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
+ });