@xenon-device-management/xenon 2.14.0 → 2.15.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 (259) hide show
  1. package/README.md +7 -4
  2. package/lib/package.json +5 -5
  3. package/lib/public/assets/{AnnotationOverlay-B_VNtKBH.js → AnnotationOverlay-Btq3dZRI.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-BwuaMjkX.js → ApiKeyGate-C9OoAgg_.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-DBC_xczB.js +2 -0
  6. package/lib/public/assets/{EmptyState-BSjhk_IV.js → EmptyState-DqWU2-vM.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-Cb5QZsE1.js → FieldGroup-CveQ3xrA.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BAaSlWbW.js → FilterMenu-BfKDSfHz.js} +1 -1
  9. package/lib/public/assets/{Menu-JGGYrSQQ.js → Menu-CtdCybzA.js} +1 -1
  10. package/lib/public/assets/{Modal-CFCp3n9d.js → Modal-Bsvj1tm2.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CbJj_EBT.js → RecordingPage-CRofOiyx.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-BJeYnygR.js → RecordingsPage-jNQHb2IR.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-Bg42Atjy.js → SegmentedControl-C8HtTha-.js} +1 -1
  14. package/lib/public/assets/{SettingCard-Njz1pUIJ.js → SettingCard-D8K1LZNo.js} +1 -1
  15. package/lib/public/assets/{Table-BcTm5xV2.js → Table-zNTX92Pn.js} +1 -1
  16. package/lib/public/assets/{activity-DOVdsfMQ.js → activity-CJ8LwZq9.js} +1 -1
  17. package/lib/public/assets/ai-settings-CgxQZqrc.js +16 -0
  18. package/lib/public/assets/{api-keys-BKnNSTw9.js → api-keys-ECpo-zTo.js} +1 -1
  19. package/lib/public/assets/{apps-D0SC6uw6.js → apps-BjGymyf3.js} +1 -1
  20. package/lib/public/assets/{arrow-left-MMbTmgoE.js → arrow-left-BWfCbeHP.js} +1 -1
  21. package/lib/public/assets/{arrow-right-BMfnGmxW.js → arrow-right-DC7UGf_n.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-DuAftMWB.js → arrow-up-right-BOjwybDk.js} +1 -1
  23. package/lib/public/assets/{auth-shell-nZC2JNDO.js → auth-shell-CxMTo9aH.js} +2 -2
  24. package/lib/public/assets/{builds-page-hDCgTwkS.js → builds-page-DOCm4HkA.js} +1 -1
  25. package/lib/public/assets/{button-Dda4irn_.js → button-Cg08-EMT.js} +1 -1
  26. package/lib/public/assets/{calendar-BMr0Q62v.js → calendar-CA1ikLJs.js} +1 -1
  27. package/lib/public/assets/{check-CZ9iw9-e.js → check-BMTaH72W.js} +1 -1
  28. package/lib/public/assets/{chevron-right-BRYJJVC9.js → chevron-right-B2QOv9Nx.js} +1 -1
  29. package/lib/public/assets/{circle-check-CqAJjQ5O.js → circle-check-CYXBoUqT.js} +1 -1
  30. package/lib/public/assets/{circle-x-DlDJtnVz.js → circle-x-DTwnSAnl.js} +1 -1
  31. package/lib/public/assets/{clock-CpuDKysU.js → clock-C8_HViQP.js} +1 -1
  32. package/lib/public/assets/{copy-CAQydNFe.js → copy-Be7LNMXV.js} +1 -1
  33. package/lib/public/assets/derive-UnLRp4r3.js +1 -0
  34. package/lib/public/assets/device-explorer-BOqtfGXz.js +282 -0
  35. package/lib/public/assets/{download-C9XG6lLv.js → download-B56lwxWP.js} +1 -1
  36. package/lib/public/assets/forgot-password-CCU6bzLn.js +1 -0
  37. package/lib/public/assets/{h264Stream-VQBphYhg.js → h264Stream-C-OtmIuk.js} +6 -6
  38. package/lib/public/assets/index-B9KQ6LsY.js +15 -0
  39. package/lib/public/assets/{index-BbKEXUFH.js → index-BOxe43p1.js} +2 -2
  40. package/lib/public/assets/{index-DJGc1M2f.js → index-CfxsUslp.js} +1 -1
  41. package/lib/public/assets/{input-Cm0tmSoc.js → input-CZHFjbSg.js} +1 -1
  42. package/lib/public/assets/{line-chart-D2DyJCml.js → line-chart-Gl_Dz3X_.js} +1 -1
  43. package/lib/public/assets/{list-checks-UWf1GY99.js → list-checks-CCgjeyGY.js} +1 -1
  44. package/lib/public/assets/{lock-CCJv9m_I.js → lock-CkOwgDcx.js} +1 -1
  45. package/lib/public/assets/{login-f8vOVW-U.js → login-D5cc4w7M.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-C1PrkuAH.js → maintenance-settings-t7qr3fvX.js} +1 -1
  47. package/lib/public/assets/{monitor-B2jDgwm7.js → monitor-DEJixKQO.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-DLI1RtzU.js → mouse-pointer-2-CtOrk4M8.js} +1 -1
  49. package/lib/public/assets/{network-ebT86qoi.js → network-Dii9qdvk.js} +1 -1
  50. package/lib/public/assets/{overview-C0aykVrd.js → overview-rolAACrO.js} +1 -1
  51. package/lib/public/assets/{page-header-BjzgWz-T.js → page-header-COQZxJfR.js} +1 -1
  52. package/lib/public/assets/{play-DLG9t8FU.js → play-DrN1Se_R.js} +1 -1
  53. package/lib/public/assets/{plus-nBFSMu_X.js → plus-5aq-9uRx.js} +1 -1
  54. package/lib/public/assets/{profile-page-CSl0UYlv.js → profile-page-6i5kY6kF.js} +1 -1
  55. package/lib/public/assets/{reset-password-LeOuHqmB.js → reset-password-sp2IBrc7.js} +1 -1
  56. package/lib/public/assets/runbook-page-DJDjeopU.js +253 -0
  57. package/lib/public/assets/{select-88jHcl-Z.js → select-Bpyn7rWO.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-BbXKJjZM.js → selector-detail-redirect-BH74vmKv.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-B4rA1Xkv.js → selector-health-page-Cw_g218H.js} +1 -1
  60. package/lib/public/assets/session-detail-page-AZJE6XXE.js +4 -0
  61. package/lib/public/assets/{settings-GF6ZMSQ6.js → settings-C3uyGhyc.js} +1 -1
  62. package/lib/public/assets/{settings-Dep_3kbA.css → settings-K8UHWMdI.css} +1 -1
  63. package/lib/public/assets/{stat-tile-BV98e_Sv.js → stat-tile-xVqAKIlI.js} +1 -1
  64. package/lib/public/assets/{tablet-BiAsnFDj.js → tablet-BxUScAeO.js} +1 -1
  65. package/lib/public/assets/{teams-B4-jrr-1.js → teams-B5ljGkY2.js} +1 -1
  66. package/lib/public/assets/{trash-2-C5RSuX0e.js → trash-2-B701i8sy.js} +1 -1
  67. package/lib/public/assets/{upload-BG7_rMXK.js → upload-C2gr44RS.js} +1 -1
  68. package/lib/public/assets/{use-builds-data-J-riQYyC.js → use-builds-data-NeQOFEoy.js} +1 -1
  69. package/lib/public/assets/{use-password-reset-mode-C8-eH-UG.js → use-password-reset-mode-DEzJq5ef.js} +1 -1
  70. package/lib/public/assets/{users-CFba5TSy.js → users-D_vv_yz4.js} +1 -1
  71. package/lib/public/assets/{users-Cp0beJm3.js → users-MY6eNe6s.js} +1 -1
  72. package/lib/public/assets/{video-off-DDsEISjV.js → video-off-OK1lNZQn.js} +1 -1
  73. package/lib/public/assets/{webhook-settings-BGu32vSB.js → webhook-settings-BU-C9lBf.js} +1 -1
  74. package/lib/public/assets/{zap-l4VaXzq8.js → zap-DO9ry1iz.js} +1 -1
  75. package/lib/public/index.html +1 -1
  76. package/lib/schema.json +5 -5
  77. package/lib/src/app/dashboardPluginLink.js +117 -0
  78. package/lib/src/app/index.js +13 -34
  79. package/lib/src/app/openapi/control.yaml +6 -4
  80. package/lib/src/app/openapi/grid.yaml +8 -2
  81. package/lib/src/app/openapi/identity.yaml +103 -31
  82. package/lib/src/app/openapi/platform.yaml +33 -15
  83. package/lib/src/app/openapi/sessions.yaml +10 -3
  84. package/lib/src/app/routers/apikeys.js +14 -2
  85. package/lib/src/app/routers/auth.js +29 -5
  86. package/lib/src/app/routers/control.js +13 -5
  87. package/lib/src/app/routers/dashboard.js +42 -27
  88. package/lib/src/app/routers/grid.js +5 -3
  89. package/lib/src/app/routers/profile.js +21 -0
  90. package/lib/src/app/routers/users.js +10 -5
  91. package/lib/src/app/swagger.js +2 -0
  92. package/lib/src/app/ws/h264StreamWs.js +10 -1
  93. package/lib/src/config.js +1 -0
  94. package/lib/src/dashboard/commands.js +5 -5
  95. package/lib/src/dashboard/event-manager.js +49 -61
  96. package/lib/src/dashboard/services/failure-analysis-service.js +136 -45
  97. package/lib/src/dashboard/sessionCommandSummary.js +39 -0
  98. package/lib/src/data-service/device-store.js +14 -1
  99. package/lib/src/data-service/prisma-store.js +19 -7
  100. package/lib/src/data-service/web-config-service.js +7 -0
  101. package/lib/src/device-managers/android/AndroidH264StreamService.js +69 -20
  102. package/lib/src/device-managers/android/H264Multiplexer.js +38 -4
  103. package/lib/src/device-utils.js +20 -2
  104. package/lib/src/gateway/defaultGateway.js +1 -1
  105. package/lib/src/gateway/forwardToNode.js +2 -0
  106. package/lib/src/gateway/healReport.js +87 -0
  107. package/lib/src/gateway/sessionGateway.js +12 -2
  108. package/lib/src/generated/client/edge.js +4 -3
  109. package/lib/src/generated/client/index-browser.js +1 -0
  110. package/lib/src/generated/client/index.d.ts +32 -0
  111. package/lib/src/generated/client/index.js +4 -3
  112. package/lib/src/generated/client/package.json +1 -1
  113. package/lib/src/generated/client/schema.prisma +3 -0
  114. package/lib/src/generated/client/wasm.js +1 -0
  115. package/lib/src/interceptors/CommandInterceptor.js +243 -181
  116. package/lib/src/middleware/authMiddleware.js +15 -3
  117. package/lib/src/middleware/commandCaller.js +3 -3
  118. package/lib/src/middleware/verifyCredential.js +58 -1
  119. package/lib/src/plugin.js +8 -3
  120. package/lib/src/services/AICommandService.js +9 -2
  121. package/lib/src/services/AIService.js +192 -42
  122. package/lib/src/services/EmailService.js +29 -0
  123. package/lib/src/services/OrphanSweeper.js +2 -0
  124. package/lib/src/services/SelectorVerificationJob.js +15 -4
  125. package/lib/src/services/ServerManager.js +11 -1
  126. package/lib/src/services/SessionLifecycleService.js +86 -26
  127. package/lib/src/services/SocketServer.js +19 -3
  128. package/lib/src/services/bug-report/video-slice.js +4 -1
  129. package/lib/src/services/device-access/leaseOverride.js +7 -5
  130. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +51 -0
  131. package/lib/src/services/healing/HealEtalonService.js +3 -2
  132. package/lib/src/services/healing/HealingOrchestrator.js +55 -72
  133. package/lib/src/services/healing/OcrHealingProvider.js +41 -64
  134. package/lib/src/services/healing/ResilioTreeHealingProvider.js +41 -50
  135. package/lib/src/services/healing/VisualAiHealingProvider.js +1 -1
  136. package/lib/src/services/healing/fingerprintIdentity.js +126 -0
  137. package/lib/src/services/healing/resilioPath.js +274 -0
  138. package/lib/src/services/logcat/SessionDeviceLogs.js +271 -0
  139. package/lib/src/services/logcat/deviceLogBook.js +163 -0
  140. package/lib/src/services/ocr/ocrData.js +118 -0
  141. package/lib/src/services/ocr/vendor/LICENSE-tessdata.txt +202 -0
  142. package/lib/src/services/ocr/vendor/eng.traineddata.gz +0 -0
  143. package/lib/src/services/omni-vision/OmniVisionService.js +83 -56
  144. package/lib/src/services/passwordResetLink.js +50 -9
  145. package/lib/src/services/passwordResetMode.js +3 -1
  146. package/lib/src/services/publicUrl.js +45 -0
  147. package/lib/src/services/session/sessionIdentity.js +5 -3
  148. package/lib/src/services/session/shutdownReason.js +26 -0
  149. package/lib/src/services/settings/aiEngineSettings.js +219 -0
  150. package/lib/src/services/token/mintedLifetime.js +51 -0
  151. package/lib/src/sessions/SessionManager.js +8 -2
  152. package/lib/src/sessions/sessionMemory.js +25 -0
  153. package/lib/test/helpers/fake-ai-provider.js +188 -0
  154. package/lib/test/helpers/loki-stores.js +33 -0
  155. package/lib/test/helpers/page-sources.js +164 -0
  156. package/lib/test/helpers/scratch-database.js +57 -13
  157. package/lib/test/integration/audit-ingest.spec.js +10 -10
  158. package/lib/test/integration/auth-flow.spec.js +9 -14
  159. package/lib/test/integration/auth-rate-limit.spec.js +6 -3
  160. package/lib/test/integration/bug-report-route.spec.js +22 -2
  161. package/lib/test/integration/forgot-password-rate-limit.spec.js +6 -3
  162. package/lib/test/integration/forgot-password.spec.js +17 -18
  163. package/lib/test/integration/legacy-deprecation-headers.spec.js +5 -2
  164. package/lib/test/integration/migration.spec.js +6 -1
  165. package/lib/test/integration/node-pair-auth-rest.spec.js +5 -9
  166. package/lib/test/integration/ports-router.spec.js +4 -4
  167. package/lib/test/integration/prisma-device-store-add-devices.spec.js +2 -3
  168. package/lib/test/integration/prisma-device-store-team-filter.spec.js +2 -5
  169. package/lib/test/integration/profile-tokens.spec.js +10 -16
  170. package/lib/test/integration/recordings-router.spec.js +16 -16
  171. package/lib/test/integration/reset-revokes-sessions.spec.js +4 -9
  172. package/lib/test/integration/role-matrix.spec.js +4 -9
  173. package/lib/test/integration/sdk-leases-router.spec.js +9 -9
  174. package/lib/test/integration/sdk-version.spec.js +2 -2
  175. package/lib/test/integration/selector-health-endpoint.spec.js +5 -5
  176. package/lib/test/integration/team-membership-migration.spec.js +73 -12
  177. package/lib/test/integration/team-visibility-apps.spec.js +15 -22
  178. package/lib/test/integration/team-visibility-control.spec.js +96 -69
  179. package/lib/test/integration/team-visibility-dashboard.spec.js +7 -23
  180. package/lib/test/integration/team-visibility-grid.spec.js +16 -15
  181. package/lib/test/integration/team-visibility-leases.spec.js +8 -11
  182. package/lib/test/integration/team-visibility-recordings.spec.js +6 -19
  183. package/lib/test/integration/team-visibility-sessions.spec.js +6 -19
  184. package/lib/test/integration/users-flow.spec.js +7 -12
  185. package/lib/test/integration/users-lockout.spec.js +5 -3
  186. package/lib/test/unit/EmailService.test.js +66 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +46 -19
  188. package/lib/test/unit/ai-call-time-limit.spec.js +130 -0
  189. package/lib/test/unit/ai-engine-settings.spec.js +338 -0
  190. package/lib/test/unit/ai-provider-switch.spec.js +130 -0
  191. package/lib/test/unit/ai-rate-limit.spec.js +126 -0
  192. package/lib/test/unit/android-h264-viewers.spec.js +272 -0
  193. package/lib/test/unit/android-preview-hold.spec.js +3 -2
  194. package/lib/test/unit/apikeys-router.spec.js +84 -0
  195. package/lib/test/unit/authMiddleware.bearer.spec.js +9 -0
  196. package/lib/test/unit/authMiddleware.credentialLifetime.spec.js +149 -0
  197. package/lib/test/unit/bug-report/video-slice.spec.js +19 -0
  198. package/lib/test/unit/dashboard-events-device-logs.spec.js +207 -0
  199. package/lib/test/unit/dashboard-plugin-link.spec.js +213 -0
  200. package/lib/test/unit/device-log-book.spec.js +180 -0
  201. package/lib/test/unit/enable-dashboard-option.spec.js +57 -0
  202. package/lib/test/unit/event-log-session-data.spec.js +244 -0
  203. package/lib/test/unit/failure-analysis.spec.js +182 -0
  204. package/lib/test/unit/failure-categories.spec.js +128 -0
  205. package/lib/test/unit/fingerprint-identity.spec.js +433 -0
  206. package/lib/test/unit/h264-multiplexer.spec.js +46 -0
  207. package/lib/test/unit/heal-report.spec.js +71 -0
  208. package/lib/test/unit/healing-instrumentation.spec.js +6 -1
  209. package/lib/test/unit/healing-tier-gate.spec.js +73 -29
  210. package/lib/test/unit/healing-tiers-every-session.spec.js +219 -0
  211. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +171 -0
  212. package/lib/test/unit/interceptor-visual-elements.spec.js +192 -0
  213. package/lib/test/unit/lease/lease-override.spec.js +23 -6
  214. package/lib/test/unit/lease/session-lease-proof.spec.js +16 -4
  215. package/lib/test/unit/ocr-healing-tier-real-ocr.spec.js +113 -0
  216. package/lib/test/unit/ocr-healing-tier.spec.js +122 -0
  217. package/lib/test/unit/ocr-offline.spec.js +213 -0
  218. package/lib/test/unit/omni-virtual-element-store.spec.js +301 -0
  219. package/lib/test/unit/omni-vision-ios-points.spec.js +0 -2
  220. package/lib/test/unit/omni-vision.spec.js +5 -7
  221. package/lib/test/unit/package-engines.spec.js +27 -0
  222. package/lib/test/unit/passwordResetMode.test.js +10 -2
  223. package/lib/test/unit/profile-router.test.js +122 -1
  224. package/lib/test/unit/queue-requester.spec.js +1 -1
  225. package/lib/test/unit/reset-link-address.spec.js +266 -0
  226. package/lib/test/unit/reset-link.test.js +10 -4
  227. package/lib/test/unit/resilio-path-healing.spec.js +270 -0
  228. package/lib/test/unit/selector-learning-every-session.spec.js +179 -0
  229. package/lib/test/unit/selector-verification-clean-build.spec.js +130 -0
  230. package/lib/test/unit/selector-verification-job.spec.js +13 -13
  231. package/lib/test/unit/self-healing-boot.spec.js +10 -5
  232. package/lib/test/unit/self-healing-setting.spec.js +7 -6
  233. package/lib/test/unit/session-app-resolution.spec.js +5 -1
  234. package/lib/test/unit/session-attribution-wiring.spec.js +41 -13
  235. package/lib/test/unit/session-create-credential-owner.spec.js +242 -0
  236. package/lib/test/unit/session-credential-strip.spec.js +1 -1
  237. package/lib/test/unit/session-device-logs.spec.js +347 -0
  238. package/lib/test/unit/session-end-failure-analysis.spec.js +133 -0
  239. package/lib/test/unit/session-log-indexes.spec.js +64 -3
  240. package/lib/test/unit/socketServer.sessionCookie.spec.js +2 -1
  241. package/lib/test/unit/stream-start-pipelines.spec.js +72 -18
  242. package/lib/test/unit/token-mint-limits.spec.js +165 -0
  243. package/lib/test/unit/unexpected-shutdown-reason.spec.js +163 -0
  244. package/lib/test/unit/verify-credential.spec.js +33 -0
  245. package/lib/test/unit/vision-assertion.spec.js +3 -2
  246. package/lib/test/unit/web-config-service.spec.js +19 -0
  247. package/lib/tsconfig.tsbuildinfo +1 -1
  248. package/package.json +5 -5
  249. package/prisma/migrations/20261004174413_locator_etalon_path/migration.sql +2 -0
  250. package/prisma/schema.prisma +3 -0
  251. package/schema.json +5 -5
  252. package/lib/public/assets/BugReportButton-BAt0_pzq.js +0 -15
  253. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +0 -2
  254. package/lib/public/assets/ai-settings-CsuJroRf.js +0 -21
  255. package/lib/public/assets/device-explorer-CdO6_nrn.js +0 -282
  256. package/lib/public/assets/forgot-password-CGnkRgE9.js +0 -1
  257. package/lib/public/assets/runbook-page-CMMeQml9.js +0 -124
  258. package/lib/public/assets/session-detail-page-COL_RZxw.js +0 -4
  259. /package/lib/public/assets/{BugReportButton-CPBjfiv4.css → index-CPBjfiv4.css} +0 -0
@@ -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 recordings_1 = __importDefault(require("../../src/app/routers/recordings"));
22
22
  const authMiddleware_1 = require("../../src/middleware/authMiddleware");
@@ -76,7 +76,7 @@ describe('Recordings router (integration)', () => {
76
76
  startedAt: new Date(),
77
77
  compositeEnabled: false,
78
78
  });
79
- const r = yield (0, supertest_1.default)(makeApp())
79
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
80
80
  .post('/xenon/api/recordings')
81
81
  .send({ udids: ['U1'] });
82
82
  (0, chai_1.expect)(r.status).to.equal(202);
@@ -88,7 +88,7 @@ describe('Recordings router (integration)', () => {
88
88
  .rejects(new RecordingOrchestrator_1.RecordingError('device_busy', [
89
89
  { udid: 'U2', reason: 'automation', sessionId: 'sess-x' },
90
90
  ]));
91
- const r = yield (0, supertest_1.default)(makeApp())
91
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
92
92
  .post('/xenon/api/recordings')
93
93
  .send({ udids: ['U1', 'U2'] });
94
94
  (0, chai_1.expect)(r.status).to.equal(409);
@@ -101,7 +101,7 @@ describe('Recordings router (integration)', () => {
101
101
  sinon_1.default
102
102
  .stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'start')
103
103
  .rejects(new RecordingOrchestrator_1.RecordingError('concurrency_cap', undefined, 4, 4));
104
- const r = yield (0, supertest_1.default)(makeApp())
104
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
105
105
  .post('/xenon/api/recordings')
106
106
  .send({ udids: ['U1'] });
107
107
  (0, chai_1.expect)(r.status).to.equal(409);
@@ -109,16 +109,16 @@ describe('Recordings router (integration)', () => {
109
109
  }));
110
110
  it('POST /recordings: 400 when udids missing or empty', () => __awaiter(void 0, void 0, void 0, function* () {
111
111
  const app = makeApp();
112
- yield (0, supertest_1.default)(app).post('/xenon/api/recordings').send({}).expect(400);
113
- yield (0, supertest_1.default)(app).post('/xenon/api/recordings').send({ udids: [] }).expect(400);
114
- yield (0, supertest_1.default)(app).post('/xenon/api/recordings').send({ udids: [123] }).expect(400);
112
+ yield (0, loopbackRequest_1.default)(app).post('/xenon/api/recordings').send({}).expect(400);
113
+ yield (0, loopbackRequest_1.default)(app).post('/xenon/api/recordings').send({ udids: [] }).expect(400);
114
+ yield (0, loopbackRequest_1.default)(app).post('/xenon/api/recordings').send({ udids: [123] }).expect(400);
115
115
  }));
116
116
  it('POST /recordings/:groupId/stop: 200', () => __awaiter(void 0, void 0, void 0, function* () {
117
117
  sinon_1.default.stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'stop').resolves({
118
118
  groupId: 'g-1',
119
119
  recordings: [{ id: 'r-1', udid: 'U1', status: 'STOPPED', durationMs: 5000, sizeBytes: 1024 }],
120
120
  });
121
- const r = yield (0, supertest_1.default)(makeApp()).post('/xenon/api/recordings/g-1/stop');
121
+ const r = yield (0, loopbackRequest_1.default)(makeApp()).post('/xenon/api/recordings/g-1/stop');
122
122
  (0, chai_1.expect)(r.status).to.equal(200);
123
123
  (0, chai_1.expect)(r.body.recordings[0].status).to.equal('STOPPED');
124
124
  }));
@@ -127,11 +127,11 @@ describe('Recordings router (integration)', () => {
127
127
  sinon_1.default
128
128
  .stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'addBookmark')
129
129
  .resolves({ id: 'bm-1', label: 'bug here' });
130
- const ok = yield (0, supertest_1.default)(makeApp())
130
+ const ok = yield (0, loopbackRequest_1.default)(makeApp())
131
131
  .post('/xenon/api/recordings/g-1/bookmark')
132
132
  .send({ recordingId: 'r-1', timecodeMs: 1000, label: 'bug here' });
133
133
  (0, chai_1.expect)(ok.status).to.equal(201);
134
- const bad = yield (0, supertest_1.default)(makeApp())
134
+ const bad = yield (0, loopbackRequest_1.default)(makeApp())
135
135
  .post('/xenon/api/recordings/g-1/bookmark')
136
136
  .send({ timecodeMs: 1000 });
137
137
  (0, chai_1.expect)(bad.status).to.equal(400);
@@ -166,7 +166,7 @@ describe('Recordings router (integration)', () => {
166
166
  startedAt: new Date(),
167
167
  compositeEnabled: false,
168
168
  });
169
- const r = yield (0, supertest_1.default)(makeBearerApp())
169
+ const r = yield (0, loopbackRequest_1.default)(makeBearerApp())
170
170
  .post('/xenon/api/recordings')
171
171
  .send({ udids: ['U1'] });
172
172
  (0, chai_1.expect)(r.status).to.equal(202);
@@ -176,7 +176,7 @@ describe('Recordings router (integration)', () => {
176
176
  groupId: 'g-1',
177
177
  recordings: [{ id: 'r-2', udid: 'U2', status: 'RECORDING' }],
178
178
  });
179
- const r = yield (0, supertest_1.default)(makeBearerApp())
179
+ const r = yield (0, loopbackRequest_1.default)(makeBearerApp())
180
180
  .post('/xenon/api/recordings/g-1/add-device')
181
181
  .send({ udid: 'U2' });
182
182
  (0, chai_1.expect)(r.status).to.equal(201);
@@ -215,7 +215,7 @@ describe('Recordings router (integration)', () => {
215
215
  startedAt: new Date(),
216
216
  compositeEnabled: false,
217
217
  });
218
- const r = yield (0, supertest_1.default)(makeHeaderPairApp())
218
+ const r = yield (0, loopbackRequest_1.default)(makeHeaderPairApp())
219
219
  .post('/xenon/api/recordings')
220
220
  .send({ udids: ['U1'] });
221
221
  (0, chai_1.expect)(r.status).to.equal(202);
@@ -227,7 +227,7 @@ describe('Recordings router (integration)', () => {
227
227
  groupId: 'g-1',
228
228
  recordings: [{ id: 'r-2', udid: 'U2', status: 'RECORDING' }],
229
229
  });
230
- const r = yield (0, supertest_1.default)(makeHeaderPairApp())
230
+ const r = yield (0, loopbackRequest_1.default)(makeHeaderPairApp())
231
231
  .post('/xenon/api/recordings/g-1/add-device')
232
232
  .send({ udid: 'U2' });
233
233
  (0, chai_1.expect)(r.status).to.equal(201);
@@ -239,7 +239,7 @@ describe('Recordings router (integration)', () => {
239
239
  sinon_1.default
240
240
  .stub(typedi_1.Container.get(RecordingOrchestrator_1.RecordingOrchestrator), 'addAnnotation')
241
241
  .resolves({ id: 'an-1' });
242
- const ok = yield (0, supertest_1.default)(makeApp())
242
+ const ok = yield (0, loopbackRequest_1.default)(makeApp())
243
243
  .post('/xenon/api/recordings/g-1/annotation')
244
244
  .send({
245
245
  recordingId: 'r-1',
@@ -249,7 +249,7 @@ describe('Recordings router (integration)', () => {
249
249
  color: '#ff0000',
250
250
  });
251
251
  (0, chai_1.expect)(ok.status).to.equal(201);
252
- const bad = yield (0, supertest_1.default)(makeApp())
252
+ const bad = yield (0, loopbackRequest_1.default)(makeApp())
253
253
  .post('/xenon/api/recordings/g-1/annotation')
254
254
  .send({ shape: 'RECT' });
255
255
  (0, chai_1.expect)(bad.status).to.equal(400);
@@ -15,20 +15,20 @@ 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 auth_1 = require("../../src/app/routers/auth");
20
20
  const typedi_1 = require("typedi");
21
21
  const UserService_1 = require("../../src/services/UserService");
22
22
  const UserSessionService_1 = require("../../src/services/UserSessionService");
23
23
  const PasswordResetService_1 = require("../../src/services/PasswordResetService");
24
24
  const prisma_1 = require("../../src/prisma");
25
+ const scratch_database_1 = require("../helpers/scratch-database");
25
26
  describe('reset-password revokes all sessions', function () {
26
27
  this.timeout(30000);
28
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
27
29
  const email = 'reset-revokes-it@xenon.local';
28
30
  let user;
29
31
  before(() => __awaiter(this, void 0, void 0, function* () {
30
- yield prisma_1.prisma.userSession.deleteMany({ where: { user: { email } } });
31
- yield prisma_1.prisma.user.deleteMany({ where: { email } });
32
32
  user = yield typedi_1.Container.get(UserService_1.UserService).createUser({
33
33
  email,
34
34
  name: 'Reset Revokes IT',
@@ -36,11 +36,6 @@ describe('reset-password revokes all sessions', function () {
36
36
  role: 'MEMBER',
37
37
  });
38
38
  }));
39
- after(() => __awaiter(this, void 0, void 0, function* () {
40
- yield prisma_1.prisma.passwordResetToken.deleteMany({ where: { userId: user.id } });
41
- yield prisma_1.prisma.userSession.deleteMany({ where: { userId: user.id } });
42
- yield prisma_1.prisma.user.delete({ where: { id: user.id } });
43
- }));
44
39
  it('deletes every UserSession for the resetting user', () => __awaiter(this, void 0, void 0, function* () {
45
40
  const sessions = [
46
41
  yield typedi_1.Container.get(UserSessionService_1.UserSessionService).create(user.id),
@@ -51,7 +46,7 @@ describe('reset-password revokes all sessions', function () {
51
46
  const app = (0, express_1.default)();
52
47
  app.use(express_1.default.json());
53
48
  app.use('/auth', (0, auth_1.authPublicRouter)());
54
- const r = yield (0, supertest_1.default)(app)
49
+ const r = yield (0, loopbackRequest_1.default)(app)
55
50
  .post('/auth/reset-password')
56
51
  .send({ token: raw, newPassword: 'BrandNewPw1' });
57
52
  (0, chai_1.expect)(r.status).to.equal(204);
@@ -15,13 +15,13 @@ 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 authMiddleware_1 = require("../../src/middleware/authMiddleware");
20
20
  const apikeys_1 = require("../../src/app/routers/apikeys");
21
21
  const teams_1 = require("../../src/app/routers/teams");
22
22
  const processes_1 = require("../../src/app/routers/processes");
23
23
  const users_1 = require("../../src/app/routers/users");
24
- const prisma_1 = require("../../src/prisma");
24
+ const scratch_database_1 = require("../helpers/scratch-database");
25
25
  const seedUser_1 = require("../helpers/seedUser");
26
26
  // scopesForRole now grants ADMIN the full 'admin,devices,sessions,read' set on
27
27
  // cookie sessions (the user themselves logged in via /login). Token-default
@@ -84,6 +84,7 @@ function buildApp() {
84
84
  }
85
85
  describe('role matrix (integration)', function () {
86
86
  this.timeout(60000);
87
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
87
88
  let users;
88
89
  before(() => __awaiter(this, void 0, void 0, function* () {
89
90
  users = {
@@ -92,18 +93,12 @@ describe('role matrix (integration)', function () {
92
93
  MEMBER: yield (0, seedUser_1.seedUser)('MEMBER'),
93
94
  };
94
95
  }));
95
- after(() => __awaiter(this, void 0, void 0, function* () {
96
- yield users.SUPER_ADMIN.cleanup();
97
- yield users.ADMIN.cleanup();
98
- yield users.MEMBER.cleanup();
99
- yield prisma_1.prisma.user.deleteMany({ where: { email: { contains: 'rmcase-' } } });
100
- }));
101
96
  CASES.forEach((c) => {
102
97
  ['SUPER_ADMIN', 'ADMIN', 'MEMBER'].forEach((role) => {
103
98
  it(`${role} ${c.method} ${c.path} → expect ${c.expect[role]}`, () => __awaiter(this, void 0, void 0, function* () {
104
99
  const app = buildApp();
105
100
  const expected = c.expect[role];
106
- const reqBuilder = (0, supertest_1.default)(app)[c.method.toLowerCase()](c.path).set('Cookie', users[role].cookie);
101
+ const reqBuilder = (0, loopbackRequest_1.default)(app)[c.method.toLowerCase()](c.path).set('Cookie', users[role].cookie);
107
102
  const body = typeof c.body === 'function' ? c.body() : c.body;
108
103
  const r = body ? yield reqBuilder.send(body) : yield reqBuilder;
109
104
  if (expected >= 200 && expected < 300) {
@@ -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 sinon_1 = __importDefault(require("sinon"));
20
20
  // Static imports (recordings-router.spec idiom): the previous extensionless
21
21
  // dynamic `await import(...)` fails to resolve when Node's native ESM loader
@@ -53,7 +53,7 @@ describe('POST /xenon/api/sdk/leases', () => {
53
53
  allocatedPorts: { systemPort: 9001 },
54
54
  appiumCapabilities: { 'appium:udid': 'u1', 'xe:options': { leaseId: 'lse_1' } },
55
55
  });
56
- const res = yield (0, supertest_1.default)(app)
56
+ const res = yield (0, loopbackRequest_1.default)(app)
57
57
  .post('/xenon/api/sdk/leases')
58
58
  .send({ filters: { platform: 'android' }, durationMs: 60000, heartbeatSeconds: 30 });
59
59
  (0, chai_1.expect)(res.status).to.equal(201);
@@ -62,7 +62,7 @@ describe('POST /xenon/api/sdk/leases', () => {
62
62
  }));
63
63
  it('records the api-key principal as actor/team on create', () => __awaiter(void 0, void 0, void 0, function* () {
64
64
  leaseSvc.create.resolves({ leaseId: 'lse_1', leaseToken: 't'.repeat(64) });
65
- yield (0, supertest_1.default)(app)
65
+ yield (0, loopbackRequest_1.default)(app)
66
66
  .post('/xenon/api/sdk/leases')
67
67
  .send({ filters: { platform: 'android' } });
68
68
  (0, chai_1.expect)(leaseSvc.create.calledOnce).to.equal(true);
@@ -91,7 +91,7 @@ describe('POST /xenon/api/sdk/leases', () => {
91
91
  });
92
92
  bearerApp.use('/xenon/api/sdk/leases', (0, sdk_leases_1.makeRouter)({ leaseService: leaseSvc }));
93
93
  leaseSvc.create.resolves({ leaseId: 'lse_2', leaseToken: 't'.repeat(64) });
94
- const res = yield (0, supertest_1.default)(bearerApp)
94
+ const res = yield (0, loopbackRequest_1.default)(bearerApp)
95
95
  .post('/xenon/api/sdk/leases')
96
96
  .send({ filters: { platform: 'android' } });
97
97
  (0, chai_1.expect)(res.status).to.equal(201);
@@ -103,7 +103,7 @@ describe('POST /xenon/api/sdk/leases', () => {
103
103
  }));
104
104
  it('returns 404 when no device matches', () => __awaiter(void 0, void 0, void 0, function* () {
105
105
  leaseSvc.create.rejects(new LeaseService_1.NoMatchingDevice('no device'));
106
- const res = yield (0, supertest_1.default)(app)
106
+ const res = yield (0, loopbackRequest_1.default)(app)
107
107
  .post('/xenon/api/sdk/leases')
108
108
  .send({ filters: { platform: 'android', udid: 'absent' }, durationMs: 60000 });
109
109
  (0, chai_1.expect)(res.status).to.equal(404);
@@ -111,23 +111,23 @@ describe('POST /xenon/api/sdk/leases', () => {
111
111
  }));
112
112
  it('heartbeat: 200 with valid token; 403 without', () => __awaiter(void 0, void 0, void 0, function* () {
113
113
  leaseSvc.heartbeat.resolves({ heartbeatedAt: Date.now(), expiresAt: Date.now() + 60000 });
114
- const res1 = yield (0, supertest_1.default)(app)
114
+ const res1 = yield (0, loopbackRequest_1.default)(app)
115
115
  .post('/xenon/api/sdk/leases/lse_1/heartbeat')
116
116
  .set('x-xenon-lease-token', 'cleartext');
117
117
  (0, chai_1.expect)(res1.status).to.equal(200);
118
- const res2 = yield (0, supertest_1.default)(app).post('/xenon/api/sdk/leases/lse_1/heartbeat');
118
+ const res2 = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/sdk/leases/lse_1/heartbeat');
119
119
  (0, chai_1.expect)(res2.status).to.equal(403);
120
120
  }));
121
121
  it('heartbeat: 403 on token mismatch from service', () => __awaiter(void 0, void 0, void 0, function* () {
122
122
  leaseSvc.heartbeat.rejects(new LeaseService_1.LeaseTokenMismatch());
123
- const res = yield (0, supertest_1.default)(app)
123
+ const res = yield (0, loopbackRequest_1.default)(app)
124
124
  .post('/xenon/api/sdk/leases/lse_1/heartbeat')
125
125
  .set('x-xenon-lease-token', 'wrong');
126
126
  (0, chai_1.expect)(res.status).to.equal(403);
127
127
  }));
128
128
  it('heartbeat: 410 Gone on expired lease', () => __awaiter(void 0, void 0, void 0, function* () {
129
129
  leaseSvc.heartbeat.rejects(new LeaseService_1.LeaseGone('expired'));
130
- const res = yield (0, supertest_1.default)(app)
130
+ const res = yield (0, loopbackRequest_1.default)(app)
131
131
  .post('/xenon/api/sdk/leases/lse_1/heartbeat')
132
132
  .set('x-xenon-lease-token', 'whatever');
133
133
  (0, chai_1.expect)(res.status).to.equal(410);
@@ -48,7 +48,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
48
48
  require("reflect-metadata");
49
49
  const chai_1 = require("chai");
50
50
  const express_1 = __importStar(require("express"));
51
- const supertest_1 = __importDefault(require("supertest"));
51
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
52
52
  describe('GET /xenon/api/sdk/version', () => {
53
53
  it('returns pluginVersion + supports array', () => __awaiter(void 0, void 0, void 0, function* () {
54
54
  const register = (yield Promise.resolve().then(() => __importStar(require('../../src/app/routers/sdk-version')))).default;
@@ -61,7 +61,7 @@ describe('GET /xenon/api/sdk/version', () => {
61
61
  const apiRouter = (0, express_1.Router)();
62
62
  register(apiRouter);
63
63
  app.use('/xenon/api', apiRouter);
64
- const res = yield (0, supertest_1.default)(app).get('/xenon/api/sdk/version');
64
+ const res = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/sdk/version');
65
65
  (0, chai_1.expect)(res.status).to.equal(200);
66
66
  (0, chai_1.expect)(res.body.pluginVersion).to.be.a('string');
67
67
  (0, chai_1.expect)(res.body.supports).to.include.members(['leases', 'ports', 'heartbeat']);
@@ -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 dashboard_1 = require("../../src/app/routers/dashboard");
21
21
  const roleGuard_1 = require("../../src/middleware/roleGuard");
22
22
  const prisma_1 = require("../../src/prisma");
@@ -209,23 +209,23 @@ describe('GET /healing/selector-health — MEMBER guard (router-level)', () => {
209
209
  });
210
210
  afterEach(() => sinon_1.default.restore());
211
211
  it('401s when unauthenticated', () => __awaiter(void 0, void 0, void 0, function* () {
212
- const r = yield (0, supertest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
212
+ const r = yield (0, loopbackRequest_1.default)(makeApp()).get('/xenon/api/healing/selector-health');
213
213
  (0, chai_1.expect)(r.status).to.equal(401);
214
214
  }));
215
215
  it('403s for a role below MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
216
- const r = yield (0, supertest_1.default)(makeApp())
216
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
217
217
  .get('/xenon/api/healing/selector-health')
218
218
  .set('x-test-role', 'BOGUS');
219
219
  (0, chai_1.expect)(r.status).to.equal(403);
220
220
  }));
221
221
  it('200s for MEMBER', () => __awaiter(void 0, void 0, void 0, function* () {
222
- const r = yield (0, supertest_1.default)(makeApp())
222
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
223
223
  .get('/xenon/api/healing/selector-health')
224
224
  .set('x-test-role', 'MEMBER');
225
225
  (0, chai_1.expect)(r.status).to.equal(200);
226
226
  }));
227
227
  it('200s for ADMIN', () => __awaiter(void 0, void 0, void 0, function* () {
228
- const r = yield (0, supertest_1.default)(makeApp())
228
+ const r = yield (0, loopbackRequest_1.default)(makeApp())
229
229
  .get('/xenon/api/healing/selector-health')
230
230
  .set('x-test-role', 'ADMIN');
231
231
  (0, chai_1.expect)(r.status).to.equal(200);
@@ -8,28 +8,89 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  require("reflect-metadata");
13
16
  const chai_1 = require("chai");
17
+ const child_process_1 = require("child_process");
18
+ const fs_1 = __importDefault(require("fs"));
19
+ const os_1 = __importDefault(require("os"));
20
+ const path_1 = __importDefault(require("path"));
14
21
  const typedi_1 = require("typedi");
22
+ const client_1 = require("../../src/generated/client");
15
23
  const UserService_1 = require("../../src/services/UserService");
16
24
  const TeamService_1 = require("../../src/services/TeamService");
17
25
  const prisma_1 = require("../../src/prisma");
26
+ const scratch_database_1 = require("../helpers/scratch-database");
27
+ const ROOT = path_1.default.resolve(__dirname, '../..');
28
+ const MIGRATIONS = path_1.default.join(ROOT, 'prisma', 'migrations');
29
+ /** The migration that backfills TeamMember from ApiKey.teamId. */
30
+ const BACKFILL = '20260430001747_phase_3_team_members';
31
+ /**
32
+ * `prisma migrate deploy` into `url` of the repository's migrations that
33
+ * `include` names, from a copy of the schema in `dir`. Run again with more
34
+ * of them, it applies only the ones not yet applied.
35
+ */
36
+ function deploy(dir, url, include) {
37
+ const target = path_1.default.join(dir, 'migrations');
38
+ fs_1.default.mkdirSync(target, { recursive: true });
39
+ fs_1.default.copyFileSync(path_1.default.join(MIGRATIONS, 'migration_lock.toml'), path_1.default.join(target, 'migration_lock.toml'));
40
+ for (const name of fs_1.default.readdirSync(MIGRATIONS)) {
41
+ const sql = path_1.default.join(MIGRATIONS, name, 'migration.sql');
42
+ if (!include(name) || !fs_1.default.existsSync(sql))
43
+ continue;
44
+ fs_1.default.mkdirSync(path_1.default.join(target, name), { recursive: true });
45
+ fs_1.default.copyFileSync(sql, path_1.default.join(target, name, 'migration.sql'));
46
+ }
47
+ fs_1.default.copyFileSync(path_1.default.join(ROOT, 'prisma', 'schema.prisma'), path_1.default.join(dir, 'schema.prisma'));
48
+ (0, child_process_1.execSync)(`npx prisma migrate deploy --schema "${path_1.default.join(dir, 'schema.prisma')}"`, {
49
+ cwd: ROOT,
50
+ env: Object.assign(Object.assign({}, process.env), { DATABASE_URL: url }),
51
+ stdio: 'pipe',
52
+ });
53
+ }
18
54
  describe('team-membership migration backfill (integration)', function () {
19
55
  this.timeout(60000);
20
- it('no TeamMember row points at Legacy Admin', () => __awaiter(this, void 0, void 0, function* () {
21
- const legacy = yield prisma_1.prisma.user.findFirst({
22
- where: { email: 'legacy-admin@xenon.local' },
23
- });
24
- if (!legacy) {
25
- // Legacy Admin only exists if Phase 1's adoption migration actually ran
26
- // against a non-empty ApiKey table. Skip if absent.
27
- // eslint-disable-next-line no-console
28
- console.log('skipped: no Legacy Admin in this DB');
29
- return;
56
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
57
+ // Through 2.14 this read the developer's own database, and returned early
58
+ // unless an old Legacy Admin happened to be in it. It runs the migration
59
+ // itself now, on a database migrated up to it and seeded as such a lab was.
60
+ it('backfills TeamMember from team keys, and gives Legacy Admin no membership', () => __awaiter(this, void 0, void 0, function* () {
61
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-team-backfill-'));
62
+ const url = `file:${path_1.default.join(dir, 'xenon.db')}`;
63
+ try {
64
+ deploy(dir, url, (name) => name < BACKFILL);
65
+ const before = new client_1.PrismaClient({ datasources: { db: { url } } });
66
+ try {
67
+ yield before.$executeRawUnsafe('INSERT INTO "Team" ("id", "name") VALUES (?, ?)', 'team-1', 'Team 1');
68
+ for (const [id, email] of [
69
+ ['legacy', 'legacy-admin@xenon.local'],
70
+ ['alice', 'alice@xenon.local'],
71
+ ]) {
72
+ yield before.$executeRawUnsafe(`INSERT INTO "User" ("id", "email", "name", "passwordHash", "accessKey", "updatedAt")
73
+ VALUES (?, ?, ?, 'x', ?, CURRENT_TIMESTAMP)`, id, email, id, `ak-${id}`);
74
+ yield before.$executeRawUnsafe(`INSERT INTO "ApiKey" ("id", "name", "keyHash", "scopes", "teamId", "userId")
75
+ VALUES (?, ?, ?, 'devices', 'team-1', ?)`, `key-${id}`, id, `hash-${id}`, id);
76
+ }
77
+ }
78
+ finally {
79
+ yield before.$disconnect();
80
+ }
81
+ deploy(dir, url, () => true);
82
+ const after = new client_1.PrismaClient({ datasources: { db: { url } } });
83
+ try {
84
+ const members = yield after.teamMember.findMany({ select: { teamId: true, userId: true } });
85
+ (0, chai_1.expect)(members).to.deep.equal([{ teamId: 'team-1', userId: 'alice' }]);
86
+ }
87
+ finally {
88
+ yield after.$disconnect();
89
+ }
90
+ }
91
+ finally {
92
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
30
93
  }
31
- const polluted = yield prisma_1.prisma.teamMember.count({ where: { userId: legacy.id } });
32
- (0, chai_1.expect)(polluted).to.equal(0);
33
94
  }));
34
95
  it('addMember / removeMember round-trip an arbitrary user', () => __awaiter(this, void 0, void 0, function* () {
35
96
  const u = yield typedi_1.Container.get(UserService_1.UserService).createUser({
@@ -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 fs_1 = __importDefault(require("fs"));
20
20
  const os_1 = __importDefault(require("os"));
21
21
  const path_1 = __importDefault(require("path"));
@@ -29,17 +29,17 @@ const app_service_1 = require("../../src/dashboard/services/app-service");
29
29
  const prisma_1 = require("../../src/prisma");
30
30
  const seedUser_1 = require("../helpers/seedUser");
31
31
  const container_registration_1 = require("../helpers/container-registration");
32
+ const scratch_database_1 = require("../helpers/scratch-database");
32
33
  /**
33
34
  * Uploaded apps follow the device team rule, run as real queries: the list
34
35
  * builds a Prisma filter (`OR: [{ teamId: null }, { teamId: { in } }]`, with an
35
36
  * empty `in` for a member in no team) that a stub would only echo back.
36
37
  *
37
- * Run on a migrated database:
38
- * DATABASE_URL=file:/tmp/x.db npx prisma migrate deploy
39
- * DATABASE_URL=file:/tmp/x.db npx mocha test/integration/team-visibility-apps.spec.ts
38
+ * On a scratch database of its own, so `npx mocha <file>` is enough.
40
39
  */
41
40
  describe('team visibility on uploaded apps (integration)', function () {
42
41
  this.timeout(60000);
42
+ (0, scratch_database_1.useScratchDatabase)({ wholeSuite: true });
43
43
  let sa;
44
44
  let alice;
45
45
  let loner;
@@ -87,17 +87,10 @@ describe('team visibility on uploaded apps (integration)', function () {
87
87
  });
88
88
  }
89
89
  }));
90
- after(() => __awaiter(this, void 0, void 0, function* () {
91
- yield prisma_1.prisma.app.deleteMany({ where: { id: { in: Object.values(A) } } });
92
- yield prisma_1.prisma.teamMember.deleteMany({ where: { teamId: { in: [teamA.id, teamB.id] } } });
93
- yield prisma_1.prisma.team.delete({ where: { id: teamA.id } }).catch(() => undefined);
94
- yield prisma_1.prisma.team.delete({ where: { id: teamB.id } }).catch(() => undefined);
95
- yield sa.cleanup();
96
- yield alice.cleanup();
97
- yield loner.cleanup();
90
+ after(() => {
98
91
  restore();
99
92
  fs_1.default.rmSync(dir, { recursive: true, force: true });
100
- }));
93
+ });
101
94
  function app() {
102
95
  const a = (0, express_1.default)();
103
96
  a.use(express_1.default.json());
@@ -106,13 +99,13 @@ describe('team visibility on uploaded apps (integration)', function () {
106
99
  return a;
107
100
  }
108
101
  const ids = (cookie) => __awaiter(this, void 0, void 0, function* () {
109
- return (yield (0, supertest_1.default)(app()).get('/apps').set('Cookie', cookie)).body
102
+ return (yield (0, loopbackRequest_1.default)(app()).get('/apps').set('Cookie', cookie)).body
110
103
  .map((r) => r.id)
111
104
  .filter((id) => Object.values(A).includes(id));
112
105
  });
113
106
  const UNKNOWN = { error: 'App not found' };
114
107
  it("lists a member's shared and own-team apps, with the team's name", () => __awaiter(this, void 0, void 0, function* () {
115
- const res = yield (0, supertest_1.default)(app()).get('/apps').set('Cookie', alice.cookie);
108
+ const res = yield (0, loopbackRequest_1.default)(app()).get('/apps').set('Cookie', alice.cookie);
116
109
  (0, chai_1.expect)(res.status).to.equal(200);
117
110
  const mine = res.body.filter((r) => Object.values(A).includes(r.id));
118
111
  (0, chai_1.expect)(mine.map((r) => r.id)).to.have.members([A.shared, A.a]);
@@ -127,20 +120,20 @@ describe('team visibility on uploaded apps (integration)', function () {
127
120
  (0, chai_1.expect)(yield ids(sa.cookie)).to.have.members(Object.values(A));
128
121
  }));
129
122
  it("answers another team's app exactly as an unknown id", () => __awaiter(this, void 0, void 0, function* () {
130
- const hidden = yield (0, supertest_1.default)(app()).get(`/apps/${A.b}/download`).set('Cookie', alice.cookie);
131
- const unknown = yield (0, supertest_1.default)(app())
123
+ const hidden = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.b}/download`).set('Cookie', alice.cookie);
124
+ const unknown = yield (0, loopbackRequest_1.default)(app())
132
125
  .get(`/apps/nope-${stamp}/download`)
133
126
  .set('Cookie', alice.cookie);
134
127
  (0, chai_1.expect)(hidden.status).to.equal(404);
135
128
  (0, chai_1.expect)(hidden.body).to.deep.equal(UNKNOWN);
136
129
  (0, chai_1.expect)(unknown.status).to.equal(404);
137
130
  (0, chai_1.expect)(unknown.body).to.deep.equal(UNKNOWN);
138
- const own = yield (0, supertest_1.default)(app()).get(`/apps/${A.a}/download`).set('Cookie', alice.cookie);
131
+ const own = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.a}/download`).set('Cookie', alice.cookie);
139
132
  (0, chai_1.expect)(own.status).to.equal(200);
140
133
  }));
141
134
  it('a download ticket serves its app once, with no login', () => __awaiter(this, void 0, void 0, function* () {
142
135
  const t = yield typedi_1.Container.get(AppDownloadTicketService_1.AppDownloadTicketService).mint(A.b);
143
- const first = yield (0, supertest_1.default)(app())
136
+ const first = yield (0, loopbackRequest_1.default)(app())
144
137
  .get(`/apps/${A.b}/download`)
145
138
  .query({ ticket: t })
146
139
  .buffer(true)
@@ -151,16 +144,16 @@ describe('team visibility on uploaded apps (integration)', function () {
151
144
  });
152
145
  (0, chai_1.expect)(first.status).to.equal(200);
153
146
  (0, chai_1.expect)(Buffer.compare(first.body, bytesOf(A.b))).to.equal(0);
154
- const again = yield (0, supertest_1.default)(app()).get(`/apps/${A.b}/download`).query({ ticket: t });
147
+ const again = yield (0, loopbackRequest_1.default)(app()).get(`/apps/${A.b}/download`).query({ ticket: t });
155
148
  (0, chai_1.expect)(again.status).to.equal(401);
156
149
  }));
157
150
  it('moving an app to a team changes who sees it', () => __awaiter(this, void 0, void 0, function* () {
158
- const put = (teamId) => (0, supertest_1.default)(app()).put(`/apps/${A.b}/team`).set('Cookie', sa.cookie).send({ teamId });
151
+ const put = (teamId) => (0, loopbackRequest_1.default)(app()).put(`/apps/${A.b}/team`).set('Cookie', sa.cookie).send({ teamId });
159
152
  (0, chai_1.expect)((yield put(teamA.id)).body).to.deep.equal({ ok: true, updated: 1 });
160
153
  (0, chai_1.expect)(yield ids(alice.cookie)).to.include(A.b);
161
154
  (0, chai_1.expect)((yield put(teamB.id)).status).to.equal(200);
162
155
  (0, chai_1.expect)(yield ids(alice.cookie)).to.not.include(A.b);
163
- const member = yield (0, supertest_1.default)(app())
156
+ const member = yield (0, loopbackRequest_1.default)(app())
164
157
  .put(`/apps/${A.b}/team`)
165
158
  .set('Cookie', alice.cookie)
166
159
  .send({ teamId: teamA.id });