@xenon-device-management/xenon 2.16.0 → 2.17.1

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 (135) hide show
  1. package/README.md +2 -2
  2. package/lib/package.json +2 -3
  3. package/lib/public/assets/{AnnotationOverlay-2W4JvEyZ.js → AnnotationOverlay-CB8njQvS.js} +1 -1
  4. package/lib/public/assets/{ApiKeyGate-GmckX_7r.js → ApiKeyGate-B-804QBS.js} +1 -1
  5. package/lib/public/assets/DeviceMosaicView-BsDvfjR1.js +2 -0
  6. package/lib/public/assets/{EmptyState-7hETKEWb.js → EmptyState-WzLw1cEV.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-GOgssFEP.js → FieldGroup-CaQNXgcE.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-Yvf6RGMd.js → FilterMenu-C9gAiypL.js} +1 -1
  9. package/lib/public/assets/{Menu-KrPnfdsf.js → Menu-CfTti3rw.js} +1 -1
  10. package/lib/public/assets/{Modal-C6WjZFeI.js → Modal-BcZUFhDm.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-CSRIhZEh.js → RecordingPage-B_pbl3UE.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CB8ST3nE.js → RecordingsPage-BkVhplE3.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-C9o90KId.js → SegmentedControl-CDLMLNl5.js} +1 -1
  14. package/lib/public/assets/{SettingCard-CPm3w9l2.js → SettingCard-Lyw5CB1F.js} +1 -1
  15. package/lib/public/assets/{Table-DwIgERRt.js → Table-CK414bOU.js} +1 -1
  16. package/lib/public/assets/{activity-DrLbHifn.js → activity-pEyAdE-M.js} +1 -1
  17. package/lib/public/assets/{ai-settings-DU_Ls06P.js → ai-settings-CIhS7MMq.js} +1 -1
  18. package/lib/public/assets/{api-keys-CWYdEzJ_.js → api-keys-D2J44Ebv.js} +1 -1
  19. package/lib/public/assets/{apps-BqNVdln7.js → apps-CCV3RZY8.js} +1 -1
  20. package/lib/public/assets/{arrow-left-BtB0_Jg_.js → arrow-left-DaEUmERk.js} +1 -1
  21. package/lib/public/assets/{arrow-right-C8NhdtyN.js → arrow-right-CnRXHXit.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-gMg0ffvv.js → arrow-up-right-BGCS7zhi.js} +1 -1
  23. package/lib/public/assets/{auth-shell-CiYZ3N4M.js → auth-shell-CpW1zsxD.js} +2 -2
  24. package/lib/public/assets/{builds-page-EaaqDmLN.js → builds-page-B-xdzjMx.js} +1 -1
  25. package/lib/public/assets/{button-DDau4Axv.js → button-B5QkOVa4.js} +1 -1
  26. package/lib/public/assets/{calendar-DQ83B1ed.js → calendar-gX_1EKue.js} +1 -1
  27. package/lib/public/assets/{check-K1ovTQam.js → check-MIM4THvH.js} +1 -1
  28. package/lib/public/assets/{chevron-right-Biq00pMt.js → chevron-right-BZIp3-9t.js} +1 -1
  29. package/lib/public/assets/{circle-check-CBv88cH7.js → circle-check-Crt-G_uy.js} +1 -1
  30. package/lib/public/assets/{circle-x-Bmltailn.js → circle-x-C8GvvXVt.js} +1 -1
  31. package/lib/public/assets/{clock-BPrld-3h.js → clock-MFKV4qGa.js} +1 -1
  32. package/lib/public/assets/{copy-t0QB14JI.js → copy-BU-Z7mBK.js} +1 -1
  33. package/lib/public/assets/device-explorer-D-78Twa0.js +282 -0
  34. package/lib/public/assets/{download-BroHxNOh.js → download-DIoLCCYv.js} +1 -1
  35. package/lib/public/assets/{forgot-password-Bjmf49i6.js → forgot-password-vNYlPnia.js} +1 -1
  36. package/lib/public/assets/{h264Stream-DeD__6wa.js → h264Stream-BLcSplMU.js} +1 -1
  37. package/lib/public/assets/{index-B0gQkEg0.js → index-BpqyclNY.js} +2 -2
  38. package/lib/public/assets/{index-B0JeuZbg.js → index-Hl1OL-k_.js} +1 -1
  39. package/lib/public/assets/{index-72oF89LJ.js → index-QbukVVrl.js} +1 -1
  40. package/lib/public/assets/{input-DIc4MSC7.js → input-DHH3fHer.js} +1 -1
  41. package/lib/public/assets/{line-chart-BcWm2vW8.js → line-chart-PdViEphw.js} +1 -1
  42. package/lib/public/assets/{list-checks-B06IJJad.js → list-checks-DI_heIg9.js} +1 -1
  43. package/lib/public/assets/{lock-pVLvnN30.js → lock-DVP_dL6J.js} +1 -1
  44. package/lib/public/assets/{login-CN77vy--.js → login-B8vtTDcr.js} +1 -1
  45. package/lib/public/assets/{maintenance-settings-CSdzQSkY.js → maintenance-settings-BMa5EfgI.js} +1 -1
  46. package/lib/public/assets/{monitor-BtrN5IkI.js → monitor-CtzX_RkD.js} +1 -1
  47. package/lib/public/assets/{mouse-pointer-2-CNsQfo6w.js → mouse-pointer-2-BA09sdS9.js} +1 -1
  48. package/lib/public/assets/{network-BSfA8ZxG.js → network-B_F9Nox-.js} +1 -1
  49. package/lib/public/assets/{overview-BfTebCK5.js → overview-kWCYMHMV.js} +1 -1
  50. package/lib/public/assets/{page-header-D8KUX6Qg.js → page-header-CAbH1O1a.js} +1 -1
  51. package/lib/public/assets/{play-Cvc8xzic.js → play-sg95hLqA.js} +1 -1
  52. package/lib/public/assets/{plus-C9Zj84UF.js → plus-BT9mk2gj.js} +1 -1
  53. package/lib/public/assets/{profile-page-Bi27STgs.js → profile-page-DZOz5Ypc.js} +1 -1
  54. package/lib/public/assets/{reset-password-CPsOuwSu.js → reset-password-CqeImlEO.js} +1 -1
  55. package/lib/public/assets/{runbook-page-DEQ-DBOX.js → runbook-page-U_8TSysj.js} +1 -1
  56. package/lib/public/assets/{select-BWpnhWXj.js → select-Cx4JDzsR.js} +1 -1
  57. package/lib/public/assets/{selector-detail-redirect-BefSCSpe.js → selector-detail-redirect-DiMRe7DW.js} +1 -1
  58. package/lib/public/assets/{selector-health-page-MSk96Zz7.js → selector-health-page-CKR5URll.js} +1 -1
  59. package/lib/public/assets/{session-detail-page-DPyBNef_.js → session-detail-page-DuIVzTW_.js} +1 -1
  60. package/lib/public/assets/{settings-V6x-cXwy.js → settings-B5fuvgTP.js} +1 -1
  61. package/lib/public/assets/{stat-tile-Duex-GZD.js → stat-tile-CtHX1dkh.js} +1 -1
  62. package/lib/public/assets/{tablet-BBcoqBmL.js → tablet-DCGD0GQt.js} +1 -1
  63. package/lib/public/assets/{teams-ts8p5rEh.js → teams-MzlELlw9.js} +1 -1
  64. package/lib/public/assets/{trash-2-CZ2ew6AZ.js → trash-2-BIRm2KVs.js} +1 -1
  65. package/lib/public/assets/{upload-Dv99y_le.js → upload-C-lgyswH.js} +1 -1
  66. package/lib/public/assets/{use-builds-data-DrYC8JCF.js → use-builds-data-IOydXA8d.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-C7QkBNkP.js → use-password-reset-mode-CIVXG7Me.js} +1 -1
  68. package/lib/public/assets/{users-gJ5Y01yo.js → users-hwK5XGIy.js} +1 -1
  69. package/lib/public/assets/{users-D7tKMESH.js → users-ivNCjeus.js} +1 -1
  70. package/lib/public/assets/{video-off-CqZVuZGs.js → video-off-DvcIEsdZ.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-DI5DSW99.js → webhook-settings-DukgCe1u.js} +1 -1
  72. package/lib/public/assets/{zap-ClcHFxfo.js → zap-BE9gacFj.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/schema.json +1 -1
  75. package/lib/src/app/index.js +2 -0
  76. package/lib/src/app/openapi/control.yaml +69 -4
  77. package/lib/src/app/openapi/grid.yaml +102 -0
  78. package/lib/src/app/routers/control.js +24 -5
  79. package/lib/src/dashboard/event-manager.js +5 -8
  80. package/lib/src/device-managers/ios/IOSStreamService.js +147 -18
  81. package/lib/src/device-utils.js +61 -24
  82. package/lib/src/gateway/internalCall.js +33 -5
  83. package/lib/src/gateway/nodeAsk.js +57 -0
  84. package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
  85. package/lib/src/gateway/sessionGateway.js +9 -4
  86. package/lib/src/helpers/index.js +9 -6
  87. package/lib/src/interceptors/CommandInterceptor.js +10 -0
  88. package/lib/src/plugin.js +4 -1
  89. package/lib/src/scripts/initialize-database.js +0 -6
  90. package/lib/src/scripts/run-migrations.js +123 -880
  91. package/lib/src/services/ServerManager.js +25 -4
  92. package/lib/src/services/SessionLifecycleService.js +20 -5
  93. package/lib/src/services/UserService.js +8 -3
  94. package/lib/src/services/interceptor/CertManager.js +50 -16
  95. package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
  96. package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
  97. package/lib/src/services/logcat/SessionDeviceLogs.js +199 -44
  98. package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
  99. package/lib/src/services/metrics/nodeMetrics.js +20 -49
  100. package/lib/src/sessions/RemoteSession.js +11 -2
  101. package/lib/src/sessions/SessionManager.js +28 -1
  102. package/lib/src/sessions/appiumUmbrella.js +11 -0
  103. package/lib/src/sessions/sessionMemory.js +11 -2
  104. package/lib/test/helpers/fake-logcat.js +80 -0
  105. package/lib/test/helpers/hub-node-pair.js +227 -0
  106. package/lib/test/unit/UserService.test.js +8 -0
  107. package/lib/test/unit/boot-device-sync.spec.js +195 -0
  108. package/lib/test/unit/dashboard-events-device-logs.spec.js +15 -0
  109. package/lib/test/unit/hub-node-gateway-appium-server.spec.js +21 -0
  110. package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
  111. package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
  112. package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
  113. package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
  114. package/lib/test/unit/install-scripts.spec.js +98 -0
  115. package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
  116. package/lib/test/unit/internal-call.spec.js +99 -0
  117. package/lib/test/unit/internal-calls-session-record.spec.js +312 -0
  118. package/lib/test/unit/ios-simulator-preview.spec.js +315 -0
  119. package/lib/test/unit/is-xenon-running.spec.js +10 -0
  120. package/lib/test/unit/node-device-log-store.spec.js +102 -0
  121. package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
  122. package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
  123. package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
  124. package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
  125. package/lib/test/unit/run-migrations-database.spec.js +57 -424
  126. package/lib/test/unit/run-migrations.test.js +74 -674
  127. package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
  128. package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
  129. package/lib/test/unit/session-device-logs.spec.js +32 -94
  130. package/lib/test/unit/session-log-indexes.spec.js +15 -0
  131. package/lib/tsconfig.tsbuildinfo +1 -1
  132. package/package.json +2 -3
  133. package/schema.json +1 -1
  134. package/lib/public/assets/DeviceMosaicView-DqODzkyo.js +0 -2
  135. package/lib/public/assets/device-explorer-BgGZ8AvV.js +0 -282
@@ -1,13 +1,19 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.forgetSessionMemory = forgetSessionMemory;
4
7
  const typedi_1 = require("typedi");
5
8
  const OmniVisionService_1 = require("../services/omni-vision/OmniVisionService");
6
9
  const AutowaitService_1 = require("../services/autowait/AutowaitService");
10
+ const IOSStreamService_1 = __importDefault(require("../device-managers/ios/IOSStreamService"));
11
+ const logger_1 = __importDefault(require("../logger"));
7
12
  /**
8
13
  * Drop what this server keeps in memory for a session's commands: the
9
- * elements its finds and heals found in screenshots (OmniVisionService) and
10
- * its autowait settings (AutowaitService). Called however the session ends
14
+ * elements its finds and heals found in screenshots (OmniVisionService), its
15
+ * autowait settings (AutowaitService), and a simulator's live preview of its
16
+ * picture (IOSStreamService). Called however the session ends
11
17
  * here: the test's delete (SessionLifecycleService.deleteSession), Appium's
12
18
  * new-command timeout or a crash (XenonPlugin.onUnexpectedShutdown), the idle
13
19
  * release (releaseBlockedDevices), a stale heartbeat (OrphanSweeper) and a
@@ -22,4 +28,7 @@ function forgetSessionMemory(sessionId) {
22
28
  return;
23
29
  typedi_1.Container.get(OmniVisionService_1.OmniVisionService).forgetSession(sessionId);
24
30
  typedi_1.Container.get(AutowaitService_1.AutowaitService).clearSession(sessionId);
31
+ typedi_1.Container.get(IOSStreamService_1.default)
32
+ .endSimulatorPreviews(sessionId)
33
+ .catch((err) => { var _a; return logger_1.default.warn(`Ending the simulator preview of ${sessionId} failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`); });
25
34
  }
@@ -0,0 +1,80 @@
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
+ exports.until = exports.settle = exports.FakeLogcatStreams = void 0;
13
+ exports.lineAt = lineAt;
14
+ exports.fakeProc = fakeProc;
15
+ const stream_1 = require("stream");
16
+ const LogcatStreamService_1 = require("../../src/device-managers/android/LogcatStreamService");
17
+ const PackageResolver_1 = require("../../src/services/logcat/PackageResolver");
18
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
19
+ /** A threadtime line logged at `at` by this machine's local clock. */
20
+ function lineAt(at, msg, level = 'I', tag = 'Tag', pid = 4127) {
21
+ const t = new Date(at);
22
+ return (`${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:` +
23
+ `${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)} ${pid} ${pid} ${level} ${tag}: ${msg}\n`);
24
+ }
25
+ /** An `adb logcat` child whose output the test writes. */
26
+ function fakeProc() {
27
+ const stdout = new stream_1.PassThrough();
28
+ const handlers = {};
29
+ return {
30
+ stdout,
31
+ killed: false,
32
+ on(ev, cb) {
33
+ (handlers[ev] || (handlers[ev] = [])).push(cb);
34
+ },
35
+ /** The process ends, as when the phone restarts or is unplugged. */
36
+ exit() {
37
+ stdout.end();
38
+ (handlers.close || []).forEach((h) => h());
39
+ },
40
+ kill() {
41
+ this.killed = true;
42
+ },
43
+ };
44
+ }
45
+ /** The real stream service, its `adb logcat` children fake. */
46
+ class FakeLogcatStreams extends LogcatStreamService_1.LogcatStreamService {
47
+ constructor() {
48
+ super(...arguments);
49
+ this.procs = [];
50
+ }
51
+ spawnLogcat() {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ const proc = fakeProc();
54
+ this.procs.push(proc);
55
+ return proc;
56
+ });
57
+ }
58
+ makeResolver() {
59
+ return new PackageResolver_1.PackageResolver(() => __awaiter(this, void 0, void 0, function* () { return ' PID NAME\n 4127 com.example.app\n'; }));
60
+ }
61
+ get proc() {
62
+ return this.procs[this.procs.length - 1];
63
+ }
64
+ }
65
+ exports.FakeLogcatStreams = FakeLogcatStreams;
66
+ /** Lets readline, the stream's package lookups and the mux deliver what was written. */
67
+ const settle = () => __awaiter(void 0, void 0, void 0, function* () {
68
+ for (let i = 0; i < 5; i++)
69
+ yield new Promise((r) => setImmediate(r));
70
+ });
71
+ exports.settle = settle;
72
+ const until = (check_1, ...args_1) => __awaiter(void 0, [check_1, ...args_1], void 0, function* (check, ms = 2000) {
73
+ const end = Date.now() + ms;
74
+ while (!check()) {
75
+ if (Date.now() > end)
76
+ throw new Error('timed out');
77
+ yield new Promise((r) => setTimeout(r, 5));
78
+ }
79
+ });
80
+ exports.until = until;
@@ -0,0 +1,227 @@
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
+ exports.NODE_ID = void 0;
16
+ exports.useHubAndNode = useHubAndNode;
17
+ const chai_1 = require("chai");
18
+ const express_1 = __importDefault(require("express"));
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const http_1 = __importDefault(require("http"));
21
+ const os_1 = __importDefault(require("os"));
22
+ const path_1 = __importDefault(require("path"));
23
+ const sinon_1 = __importDefault(require("sinon"));
24
+ const typedi_1 = require("typedi");
25
+ const loopbackRequest_1 = __importDefault(require("./loopbackRequest"));
26
+ const plugin_1 = require("../../src/plugin");
27
+ const registerCommandAuth_1 = require("../../src/app/registerCommandAuth");
28
+ const defaultGateway_1 = require("../../src/gateway/defaultGateway");
29
+ const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
30
+ const nodeSessionStatus_1 = require("../../src/gateway/nodeSessionStatus");
31
+ const nodeSessionMetrics_1 = require("../../src/gateway/nodeSessionMetrics");
32
+ const nodeSessionDeviceLogs_1 = require("../../src/gateway/nodeSessionDeviceLogs");
33
+ const JwtKeyService_1 = require("../../src/services/token/JwtKeyService");
34
+ const PluginContext_1 = require("../../src/PluginContext");
35
+ const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
36
+ const device_managers_1 = require("../../src/device-managers");
37
+ const device_store_1 = require("../../src/data-service/device-store");
38
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
39
+ const SessionManager_1 = require("../../src/sessions/SessionManager");
40
+ const RemoteSession_1 = require("../../src/sessions/RemoteSession");
41
+ const appiumUmbrella_1 = require("../../src/sessions/appiumUmbrella");
42
+ const config_1 = require("../../src/config");
43
+ const container_registration_1 = require("./container-registration");
44
+ const appium_umbrella_1 = require("./appium-umbrella");
45
+ exports.NODE_ID = 'node-1';
46
+ const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
47
+ /**
48
+ * A hub and a node in one process, for what a hub collects from a node about
49
+ * a session it runs: the node in Appium 3's own server() (Appium's umbrella
50
+ * with Xenon's plugin, the production node gateway, and the node's
51
+ * session-status, metrics and device-log routes), with per-command auth on,
52
+ * so every ask carries the hub's session token. The hub serves only its
53
+ * public keys: the spec plays the hub's side through the session's
54
+ * RemoteSession.
55
+ *
56
+ * Call it inside a `describe`, after useScratchDatabase(). `services` are the
57
+ * container ids the spec replaces in its own `beforeEach`; they are put back
58
+ * after each test.
59
+ */
60
+ function useHubAndNode(scratch, services = []) {
61
+ const servers = [];
62
+ let dirs;
63
+ let hubKeys;
64
+ let appiumLogs;
65
+ let restore;
66
+ let saved;
67
+ let authDisabledBefore;
68
+ let commandAuthBefore;
69
+ let nodeOrigin = '';
70
+ before(() => __awaiter(this, void 0, void 0, function* () {
71
+ appiumLogs = (0, appium_umbrella_1.quietAppiumLogs)();
72
+ dirs = [fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-hub-node-keys-'))];
73
+ hubKeys = new JwtKeyService_1.JwtKeyService();
74
+ yield hubKeys.init(dirs[0]);
75
+ }));
76
+ after(() => {
77
+ appiumLogs.restore();
78
+ for (const dir of dirs)
79
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
80
+ });
81
+ beforeEach(() => __awaiter(this, void 0, void 0, function* () {
82
+ restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, appiumUmbrella_1.AppiumUmbrella, nodeSessionStatus_1.NodeSessionProbeSupport, ...services);
83
+ // One process plays both sides: the hub's keys sign, the node checks them.
84
+ typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
85
+ typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
86
+ typedi_1.Container.set(appiumUmbrella_1.AppiumUmbrella, new appiumUmbrella_1.AppiumUmbrella());
87
+ typedi_1.Container.set(nodeSessionStatus_1.NodeSessionProbeSupport, new nodeSessionStatus_1.NodeSessionProbeSupport());
88
+ typedi_1.Container.set(device_managers_1.XenonManager, {
89
+ getMaxSessionCount: () => undefined,
90
+ deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
91
+ });
92
+ const context = typedi_1.Container.get(PluginContext_1.PluginContext);
93
+ saved = {
94
+ store: device_store_1.DeviceStoreFactory._deviceStore,
95
+ pending: device_store_1.DeviceStoreFactory._pendingSessionStore,
96
+ context: Object.assign({}, context),
97
+ };
98
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
99
+ device_store_1.DeviceStoreFactory._pendingSessionStore = new prisma_store_1.PrismaPendingSessionStore();
100
+ authDisabledBefore = config_1.config.authDisabled;
101
+ config_1.config.authDisabled = false;
102
+ // Per-command auth on: the node then takes the hub's token and nothing else.
103
+ commandAuthBefore = process.env.XENON_REQUIRE_COMMAND_AUTH;
104
+ process.env.XENON_REQUIRE_COMMAND_AUTH = '1';
105
+ yield scratch.db.pendingSession.deleteMany({});
106
+ yield scratch.db.device.deleteMany({});
107
+ }));
108
+ afterEach(() => __awaiter(this, void 0, void 0, function* () {
109
+ for (const s of SessionManager_1.SESSION_MANAGER.getAllSessions())
110
+ SessionManager_1.SESSION_MANAGER.removeSession(s.getId());
111
+ device_store_1.DeviceStoreFactory._deviceStore = saved.store;
112
+ device_store_1.DeviceStoreFactory._pendingSessionStore = saved.pending;
113
+ Object.assign(typedi_1.Container.get(PluginContext_1.PluginContext), saved.context);
114
+ config_1.config.authDisabled = authDisabledBefore;
115
+ if (commandAuthBefore === undefined)
116
+ delete process.env.XENON_REQUIRE_COMMAND_AUTH;
117
+ else
118
+ process.env.XENON_REQUIRE_COMMAND_AUTH = commandAuthBefore;
119
+ sinon_1.default.restore();
120
+ restore();
121
+ for (const s of servers.splice(0)) {
122
+ s.closeAllConnections();
123
+ yield new Promise((resolve) => http_1.default.Server.prototype.close.call(s, () => resolve()));
124
+ }
125
+ }));
126
+ const port = (s) => s.address().port;
127
+ function boot() {
128
+ return __awaiter(this, arguments, void 0, function* (nodeArgs = {}) {
129
+ const baseDriver = (0, appium_umbrella_1.appiumBaseDriver)();
130
+ const hubServer = yield baseDriver.server({
131
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction((0, appium_umbrella_1.umbrellaWith)([])),
132
+ port: 0,
133
+ hostname: '127.0.0.1',
134
+ basePath: '/wd/hub',
135
+ cliArgs: { basePath: '/wd/hub' },
136
+ serverUpdaters: [
137
+ (app) => {
138
+ app.get('/xenon/api/auth/jwks.json', (_req, res) => res.json(hubKeys.jwks()));
139
+ },
140
+ ],
141
+ });
142
+ servers.push(hubServer);
143
+ const hubUrl = `http://127.0.0.1:${port(hubServer)}`;
144
+ const pluginArgs = Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: hubUrl, bindHostOrIp: '127.0.0.1', enableDashboard: false, deviceAvailabilityTimeoutMs: 1000, deviceAvailabilityQueryIntervalMs: 100 }), nodeArgs);
145
+ const umbrella = (0, appium_umbrella_1.umbrellaWith)([[plugin_1.XenonPlugin, 'xenon']]);
146
+ const nodeServer = yield baseDriver.server({
147
+ routeConfiguringFunction: baseDriver.routeConfiguringFunction(umbrella),
148
+ port: 0,
149
+ hostname: '127.0.0.1',
150
+ basePath: '/node',
151
+ cliArgs: { basePath: '/node' },
152
+ serverUpdaters: [
153
+ (app) => {
154
+ (0, registerCommandAuth_1.registerSessionGateway)(app, { basePath: '/node' }, (0, registerCommandAuth_1.commandAuthDeps)({ enabled: () => true, authDisabled: () => false, logger: quiet }), (0, defaultGateway_1.nodeGatewayOptions)(hubUrl));
155
+ // What ServerManager mounts under /xenon/api on a node.
156
+ const api = express_1.default.Router();
157
+ (0, nodeSessionStatus_1.registerNodeSessionStatus)(api, pluginArgs);
158
+ (0, nodeSessionMetrics_1.registerNodeSessionMetrics)(api, pluginArgs);
159
+ (0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(api, pluginArgs);
160
+ app.use('/xenon/api', api);
161
+ },
162
+ ],
163
+ });
164
+ servers.push(nodeServer);
165
+ nodeOrigin = `http://127.0.0.1:${port(nodeServer)}`;
166
+ typedi_1.Container.get(PluginContext_1.PluginContext).setContext(pluginArgs, port(nodeServer), exports.NODE_ID, '/node');
167
+ yield scratch.db.device.create({
168
+ data: {
169
+ udid: 'phone-1',
170
+ host: nodeOrigin,
171
+ nodeId: exports.NODE_ID,
172
+ platform: 'android',
173
+ name: 'phone-1',
174
+ sdk: '14',
175
+ busy: false,
176
+ userBlocked: false,
177
+ offline: false,
178
+ },
179
+ });
180
+ });
181
+ }
182
+ function nodeSession() {
183
+ return __awaiter(this, arguments, void 0, function* (caps = {}) {
184
+ const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).createTokenFor({
185
+ userId: 'alice',
186
+ udid: 'phone-1',
187
+ host: nodeOrigin,
188
+ });
189
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
190
+ .post('/node/session')
191
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, token)
192
+ .send({
193
+ capabilities: {
194
+ alwaysMatch: Object.assign({ platformName: 'Android', 'appium:automationName': appium_umbrella_1.FAKE_AUTOMATION, 'appium:newCommandTimeout': 0, 'appium:udid': 'phone-1', 'xe:options': { recordVideo: false } }, caps),
195
+ firstMatch: [{}],
196
+ },
197
+ });
198
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
199
+ return res.body.value.sessionId;
200
+ });
201
+ }
202
+ const hubSide = (sessionId) => new RemoteSession_1.RemoteSession({
203
+ sessionId,
204
+ device: { udid: 'phone-1', host: nodeOrigin, nodeId: exports.NODE_ID, platform: 'android' },
205
+ sessionResponse: {},
206
+ xenonOption: {},
207
+ baseUrl: `${nodeOrigin}/node`,
208
+ });
209
+ function deleteOnNode(sessionId) {
210
+ return __awaiter(this, void 0, void 0, function* () {
211
+ const token = yield typedi_1.Container.get(hubSessionToken_1.HubSessionTokenIssuer).tokenFor(sessionId);
212
+ const res = yield (0, loopbackRequest_1.default)(nodeOrigin)
213
+ .delete(`/node/session/${sessionId}`)
214
+ .set(hubSessionToken_1.HUB_TOKEN_HEADER, token);
215
+ (0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(200);
216
+ });
217
+ }
218
+ return {
219
+ get nodeOrigin() {
220
+ return nodeOrigin;
221
+ },
222
+ boot,
223
+ nodeSession,
224
+ hubSide,
225
+ deleteOnNode,
226
+ };
227
+ }
@@ -26,6 +26,14 @@ describe('UserService', () => {
26
26
  (0, chai_1.expect)(yield svc.verifyPassword('correct-horse-staple', hash)).to.be.true;
27
27
  (0, chai_1.expect)(yield svc.verifyPassword('wrong', hash)).to.be.false;
28
28
  }));
29
+ // Written by the native `bcrypt` 5 that hashed passwords through 2.17, at
30
+ // its cost of 12: every stored password is one of these.
31
+ it('verifies a password stored before 2.18', () => __awaiter(void 0, void 0, void 0, function* () {
32
+ const stored = '$2b$12$5kTngvxC6mQpOaiVCcexfuHPHKvxQMKz/IFm3Gxzia4Sb3lzK85dC';
33
+ const svc = new UserService_1.UserService();
34
+ (0, chai_1.expect)(yield svc.verifyPassword('signed-in-before-2.18', stored)).to.be.true;
35
+ (0, chai_1.expect)(yield svc.verifyPassword('signed-in-before-2.17', stored)).to.be.false;
36
+ }));
29
37
  it('rejects passwords shorter than 8 characters', () => __awaiter(void 0, void 0, void 0, function* () {
30
38
  const svc = new UserService_1.UserService();
31
39
  let err;
@@ -0,0 +1,195 @@
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 net_1 = __importDefault(require("net"));
50
+ const chai_1 = require("chai");
51
+ const sinon_1 = __importDefault(require("sinon"));
52
+ const typedi_1 = require("typedi");
53
+ const async_wait_until_1 = __importDefault(require("async-wait-until"));
54
+ const prisma_1 = require("../../src/prisma");
55
+ const prisma_store_1 = require("../../src/data-service/prisma-store");
56
+ const device_store_1 = require("../../src/data-service/device-store");
57
+ const deviceUtils = __importStar(require("../../src/device-utils"));
58
+ const helpers = __importStar(require("../../src/helpers"));
59
+ const NodeDevices_1 = __importDefault(require("../../src/device-managers/NodeDevices"));
60
+ const device_managers_1 = require("../../src/device-managers");
61
+ const iOSTracker_1 = require("../../src/device-managers/iOSTracker");
62
+ const localDeviceHosts_1 = require("../../src/device-managers/localDeviceHosts");
63
+ const ServerManager_1 = require("../../src/services/ServerManager");
64
+ const SocketServer_1 = require("../../src/services/SocketServer");
65
+ const NotificationService_1 = require("../../src/services/NotificationService");
66
+ const container_registration_1 = require("../helpers/container-registration");
67
+ const fake_device_table_1 = require("../helpers/fake-device-table");
68
+ /**
69
+ * Appium opens its port only once every plugin's updateServer has finished,
70
+ * so a start must not wait long on another server. Seen with 2.17.0: a hub
71
+ * whose database still listed its own phones under its old DHCP address
72
+ * (http://192.168.0.104:4723, now .102) asked that address whether it was a
73
+ * live node, and a host that drops packets takes four 30 s timeouts to give
74
+ * up, so the port opened 2 min 4 s after the plugin loaded. A node whose hub
75
+ * doesn't answer waited the same way before reporting its phones.
76
+ */
77
+ const IP = '192.168.0.102';
78
+ const HUB = `http://${IP}:4723`;
79
+ const NODE = `http://${IP}:4725`;
80
+ const phone = (udid, host, over = {}) => (Object.assign({ udid,
81
+ host, name: udid, platform: 'android', deviceType: 'real', realDevice: true, state: 'device', sdk: '14' }, over));
82
+ /** A promise this test settles when it chooses, as a host that drops packets never does. */
83
+ const deferred = () => {
84
+ let resolve;
85
+ const promise = new Promise((r) => (resolve = r));
86
+ return { promise, resolve };
87
+ };
88
+ /** Whether a promise settles within a time no probe of another server can beat. */
89
+ const settlesSoon = (p) => Promise.race([
90
+ p.then(() => true),
91
+ new Promise((r) => setTimeout(() => r(false), 1000)),
92
+ ]);
93
+ describe('A start never waits on another server', () => {
94
+ let table;
95
+ let discover;
96
+ let restoreContainer;
97
+ let savedStore;
98
+ const seed = (rows) => {
99
+ table = (0, fake_device_table_1.fakeDeviceTable)(rows);
100
+ for (const m of Object.keys(table.delegate)) {
101
+ sinon_1.default.stub(prisma_1.prisma.device, m).callsFake(table.delegate[m]);
102
+ }
103
+ const settings = (0, fake_device_table_1.fakeDeviceSettingTable)();
104
+ for (const m of Object.keys(settings.delegate)) {
105
+ sinon_1.default.stub(prisma_1.prisma.deviceSetting, m).callsFake(settings.delegate[m]);
106
+ }
107
+ };
108
+ const boot = (port, args) => typedi_1.Container.get(ServerManager_1.ServerManager).syncDevicesAtBoot((0, localDeviceHosts_1.localDeviceHosts)({ bindHostOrIp: IP }, port), args);
109
+ beforeEach(() => {
110
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return []; });
111
+ restoreContainer = (0, container_registration_1.saveRegistrations)(device_managers_1.XenonManager, SocketServer_1.SocketServer, NotificationService_1.NotificationService, iOSTracker_1.IosTracker);
112
+ typedi_1.Container.set({
113
+ id: device_managers_1.XenonManager,
114
+ value: { getDevices: (known) => discover(known) },
115
+ });
116
+ typedi_1.Container.set({
117
+ id: SocketServer_1.SocketServer,
118
+ value: {
119
+ emitToDashboard: () => undefined,
120
+ emitToDashboardForDevices: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }),
121
+ hasScopedDashboard: () => false,
122
+ },
123
+ });
124
+ typedi_1.Container.set({ id: NotificationService_1.NotificationService, value: { dispatchEvent: () => __awaiter(void 0, void 0, void 0, function* () { return undefined; }) } });
125
+ sinon_1.default.stub(prisma_1.prisma.lease, 'findMany').resolves([]);
126
+ savedStore = device_store_1.DeviceStoreFactory._deviceStore;
127
+ device_store_1.DeviceStoreFactory._deviceStore = new prisma_store_1.PrismaDeviceStore();
128
+ });
129
+ afterEach(() => {
130
+ device_store_1.DeviceStoreFactory._deviceStore = savedStore;
131
+ sinon_1.default.restore();
132
+ restoreContainer();
133
+ });
134
+ describe('on a hub, a row whose server never answers', () => {
135
+ // Takes the connection and never answers, as a host past a dropped route
136
+ // looks to the client: nothing but the timeout ends the request.
137
+ let silent;
138
+ let sockets;
139
+ let silentHost;
140
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
141
+ sockets = [];
142
+ silent = net_1.default.createServer((s) => {
143
+ sockets.push(s);
144
+ });
145
+ yield new Promise((r) => silent.listen(0, '127.0.0.1', r));
146
+ silentHost = `http://127.0.0.1:${silent.address().port}`;
147
+ // The real probe, with a limit this test can wait for.
148
+ sinon_1.default.stub(deviceUtils.BOOT_PROBE, 'timeoutMs').value(300);
149
+ }));
150
+ afterEach(() => __awaiter(void 0, void 0, void 0, function* () {
151
+ sockets.forEach((s) => s.destroy());
152
+ yield new Promise((r) => silent.close(() => r()));
153
+ }));
154
+ it('is asked once, briefly, and gone before the port opens', () => __awaiter(void 0, void 0, void 0, function* () {
155
+ seed([phone('old-row', silentHost)]);
156
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return [phone('own-phone', HUB)]; });
157
+ (0, chai_1.expect)(yield settlesSoon(boot(4723, {}))).to.equal(true);
158
+ (0, chai_1.expect)(table.row('own-phone')).to.include({ host: HUB });
159
+ (0, chai_1.expect)(table.row('old-row')).to.equal(undefined);
160
+ (0, chai_1.expect)(sockets).to.have.length(1);
161
+ }));
162
+ });
163
+ it('a node opens while its hub never answers, and reports its phones once it does', () => __awaiter(void 0, void 0, void 0, function* () {
164
+ seed([]);
165
+ discover = () => __awaiter(void 0, void 0, void 0, function* () { return [phone('node-phone', NODE)]; });
166
+ const hubAnswer = deferred();
167
+ const alive = sinon_1.default.stub(helpers, 'isXenonRunning').returns(hubAnswer.promise);
168
+ const post = sinon_1.default.stub(NodeDevices_1.default.prototype, 'postDevicesToHub').resolves();
169
+ (0, chai_1.expect)(yield settlesSoon(boot(4725, { hub: HUB }))).to.equal(true);
170
+ (0, chai_1.expect)(table.row('node-phone')).to.include({ host: NODE });
171
+ (0, chai_1.expect)(alive.calledWith(HUB)).to.equal(true);
172
+ (0, chai_1.expect)(post.called).to.equal(false);
173
+ hubAnswer.resolve(true);
174
+ yield (0, async_wait_until_1.default)(() => post.called, { timeout: 2000 });
175
+ (0, chai_1.expect)(post.firstCall.args[1]).to.equal('add');
176
+ (0, chai_1.expect)(post.firstCall.args[0].map((d) => d.udid)).to.deep.equal(['node-phone']);
177
+ }));
178
+ it("a node's report that fails is logged, and never fails the start", () => __awaiter(void 0, void 0, void 0, function* () {
179
+ seed([]);
180
+ sinon_1.default.stub(deviceUtils, 'reportDevicesToHub').rejects(new Error('hub refused'));
181
+ const warn = sinon_1.default.spy(typedi_1.Container.get(ServerManager_1.ServerManager).logger, 'warn');
182
+ const unhandled = sinon_1.default.spy();
183
+ process.on('unhandledRejection', unhandled);
184
+ try {
185
+ yield boot(4725, { hub: HUB });
186
+ yield (0, async_wait_until_1.default)(() => warn.called, { timeout: 2000 });
187
+ (0, chai_1.expect)(String(warn.firstCall.args[0])).to.include('hub refused');
188
+ yield new Promise((r) => setImmediate(r));
189
+ (0, chai_1.expect)(unhandled.called).to.equal(false);
190
+ }
191
+ finally {
192
+ process.off('unhandledRejection', unhandled);
193
+ }
194
+ }));
195
+ });
@@ -175,6 +175,21 @@ describe('EventManager: an Android session’s device log', () => {
175
175
  (0, chai_1.expect)(deviceLogs.start.firstCall.args[0].driverLog).to.equal(driverLog);
176
176
  (0, chai_1.expect)(deviceLogs.start.firstCall.args[0].appUnderTest).to.equal('com.example.shop');
177
177
  }));
178
+ it("collects a node session's from the node, through the session", () => __awaiter(void 0, void 0, void 0, function* () {
179
+ const { device } = stubSessionStart();
180
+ const session = {
181
+ getId: () => 's-logs-node',
182
+ getType: () => 'remote',
183
+ getCapabilities: () => ({ platformName: 'Android' }),
184
+ getLiveVideoUrl: () => null,
185
+ nodeOrigin: () => 'http://node:4723',
186
+ nodeDeviceLogs: () => __awaiter(void 0, void 0, void 0, function* () { return ({ kind: 'refused' }); }),
187
+ apiKeyId: null,
188
+ userId: null,
189
+ };
190
+ yield event_manager_1.DASHBORD_EVENT_MANAGER.onSessionStarted(parsed({}), session, Object.assign(Object.assign({}, device), { host: 'http://node:4723' }));
191
+ (0, chai_1.expect)(deviceLogs.start.firstCall.args[0].source).to.equal(session);
192
+ }));
178
193
  it('writes it to the end when the session stops, even one no longer in memory', () => __awaiter(void 0, void 0, void 0, function* () {
179
194
  sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getSession').returns(undefined);
180
195
  sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({ getDevices: () => __awaiter(void 0, void 0, void 0, function* () { return []; }) });
@@ -67,6 +67,7 @@ const prisma_store_1 = require("../../src/data-service/prisma-store");
67
67
  const SessionManager_1 = require("../../src/sessions/SessionManager");
68
68
  const RemoteSession_1 = require("../../src/sessions/RemoteSession");
69
69
  const event_manager_1 = require("../../src/dashboard/event-manager");
70
+ const deviceService = __importStar(require("../../src/data-service/device-service"));
70
71
  const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
71
72
  const internalCall_1 = require("../../src/gateway/internalCall");
72
73
  const nodeWebDriverUrl_1 = require("../../src/gateway/nodeWebDriverUrl");
@@ -657,6 +658,26 @@ describe('the session gateway between a hub and a node, in Appium 3’s own serv
657
658
  (0, chai_1.expect)(nodeRequests).to.have.length(1);
658
659
  (0, chai_1.expect)(nodeRequests[0].headers[internalCall_1.INTERNAL_CALL_HEADER]).to.equal(undefined);
659
660
  }));
661
+ it('is never logged as one of the session’s commands, though forwarded', () => __awaiter(this, void 0, void 0, function* () {
662
+ yield boot({ dashboard: true });
663
+ const { id } = yield remoteSession();
664
+ const before = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'beforeSessionCommand').resolves(true);
665
+ const after = sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'afterSessionCommand').resolves();
666
+ const touched = sinon_1.default.stub(deviceService, 'updateCmdExecutedTime').resolves();
667
+ yield (0, loopbackRequest_1.default)(hubUrl)
668
+ .get(`/wd/hub/wd-internal/session/${id}/url`)
669
+ .set((0, internalCall_1.internalCallHeaders)())
670
+ .expect(200);
671
+ (0, chai_1.expect)(node.commands).to.deep.equal(['getUrl']);
672
+ (0, chai_1.expect)(before.called, 'the dashboard’s before hook').to.equal(false);
673
+ (0, chai_1.expect)(after.called, 'the dashboard’s after hook').to.equal(false);
674
+ (0, chai_1.expect)(touched.called, 'counted as the session’s activity').to.equal(false);
675
+ // The test's own command is, as before.
676
+ yield (0, loopbackRequest_1.default)(hubUrl).get(`/wd/hub/session/${id}/url`).expect(200);
677
+ (0, chai_1.expect)(before.callCount).to.equal(1);
678
+ (0, chai_1.expect)(after.callCount).to.equal(1);
679
+ (0, chai_1.expect)(touched.calledOnceWith(id)).to.equal(true);
680
+ }));
660
681
  });
661
682
  describe('per-command auth on a remote session (at the hub)', () => {
662
683
  it('lets the owner through to the node', () => __awaiter(this, void 0, void 0, function* () {