@xenon-device-management/xenon 2.15.0 → 2.17.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 (206) hide show
  1. package/README.md +38 -26
  2. package/lib/package.json +2 -1
  3. package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
  6. package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
  9. package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
  10. package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
  14. package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
  15. package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
  16. package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
  18. package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
  19. package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
  21. package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
  24. package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
  25. package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
  26. package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
  27. package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
  28. package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
  29. package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
  30. package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
  31. package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
  32. package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
  33. package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
  34. package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
  35. package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
  36. package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
  37. package/lib/public/assets/index-Dk5J6HB1.js +219 -0
  38. package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
  39. package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
  40. package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
  41. package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
  42. package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
  43. package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
  44. package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
  46. package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
  48. package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
  49. package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
  50. package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
  51. package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
  52. package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
  53. package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
  54. package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
  56. package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
  59. package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
  60. package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
  61. package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
  62. package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
  63. package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
  64. package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
  65. package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
  68. package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
  69. package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
  70. package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
  72. package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +11 -7
  75. package/lib/src/InternalHttpClient.js +27 -5
  76. package/lib/src/XenonCapabilityManager.js +10 -3
  77. package/lib/src/app/index.js +2 -0
  78. package/lib/src/app/openapi/grid.yaml +102 -0
  79. package/lib/src/app/openapi/sessions.yaml +7 -5
  80. package/lib/src/app/routers/auth.js +2 -1
  81. package/lib/src/app/routers/users.js +2 -1
  82. package/lib/src/app/ws/nodeSocketRelay.js +77 -11
  83. package/lib/src/dashboard/commandLogFields.js +42 -0
  84. package/lib/src/dashboard/event-manager.js +23 -154
  85. package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
  86. package/lib/src/dashboard/services/failureCategories.js +264 -0
  87. package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
  88. package/lib/src/device-managers/IOSDeviceManager.js +10 -0
  89. package/lib/src/device-utils.js +25 -8
  90. package/lib/src/gateway/forwardToNode.js +4 -11
  91. package/lib/src/gateway/hubSessionToken.js +27 -4
  92. package/lib/src/gateway/nodeAsk.js +57 -0
  93. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  94. package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
  95. package/lib/src/helpers/outboundProxy.js +219 -0
  96. package/lib/src/index.js +21 -0
  97. package/lib/src/interceptors/CommandInterceptor.js +13 -15
  98. package/lib/src/interceptors/xenonScripts.js +28 -7
  99. package/lib/src/logger.js +3 -1
  100. package/lib/src/plugin.js +12 -4
  101. package/lib/src/scripts/database-check.js +3 -3
  102. package/lib/src/scripts/initialize-database.js +29 -33
  103. package/lib/src/scripts/run-migrations.js +320 -25
  104. package/lib/src/services/ApiKeyService.js +16 -5
  105. package/lib/src/services/InterceptorService.js +6 -2
  106. package/lib/src/services/OrphanSweeper.js +6 -0
  107. package/lib/src/services/SelectorStateService.js +11 -7
  108. package/lib/src/services/SelectorVerificationJob.js +6 -2
  109. package/lib/src/services/SessionLifecycleService.js +52 -23
  110. package/lib/src/services/SocketClient.js +11 -0
  111. package/lib/src/services/SocketServer.js +361 -42
  112. package/lib/src/services/TeamService.js +5 -1
  113. package/lib/src/services/TracingService.js +171 -60
  114. package/lib/src/services/UserService.js +14 -3
  115. package/lib/src/services/UserSessionService.js +16 -4
  116. package/lib/src/services/healing/HealingOrchestrator.js +3 -1
  117. package/lib/src/services/identity/identityChanges.js +41 -0
  118. package/lib/src/services/interceptor/CertManager.js +50 -16
  119. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  120. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  121. package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
  122. package/lib/src/services/logcat/deviceLogBook.js +5 -4
  123. package/lib/src/services/logcat/iosDriverLog.js +178 -0
  124. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  125. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  126. package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
  127. package/lib/src/services/token/JwtKeyService.js +8 -2
  128. package/lib/src/services/token/mcpTokenTtl.js +27 -0
  129. package/lib/src/sessions/LocalSession.js +29 -1
  130. package/lib/src/sessions/RemoteSession.js +23 -4
  131. package/lib/src/sessions/SessionManager.js +33 -9
  132. package/lib/test/helpers/driver-log.js +38 -0
  133. package/lib/test/helpers/fake-http-proxy.js +106 -0
  134. package/lib/test/helpers/fake-logcat.js +80 -0
  135. package/lib/test/helpers/hub-node-pair.js +227 -0
  136. package/lib/test/helpers/otlp-probe.js +107 -0
  137. package/lib/test/integration/androidDevices.spec.js +1 -1
  138. package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
  139. package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
  140. package/lib/test/integration/selector-state-flow.spec.js +5 -5
  141. package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
  142. package/lib/test/unit/android-adb-retry.spec.js +112 -0
  143. package/lib/test/unit/command-log-network-capture.spec.js +191 -0
  144. package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
  145. package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
  146. package/lib/test/unit/database-check.spec.js +1 -0
  147. package/lib/test/unit/device-utils.spec.js +13 -4
  148. package/lib/test/unit/event-log-session-data.spec.js +11 -4
  149. package/lib/test/unit/failure-analysis.spec.js +22 -8
  150. package/lib/test/unit/failure-categories.spec.js +24 -22
  151. package/lib/test/unit/failure-categorization.spec.js +393 -0
  152. package/lib/test/unit/healing-tier-gate.spec.js +3 -2
  153. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
  154. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  155. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  156. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  157. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  158. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  159. package/lib/test/unit/ios-driver-log.spec.js +168 -0
  160. package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
  161. package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
  162. package/lib/test/unit/logcat-ws.spec.js +78 -37
  163. package/lib/test/unit/logger-json-levels.spec.js +44 -0
  164. package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
  165. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  166. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  167. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  168. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  169. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  170. package/lib/test/unit/observability-example-loki.spec.js +25 -0
  171. package/lib/test/unit/openapi-for-site.spec.js +80 -0
  172. package/lib/test/unit/outbound-proxy.spec.js +320 -0
  173. package/lib/test/unit/port-exhaustion.spec.js +2 -1
  174. package/lib/test/unit/proxy-option.spec.js +358 -0
  175. package/lib/test/unit/run-migrations-database.spec.js +440 -0
  176. package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
  177. package/lib/test/unit/run-migrations.test.js +264 -0
  178. package/lib/test/unit/selector-health-actions.spec.js +3 -1
  179. package/lib/test/unit/selector-state-service.spec.js +72 -23
  180. package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
  181. package/lib/test/unit/selector-verification-job.spec.js +15 -6
  182. package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
  183. package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
  184. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  185. package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
  186. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  187. package/lib/test/unit/session-device-logs.spec.js +43 -96
  188. package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
  189. package/lib/test/unit/session-log-indexes.spec.js +16 -1
  190. package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
  191. package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
  192. package/lib/test/unit/tracing-export-routing.spec.js +137 -0
  193. package/lib/test/unit/tracing-session-spans.spec.js +337 -0
  194. package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
  195. package/lib/tsconfig.tsbuildinfo +1 -1
  196. package/package.json +2 -1
  197. package/schema.json +11 -7
  198. package/scripts/dev/readme-screenshots.js +276 -23
  199. package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
  200. package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
  201. package/scripts/dev/screenshot-fixtures/session.js +242 -0
  202. package/scripts/export-openapi.js +32 -0
  203. package/scripts/lib/openapi-for-site.js +79 -0
  204. package/lib/public/assets/index-BOxe43p1.js +0 -219
  205. package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
  206. package/lib/src/scripts/prepare-prisma.js +0 -89
@@ -0,0 +1,320 @@
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 http_1 = __importDefault(require("http"));
18
+ const axios_1 = __importDefault(require("axios"));
19
+ const typedi_1 = require("typedi");
20
+ const ws_1 = require("ws");
21
+ const forwardToNode_1 = require("../../src/gateway/forwardToNode");
22
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
23
+ const nodeSocketRelay_1 = require("../../src/app/ws/nodeSocketRelay");
24
+ const PluginContext_1 = require("../../src/PluginContext");
25
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
26
+ const outboundProxy_1 = require("../../src/helpers/outboundProxy");
27
+ const SocketClient_1 = require("../../src/services/SocketClient");
28
+ const socket_io_1 = require("socket.io");
29
+ const fake_http_proxy_1 = require("../helpers/fake-http-proxy");
30
+ const PROXY_ENV = [
31
+ 'HTTP_PROXY',
32
+ 'http_proxy',
33
+ 'HTTPS_PROXY',
34
+ 'https_proxy',
35
+ 'NO_PROXY',
36
+ 'no_proxy',
37
+ ];
38
+ /**
39
+ * Xenon's axios calls take a proxy from the environment by axios's rule: the
40
+ * URL's own scheme's variable, lower-case name first, unless NO_PROXY names
41
+ * the host. Its other calls to a node didn't:
42
+ * - a forwarded command, device control action, recording relay and socket
43
+ * ticket took HTTP_PROXY or HTTPS_PROXY whatever the scheme, ignored the
44
+ * lower-case names and ignored NO_PROXY;
45
+ * - the live-preview and logcat sockets ignored every proxy, after asking for
46
+ * their ticket through one;
47
+ * - a node fetching its hub's keys (JWKS) ignored every proxy.
48
+ * They all follow axios's rule now.
49
+ */
50
+ describe('proxies for the calls Xenon makes to another server', function () {
51
+ this.timeout(20000);
52
+ let saved;
53
+ let proxy;
54
+ const servers = [];
55
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
56
+ saved = Object.fromEntries(PROXY_ENV.map((k) => [k, process.env[k]]));
57
+ for (const k of PROXY_ENV)
58
+ delete process.env[k];
59
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)();
60
+ }));
61
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
62
+ for (const k of PROXY_ENV) {
63
+ if (saved[k] === undefined)
64
+ delete process.env[k];
65
+ else
66
+ process.env[k] = saved[k];
67
+ }
68
+ yield proxy.close();
69
+ yield Promise.all(servers.splice(0).map((s) => new Promise((resolve) => {
70
+ var _a;
71
+ (_a = s.closeAllConnections) === null || _a === void 0 ? void 0 : _a.call(s);
72
+ s.close(() => resolve());
73
+ })));
74
+ }));
75
+ /** What `promise` settled with (its error, if it failed), or 'pending' after `ms`. */
76
+ function settlesWithin(promise, ms) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ let timer;
79
+ const pending = new Promise((resolve) => {
80
+ timer = setTimeout(() => resolve('pending'), ms);
81
+ });
82
+ try {
83
+ return yield Promise.race([
84
+ promise.then((v) => v, (e) => e),
85
+ pending,
86
+ ]);
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ });
92
+ }
93
+ /** A server on 127.0.0.1 that answers everything 200, and its origin. */
94
+ function target(handler) {
95
+ return __awaiter(this, void 0, void 0, function* () {
96
+ const server = http_1.default.createServer(handler !== null && handler !== void 0 ? handler : ((_req, res) => {
97
+ res.writeHead(200, { 'content-type': 'application/json' });
98
+ res.end('{}');
99
+ }));
100
+ servers.push(server);
101
+ yield new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
102
+ return `http://127.0.0.1:${server.address().port}`;
103
+ });
104
+ }
105
+ describe('the rule', () => {
106
+ const env = (vars) => vars;
107
+ it("takes the URL's own scheme's variable, the lower-case name first", () => {
108
+ const vars = env({ HTTP_PROXY: 'http://upper:1', http_proxy: 'http://lower:1' });
109
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('http://node:4723/x', vars)).to.equal('http://lower:1');
110
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('https://node/x', env({ HTTP_PROXY: 'http://p:1' }))).to.equal(undefined);
111
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('https://node/x', env({ HTTPS_PROXY: 'http://p:2' }))).to.equal('http://p:2');
112
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('http://node/x', env({ HTTPS_PROXY: 'http://p:2' }))).to.equal(undefined);
113
+ });
114
+ it("treats a WebSocket URL as its scheme's HTTP counterpart", () => {
115
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('ws://node/x', env({ HTTP_PROXY: 'http://p:1' }))).to.equal('http://p:1');
116
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('wss://node/x', env({ HTTPS_PROXY: 'http://p:2' }))).to.equal('http://p:2');
117
+ });
118
+ it('takes none for a host NO_PROXY names: *, the host, or a .suffix of it', () => {
119
+ const proxied = { HTTP_PROXY: 'http://p:1' };
120
+ const via = (noProxy, url = 'http://node.lab.example:4723/x') => (0, outboundProxy_1.envProxyFor)(url, env(Object.assign(Object.assign({}, proxied), { NO_PROXY: noProxy })));
121
+ (0, chai_1.expect)(via('*')).to.equal(undefined);
122
+ (0, chai_1.expect)(via('other, node.lab.example')).to.equal(undefined);
123
+ (0, chai_1.expect)(via('.lab.example')).to.equal(undefined);
124
+ (0, chai_1.expect)(via('lab.example')).to.equal('http://p:1');
125
+ (0, chai_1.expect)(via('node.lab.example:4723')).to.equal('http://p:1');
126
+ (0, chai_1.expect)((0, outboundProxy_1.envProxyFor)('http://node/x', env(Object.assign(Object.assign({}, proxied), { no_proxy: 'node', NO_PROXY: 'x' })))).to.equal(undefined);
127
+ });
128
+ });
129
+ describe('a call to a node takes the path an axios call takes', () => {
130
+ const cases = [
131
+ { name: 'HTTP_PROXY', vars: (p) => ({ HTTP_PROXY: p }) },
132
+ { name: 'http_proxy', vars: (p) => ({ http_proxy: p }) },
133
+ {
134
+ name: 'HTTP_PROXY, NO_PROXY naming the node',
135
+ vars: (p) => ({ HTTP_PROXY: p, NO_PROXY: '127.0.0.1' }),
136
+ },
137
+ { name: 'HTTP_PROXY, no_proxy=*', vars: (p) => ({ HTTP_PROXY: p, no_proxy: '*' }) },
138
+ {
139
+ name: 'HTTP_PROXY, NO_PROXY listing the node second',
140
+ vars: (p) => ({ HTTP_PROXY: p, NO_PROXY: 'hub.lab, 127.0.0.1' }),
141
+ },
142
+ { name: 'only HTTPS_PROXY, for an http node', vars: (p) => ({ HTTPS_PROXY: p }) },
143
+ ];
144
+ for (const { name, vars } of cases) {
145
+ it(name, () => __awaiter(this, void 0, void 0, function* () {
146
+ Object.assign(process.env, vars(proxy.url));
147
+ const node = yield target();
148
+ yield axios_1.default.get(`${node}/by-axios`);
149
+ yield (0, forwardToNode_1.readAnswer)(yield (0, forwardToNode_1.sendToNode)({ url: `${node}/by-xenon`, method: 'GET', headers: {} }));
150
+ const axiosProxied = proxy.seen.includes(`GET ${node}/by-axios`);
151
+ const xenonProxied = proxy.seen.includes(`GET ${node}/by-xenon`);
152
+ (0, chai_1.expect)(xenonProxied, `forwarded call via the proxy (axios: ${axiosProxied})`).to.equal(axiosProxied);
153
+ }));
154
+ }
155
+ });
156
+ describe("a node fetching its hub's keys", () => {
157
+ // A JWS that parses, so jose asks the key set for its key.
158
+ const token = [
159
+ Buffer.from(JSON.stringify({ alg: 'RS256', kid: 'k1' })).toString('base64url'),
160
+ Buffer.from(JSON.stringify({ sid: 's' })).toString('base64url'),
161
+ 'c2lnbmF0dXJl',
162
+ ].join('.');
163
+ function fetchKeys(hub) {
164
+ return __awaiter(this, void 0, void 0, function* () {
165
+ yield new hubSessionToken_1.HubSessionTokenVerifier(hub).verify(token, 's').catch(() => undefined);
166
+ });
167
+ }
168
+ it('goes through the proxy', () => __awaiter(this, void 0, void 0, function* () {
169
+ const hub = yield target((_req, res) => {
170
+ res.writeHead(200, { 'content-type': 'application/json' });
171
+ res.end('{"keys":[]}');
172
+ });
173
+ process.env.HTTP_PROXY = proxy.url;
174
+ yield fetchKeys(hub);
175
+ (0, chai_1.expect)(proxy.seen).to.include(`GET ${hub}/xenon/api/auth/jwks.json`);
176
+ }));
177
+ it('gives up in its time when the proxy never answers', () => __awaiter(this, void 0, void 0, function* () {
178
+ // The fetch's own timeout starts once the request has a socket, which a
179
+ // proxy agent hands it only after its CONNECT is answered.
180
+ yield proxy.close();
181
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ ignoreConnect: true });
182
+ process.env.HTTPS_PROXY = proxy.url;
183
+ const outcome = yield settlesWithin(new hubSessionToken_1.HubSessionTokenVerifier('https://hub.lab.example:4723', undefined, {
184
+ timeoutMs: 300,
185
+ }).verify(token, 's'), 3000);
186
+ (0, chai_1.expect)(outcome).to.be.instanceOf(hubSessionToken_1.HubTokenUnavailableError);
187
+ }));
188
+ it('goes straight to a hub NO_PROXY names', () => __awaiter(this, void 0, void 0, function* () {
189
+ const asked = [];
190
+ const hub = yield target((req, res) => {
191
+ asked.push(String(req.url));
192
+ res.writeHead(200, { 'content-type': 'application/json' });
193
+ res.end('{"keys":[]}');
194
+ });
195
+ process.env.HTTP_PROXY = proxy.url;
196
+ process.env.NO_PROXY = '127.0.0.1';
197
+ yield fetchKeys(hub);
198
+ (0, chai_1.expect)(proxy.seen).to.deep.equal([]);
199
+ (0, chai_1.expect)(asked).to.include('/xenon/api/auth/jwks.json');
200
+ }));
201
+ });
202
+ describe("a node's live-events connection to its hub", () => {
203
+ let client;
204
+ let hubIo;
205
+ afterEach(() => {
206
+ var _a;
207
+ (_a = client === null || client === void 0 ? void 0 : client.socket) === null || _a === void 0 ? void 0 : _a.close();
208
+ client = undefined;
209
+ hubIo === null || hubIo === void 0 ? void 0 : hubIo.close();
210
+ hubIo = undefined;
211
+ });
212
+ function connect() {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ const hub = yield target();
215
+ hubIo = new socket_io_1.Server(servers[servers.length - 1]);
216
+ client = new SocketClient_1.SocketClient();
217
+ client.initialize(hub, 'http://node.lab:4723');
218
+ const deadline = Date.now() + 10000;
219
+ while (!client.isConnected()) {
220
+ if (Date.now() > deadline)
221
+ throw new Error('the node never connected to its hub');
222
+ yield new Promise((r) => setTimeout(r, 25));
223
+ }
224
+ return hub;
225
+ });
226
+ }
227
+ it('goes through the proxy: polling as plain requests, as axios sends them', () => __awaiter(this, void 0, void 0, function* () {
228
+ process.env.HTTP_PROXY = proxy.url;
229
+ const hub = yield connect();
230
+ (0, chai_1.expect)(proxy.seen.some((s) => s.startsWith(`GET ${hub}/socket.io/?EIO=4&transport=polling`)), proxy.seen.join('\n')).to.equal(true);
231
+ }));
232
+ it('stays connected through a proxy that refuses tunnels, by polling', () => __awaiter(this, void 0, void 0, function* () {
233
+ // A stock Squid refuses CONNECT to any port but 443, and a node's hub
234
+ // is on 4723: the WebSocket upgrade fails, polling carries on.
235
+ yield proxy.close();
236
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ refuseConnect: true });
237
+ process.env.HTTP_PROXY = proxy.url;
238
+ yield connect();
239
+ (0, chai_1.expect)(client === null || client === void 0 ? void 0 : client.isConnected()).to.equal(true);
240
+ }));
241
+ it('goes straight to a hub NO_PROXY names', () => __awaiter(this, void 0, void 0, function* () {
242
+ process.env.HTTP_PROXY = proxy.url;
243
+ process.env.NO_PROXY = '127.0.0.1';
244
+ yield connect();
245
+ (0, chai_1.expect)(proxy.seen).to.deep.equal([]);
246
+ }));
247
+ });
248
+ describe("the live-preview and logcat sockets to a node's phone", () => {
249
+ let context;
250
+ let savedContext;
251
+ const sockets = [];
252
+ beforeEach(() => {
253
+ context = typedi_1.Container.get(PluginContext_1.PluginContext);
254
+ savedContext = Object.assign({}, context);
255
+ context.setContext(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { bindHostOrIp: '127.0.0.1' }), 1, 'hub-1', '');
256
+ });
257
+ afterEach(() => {
258
+ Object.assign(context, savedContext);
259
+ for (const wss of sockets.splice(0)) {
260
+ for (const client of wss.clients)
261
+ client.terminate();
262
+ wss.close();
263
+ }
264
+ });
265
+ function nodeWithSocket() {
266
+ return __awaiter(this, void 0, void 0, function* () {
267
+ const origin = yield target((req, res) => {
268
+ res.writeHead(200, { 'content-type': 'application/json' });
269
+ res.end(JSON.stringify({ ticket: 'node-ticket', expiresIn: 60 }));
270
+ });
271
+ const wss = new ws_1.WebSocketServer({ server: servers[servers.length - 1] });
272
+ sockets.push(wss);
273
+ return origin;
274
+ });
275
+ }
276
+ function open(origin) {
277
+ return __awaiter(this, void 0, void 0, function* () {
278
+ const socket = yield (0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor: { actorId: 'alice' }, path: 'logcat' }, {
279
+ findDevice: () => __awaiter(this, void 0, void 0, function* () { return ({ udid: 'p', host: origin, nodeId: 'node-1' }); }),
280
+ controlToken: () => __awaiter(this, void 0, void 0, function* () { return null; }),
281
+ });
282
+ socket.terminate();
283
+ });
284
+ }
285
+ it('take the proxy their ticket takes', () => __awaiter(this, void 0, void 0, function* () {
286
+ const node = yield nodeWithSocket();
287
+ process.env.HTTP_PROXY = proxy.url;
288
+ yield open(node);
289
+ (0, chai_1.expect)(proxy.seen).to.include(`POST ${node}/xenon/api/control/p/stream/ticket`);
290
+ (0, chai_1.expect)(proxy.seen).to.include(`CONNECT ${node.replace('http://', '')}`);
291
+ }));
292
+ it('go straight to the node when the proxy refuses a tunnel, as they did before', () => __awaiter(this, void 0, void 0, function* () {
293
+ yield proxy.close();
294
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ refuseConnect: true });
295
+ const node = yield nodeWithSocket();
296
+ process.env.HTTP_PROXY = proxy.url;
297
+ yield open(node);
298
+ (0, chai_1.expect)(proxy.seen).to.include(`CONNECT ${node.replace('http://', '')}`);
299
+ }));
300
+ it('give up in their time when the proxy never answers the tunnel', () => __awaiter(this, void 0, void 0, function* () {
301
+ yield proxy.close();
302
+ proxy = yield (0, fake_http_proxy_1.startFakeProxy)({ ignoreConnect: true });
303
+ const node = yield nodeWithSocket();
304
+ process.env.HTTP_PROXY = proxy.url;
305
+ const outcome = yield settlesWithin((0, nodeSocketRelay_1.openNodeSocket)({ udid: 'p', actor: { actorId: 'alice' }, path: 'logcat' }, {
306
+ findDevice: () => __awaiter(this, void 0, void 0, function* () { return ({ udid: 'p', host: node, nodeId: 'node-1' }); }),
307
+ controlToken: () => __awaiter(this, void 0, void 0, function* () { return null; }),
308
+ timeoutMs: 300,
309
+ }), 3000);
310
+ (0, chai_1.expect)(outcome).to.be.instanceOf(Error);
311
+ }));
312
+ it('go straight to a node NO_PROXY names', () => __awaiter(this, void 0, void 0, function* () {
313
+ const node = yield nodeWithSocket();
314
+ process.env.HTTP_PROXY = proxy.url;
315
+ process.env.NO_PROXY = '127.0.0.1';
316
+ yield open(node);
317
+ (0, chai_1.expect)(proxy.seen).to.deep.equal([]);
318
+ }));
319
+ });
320
+ });
@@ -196,8 +196,9 @@ describe('iOSCapabilities port acquisition', () => {
196
196
  sinon_1.default
197
197
  .stub(typedi_1.Container.get(IOSStreamService), 'getStreamStatus')
198
198
  .returns({ status: 'running', wdaPort: 8100 });
199
+ // An iPhone: a simulator's WDA is the driver's, never the stream's.
199
200
  const caps = { alwaysMatch: {}, firstMatch: [{}] };
200
- yield (0, XenonCapabilityManager_1.iOSCapabilities)(caps, device);
201
+ yield (0, XenonCapabilityManager_1.iOSCapabilities)(caps, Object.assign(Object.assign({}, device), { realDevice: true }));
201
202
  (0, chai_1.expect)(caps.firstMatch[0]['appium:webDriverAgentUrl']).to.equal('http://127.0.0.1:8100');
202
203
  (0, chai_1.expect)(caps.firstMatch[0]['appium:wdaLocalPort']).to.equal(undefined);
203
204
  (0, chai_1.expect)(acquireStub.called).to.equal(false);