@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
@@ -353,6 +353,8 @@ describe('Auth disabled: the lab path looks nothing up', () => {
353
353
  const row = { udid: 'phone-a', host: 'h', teamId: 'team-a' };
354
354
  sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
355
355
  removeDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
356
+ // removeDevice reads the row for the device_offline webhook's name and platform.
357
+ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
356
358
  addDevices: () => __awaiter(void 0, void 0, void 0, function* () { return [row]; }),
357
359
  updateDevice: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
358
360
  getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return [Object.assign(Object.assign({}, row), { sessionStartTime: 0, totalUtilizationTimeMilliSec: 0 })]; }),
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ require("reflect-metadata");
49
+ const chai_1 = require("chai");
50
+ const sinon_1 = __importDefault(require("sinon"));
51
+ const express_1 = __importDefault(require("express"));
52
+ const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
53
+ const typedi_1 = require("typedi");
54
+ const control_1 = __importDefault(require("../../src/app/routers/control"));
55
+ const device_store_1 = require("../../src/data-service/device-store");
56
+ const deviceService = __importStar(require("../../src/data-service/device-service"));
57
+ const recording_store_1 = require("../../src/services/recording/recording-store");
58
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
59
+ const AndroidStreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidStreamService"));
60
+ const AndroidH264StreamService_1 = __importDefault(require("../../src/device-managers/android/AndroidH264StreamService"));
61
+ const PluginContext_1 = require("../../src/PluginContext");
62
+ const container_registration_1 = require("../helpers/container-registration");
63
+ const loopbackServer_1 = require("../helpers/loopbackServer");
64
+ const own_node_id_1 = require("../helpers/own-node-id");
65
+ /**
66
+ * Which capture pipelines POST /control/:udid/stream/start runs.
67
+ *
68
+ * With `streaming.androidH264` on, the start ran the H.264 service (scrcpy),
69
+ * and device control's `<img>` then asked GET /stream, which ran the screencap
70
+ * MJPEG loop beside it: two captures of one phone, the very thing the comment
71
+ * in the handler says must not happen. Device control, and any browser that
72
+ * cannot decode H.264 (WebCodecs exists only in a secure context, so plain
73
+ * http://hub:4723 has none), shows MJPEG, so it says so when it starts the
74
+ * stream (`player: 'mjpeg'`), and the server then runs MJPEG only.
75
+ */
76
+ const UDID = 'DEV-1';
77
+ function buildApp() {
78
+ const app = (0, express_1.default)();
79
+ app.use(express_1.default.json());
80
+ const apiRouter = express_1.default.Router();
81
+ apiRouter.use((req, _res, next) => {
82
+ req.auth = {
83
+ kind: 'user-session',
84
+ userId: 'usr_alice',
85
+ role: 'ADMIN',
86
+ scopes: 'admin,devices,sessions,read',
87
+ rateLimit: 100,
88
+ };
89
+ next();
90
+ });
91
+ control_1.default.register(apiRouter);
92
+ app.use('/xenon/api', apiRouter);
93
+ return app;
94
+ }
95
+ describe('POST /control/:udid/stream/start: which capture runs', () => {
96
+ (0, own_node_id_1.useOwnNodeId)();
97
+ const loopback = (0, loopbackServer_1.loopbackServers)();
98
+ let restoreContainer;
99
+ let savedArgs;
100
+ let mjpegStart;
101
+ let h264Start;
102
+ let h264Stop;
103
+ let h264Running;
104
+ let row;
105
+ const flag = (androidH264) => {
106
+ typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = Object.assign(Object.assign({}, savedArgs), { streaming: { androidH264 } });
107
+ };
108
+ beforeEach(() => {
109
+ restoreContainer = (0, container_registration_1.saveRegistrations)(AndroidStreamService_1.default, AndroidH264StreamService_1.default, IOSStreamService_1.default, recording_store_1.RecordingStore);
110
+ savedArgs = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs;
111
+ row = {
112
+ udid: UDID,
113
+ host: '127.0.0.1',
114
+ nodeId: own_node_id_1.OWN_NODE_ID,
115
+ platform: 'android',
116
+ busy: false,
117
+ session_id: null,
118
+ screenWidth: '1080',
119
+ screenHeight: '2220',
120
+ };
121
+ sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return row; }) });
122
+ sinon_1.default.stub(deviceService, 'blockDevice').resolves();
123
+ h264Running = false;
124
+ mjpegStart = sinon_1.default.stub().resolves({ mjpegPort: 9100 });
125
+ h264Start = sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
126
+ h264Running = true;
127
+ return {};
128
+ }));
129
+ h264Stop = sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () {
130
+ h264Running = false;
131
+ }));
132
+ typedi_1.Container.set(AndroidStreamService_1.default, {
133
+ startStream: mjpegStart,
134
+ getStreamStatus: () => undefined,
135
+ });
136
+ typedi_1.Container.set(IOSStreamService_1.default, { getStreamStatus: () => undefined });
137
+ typedi_1.Container.set(AndroidH264StreamService_1.default, {
138
+ start: h264Start,
139
+ stop: h264Stop,
140
+ getMultiplexer: () => (h264Running ? { clientCount: 0 } : undefined),
141
+ });
142
+ typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return false; }) });
143
+ });
144
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
145
+ sinon_1.default.restore();
146
+ typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs = savedArgs;
147
+ restoreContainer();
148
+ yield loopback.closeAll();
149
+ }));
150
+ const start = (body) => __awaiter(void 0, void 0, void 0, function* () {
151
+ return (0, loopbackRequest_1.default)(yield loopback.serve(buildApp()))
152
+ .post(`/xenon/api/control/${UDID}/stream/start`)
153
+ .send(body !== null && body !== void 0 ? body : {});
154
+ });
155
+ it('runs the H.264 service alone when the flag is on and the page can play it', () => __awaiter(void 0, void 0, void 0, function* () {
156
+ flag(true);
157
+ const res = yield start();
158
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
159
+ (0, chai_1.expect)(res.body.type).to.equal('h264');
160
+ (0, chai_1.expect)(res.body.h264Path).to.equal(`/xenon/api/control/${UDID}/stream/h264`);
161
+ (0, chai_1.expect)(h264Start.calledOnce).to.equal(true);
162
+ (0, chai_1.expect)(mjpegStart.called).to.equal(false);
163
+ }));
164
+ it('runs the MJPEG capture alone when the page says it shows MJPEG', () => __awaiter(void 0, void 0, void 0, function* () {
165
+ flag(true);
166
+ const res = yield start({ player: 'mjpeg' });
167
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
168
+ (0, chai_1.expect)(res.body.type).to.equal('mjpeg');
169
+ (0, chai_1.expect)(res.body.streamUrl).to.equal(`/xenon/api/control/${UDID}/stream`);
170
+ (0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
171
+ (0, chai_1.expect)(h264Start.called).to.equal(false);
172
+ }));
173
+ it('ends an H.264 capture already running for the phone, so the two never overlap', () => __awaiter(void 0, void 0, void 0, function* () {
174
+ flag(true);
175
+ h264Running = true;
176
+ yield start({ player: 'mjpeg' });
177
+ (0, chai_1.expect)(h264Stop.calledOnceWith(UDID)).to.equal(true);
178
+ (0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
179
+ (0, chai_1.expect)(h264Running).to.equal(false);
180
+ }));
181
+ it('stops the H.264 capture before it starts the MJPEG one', () => __awaiter(void 0, void 0, void 0, function* () {
182
+ flag(true);
183
+ h264Running = true;
184
+ yield start({ player: 'mjpeg' });
185
+ (0, chai_1.expect)(h264Stop.calledBefore(mjpegStart)).to.equal(true);
186
+ }));
187
+ it('runs MJPEG alone, and stops nothing, when the flag is off', () => __awaiter(void 0, void 0, void 0, function* () {
188
+ flag(false);
189
+ const res = yield start();
190
+ (0, chai_1.expect)(res.body.type).to.equal('mjpeg');
191
+ (0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
192
+ (0, chai_1.expect)(h264Start.called).to.equal(false);
193
+ (0, chai_1.expect)(h264Stop.called).to.equal(false);
194
+ }));
195
+ it('ignores a player it does not know, and uses the server’s choice', () => __awaiter(void 0, void 0, void 0, function* () {
196
+ flag(true);
197
+ const res = yield start({ player: 'hologram' });
198
+ (0, chai_1.expect)(res.body.type).to.equal('h264');
199
+ (0, chai_1.expect)(h264Start.calledOnce).to.equal(true);
200
+ (0, chai_1.expect)(mjpegStart.called).to.equal(false);
201
+ }));
202
+ it('keeps MJPEG for a phone that is being recorded, as before', () => __awaiter(void 0, void 0, void 0, function* () {
203
+ flag(true);
204
+ typedi_1.Container.set(recording_store_1.RecordingStore, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return true; }) });
205
+ const res = yield start();
206
+ (0, chai_1.expect)(res.body.type).to.equal('mjpeg');
207
+ (0, chai_1.expect)(mjpegStart.calledOnce).to.equal(true);
208
+ (0, chai_1.expect)(h264Start.called).to.equal(false);
209
+ }));
210
+ });
@@ -20,4 +20,8 @@ describe('resolveStreamType', () => {
20
20
  (0, chai_1.expect)((0, streamType_1.resolveStreamType)('android', true, false)).to.equal('h264');
21
21
  (0, chai_1.expect)((0, streamType_1.resolveStreamType)('androidtv', true, false)).to.equal('h264');
22
22
  });
23
+ it('a page that shows MJPEG => mjpeg, whatever the flag', () => {
24
+ (0, chai_1.expect)((0, streamType_1.resolveStreamType)('android', true, false, false)).to.equal('mjpeg');
25
+ (0, chai_1.expect)((0, streamType_1.resolveStreamType)('android', true, false, true)).to.equal('h264');
26
+ });
23
27
  });
@@ -0,0 +1,118 @@
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 VisionAssertionService_1 = require("../../src/services/omni-vision/VisionAssertionService");
19
+ const AIService_1 = require("../../src/services/AIService");
20
+ /**
21
+ * `xenon: assertVisualState` answered `{ result: true, message: 'Assertion
22
+ * placeholder' }` whenever the AI step gave it nothing usable: with no AI
23
+ * provider, when the provider failed, and usually even with one, because the
24
+ * condition was wrapped in a "find the centre coordinates, answer {x, y}"
25
+ * prompt. A test asserting a screen state passed without anything being
26
+ * checked.
27
+ *
28
+ * Now it asks the provider a yes/no question of the screenshot and answers
29
+ * with the provider's verdict. Whenever there is no verdict (no provider, a
30
+ * failed call, an answer that isn't true or false, no screenshot, no
31
+ * condition) it fails with an error the test sees, never `true` and never
32
+ * `false`: a `false` would pass a test that asserts a state is absent.
33
+ */
34
+ describe('xenon: assertVisualState', () => {
35
+ const SCREEN = 'c2NyZWVuc2hvdA==';
36
+ let service;
37
+ let driver;
38
+ beforeEach(() => {
39
+ service = new VisionAssertionService_1.VisionAssertionService();
40
+ driver = { getScreenshot: sinon_1.default.stub().resolves(SCREEN) };
41
+ // The real provider set-up reads the server's AI settings; these tests set the provider themselves.
42
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
43
+ });
44
+ afterEach(() => sinon_1.default.restore());
45
+ const failureOf = (p) => p.then((value) => {
46
+ throw new Error(`expected a failure, got ${JSON.stringify(value)}`);
47
+ }, (e) => e);
48
+ function withProvider(answer) {
49
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value({ analyze: () => __awaiter(this, void 0, void 0, function* () { return ''; }) });
50
+ const call = sinon_1.default.stub(AIService_1.AI_SERVICE, 'callProvider');
51
+ if (answer instanceof Error)
52
+ call.rejects(answer);
53
+ else
54
+ call.resolves(answer);
55
+ return call;
56
+ }
57
+ it('fails, rather than passing, when no AI provider is configured', () => __awaiter(void 0, void 0, void 0, function* () {
58
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'provider').value(null);
59
+ const err = yield failureOf(service.assertState(driver, 'The Login button is visible'));
60
+ (0, chai_1.expect)(err.message).to.match(/No AI provider is configured/);
61
+ (0, chai_1.expect)(err.message).to.match(/not checked/);
62
+ }));
63
+ it('asks a yes/no question about the condition, with the screenshot', () => __awaiter(void 0, void 0, void 0, function* () {
64
+ const call = withProvider('{"result": true, "reason": "A Login button is shown."}');
65
+ const answer = yield service.assertState(driver, 'The Login button is visible');
66
+ (0, chai_1.expect)(answer).to.deep.equal({ result: true, message: 'A Login button is shown.' });
67
+ (0, chai_1.expect)(call.calledOnce).to.equal(true);
68
+ const [prompt, image] = call.firstCall.args;
69
+ (0, chai_1.expect)(image).to.equal(SCREEN);
70
+ (0, chai_1.expect)(prompt).to.include('The Login button is visible');
71
+ (0, chai_1.expect)(prompt).to.match(/true or false/i);
72
+ (0, chai_1.expect)(prompt).to.not.match(/coordinates/i);
73
+ }));
74
+ it("answers false when the provider says the condition doesn't hold", () => __awaiter(void 0, void 0, void 0, function* () {
75
+ withProvider('```json\n{"result": false, "reason": "The cart shows 2 items."}\n```');
76
+ (0, chai_1.expect)(yield service.assertState(driver, 'The cart is empty')).to.deep.equal({
77
+ result: false,
78
+ message: 'The cart shows 2 items.',
79
+ });
80
+ }));
81
+ it("fails when the provider's answer is not a true/false verdict", () => __awaiter(void 0, void 0, void 0, function* () {
82
+ for (const answer of ['{"x": 120, "y": 340}', 'Yes, I can see it.', '{"result": "maybe"}']) {
83
+ sinon_1.default.restore();
84
+ sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
85
+ driver = { getScreenshot: sinon_1.default.stub().resolves(SCREEN) };
86
+ withProvider(answer);
87
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
88
+ (0, chai_1.expect)(err.message, answer).to.match(/could not read/i);
89
+ }
90
+ }));
91
+ it('fails when the provider call fails', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ withProvider(new Error('429 quota exceeded'));
93
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
94
+ (0, chai_1.expect)(err.message).to.match(/429 quota exceeded/);
95
+ }));
96
+ it('fails when the provider is rate-limited', () => __awaiter(void 0, void 0, void 0, function* () {
97
+ // Gemini's provider answers this instead of throwing on a 429.
98
+ withProvider('CONNECTION_OK_RATE_LIMITED');
99
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
100
+ (0, chai_1.expect)(err.message).to.match(/rate-limited/i);
101
+ }));
102
+ it('fails without asking when there is no condition', () => __awaiter(void 0, void 0, void 0, function* () {
103
+ const call = withProvider('{"result": true, "reason": "ok"}');
104
+ for (const empty of ['', ' ', undefined]) {
105
+ const err = yield failureOf(service.assertState(driver, empty));
106
+ (0, chai_1.expect)(err.message).to.match(/needs a condition/);
107
+ }
108
+ (0, chai_1.expect)(call.called).to.equal(false);
109
+ (0, chai_1.expect)(driver.getScreenshot.called).to.equal(false);
110
+ }));
111
+ it('fails when no screenshot can be taken', () => __awaiter(void 0, void 0, void 0, function* () {
112
+ const call = withProvider('{"result": true, "reason": "ok"}');
113
+ driver.getScreenshot.rejects(new Error('WDA is not running'));
114
+ const err = yield failureOf(service.assertState(driver, 'The cart is empty'));
115
+ (0, chai_1.expect)(err.message).to.match(/WDA is not running/);
116
+ (0, chai_1.expect)(call.called).to.equal(false);
117
+ }));
118
+ });
@@ -0,0 +1,133 @@
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 fs_1 = __importDefault(require("fs"));
18
+ const os_1 = __importDefault(require("os"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const sinon_1 = __importDefault(require("sinon"));
21
+ const typedi_1 = require("typedi");
22
+ const IOSStreamService_1 = __importDefault(require("../../src/device-managers/ios/IOSStreamService"));
23
+ const IOSTunnels_1 = require("../../src/device-managers/ios/IOSTunnels");
24
+ const WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
25
+ const device_store_1 = require("../../src/data-service/device-store");
26
+ /**
27
+ * The Shell tab on iOS: simctl commands and a few generic ones on simulators,
28
+ * go-ios commands on real iPhones. Through 2.13.2 a real iPhone's command could
29
+ * carry its own `--udid` (another phone on the Mac), `list` named every device
30
+ * attached to the Mac, simulator commands took any path on this Mac, and the
31
+ * go-ios call ran without the phone's tunnel, so iOS 17+ phones never answered.
32
+ *
33
+ * `xcrun` and `ios` here are scripts that print the arguments they were given.
34
+ */
35
+ const SIMULATOR = 'TEST-SIM-0000-AAAA';
36
+ const IPHONE = '00008110-00084CE80E51401E';
37
+ const ECHO = `#!/bin/sh
38
+ printf '%s|' "$@"
39
+ printf 'port=%s' "$GO_IOS_AGENT_PORT"
40
+ `;
41
+ describe('WDAClient.executeShell', () => {
42
+ let dir;
43
+ let savedPath;
44
+ let realDevice;
45
+ const borrowed = [];
46
+ before(() => {
47
+ dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-fake-shell-'));
48
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'xcrun'), ECHO, { mode: 0o755 });
49
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'ios'), ECHO, { mode: 0o755 });
50
+ });
51
+ after(() => fs_1.default.rmSync(dir, { recursive: true, force: true }));
52
+ beforeEach(() => {
53
+ savedPath = process.env.PATH;
54
+ process.env.PATH = `${dir}:${process.env.PATH}`;
55
+ borrowed.length = 0;
56
+ const stream = { isGoIOSAvailable: () => __awaiter(void 0, void 0, void 0, function* () { return true; }), goIOSPath: path_1.default.join(dir, 'ios') };
57
+ const tunnels = {
58
+ borrow(udid) {
59
+ return __awaiter(this, void 0, void 0, function* () {
60
+ borrowed.push(udid);
61
+ return 12100;
62
+ });
63
+ },
64
+ envFor: () => (Object.assign(Object.assign({}, process.env), { GO_IOS_AGENT_PORT: '12100' })),
65
+ };
66
+ const real = typedi_1.Container.get.bind(typedi_1.Container);
67
+ sinon_1.default.stub(typedi_1.Container, 'get').callsFake((token) => {
68
+ if (token === IOSStreamService_1.default)
69
+ return stream;
70
+ if (token === IOSTunnels_1.IOSTunnels)
71
+ return tunnels;
72
+ return real(token);
73
+ });
74
+ sinon_1.default
75
+ .stub(device_store_1.DeviceStoreFactory, 'getStore')
76
+ .returns({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ realDevice }); }) });
77
+ });
78
+ afterEach(() => {
79
+ process.env.PATH = savedPath;
80
+ sinon_1.default.restore();
81
+ });
82
+ const run = (udid, command) => new WDAClient_1.WDAClient().executeShell(udid, command).then((out) => ({ out }), (err) => ({ err: err.message }));
83
+ describe('on a simulator', () => {
84
+ beforeEach(() => {
85
+ realDevice = false;
86
+ });
87
+ it('runs a simctl command for this simulator', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ const { out } = (yield run(SIMULATOR, 'getenv HOME'));
89
+ (0, chai_1.expect)(out).to.match(new RegExp(`^simctl\\|getenv\\|${SIMULATOR}\\|HOME\\|`));
90
+ }));
91
+ it('spawns a generic command in this simulator, without arguments', () => __awaiter(void 0, void 0, void 0, function* () {
92
+ const { out } = (yield run(SIMULATOR, 'ls'));
93
+ (0, chai_1.expect)(out).to.match(new RegExp(`^simctl\\|spawn\\|${SIMULATOR}\\|ls\\|`));
94
+ (0, chai_1.expect)(yield run(SIMULATOR, 'ls /Users'))
95
+ .to.have.property('err')
96
+ .that.match(/not allowed/);
97
+ }));
98
+ it('refuses list, chaining and look-alikes', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ for (const cmd of ['list', 'ls; id', 'lsof', 'getenv $HOME']) {
100
+ (0, chai_1.expect)(yield run(SIMULATOR, cmd), cmd)
101
+ .to.have.property('err')
102
+ .that.match(/not allowed/);
103
+ }
104
+ }));
105
+ });
106
+ describe('on a real iPhone', () => {
107
+ beforeEach(() => {
108
+ realDevice = true;
109
+ });
110
+ it('runs a go-ios command for this phone, through its tunnel', () => __awaiter(void 0, void 0, void 0, function* () {
111
+ const { out } = (yield run(IPHONE, 'apps --list'));
112
+ (0, chai_1.expect)(out).to.equal(`apps|--list|--udid|${IPHONE}|port=12100`);
113
+ (0, chai_1.expect)(borrowed).to.deep.equal([IPHONE]);
114
+ }));
115
+ it('refuses a command that names another device, and list', () => __awaiter(void 0, void 0, void 0, function* () {
116
+ for (const cmd of [
117
+ 'apps --udid 00008101-OTHER',
118
+ 'apps --udid=00008101-OTHER',
119
+ 'list',
120
+ 'info --json',
121
+ ]) {
122
+ (0, chai_1.expect)(yield run(IPHONE, cmd), cmd)
123
+ .to.have.property('err')
124
+ .that.match(/not allowed/);
125
+ }
126
+ }));
127
+ it('says a generic command needs a simulator', () => __awaiter(void 0, void 0, void 0, function* () {
128
+ (0, chai_1.expect)(yield run(IPHONE, 'ls'))
129
+ .to.have.property('err')
130
+ .that.match(/real/i);
131
+ }));
132
+ });
133
+ });
@@ -0,0 +1,79 @@
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 typedi_1 = require("typedi");
15
+ const web_config_service_1 = require("../../src/data-service/web-config-service");
16
+ const scratch_database_1 = require("../helpers/scratch-database");
17
+ /**
18
+ * WebConfig holds the dashboard's saved settings, one row each. Its `id`
19
+ * column is the table's primary key and defaulted to 'global', and every
20
+ * setting was written with that id, so the table could hold one setting:
21
+ * saving a second failed with "Unique constraint failed on the fields (`id`)".
22
+ * These run the service against a real (scratch) database.
23
+ */
24
+ describe('WebConfigService', () => {
25
+ const scratch = (0, scratch_database_1.useScratchDatabase)();
26
+ const service = () => typedi_1.Container.get(web_config_service_1.WebConfigService);
27
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
28
+ yield scratch.db.webConfig.deleteMany({});
29
+ }));
30
+ it('saves every setting of one update', () => __awaiter(void 0, void 0, void 0, function* () {
31
+ yield service().setConfig({
32
+ buildCleanupDays: 7,
33
+ buildCleanupMaxCount: 5,
34
+ buildCleanupSchedule: '0 3 * * *',
35
+ deleteBuildAssets: false,
36
+ });
37
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
38
+ buildCleanupDays: 7,
39
+ buildCleanupMaxCount: 5,
40
+ buildCleanupSchedule: '0 3 * * *',
41
+ deleteBuildAssets: false,
42
+ });
43
+ }));
44
+ it('saves settings from separate updates, as two pages save them', () => __awaiter(void 0, void 0, void 0, function* () {
45
+ yield service().setConfig({ healthCheckIntervalMs: 600000 });
46
+ yield service().setConfig({ buildCleanupDays: 7 });
47
+ yield service().setConfig({ healthCheckSchedule: '0 * * * *', deleteBuildAssets: true });
48
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
49
+ healthCheckIntervalMs: 600000,
50
+ buildCleanupDays: 7,
51
+ healthCheckSchedule: '0 * * * *',
52
+ deleteBuildAssets: true,
53
+ });
54
+ }));
55
+ it('keeps the self-healing switch, off as well as on', () => __awaiter(void 0, void 0, void 0, function* () {
56
+ yield service().setConfig({ enableSelfHealing: false });
57
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: false });
58
+ yield service().setConfig({ enableSelfHealing: true });
59
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({ enableSelfHealing: true });
60
+ }));
61
+ it('changes a setting saved before', () => __awaiter(void 0, void 0, void 0, function* () {
62
+ yield service().setConfig({ buildCleanupDays: 7, buildCleanupMaxCount: 5 });
63
+ yield service().setConfig({ buildCleanupDays: 14 });
64
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
65
+ buildCleanupDays: 14,
66
+ buildCleanupMaxCount: 5,
67
+ });
68
+ }));
69
+ it('still reads the row an earlier version saved under the id "global"', () => __awaiter(void 0, void 0, void 0, function* () {
70
+ yield scratch.db.webConfig.create({
71
+ data: { id: 'global', name: 'buildCleanupDays', value: '9' },
72
+ });
73
+ yield service().setConfig({ buildCleanupMaxCount: 5 });
74
+ (0, chai_1.expect)(yield service().getConfig()).to.deep.equal({
75
+ buildCleanupDays: 9,
76
+ buildCleanupMaxCount: 5,
77
+ });
78
+ }));
79
+ });
@@ -129,6 +129,30 @@ describe('webhook delivery', () => {
129
129
  (0, chai_1.expect)(r.status).to.equal(200);
130
130
  (0, chai_1.expect)(r.body).to.deep.equal({ success: true });
131
131
  }));
132
+ it('POST /api/webhook/test sends a sample of the event it is asked for, through the template', () => __awaiter(void 0, void 0, void 0, function* () {
133
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
134
+ const r = yield (0, loopbackRequest_1.default)(app)
135
+ .post('/xenon/api/webhook/test')
136
+ .send({
137
+ url: URL,
138
+ type: 'slack',
139
+ payloadTemplate: '{"text": "{{eventType}}: {{sessionId}}"}',
140
+ event: 'session_failed',
141
+ })
142
+ .timeout(5000);
143
+ (0, chai_1.expect)(r.status).to.equal(200);
144
+ (0, chai_1.expect)(post.firstCall.args[1]).to.deep.equal({ text: 'session_failed: test-session-id' });
145
+ }));
146
+ it('POST /api/webhook/test with an event it does not know answers 400, sending nothing', () => __awaiter(void 0, void 0, void 0, function* () {
147
+ const post = sinon_1.default.stub(axios_1.default, 'post').resolves({ status: 200 });
148
+ const r = yield (0, loopbackRequest_1.default)(app)
149
+ .post('/xenon/api/webhook/test')
150
+ .send({ url: URL, event: 'device_exploded' })
151
+ .timeout(5000);
152
+ (0, chai_1.expect)(r.status).to.equal(400);
153
+ (0, chai_1.expect)(r.body.error).to.contain('session_failed');
154
+ (0, chai_1.expect)(post.called).to.equal(false);
155
+ }));
132
156
  it('POST /api/webhook/test without a url answers 400', () => __awaiter(void 0, void 0, void 0, function* () {
133
157
  const r = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/webhook/test').send({}).timeout(5000);
134
158
  (0, chai_1.expect)(r.status).to.equal(400);