@xenon-device-management/xenon 2.13.1 → 2.14.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 (223) hide show
  1. package/README.md +11 -5
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-BwuaMjkX.js} +1 -1
  5. package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BAt0_pzq.js} +1 -1
  6. package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
  7. package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-BSjhk_IV.js} +1 -1
  8. package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Cb5QZsE1.js} +1 -1
  9. package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-BAaSlWbW.js} +1 -1
  10. package/lib/public/assets/{Menu-C173JJ0l.js → Menu-JGGYrSQQ.js} +1 -1
  11. package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-CFCp3n9d.js} +1 -1
  12. package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-CbJj_EBT.js} +1 -1
  13. package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-BJeYnygR.js} +2 -2
  14. package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Bg42Atjy.js} +1 -1
  15. package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-Njz1pUIJ.js} +1 -1
  16. package/lib/public/assets/{Table-B1DB_ZN-.js → Table-BcTm5xV2.js} +1 -1
  17. package/lib/public/assets/{activity-Css0xNvp.js → activity-DOVdsfMQ.js} +1 -1
  18. package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-CsuJroRf.js} +2 -2
  19. package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-BKnNSTw9.js} +1 -1
  20. package/lib/public/assets/{apps-DW1pw2re.js → apps-D0SC6uw6.js} +2 -2
  21. package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-MMbTmgoE.js} +1 -1
  22. package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-BMfnGmxW.js} +1 -1
  23. package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-DuAftMWB.js} +1 -1
  24. package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-nZC2JNDO.js} +2 -2
  25. package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-hDCgTwkS.js} +1 -1
  26. package/lib/public/assets/{button-Cq_zHETv.js → button-Dda4irn_.js} +1 -1
  27. package/lib/public/assets/{calendar-CKxABX9q.js → calendar-BMr0Q62v.js} +1 -1
  28. package/lib/public/assets/{check-C7xu-hGU.js → check-CZ9iw9-e.js} +1 -1
  29. package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-BRYJJVC9.js} +1 -1
  30. package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-CqAJjQ5O.js} +1 -1
  31. package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-DlDJtnVz.js} +1 -1
  32. package/lib/public/assets/{clock-U1rDTT1h.js → clock-CpuDKysU.js} +1 -1
  33. package/lib/public/assets/{copy-BIM0ZX0L.js → copy-CAQydNFe.js} +1 -1
  34. package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
  35. package/lib/public/assets/{download-DDBRXnAZ.js → download-C9XG6lLv.js} +1 -1
  36. package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-CGnkRgE9.js} +1 -1
  37. package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
  38. package/lib/public/assets/{index-BGBqohfQ.js → index-BbKEXUFH.js} +5 -5
  39. package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
  40. package/lib/public/assets/{index-BQ2luT7L.js → index-DJGc1M2f.js} +1 -1
  41. package/lib/public/assets/{input-BvB1cjHE.js → input-Cm0tmSoc.js} +1 -1
  42. package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-D2DyJCml.js} +1 -1
  43. package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-UWf1GY99.js} +1 -1
  44. package/lib/public/assets/{lock-loz8txQm.js → lock-CCJv9m_I.js} +1 -1
  45. package/lib/public/assets/{login-BYOvz4lx.js → login-f8vOVW-U.js} +1 -1
  46. package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
  47. package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-B2jDgwm7.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
  49. package/lib/public/assets/{network-C5e0MIWo.js → network-ebT86qoi.js} +1 -1
  50. package/lib/public/assets/{overview-tlu8ijGd.js → overview-C0aykVrd.js} +1 -1
  51. package/lib/public/assets/{page-header-FXPlo6az.js → page-header-BjzgWz-T.js} +1 -1
  52. package/lib/public/assets/{play-CWdk0_Sy.js → play-DLG9t8FU.js} +1 -1
  53. package/lib/public/assets/{plus-B2xCnntV.js → plus-nBFSMu_X.js} +1 -1
  54. package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-CSl0UYlv.js} +1 -1
  55. package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-LeOuHqmB.js} +1 -1
  56. package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-CMMeQml9.js} +1 -1
  57. package/lib/public/assets/{select-12RThAXc.js → select-88jHcl-Z.js} +1 -1
  58. package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-B4rA1Xkv.js} +2 -2
  60. package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
  61. package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
  62. package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-BV98e_Sv.js} +1 -1
  63. package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-BiAsnFDj.js} +1 -1
  64. package/lib/public/assets/{teams-uL4fjTDR.js → teams-B4-jrr-1.js} +1 -1
  65. package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-C5RSuX0e.js} +1 -1
  66. package/lib/public/assets/{upload-B3k8WCSP.js → upload-BG7_rMXK.js} +1 -1
  67. package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-J-riQYyC.js} +1 -1
  68. package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
  69. package/lib/public/assets/{users-CqBuxMlX.js → users-CFba5TSy.js} +1 -1
  70. package/lib/public/assets/{users-DjQSucKb.js → users-Cp0beJm3.js} +1 -1
  71. package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-DDsEISjV.js} +1 -1
  72. package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
  73. package/lib/public/assets/{zap-gBHqutZV.js → zap-l4VaXzq8.js} +1 -1
  74. package/lib/public/index.html +2 -2
  75. package/lib/schema.json +19 -46
  76. package/lib/src/XenonCapabilityManager.js +22 -5
  77. package/lib/src/app/openapi/control.yaml +71 -20
  78. package/lib/src/app/openapi/grid.yaml +23 -5
  79. package/lib/src/app/openapi/platform.yaml +136 -28
  80. package/lib/src/app/routers/control.js +30 -5
  81. package/lib/src/app/routers/dashboard.js +29 -1
  82. package/lib/src/app/routers/grid.js +32 -10
  83. package/lib/src/app/routers/streamStartConflict.js +3 -0
  84. package/lib/src/app/routers/streamType.js +7 -2
  85. package/lib/src/app/routers/webhook.js +8 -4
  86. package/lib/src/config.js +50 -5
  87. package/lib/src/dashboard/commands.js +185 -141
  88. package/lib/src/dashboard/event-manager.js +39 -6
  89. package/lib/src/data-service/appleFamily.js +43 -0
  90. package/lib/src/data-service/db.js +11 -0
  91. package/lib/src/data-service/device-service.js +46 -1
  92. package/lib/src/data-service/device-store.js +66 -3
  93. package/lib/src/data-service/deviceClaims.js +37 -0
  94. package/lib/src/data-service/deviceFieldOwners.js +25 -11
  95. package/lib/src/data-service/deviceSettings.js +79 -0
  96. package/lib/src/data-service/prisma-store.js +152 -9
  97. package/lib/src/data-service/web-config-service.js +43 -92
  98. package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
  99. package/lib/src/device-managers/ios/WDAClient.js +44 -43
  100. package/lib/src/device-managers/shellCommands.js +96 -0
  101. package/lib/src/device-utils.js +143 -37
  102. package/lib/src/generated/client/edge.js +17 -3
  103. package/lib/src/generated/client/index-browser.js +14 -0
  104. package/lib/src/generated/client/index.d.ts +2054 -379
  105. package/lib/src/generated/client/index.js +17 -3
  106. package/lib/src/generated/client/package.json +1 -1
  107. package/lib/src/generated/client/schema.prisma +35 -7
  108. package/lib/src/generated/client/wasm.js +14 -0
  109. package/lib/src/interceptors/CommandInterceptor.js +29 -8
  110. package/lib/src/interceptors/xenonScripts.js +55 -0
  111. package/lib/src/interfaces/IPluginArgs.js +1 -2
  112. package/lib/src/middleware/deviceAccessGuard.js +63 -9
  113. package/lib/src/plugin.js +15 -1
  114. package/lib/src/prisma.js +1 -1
  115. package/lib/src/scripts/database-check.js +124 -0
  116. package/lib/src/services/AIService.js +99 -0
  117. package/lib/src/services/CleanupService.js +6 -1
  118. package/lib/src/services/InterceptorService.js +70 -13
  119. package/lib/src/services/NetworkConditioningService.js +85 -88
  120. package/lib/src/services/NotificationService.js +36 -29
  121. package/lib/src/services/OrphanSweeper.js +5 -0
  122. package/lib/src/services/PortAllocator.js +5 -0
  123. package/lib/src/services/ServerManager.js +97 -8
  124. package/lib/src/services/SessionLifecycleService.js +63 -53
  125. package/lib/src/services/ShutdownCoordinator.js +33 -0
  126. package/lib/src/services/TeamService.js +20 -3
  127. package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
  128. package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
  129. package/lib/src/services/device-access/leaseHold.js +26 -0
  130. package/lib/src/services/device-access/ticketActorAccess.js +6 -0
  131. package/lib/src/services/healing/HealingOrchestrator.js +32 -1
  132. package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
  133. package/lib/src/services/interceptor/interceptorOptions.js +44 -0
  134. package/lib/src/services/lease/activeLeases.js +37 -0
  135. package/lib/src/services/network/AndroidRadios.js +71 -0
  136. package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
  137. package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
  138. package/lib/src/services/network/adbForPhone.js +60 -0
  139. package/lib/src/services/network/phoneNetworkLock.js +20 -0
  140. package/lib/src/services/network/xenonProxy.js +107 -0
  141. package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
  142. package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
  143. package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
  144. package/lib/src/services/omni-vision/screenScale.js +68 -0
  145. package/lib/src/services/recording/busy-precheck.js +24 -3
  146. package/lib/src/services/recording/concurrency-gate.js +12 -3
  147. package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
  148. package/lib/src/services/settings/labSettings.js +149 -0
  149. package/lib/src/services/webhookEvents.js +173 -0
  150. package/lib/test/helpers/fake-adb.js +58 -0
  151. package/lib/test/helpers/fake-device-table.js +63 -24
  152. package/lib/test/helpers/scratch-database.js +1 -0
  153. package/lib/test/unit/TeamService.test.js +3 -0
  154. package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
  155. package/lib/test/unit/apple-family.spec.js +36 -0
  156. package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
  157. package/lib/test/unit/cleanup-settings.spec.js +244 -0
  158. package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
  159. package/lib/test/unit/dashboard-commands.spec.js +249 -0
  160. package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
  161. package/lib/test/unit/database-check.spec.js +108 -0
  162. package/lib/test/unit/default-plugin-args.spec.js +33 -0
  163. package/lib/test/unit/device-access-lease.spec.js +270 -0
  164. package/lib/test/unit/device-offline-webhook.spec.js +81 -0
  165. package/lib/test/unit/device-setting-columns.spec.js +66 -0
  166. package/lib/test/unit/device-settings-loki.spec.js +87 -0
  167. package/lib/test/unit/device-settings-survive.spec.js +555 -0
  168. package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
  169. package/lib/test/unit/emulator-boot.spec.js +91 -0
  170. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
  171. package/lib/test/unit/hub-node-same-host.spec.js +5 -0
  172. package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
  173. package/lib/test/unit/json-logging-option.spec.js +74 -0
  174. package/lib/test/unit/lab-settings.spec.js +218 -0
  175. package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
  176. package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
  177. package/lib/test/unit/lease-held-phone.spec.js +196 -0
  178. package/lib/test/unit/max-sessions.spec.js +240 -0
  179. package/lib/test/unit/ocr-text-match.spec.js +44 -0
  180. package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
  181. package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
  182. package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
  183. package/lib/test/unit/omni-vision.spec.js +1 -1
  184. package/lib/test/unit/partial-config-file.spec.js +114 -0
  185. package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
  186. package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
  187. package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
  188. package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
  189. package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
  190. package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
  191. package/lib/test/unit/phone-network/session-start.spec.js +110 -0
  192. package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
  193. package/lib/test/unit/plugin.spec.js +3 -3
  194. package/lib/test/unit/recording-options.spec.js +181 -0
  195. package/lib/test/unit/schema-required-args.spec.js +21 -16
  196. package/lib/test/unit/self-healing-boot.spec.js +75 -0
  197. package/lib/test/unit/self-healing-setting.spec.js +272 -0
  198. package/lib/test/unit/session-failed-webhook.spec.js +170 -0
  199. package/lib/test/unit/shell-commands.spec.js +133 -0
  200. package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
  201. package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
  202. package/lib/test/unit/stream-status-type.spec.js +4 -0
  203. package/lib/test/unit/vision-assertion.spec.js +118 -0
  204. package/lib/test/unit/wda-client-shell.spec.js +133 -0
  205. package/lib/test/unit/web-config-service.spec.js +79 -0
  206. package/lib/test/unit/webhook-delivery.spec.js +24 -0
  207. package/lib/test/unit/webhook-payloads.spec.js +273 -0
  208. package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
  209. package/lib/tsconfig.tsbuildinfo +1 -1
  210. package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
  211. package/package.json +1 -1
  212. package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
  213. package/prisma/schema.prisma +35 -7
  214. package/schema.json +19 -46
  215. package/scripts/generate-types-from-schema.js +7 -2
  216. package/scripts/lib/always-present-plugin-args.js +42 -0
  217. package/lib/public/assets/DeviceMosaicView-ChhHnpFQ.js +0 -2
  218. package/lib/public/assets/device-explorer-DSpAA_t9.js +0 -279
  219. package/lib/public/assets/maintenance-settings-BxbA7SkB.js +0 -6
  220. package/lib/public/assets/recording-group-store-B75435xR.js +0 -26
  221. package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
  222. package/lib/public/assets/settings-C1VIluuT.js +0 -6
  223. package/lib/public/assets/webhook-settings-DAHgnJyZ.js +0 -1
@@ -0,0 +1,270 @@
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 express_1 = __importDefault(require("express"));
18
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
19
+ const deviceAccessPolicy_1 = require("../../src/services/device-access/deviceAccessPolicy");
20
+ const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
21
+ const ticketActorAccess_1 = require("../../src/services/device-access/ticketActorAccess");
22
+ const streamStartConflict_1 = require("../../src/app/routers/streamStartConflict");
23
+ const busy_precheck_1 = require("../../src/services/recording/busy-precheck");
24
+ // A phone an SDK lease holds is its lease holder's. The guard used to read
25
+ // only `busy` and `session_id`: a freshly leased phone (busy, no session) was
26
+ // refused to everyone but admins, its holder included, and a leased phone
27
+ // whose `busy` had been cleared (a lease-bound session's release, the idle
28
+ // sweeper) was open to anyone.
29
+ const UDID = 'DEV-1';
30
+ const HOST = 'http://hub:4723';
31
+ const ALICE = 'usr_alice';
32
+ const ALICE_KEY = 'key_alice';
33
+ const BOB = 'usr_bob';
34
+ const BOB_KEY = 'key_bob';
35
+ const aliceLease = { actorId: ALICE_KEY, holderUserId: ALICE };
36
+ function input(over = {}) {
37
+ return Object.assign({ udid: UDID, busy: true, sessionId: null, sessionOwnerUserId: null, actorUserId: BOB, actorApiKeyId: BOB_KEY, isAdmin: false, lease: aliceLease }, over);
38
+ }
39
+ describe('device access on a leased phone', () => {
40
+ describe('evaluateDeviceAccess', () => {
41
+ it('lets the lease holder through by the key that created it', () => {
42
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ actorUserId: 'usr_other', actorApiKeyId: ALICE_KEY }))).to.deep.equal({ allow: true });
43
+ });
44
+ it('lets the lease holder through with any credential of theirs', () => {
45
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ actorUserId: ALICE, actorApiKeyId: undefined }))).to.deep.equal({ allow: true });
46
+ });
47
+ it('lets a user through on a lease made with their bearer token (actorId is the user id)', () => {
48
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({
49
+ actorUserId: ALICE,
50
+ actorApiKeyId: undefined,
51
+ lease: { actorId: ALICE, holderUserId: ALICE },
52
+ }))).to.deep.equal({ allow: true });
53
+ });
54
+ it('refuses anyone else, naming the lease holder', () => {
55
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input())).to.deep.equal({
56
+ allow: false,
57
+ code: 'device_held_by_another_user',
58
+ holderId: ALICE,
59
+ heldBy: 'lease',
60
+ });
61
+ });
62
+ it("refuses anyone else even when the phone's busy flag was cleared", () => {
63
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ busy: false })).allow).to.equal(false);
64
+ });
65
+ it('lets the owner of the session running on the leased phone drive it', () => {
66
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ sessionId: 'sess-1', sessionOwnerUserId: BOB }))).to.deep.equal({ allow: true });
67
+ });
68
+ it("doesn't let a manual hold on a leased phone stand in for the lease", () => {
69
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ sessionId: `manual_${BOB}_${UDID}` })).allow).to.equal(false);
70
+ });
71
+ it('fails closed when the holder cannot be resolved', () => {
72
+ const d = (0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ lease: { actorId: 'key_gone', holderUserId: null } }));
73
+ (0, chai_1.expect)(d).to.deep.equal({
74
+ allow: false,
75
+ code: 'device_held_by_another_user',
76
+ holderId: '',
77
+ heldBy: 'lease',
78
+ });
79
+ });
80
+ it('still allows an admin', () => {
81
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ isAdmin: true }))).to.deep.equal({ allow: true });
82
+ });
83
+ it('leaves an unleased phone to the rules it had', () => {
84
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ lease: null, busy: false }))).to.deep.equal({
85
+ allow: true,
86
+ });
87
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ lease: null, sessionId: 'sess-1', sessionOwnerUserId: ALICE }))
88
+ .allow).to.equal(false);
89
+ });
90
+ it('isLeaseHolder needs a lease', () => {
91
+ (0, chai_1.expect)((0, deviceAccessPolicy_1.isLeaseHolder)(null, ALICE, ALICE_KEY)).to.equal(false);
92
+ });
93
+ });
94
+ describe('deviceAccessGuard', () => {
95
+ function appFor(opts) {
96
+ var _a;
97
+ const app = (0, express_1.default)();
98
+ app.use(express_1.default.json());
99
+ app.use((req, _res, next) => {
100
+ var _a;
101
+ req.auth = {
102
+ kind: 'api-key',
103
+ userId: opts.actorUserId,
104
+ role: (_a = opts.role) !== null && _a !== void 0 ? _a : 'MEMBER',
105
+ scopes: 'devices',
106
+ rateLimit: 100,
107
+ };
108
+ if (opts.apiKeyId)
109
+ req.apiKey = { id: opts.apiKeyId };
110
+ next();
111
+ });
112
+ const router = express_1.default.Router();
113
+ router.use((0, deviceAccessGuard_1.deviceAccessGuard)({
114
+ findDevice: () => __awaiter(this, void 0, void 0, function* () {
115
+ var _a;
116
+ return ({
117
+ udid: UDID,
118
+ host: HOST,
119
+ busy: (_a = opts.busy) !== null && _a !== void 0 ? _a : true,
120
+ session_id: null,
121
+ });
122
+ }),
123
+ resolveSessionOwner: () => __awaiter(this, void 0, void 0, function* () { return null; }),
124
+ describeHolder: (id) => __awaiter(this, void 0, void 0, function* () { return (id === ALICE ? 'alice@example.com' : null); }),
125
+ findActiveLease: (_a = opts.leaseLookup) !== null && _a !== void 0 ? _a : ((udid, host) => __awaiter(this, void 0, void 0, function* () { var _a; return (udid === UDID && host === HOST ? ((_a = opts.lease) !== null && _a !== void 0 ? _a : null) : null); })),
126
+ resolveLeaseHolder: () => __awaiter(this, void 0, void 0, function* () { var _a; return (_a = opts.leaseHolder) !== null && _a !== void 0 ? _a : null; }),
127
+ }));
128
+ router.post('/:udid/tap', (_req, res) => res.json({ reached: true }));
129
+ router.get('/:udid/clipboard', (_req, res) => res.json({ reached: true }));
130
+ app.use('/control', router);
131
+ return app;
132
+ }
133
+ it('lets the lease holder control the phone they just leased', () => __awaiter(void 0, void 0, void 0, function* () {
134
+ const res = yield (0, loopbackRequest_1.default)(appFor({ actorUserId: ALICE, lease: { actorId: ALICE_KEY }, leaseHolder: ALICE })).post(`/control/${UDID}/tap`);
135
+ (0, chai_1.expect)(res.status).to.equal(200);
136
+ (0, chai_1.expect)(res.body.reached).to.equal(true);
137
+ }));
138
+ it('refuses another member with a message about the lease', () => __awaiter(void 0, void 0, void 0, function* () {
139
+ const res = yield (0, loopbackRequest_1.default)(appFor({ actorUserId: BOB, lease: { actorId: ALICE_KEY }, leaseHolder: ALICE })).post(`/control/${UDID}/tap`);
140
+ (0, chai_1.expect)(res.status).to.equal(409);
141
+ (0, chai_1.expect)(res.body.error).to.equal('device_held_by_another_user');
142
+ (0, chai_1.expect)(res.body.message).to.match(/leased by alice@example\.com/);
143
+ (0, chai_1.expect)(res.body.holder.userId).to.equal(ALICE);
144
+ }));
145
+ it('refuses another member on a leased phone whose busy flag was cleared', () => __awaiter(void 0, void 0, void 0, function* () {
146
+ const res = yield (0, loopbackRequest_1.default)(appFor({
147
+ actorUserId: BOB,
148
+ busy: false,
149
+ lease: { actorId: ALICE_KEY },
150
+ leaseHolder: ALICE,
151
+ })).get(`/control/${UDID}/clipboard`);
152
+ (0, chai_1.expect)(res.status).to.equal(409);
153
+ }));
154
+ it('answers 503 when it cannot tell whether a lease holds the phone', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ const res = yield (0, loopbackRequest_1.default)(appFor({
156
+ actorUserId: BOB,
157
+ leaseLookup: () => __awaiter(void 0, void 0, void 0, function* () {
158
+ throw new Error('db down');
159
+ }),
160
+ })).post(`/control/${UDID}/tap`);
161
+ (0, chai_1.expect)(res.status).to.equal(503);
162
+ (0, chai_1.expect)(res.body.error).to.equal('device_ownership_unavailable');
163
+ }));
164
+ it('does not look leases up for an admin', () => __awaiter(void 0, void 0, void 0, function* () {
165
+ const res = yield (0, loopbackRequest_1.default)(appFor({
166
+ actorUserId: BOB,
167
+ role: 'SUPER_ADMIN',
168
+ leaseLookup: () => __awaiter(void 0, void 0, void 0, function* () {
169
+ throw new Error('an admin should not need this');
170
+ }),
171
+ })).post(`/control/${UDID}/tap`);
172
+ (0, chai_1.expect)(res.status).to.equal(200);
173
+ }));
174
+ });
175
+ describe('the logcat ticket authorizer', () => {
176
+ const authorize = (0, ticketActorAccess_1.makeTicketActorAuthorizer)({
177
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: UDID, host: HOST, busy: true, session_id: null }); }),
178
+ resolveSessionOwner: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
179
+ findActiveLease: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: ALICE_KEY }); }),
180
+ resolveLeaseHolder: () => __awaiter(void 0, void 0, void 0, function* () { return ALICE; }),
181
+ });
182
+ it('lets the lease holder read the leased phone', () => __awaiter(void 0, void 0, void 0, function* () {
183
+ (0, chai_1.expect)(yield authorize(UDID, { actorId: ALICE })).to.equal(true);
184
+ }));
185
+ it('refuses anyone else', () => __awaiter(void 0, void 0, void 0, function* () {
186
+ (0, chai_1.expect)(yield authorize(UDID, { actorId: BOB })).to.equal(false);
187
+ }));
188
+ });
189
+ describe('stream/start', () => {
190
+ const start = (over = {}) => (0, streamStartConflict_1.decideStreamStartConflict)(Object.assign({ udid: UDID, busy: true, sessionId: null, hasLiveManualStream: false, sessionOwnerUserId: null, actorUserId: ALICE, isAdmin: false }, over));
191
+ it('lets the lease holder preview their leased phone (busy, no session)', () => {
192
+ (0, chai_1.expect)(start({ leaseHolder: true })).to.deep.equal({ action: 'proceed' });
193
+ });
194
+ it('still refuses a busy phone with no holder on record to anyone else', () => {
195
+ (0, chai_1.expect)(start().action).to.equal('deny');
196
+ });
197
+ it("doesn't let a lease stand in for someone else's session on the phone", () => {
198
+ (0, chai_1.expect)(start({ leaseHolder: true, sessionId: 'sess-9', sessionOwnerUserId: BOB }).action).to.equal('deny');
199
+ });
200
+ function appFor(actorUserId, role = 'MEMBER', leased = true) {
201
+ const app = (0, express_1.default)();
202
+ app.use((req, _res, next) => {
203
+ req.auth = { kind: 'api-key', userId: actorUserId, role, scopes: 'devices' };
204
+ next();
205
+ });
206
+ const router = express_1.default.Router();
207
+ router.use((0, deviceAccessGuard_1.deviceAccessGuard)({
208
+ findDevice: () => __awaiter(this, void 0, void 0, function* () { return ({ udid: UDID, host: HOST, busy: false, session_id: null }); }),
209
+ resolveSessionOwner: () => __awaiter(this, void 0, void 0, function* () { return null; }),
210
+ describeHolder: () => __awaiter(this, void 0, void 0, function* () { return 'alice@example.com'; }),
211
+ findActiveLease: () => __awaiter(this, void 0, void 0, function* () {
212
+ if (role === 'SUPER_ADMIN')
213
+ throw new Error('an admin should not need this');
214
+ return leased ? { actorId: ALICE_KEY } : null;
215
+ }),
216
+ resolveLeaseHolder: () => __awaiter(this, void 0, void 0, function* () { return ALICE; }),
217
+ }));
218
+ router.post('/:udid/stream/start', (_req, res) => res.json({ reached: true }));
219
+ app.use('/control', router);
220
+ return app;
221
+ }
222
+ it('refuses a stranger before the handler (and before a hub forwards it)', () => __awaiter(void 0, void 0, void 0, function* () {
223
+ const res = yield (0, loopbackRequest_1.default)(appFor(BOB)).post(`/control/${UDID}/stream/start`);
224
+ (0, chai_1.expect)(res.status).to.equal(409);
225
+ (0, chai_1.expect)(res.body.message).to.match(/leased by/);
226
+ }));
227
+ it('passes the lease holder on to the handler', () => __awaiter(void 0, void 0, void 0, function* () {
228
+ const res = yield (0, loopbackRequest_1.default)(appFor(ALICE)).post(`/control/${UDID}/stream/start`);
229
+ (0, chai_1.expect)(res.body.reached).to.equal(true);
230
+ }));
231
+ it('leaves an unleased phone to stream/start, as before', () => __awaiter(void 0, void 0, void 0, function* () {
232
+ const res = yield (0, loopbackRequest_1.default)(appFor(BOB, 'MEMBER', false)).post(`/control/${UDID}/stream/start`);
233
+ (0, chai_1.expect)(res.body.reached).to.equal(true);
234
+ }));
235
+ it('passes an admin without looking leases up', () => __awaiter(void 0, void 0, void 0, function* () {
236
+ const res = yield (0, loopbackRequest_1.default)(appFor(BOB, 'SUPER_ADMIN')).post(`/control/${UDID}/stream/start`);
237
+ (0, chai_1.expect)(res.body.reached).to.equal(true);
238
+ }));
239
+ });
240
+ describe('recordings (BusyPrecheck)', () => {
241
+ const precheck = (row, lease) => new busy_precheck_1.BusyPrecheck({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign({ udid: UDID, host: HOST }, row)); }) }, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return false; }) }, {
242
+ find: () => __awaiter(void 0, void 0, void 0, function* () {
243
+ if (lease instanceof Error)
244
+ throw lease;
245
+ return lease;
246
+ }),
247
+ holderOf: () => __awaiter(void 0, void 0, void 0, function* () { return ALICE; }),
248
+ });
249
+ it("refuses a stranger's recording of a leased phone, busy or not", () => __awaiter(void 0, void 0, void 0, function* () {
250
+ (0, chai_1.expect)(yield precheck({ busy: false }, { actorId: ALICE_KEY }).findBusy([UDID], BOB)).to.deep.equal([{ udid: UDID, reason: 'leased' }]);
251
+ }));
252
+ it('lets the lease holder record their leased phone', () => __awaiter(void 0, void 0, void 0, function* () {
253
+ (0, chai_1.expect)(yield precheck({ busy: true }, { actorId: ALICE_KEY }).findBusy([UDID], ALICE)).to.deep.equal([]);
254
+ }));
255
+ it('still refuses the holder while a session runs on it, as for any session', () => __awaiter(void 0, void 0, void 0, function* () {
256
+ const busy = yield precheck({ busy: true, session_id: 'sess-1' }, { actorId: ALICE_KEY }).findBusy([UDID], ALICE);
257
+ (0, chai_1.expect)(busy).to.deep.equal([{ udid: UDID, reason: 'automation', sessionId: 'sess-1' }]);
258
+ }));
259
+ it('fails the start when it cannot tell whether a lease holds the phone', () => __awaiter(void 0, void 0, void 0, function* () {
260
+ let thrown;
261
+ try {
262
+ yield precheck({ busy: false }, new Error('db down')).findBusy([UDID], BOB);
263
+ }
264
+ catch (e) {
265
+ thrown = e;
266
+ }
267
+ (0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.message).to.equal('db down');
268
+ }));
269
+ });
270
+ });
@@ -0,0 +1,81 @@
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 typedi_1 = require("typedi");
19
+ const device_service_1 = require("../../src/data-service/device-service");
20
+ const NotificationService_1 = require("../../src/services/NotificationService");
21
+ const SocketServer_1 = require("../../src/services/SocketServer");
22
+ const container_registration_1 = require("../helpers/container-registration");
23
+ const loki_stores_1 = require("../helpers/loki-stores");
24
+ /**
25
+ * The `device_offline` webhook carries the device's name and platform, as its
26
+ * documented payload says (src/services/webhookEvents.ts). Every caller of
27
+ * `removeDevice` holds only a udid and a host, so the event used to carry just
28
+ * those two and `{{name}}` stayed unfilled in a template.
29
+ */
30
+ describe('device_offline: what the event carries', () => {
31
+ (0, loki_stores_1.useLokiStores)();
32
+ let restore;
33
+ let dispatch;
34
+ const HOST = 'http://127.0.0.1:4723';
35
+ beforeEach(() => {
36
+ restore = (0, container_registration_1.saveRegistrations)(NotificationService_1.NotificationService, SocketServer_1.SocketServer);
37
+ dispatch = sinon_1.default.stub().resolves();
38
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: dispatch } });
39
+ typedi_1.Container.set({
40
+ id: SocketServer_1.SocketServer,
41
+ value: {
42
+ emitToDashboard: () => undefined,
43
+ emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
44
+ hasScopedDashboard: () => false,
45
+ },
46
+ });
47
+ });
48
+ afterEach(() => {
49
+ sinon_1.default.restore();
50
+ restore();
51
+ });
52
+ const phone = () => ({
53
+ udid: 'R58M123',
54
+ name: 'Galaxy S9+',
55
+ platform: 'android',
56
+ sdk: '10',
57
+ host: HOST,
58
+ busy: false,
59
+ offline: false,
60
+ userBlocked: false,
61
+ deviceType: 'real',
62
+ realDevice: true,
63
+ });
64
+ it('names the device and its platform, though the caller passed only a udid and a host', () => __awaiter(void 0, void 0, void 0, function* () {
65
+ yield (0, device_service_1.addNewDevice)([phone()]);
66
+ dispatch.resetHistory();
67
+ yield (0, device_service_1.removeDevice)([{ udid: 'R58M123', host: HOST }]);
68
+ (0, chai_1.expect)(dispatch.calledOnce).to.equal(true);
69
+ (0, chai_1.expect)(dispatch.firstCall.args[0]).to.equal('device_offline');
70
+ (0, chai_1.expect)(dispatch.firstCall.args[1]).to.include({
71
+ udid: 'R58M123',
72
+ host: HOST,
73
+ name: 'Galaxy S9+',
74
+ platform: 'android',
75
+ });
76
+ }));
77
+ it('still reports a device the store no longer knows, by its udid and host', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ yield (0, device_service_1.removeDevice)([{ udid: 'gone-1', host: HOST }]);
79
+ (0, chai_1.expect)(dispatch.firstCall.args[1]).to.deep.include({ udid: 'gone-1', host: HOST });
80
+ }));
81
+ });
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const client_1 = require("../../src/generated/client");
5
+ const deviceFieldOwners_1 = require("../../src/data-service/deviceFieldOwners");
6
+ const deviceSettings_1 = require("../../src/data-service/deviceSettings");
7
+ /**
8
+ * The settings a phone keeps when its Device row goes (deviceSettings.ts) are
9
+ * the Device columns placed as `setting` in deviceFieldOwners.ts, and the
10
+ * DeviceSetting table has exactly those, besides its key. A setting added to
11
+ * one and not the other would be lost, or never written.
12
+ */
13
+ describe('Saved phone settings: the columns', () => {
14
+ it('DeviceSetting has exactly the setting columns, besides its key', () => {
15
+ const saved = Object.keys(client_1.Prisma.DeviceSettingScalarFieldEnum).filter((column) => !['udid', 'host', 'updatedAt'].includes(column));
16
+ (0, chai_1.expect)([...deviceFieldOwners_1.SETTING_FIELDS].sort()).to.deep.equal(saved.sort());
17
+ });
18
+ it('are team, tags, maintenance and the reservation', () => {
19
+ (0, chai_1.expect)([...deviceFieldOwners_1.SETTING_FIELDS].sort()).to.deep.equal([
20
+ 'reservationReason',
21
+ 'reservedBy',
22
+ 'reservedByUserId',
23
+ 'reservedUntil',
24
+ 'tags',
25
+ 'teamId',
26
+ 'userBlocked',
27
+ ].sort());
28
+ });
29
+ it('are never written by a sync or by a node’s report', () => {
30
+ for (const column of deviceFieldOwners_1.SETTING_FIELDS) {
31
+ (0, chai_1.expect)(deviceFieldOwners_1.DISCOVERY_FIELDS.has(column), column).to.equal(false);
32
+ (0, chai_1.expect)(deviceFieldOwners_1.NODE_REPORT_FIELDS.has(column), column).to.equal(false);
33
+ }
34
+ });
35
+ it('pickSettingFields keeps only them', () => {
36
+ (0, chai_1.expect)((0, deviceFieldOwners_1.pickSettingFields)({ teamId: 't', busy: true, session_id: 's', userBlocked: true })).to.deep.equal({ teamId: 't', userBlocked: true });
37
+ });
38
+ it('a new row starts with every one of them, so a cleared one stays cleared', () => {
39
+ const now = 1000;
40
+ (0, chai_1.expect)(Object.keys((0, deviceSettings_1.restoredColumns)({}, now)).sort()).to.deep.equal([...deviceFieldOwners_1.SETTING_FIELDS].sort());
41
+ (0, chai_1.expect)((0, deviceSettings_1.restoredColumns)({}, now)).to.deep.equal({
42
+ teamId: null,
43
+ tags: null,
44
+ userBlocked: false,
45
+ reservationReason: null,
46
+ reservedBy: null,
47
+ reservedByUserId: null,
48
+ reservedUntil: null,
49
+ });
50
+ });
51
+ it('a reservation only while it holds', () => {
52
+ const reservation = {
53
+ reservedBy: 'Dana',
54
+ reservedByUserId: 'u1',
55
+ reservationReason: 'release',
56
+ reservedUntil: 2000,
57
+ };
58
+ (0, chai_1.expect)((0, deviceSettings_1.restoredColumns)(reservation, 1999)).to.include(reservation);
59
+ (0, chai_1.expect)((0, deviceSettings_1.restoredColumns)(reservation, 2000)).to.include({
60
+ reservedBy: null,
61
+ reservedByUserId: null,
62
+ reservationReason: null,
63
+ reservedUntil: null,
64
+ });
65
+ });
66
+ });
@@ -0,0 +1,87 @@
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
+ const db_1 = require("../../src/data-service/db");
16
+ const deviceSettings_1 = require("../../src/data-service/deviceSettings");
17
+ const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
18
+ const loki_stores_1 = require("../helpers/loki-stores");
19
+ /**
20
+ * The in-memory store keeps a phone's settings apart from its row as the
21
+ * Prisma store does (device-settings-survive.spec.ts runs the real one).
22
+ */
23
+ const IP = '10.0.0.1';
24
+ const OWN = `http://${IP}:4723`;
25
+ const NODE = `http://${IP}:4725`;
26
+ const local = (0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: IP }, 4723);
27
+ const phone = (udid, host = OWN, over = {}) => (Object.assign({ udid,
28
+ host, name: udid, platform: 'android', deviceType: 'real', realDevice: true, state: 'device', sdk: '14', busy: false, userBlocked: false, offline: false, totalUtilizationTimeMilliSec: 0, sessionStartTime: 0 }, over));
29
+ describe('Saved phone settings in the in-memory store', () => {
30
+ (0, loki_stores_1.useLokiStores)();
31
+ const store = () => device_store_1.DeviceStoreFactory.getStore();
32
+ const empty = () => __awaiter(void 0, void 0, void 0, function* () {
33
+ (yield db_1.XenonDatabase.DeviceModel).removeDataOnly();
34
+ (yield db_1.XenonDatabase.DeviceSettingsModel).removeDataOnly();
35
+ });
36
+ beforeEach(empty);
37
+ after(empty);
38
+ const configure = (udid, host = OWN) => store().updateDevice(udid, host, { teamId: 'team-a', tags: ['lab-1'], userBlocked: true });
39
+ it('a phone removed and added again keeps them', () => __awaiter(void 0, void 0, void 0, function* () {
40
+ yield store().addDevices([phone('p1')]);
41
+ yield configure('p1');
42
+ yield store().removeDevices({ udid: 'p1', host: OWN });
43
+ const [back] = yield store().addDevices([phone('p1')]);
44
+ (0, chai_1.expect)(back).to.include({ teamId: 'team-a', userBlocked: true });
45
+ (0, chai_1.expect)(back.tags).to.deep.equal(['lab-1']);
46
+ }));
47
+ it('keeps them across a restart, and saves those kept only on a row', () => __awaiter(void 0, void 0, void 0, function* () {
48
+ yield store().addDevices([phone('p1'), phone('legacy', OWN, { teamId: 'team-b' })]);
49
+ yield configure('p1');
50
+ yield (0, deviceSettings_1.resetDevicesAtBoot)(store(), { removeDevicesFromDatabaseBeforeRunningThePlugin: false }, local);
51
+ (0, chai_1.expect)(yield store().getAllDevices()).to.deep.equal([]);
52
+ yield store().addDevices([phone('p1'), phone('legacy')]);
53
+ (0, chai_1.expect)(yield store().findDevice({ udid: 'p1', host: OWN })).to.include({ teamId: 'team-a' });
54
+ (0, chai_1.expect)(yield store().findDevice({ udid: 'legacy', host: OWN })).to.include({
55
+ teamId: 'team-b',
56
+ });
57
+ }));
58
+ it('gives a node’s phone the hub’s settings, never its report’s or another host’s', () => __awaiter(void 0, void 0, void 0, function* () {
59
+ var _a, _b;
60
+ yield store().addDevices([phone('emulator-5554')]);
61
+ yield configure('emulator-5554');
62
+ yield store().addDevices([phone('node-phone', NODE)], { nodeReport: true });
63
+ yield store().updateDevice('node-phone', NODE, { teamId: 'team-a' });
64
+ yield store().removeDevices({ udid: 'node-phone', host: NODE });
65
+ yield store().addDevices([
66
+ phone('node-phone', NODE, { teamId: 'team-b', userBlocked: true }),
67
+ phone('emulator-5554', NODE),
68
+ ], { nodeReport: true });
69
+ (0, chai_1.expect)(yield store().findDevice({ udid: 'node-phone', host: NODE })).to.include({
70
+ teamId: 'team-a',
71
+ userBlocked: false,
72
+ });
73
+ const nodeEmulator = yield store().findDevice({ udid: 'emulator-5554', host: NODE });
74
+ (0, chai_1.expect)((_a = nodeEmulator === null || nodeEmulator === void 0 ? void 0 : nodeEmulator.teamId) !== null && _a !== void 0 ? _a : null).to.equal(null);
75
+ (0, chai_1.expect)((_b = nodeEmulator === null || nodeEmulator === void 0 ? void 0 : nodeEmulator.userBlocked) !== null && _b !== void 0 ? _b : false).to.equal(false);
76
+ }));
77
+ it('lists the saved phones of a udid, and forgets those of the hosts named', () => __awaiter(void 0, void 0, void 0, function* () {
78
+ yield store().updateDevice('p1', OWN, { userBlocked: true });
79
+ yield store().updateDevice('p1', NODE, { userBlocked: true });
80
+ (0, chai_1.expect)(yield store().findSavedPhones('p1')).to.have.deep.members([
81
+ { udid: 'p1', host: OWN },
82
+ { udid: 'p1', host: NODE },
83
+ ]);
84
+ yield store().forgetSettings([OWN]);
85
+ (0, chai_1.expect)(yield store().findSavedPhones('p1')).to.deep.equal([{ udid: 'p1', host: NODE }]);
86
+ }));
87
+ });