@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.
- package/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-CjmMCS0S.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-B4Ob0v3C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DTVOB_NO.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-DcEG-oAi.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-CtnCDshE.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-BK0dE9cc.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-eKsmLpKS.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-DPwop3Lz.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CfgSkZkB.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-Bzl9pTUY.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-B_TZTrPm.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-DlmvZJPM.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-BigPWKXi.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-CvuZdQYu.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-lI2UJ2n7.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CPgf9XJM.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-DIzOvtUp.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-DiP5ywBT.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-DtW_8hm5.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-C89FbNU0.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-B7sUDb--.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-BBhvrMfA.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DugsdUw1.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-grKd08ZH.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-ORjyNfnX.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Ei2FxUXq.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-Day0HjW4.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-CHxSRLHp.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BDHewWTx.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-CTwT4I3D.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-DlhrKJ-g.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-D3bjz_vr.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-BNBLi023.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-C51Au2i_.js} +1 -1
- package/lib/public/assets/index-Dk5J6HB1.js +219 -0
- package/lib/public/assets/{index-B9KQ6LsY.js → index-DncSz5LG.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-Z7_BY3pJ.js} +1 -1
- package/lib/public/assets/{input-CZHFjbSg.js → input-bTXM4l09.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-B3YmVYyr.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-BClKEj1-.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-CZsenX3D.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-B6A6tOp9.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CmCJGy7O.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BdTNEglU.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-BM8CaLtt.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BS6vUc1x.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-Yfk_Y7g3.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-DhXCwpml.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-D08gP1td.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-BKboorfa.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Cd9NodrA.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-acLDyyDW.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-CWLkC1Fd.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-DZoRKNti.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-DS4qrt84.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-TpmN61G3.js} +1 -1
- package/lib/public/assets/session-detail-page-C0wYaXqy.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-CxPGEY2j.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-DUpyYgl-.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-SA02BJNQ.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-jamD0Kxr.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-DO8fv6w4.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-CaiGCStb.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-jS-fcvVf.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-BzoenegM.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-Bnw7Vs_B.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-DHvDrriG.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-FEvy0w5U.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-CBVB5lxi.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-BMrTJO6M.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/index.js +2 -0
- package/lib/src/app/openapi/grid.yaml +102 -0
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +23 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +25 -8
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeAsk.js +57 -0
- package/lib/src/gateway/nodeSessionDeviceLogs.js +35 -0
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +12 -4
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +29 -33
- package/lib/src/scripts/run-migrations.js +320 -25
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +52 -23
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/interceptor/CertManager.js +50 -16
- package/lib/src/services/logcat/NodeDeviceLogStore.js +99 -0
- package/lib/src/services/logcat/NodeDeviceLogsCollector.js +164 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +279 -37
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/logcat/nodeDeviceLogs.js +91 -0
- package/lib/src/services/metrics/nodeMetrics.js +20 -49
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +23 -4
- package/lib/src/sessions/SessionManager.js +33 -9
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/fake-logcat.js +80 -0
- package/lib/test/helpers/hub-node-pair.js +227 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +74 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/hub-node-iphone-readiness.spec.js +134 -0
- package/lib/test/unit/hub-node-session-device-logs.spec.js +194 -0
- package/lib/test/unit/hub-node-session-metrics.spec.js +24 -192
- package/lib/test/unit/hub-remote-session-heartbeat.spec.js +8 -1
- package/lib/test/unit/interceptor/CertManager.spec.js +89 -0
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/node-device-log-store.spec.js +102 -0
- package/lib/test/unit/node-device-logs-collector.spec.js +343 -0
- package/lib/test/unit/node-gateway-create-appium-server.spec.js +8 -1
- package/lib/test/unit/node-hub-session-owner.spec.js +8 -1
- package/lib/test/unit/node-session-device-logs-route.spec.js +103 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +440 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +264 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-hub-node.spec.js +340 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs-lifecycle.spec.js +137 -0
- package/lib/test/unit/session-device-logs.spec.js +43 -96
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +16 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
|
@@ -0,0 +1,343 @@
|
|
|
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 NodeDeviceLogsCollector_1 = require("../../src/services/logcat/NodeDeviceLogsCollector");
|
|
19
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
20
|
+
const nodeAsk_1 = require("../../src/gateway/nodeAsk");
|
|
21
|
+
const RemoteSession_1 = require("../../src/sessions/RemoteSession");
|
|
22
|
+
const CloudSession_1 = require("../../src/sessions/CloudSession");
|
|
23
|
+
const line = (seq) => ({ seq, message: `line ${seq}`, timestamp: 1000 + seq });
|
|
24
|
+
const answer = (state, seqs, more = false) => ({
|
|
25
|
+
kind: 'answer',
|
|
26
|
+
answer: { state, lines: seqs.map(line), more },
|
|
27
|
+
});
|
|
28
|
+
describe('NodeDeviceLogsCollector: the hub collects a node session’s device log', () => {
|
|
29
|
+
let clock;
|
|
30
|
+
let replies;
|
|
31
|
+
let asked;
|
|
32
|
+
let got;
|
|
33
|
+
let times;
|
|
34
|
+
let warned;
|
|
35
|
+
let support;
|
|
36
|
+
/** While set, an ask waits on it: a node slow to answer. */
|
|
37
|
+
let gate;
|
|
38
|
+
const source = {
|
|
39
|
+
nodeOrigin: () => 'http://node:4723',
|
|
40
|
+
nodeDeviceLogs: (after) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
var _a;
|
|
42
|
+
asked.push(after);
|
|
43
|
+
if (gate)
|
|
44
|
+
yield gate;
|
|
45
|
+
return (_a = replies.shift()) !== null && _a !== void 0 ? _a : answer('recording', []);
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
/** While set, writing an answer's lines waits on it: a slow database. */
|
|
49
|
+
let writing;
|
|
50
|
+
const make = (storedTail = []) => new NodeDeviceLogsCollector_1.NodeDeviceLogsCollector({
|
|
51
|
+
source,
|
|
52
|
+
onLines: (lines) => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
if (writing)
|
|
54
|
+
yield writing;
|
|
55
|
+
got.push(...lines.map((l) => l.message));
|
|
56
|
+
times.push(...lines.map((l) => l.timestamp.getTime()));
|
|
57
|
+
}),
|
|
58
|
+
support,
|
|
59
|
+
logger: { info: () => undefined, warn: (m) => warned.push(m) },
|
|
60
|
+
storedTail,
|
|
61
|
+
});
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
clock = sinon_1.default.useFakeTimers({ now: 0 });
|
|
64
|
+
replies = [];
|
|
65
|
+
asked = [];
|
|
66
|
+
got = [];
|
|
67
|
+
times = [];
|
|
68
|
+
warned = [];
|
|
69
|
+
gate = null;
|
|
70
|
+
writing = null;
|
|
71
|
+
support = new nodeDeviceLogs_1.NodeDeviceLogsSupport();
|
|
72
|
+
support.now = () => clock.now;
|
|
73
|
+
support.logger = { warn: (m) => warned.push(m) };
|
|
74
|
+
});
|
|
75
|
+
afterEach(() => clock.restore());
|
|
76
|
+
it('asks at once, then every 10 s after the newest line it has, and passes the lines on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
77
|
+
replies = [answer('recording', [1, 2]), answer('recording', [3])];
|
|
78
|
+
const c = make();
|
|
79
|
+
c.start();
|
|
80
|
+
yield clock.tickAsync(0);
|
|
81
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
82
|
+
yield clock.tickAsync(9999);
|
|
83
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
84
|
+
yield clock.tickAsync(1);
|
|
85
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
86
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
87
|
+
(0, chai_1.expect)(times).to.deep.equal([1001, 1002, 1003]);
|
|
88
|
+
}));
|
|
89
|
+
it('asks again at once while the node holds more, a few pages a round', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
replies = [answer('recording', [1, 2], true), answer('recording', [3, 4], true)];
|
|
91
|
+
for (let i = 0; i < NodeDeviceLogsCollector_1.MAX_PAGES_PER_ROUND; i++) {
|
|
92
|
+
replies.push(answer('recording', [5 + i], true));
|
|
93
|
+
}
|
|
94
|
+
const c = make();
|
|
95
|
+
c.start();
|
|
96
|
+
yield clock.tickAsync(0);
|
|
97
|
+
(0, chai_1.expect)(asked).to.have.length(NodeDeviceLogsCollector_1.MAX_PAGES_PER_ROUND);
|
|
98
|
+
(0, chai_1.expect)(asked.slice(0, 3)).to.deep.equal([null, 2, 4]);
|
|
99
|
+
// The rest wait for the next round, which asks until the node has no more.
|
|
100
|
+
yield clock.tickAsync(10000);
|
|
101
|
+
(0, chai_1.expect)(asked).to.have.length(NodeDeviceLogsCollector_1.MAX_PAGES_PER_ROUND + 3);
|
|
102
|
+
yield c.stop();
|
|
103
|
+
}));
|
|
104
|
+
it('asks once more when the session ends, and takes the last lines', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
replies = [answer('recording', [1]), answer('ended', [2, 3])];
|
|
106
|
+
const c = make();
|
|
107
|
+
c.start();
|
|
108
|
+
yield clock.tickAsync(0);
|
|
109
|
+
yield c.stop();
|
|
110
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1]);
|
|
111
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
112
|
+
yield clock.tickAsync(60000);
|
|
113
|
+
(0, chai_1.expect)(asked).to.have.length(2);
|
|
114
|
+
}));
|
|
115
|
+
it('stops asking once the node’s session ended and it has nothing more', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
replies = [answer('ended', [1, 2], true), answer('ended', [3])];
|
|
117
|
+
const c = make();
|
|
118
|
+
c.start();
|
|
119
|
+
yield clock.tickAsync(30000);
|
|
120
|
+
yield c.stop();
|
|
121
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
122
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
123
|
+
}));
|
|
124
|
+
for (const [what, reply] of [
|
|
125
|
+
['the node doesn’t record the session', answer('off', [])],
|
|
126
|
+
['the node refuses the hub’s token', { kind: 'refused' }],
|
|
127
|
+
]) {
|
|
128
|
+
it(`stops asking when ${what}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
replies = [reply];
|
|
130
|
+
const c = make();
|
|
131
|
+
c.start();
|
|
132
|
+
yield clock.tickAsync(30000);
|
|
133
|
+
yield c.stop();
|
|
134
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
it('says once that a node is older, and leaves it alone for 10 minutes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
replies = [{ kind: 'unsupported', status: 404 }];
|
|
139
|
+
const first = make();
|
|
140
|
+
first.start();
|
|
141
|
+
yield clock.tickAsync(30000);
|
|
142
|
+
yield first.stop();
|
|
143
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
144
|
+
(0, chai_1.expect)(warned).to.have.length(1);
|
|
145
|
+
(0, chai_1.expect)(warned[0]).to.contain('http://node:4723');
|
|
146
|
+
// Another session on that node: not asked at all.
|
|
147
|
+
const second = make();
|
|
148
|
+
second.start();
|
|
149
|
+
yield clock.tickAsync(30000);
|
|
150
|
+
yield second.stop();
|
|
151
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
152
|
+
// After 10 minutes it is asked again, and an older node is said only once.
|
|
153
|
+
yield clock.tickAsync(nodeAsk_1.OLDER_NODE_RECHECK_MS);
|
|
154
|
+
replies = [{ kind: 'unsupported', status: 404 }];
|
|
155
|
+
const third = make();
|
|
156
|
+
third.start();
|
|
157
|
+
yield clock.tickAsync(0);
|
|
158
|
+
yield third.stop();
|
|
159
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, null]);
|
|
160
|
+
(0, chai_1.expect)(warned).to.have.length(1);
|
|
161
|
+
}));
|
|
162
|
+
it('asks an unreachable node again, says so once, and doesn’t wait on it at the end', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
163
|
+
replies = [
|
|
164
|
+
answer('recording', [1]),
|
|
165
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
166
|
+
{ kind: 'unavailable', reason: 'ECONNREFUSED' },
|
|
167
|
+
];
|
|
168
|
+
const c = make();
|
|
169
|
+
c.start();
|
|
170
|
+
yield clock.tickAsync(20000);
|
|
171
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1, 1]);
|
|
172
|
+
(0, chai_1.expect)(warned).to.have.length(1);
|
|
173
|
+
(0, chai_1.expect)(warned[0]).to.contain('ECONNREFUSED');
|
|
174
|
+
yield c.stop();
|
|
175
|
+
(0, chai_1.expect)(asked).to.have.length(3);
|
|
176
|
+
}));
|
|
177
|
+
it('still asks at the end after one failed ask: the node may have been busy', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
replies = [
|
|
179
|
+
answer('recording', [1]),
|
|
180
|
+
{ kind: 'unavailable', reason: 'timeout' },
|
|
181
|
+
answer('ended', [2, 3]),
|
|
182
|
+
];
|
|
183
|
+
const c = make();
|
|
184
|
+
c.start();
|
|
185
|
+
yield clock.tickAsync(10000);
|
|
186
|
+
yield c.stop();
|
|
187
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1, 1]);
|
|
188
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
189
|
+
}));
|
|
190
|
+
it('writes each answer before asking for the next, which drops it on the node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
191
|
+
let release;
|
|
192
|
+
writing = new Promise((r) => (release = r));
|
|
193
|
+
replies = [answer('recording', [1, 2], true), answer('recording', [3])];
|
|
194
|
+
const c = make();
|
|
195
|
+
c.start();
|
|
196
|
+
yield clock.tickAsync(0);
|
|
197
|
+
(0, chai_1.expect)(asked).to.deep.equal([null]);
|
|
198
|
+
writing = null;
|
|
199
|
+
release();
|
|
200
|
+
yield clock.tickAsync(0);
|
|
201
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 2]);
|
|
202
|
+
yield c.stop();
|
|
203
|
+
}));
|
|
204
|
+
it('takes up again where it was when an unreachable node answers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
205
|
+
replies = [
|
|
206
|
+
answer('recording', [1]),
|
|
207
|
+
{ kind: 'unavailable', reason: 'timeout' },
|
|
208
|
+
answer('recording', [2, 3]),
|
|
209
|
+
];
|
|
210
|
+
const c = make();
|
|
211
|
+
c.start();
|
|
212
|
+
yield clock.tickAsync(20000);
|
|
213
|
+
yield c.stop();
|
|
214
|
+
(0, chai_1.expect)(asked).to.deep.equal([null, 1, 1, 3]);
|
|
215
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
216
|
+
}));
|
|
217
|
+
it('never asks while an ask is still waiting for its answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
+
let release;
|
|
219
|
+
gate = new Promise((r) => (release = r));
|
|
220
|
+
const c = make();
|
|
221
|
+
c.start();
|
|
222
|
+
yield clock.tickAsync(35000);
|
|
223
|
+
(0, chai_1.expect)(asked).to.have.length(1);
|
|
224
|
+
gate = null;
|
|
225
|
+
release();
|
|
226
|
+
yield clock.tickAsync(0);
|
|
227
|
+
yield c.stop();
|
|
228
|
+
}));
|
|
229
|
+
it('ignores a line it already has', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
+
replies = [answer('recording', [1, 2]), answer('recording', [2, 3])];
|
|
231
|
+
const c = make();
|
|
232
|
+
c.start();
|
|
233
|
+
yield clock.tickAsync(10000);
|
|
234
|
+
yield c.stop();
|
|
235
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 1', 'line 2', 'line 3']);
|
|
236
|
+
}));
|
|
237
|
+
describe('after a hub restart', () => {
|
|
238
|
+
it('leaves out the lines the hub had stored, which the node sends again', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
239
|
+
// The node still holds the last page the hub received before it
|
|
240
|
+
// restarted: it drops lines only when the next ask names them.
|
|
241
|
+
replies = [answer('recording', [7, 8, 9, 10])];
|
|
242
|
+
const c = make([line(6), line(7), line(8)]);
|
|
243
|
+
c.start();
|
|
244
|
+
yield clock.tickAsync(0);
|
|
245
|
+
yield c.stop();
|
|
246
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 9', 'line 10']);
|
|
247
|
+
}));
|
|
248
|
+
it('keeps them all when the node doesn’t hold the newest stored line', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
249
|
+
replies = [answer('recording', [7, 8]), answer('recording', [9])];
|
|
250
|
+
const c = make([line(5), line(6)]);
|
|
251
|
+
c.start();
|
|
252
|
+
yield clock.tickAsync(0);
|
|
253
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 7', 'line 8']);
|
|
254
|
+
yield c.stop();
|
|
255
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 7', 'line 8', 'line 9']);
|
|
256
|
+
}));
|
|
257
|
+
it('compares only the first answer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
258
|
+
replies = [answer('recording', []), answer('recording', [8, 9])];
|
|
259
|
+
const c = make([line(8)]);
|
|
260
|
+
c.start();
|
|
261
|
+
yield clock.tickAsync(0);
|
|
262
|
+
yield c.stop();
|
|
263
|
+
(0, chai_1.expect)(got).to.deep.equal(['line 8', 'line 9']);
|
|
264
|
+
}));
|
|
265
|
+
it('tells apart a line logcat printed twice in one millisecond', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
266
|
+
// B and its twin are the same line at the same time. The hub stored
|
|
267
|
+
// the first only: the twin and C are new.
|
|
268
|
+
const a = { seq: 1, message: 'A', timestamp: 1 };
|
|
269
|
+
const b = { seq: 2, message: 'B', timestamp: 2 };
|
|
270
|
+
const twin = { seq: 3, message: 'B', timestamp: 2 };
|
|
271
|
+
const c3 = { seq: 4, message: 'C', timestamp: 3 };
|
|
272
|
+
const page = {
|
|
273
|
+
kind: 'answer',
|
|
274
|
+
answer: { state: 'recording', lines: [a, b, twin, c3], more: false },
|
|
275
|
+
};
|
|
276
|
+
replies = [page];
|
|
277
|
+
const first = make([a, b]);
|
|
278
|
+
first.start();
|
|
279
|
+
yield clock.tickAsync(0);
|
|
280
|
+
yield first.stop();
|
|
281
|
+
(0, chai_1.expect)(got).to.deep.equal(['B', 'C']);
|
|
282
|
+
// Both stored: only C is new.
|
|
283
|
+
got = [];
|
|
284
|
+
replies = [Object.assign({}, page)];
|
|
285
|
+
const second = make([a, b, twin]);
|
|
286
|
+
second.start();
|
|
287
|
+
yield clock.tickAsync(0);
|
|
288
|
+
yield second.stop();
|
|
289
|
+
(0, chai_1.expect)(got).to.deep.equal(['C']);
|
|
290
|
+
}));
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
describe('reading a node’s device log answer', () => {
|
|
294
|
+
const has = { [nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]: '1' };
|
|
295
|
+
it('reads the lines, and leaves out any that isn’t one', () => {
|
|
296
|
+
const reply = (0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(200, has, {
|
|
297
|
+
value: {
|
|
298
|
+
state: 'recording',
|
|
299
|
+
more: true,
|
|
300
|
+
lines: [
|
|
301
|
+
line(1),
|
|
302
|
+
{ seq: 0, message: 'x', timestamp: 1 },
|
|
303
|
+
{ seq: 2.5, message: 'x', timestamp: 1 },
|
|
304
|
+
{ seq: 3, message: 42, timestamp: 1 },
|
|
305
|
+
{ seq: 4, message: 'x', timestamp: 'now' },
|
|
306
|
+
null,
|
|
307
|
+
line(5),
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
(0, chai_1.expect)(reply).to.deep.equal({
|
|
312
|
+
kind: 'answer',
|
|
313
|
+
answer: { state: 'recording', more: true, lines: [line(1), line(5)] },
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
it('takes a node without the header for an older one only when it answers 401, 404 or 2xx', () => {
|
|
317
|
+
for (const status of [200, 401, 404]) {
|
|
318
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(status, {}, {})).to.deep.equal({
|
|
319
|
+
kind: 'unsupported',
|
|
320
|
+
status,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// A proxy's 502 in front of a node is an outage.
|
|
324
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(502, {}, {}).kind).to.equal('unavailable');
|
|
325
|
+
});
|
|
326
|
+
it('takes a 404 with the header for a refusal, and anything else it can’t read for an outage', () => {
|
|
327
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(404, has, {}).kind).to.equal('refused');
|
|
328
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(503, has, {}).kind).to.equal('unavailable');
|
|
329
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.readNodeDeviceLogsReply)(200, has, { value: { state: 'x', lines: [] } }).kind).to.equal('unavailable');
|
|
330
|
+
});
|
|
331
|
+
it('asks only a session a node runs, never a cloud provider’s', () => {
|
|
332
|
+
const opts = {
|
|
333
|
+
sessionId: 's1',
|
|
334
|
+
device: { udid: 'p', host: 'http://node:4723' },
|
|
335
|
+
sessionResponse: {},
|
|
336
|
+
xenonOption: {},
|
|
337
|
+
baseUrl: 'http://node:4723/wd/hub',
|
|
338
|
+
};
|
|
339
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(new RemoteSession_1.RemoteSession(opts))).to.not.equal(undefined);
|
|
340
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(new CloudSession_1.CloudSession(opts))).to.equal(undefined);
|
|
341
|
+
(0, chai_1.expect)((0, nodeDeviceLogs_1.nodeDeviceLogsSourceOf)(undefined)).to.equal(undefined);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -34,6 +34,7 @@ const prisma_store_1 = require("../../src/data-service/prisma-store");
|
|
|
34
34
|
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
35
35
|
const SessionLifecycleService_1 = require("../../src/services/SessionLifecycleService");
|
|
36
36
|
const config_1 = require("../../src/config");
|
|
37
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
37
38
|
const container_registration_1 = require("../helpers/container-registration");
|
|
38
39
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
39
40
|
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
@@ -90,10 +91,16 @@ describe('a create the hub forwards, on the node, in Appium 3’s own server()',
|
|
|
90
91
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
91
92
|
});
|
|
92
93
|
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
-
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager);
|
|
94
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
94
95
|
// The hub's keys sign here only because the test mints what the hub would send.
|
|
95
96
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
96
97
|
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
98
|
+
// A node records the device log of each session its hub creates; its
|
|
99
|
+
// phone here is a row, so nothing may reach this machine's adb.
|
|
100
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, {
|
|
101
|
+
start: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
102
|
+
stop: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
103
|
+
});
|
|
97
104
|
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
98
105
|
getMaxSessionCount: () => undefined,
|
|
99
106
|
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
@@ -37,6 +37,7 @@ const SessionOwnerResolver_1 = require("../../src/services/device-access/Session
|
|
|
37
37
|
const LiveSessionOwners_1 = require("../../src/services/device-access/LiveSessionOwners");
|
|
38
38
|
const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
|
|
39
39
|
const config_1 = require("../../src/config");
|
|
40
|
+
const SessionDeviceLogs_1 = require("../../src/services/logcat/SessionDeviceLogs");
|
|
40
41
|
const container_registration_1 = require("../helpers/container-registration");
|
|
41
42
|
const scratch_database_1 = require("../helpers/scratch-database");
|
|
42
43
|
const appium_umbrella_1 = require("../helpers/appium-umbrella");
|
|
@@ -76,11 +77,17 @@ describe('the owner of a hub’s session, on the node', function () {
|
|
|
76
77
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
77
78
|
});
|
|
78
79
|
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
-
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners);
|
|
80
|
+
restore = (0, container_registration_1.saveRegistrations)(JwtKeyService_1.JwtKeyService, hubSessionToken_1.HubSessionTokenIssuer, device_managers_1.XenonManager, SessionOwnerResolver_1.SessionOwnerResolver, LiveSessionOwners_1.LiveSessionOwners, SessionDeviceLogs_1.SessionDeviceLogs);
|
|
80
81
|
typedi_1.Container.set(JwtKeyService_1.JwtKeyService, hubKeys);
|
|
81
82
|
typedi_1.Container.set(hubSessionToken_1.HubSessionTokenIssuer, new hubSessionToken_1.HubSessionTokenIssuer());
|
|
82
83
|
typedi_1.Container.set(SessionOwnerResolver_1.SessionOwnerResolver, new SessionOwnerResolver_1.SessionOwnerResolver());
|
|
83
84
|
typedi_1.Container.set(LiveSessionOwners_1.LiveSessionOwners, new LiveSessionOwners_1.LiveSessionOwners());
|
|
85
|
+
// A node records the device log of each session its hub creates; its
|
|
86
|
+
// phone here is a row, so nothing may reach this machine's adb.
|
|
87
|
+
typedi_1.Container.set(SessionDeviceLogs_1.SessionDeviceLogs, {
|
|
88
|
+
start: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
89
|
+
stop: () => __awaiter(this, void 0, void 0, function* () { return undefined; }),
|
|
90
|
+
});
|
|
84
91
|
typedi_1.Container.set(device_managers_1.XenonManager, {
|
|
85
92
|
getMaxSessionCount: () => undefined,
|
|
86
93
|
deviceInstances: () => __awaiter(this, void 0, void 0, function* () { return []; }),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
require("reflect-metadata");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
19
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
20
|
+
const nodeSessionDeviceLogs_1 = require("../../src/gateway/nodeSessionDeviceLogs");
|
|
21
|
+
const nodeDeviceLogs_1 = require("../../src/services/logcat/nodeDeviceLogs");
|
|
22
|
+
const hubSessionToken_1 = require("../../src/gateway/hubSessionToken");
|
|
23
|
+
const commandAuth_1 = require("../../src/middleware/commandAuth");
|
|
24
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
25
|
+
const quiet = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
26
|
+
const ANSWER = {
|
|
27
|
+
state: 'recording',
|
|
28
|
+
lines: [{ seq: 1, message: 'a line', timestamp: 1000 }],
|
|
29
|
+
more: false,
|
|
30
|
+
};
|
|
31
|
+
describe("a node's session device log route", () => {
|
|
32
|
+
let verify;
|
|
33
|
+
let read;
|
|
34
|
+
let enforced;
|
|
35
|
+
const app = () => {
|
|
36
|
+
const a = (0, express_1.default)();
|
|
37
|
+
a.get('/xenon/api/node/sessions/:sessionId/device-logs', (0, nodeSessionDeviceLogs_1.nodeSessionDeviceLogsHandler)({
|
|
38
|
+
hubTokens: { verify },
|
|
39
|
+
enforced: () => enforced,
|
|
40
|
+
read,
|
|
41
|
+
logger: quiet,
|
|
42
|
+
}));
|
|
43
|
+
return a;
|
|
44
|
+
};
|
|
45
|
+
const get = (q = '') => (0, supertest_1.default)(app()).get(`/xenon/api/node/sessions/s1/device-logs${q}`);
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
verify = sinon_1.default.stub();
|
|
48
|
+
read = sinon_1.default.stub().returns(ANSWER);
|
|
49
|
+
enforced = false;
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => sinon_1.default.restore());
|
|
52
|
+
it('answers the lines after `after`, with no credential when per-command auth is off', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
53
|
+
const res = yield get('?after=12');
|
|
54
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
55
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ value: ANSWER });
|
|
56
|
+
(0, chai_1.expect)(res.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal('1');
|
|
57
|
+
(0, chai_1.expect)(read.calledOnceWithExactly('s1', 12)).to.equal(true);
|
|
58
|
+
}));
|
|
59
|
+
it('reads anything but a number as no `after`', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
60
|
+
yield get('?after=-3');
|
|
61
|
+
yield get();
|
|
62
|
+
(0, chai_1.expect)(read.getCalls().map((c) => c.args[1])).to.deep.equal([null, null]);
|
|
63
|
+
}));
|
|
64
|
+
it("with per-command auth on, answers only the hub's token for that session", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
enforced = true;
|
|
66
|
+
verify.withArgs('good', 's1').resolves(true);
|
|
67
|
+
verify.withArgs('other', 's1').resolves(false);
|
|
68
|
+
const missing = yield get();
|
|
69
|
+
const wrong = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'other');
|
|
70
|
+
const right = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
71
|
+
(0, chai_1.expect)([missing.status, missing.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
72
|
+
(0, chai_1.expect)([wrong.status, wrong.body]).to.deep.equal([404, commandAuth_1.UNKNOWN_SESSION_BODY]);
|
|
73
|
+
(0, chai_1.expect)([right.status, right.body]).to.deep.equal([200, { value: ANSWER }]);
|
|
74
|
+
// A refused ask neither reads nor drops the lines the hub hasn't collected.
|
|
75
|
+
(0, chai_1.expect)(read.calledOnce).to.equal(true);
|
|
76
|
+
for (const r of [missing, wrong, right]) {
|
|
77
|
+
(0, chai_1.expect)(r.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal('1');
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
it("answers 503 when it can't check the hub's token", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
81
|
+
enforced = true;
|
|
82
|
+
verify.rejects(new Error('JWKS unreachable'));
|
|
83
|
+
const res = yield get().set(hubSessionToken_1.HUB_TOKEN_HEADER, 'good');
|
|
84
|
+
(0, chai_1.expect)([res.status, res.body]).to.deep.equal([503, commandAuth_1.COMMAND_AUTH_UNAVAILABLE_BODY]);
|
|
85
|
+
(0, chai_1.expect)(read.called).to.equal(false);
|
|
86
|
+
}));
|
|
87
|
+
it('exists only on a node', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
const mount = (hub) => {
|
|
89
|
+
const a = (0, express_1.default)();
|
|
90
|
+
const r = express_1.default.Router();
|
|
91
|
+
(0, nodeSessionDeviceLogs_1.registerNodeSessionDeviceLogs)(r, Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub }));
|
|
92
|
+
a.use('/xenon/api', r);
|
|
93
|
+
return a;
|
|
94
|
+
};
|
|
95
|
+
const onHub = yield (0, supertest_1.default)(mount()).get('/xenon/api/node/sessions/s1/device-logs');
|
|
96
|
+
const onNode = yield (0, supertest_1.default)(mount('http://127.0.0.1:1')).get('/xenon/api/node/sessions/s1/device-logs');
|
|
97
|
+
(0, chai_1.expect)(onHub.status).to.equal(404);
|
|
98
|
+
(0, chai_1.expect)(onHub.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal(undefined);
|
|
99
|
+
(0, chai_1.expect)(onNode.status).to.equal(200);
|
|
100
|
+
(0, chai_1.expect)(onNode.headers[nodeDeviceLogs_1.NODE_DEVICE_LOGS_HEADER]).to.equal('1');
|
|
101
|
+
(0, chai_1.expect)(onNode.body).to.deep.equal({ value: { state: 'off', lines: [], more: false } });
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const chai_1 = require("chai");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
/**
|
|
11
|
+
* The example Loki config set `limits_config.retention_period: 24h`, but Loki
|
|
12
|
+
* deletes nothing unless its compactor runs retention, so the logs it kept
|
|
13
|
+
* grew for ever. Loki 3 also refuses to start with retention enabled and no
|
|
14
|
+
* store for delete requests.
|
|
15
|
+
*/
|
|
16
|
+
describe('examples/observability/loki.yml', () => {
|
|
17
|
+
const config = js_yaml_1.default.load(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../examples/observability/loki.yml'), 'utf8'));
|
|
18
|
+
it('keeps logs for the retention period it sets', () => {
|
|
19
|
+
var _a, _b, _c;
|
|
20
|
+
(0, chai_1.expect)(config.limits_config.retention_period).to.equal('24h');
|
|
21
|
+
(0, chai_1.expect)((_a = config.compactor) === null || _a === void 0 ? void 0 : _a.retention_enabled, 'compactor.retention_enabled').to.equal(true);
|
|
22
|
+
(0, chai_1.expect)((_b = config.compactor) === null || _b === void 0 ? void 0 : _b.delete_request_store, 'compactor.delete_request_store').to.equal(config.schema_config.configs[0].object_store);
|
|
23
|
+
(0, chai_1.expect)((_c = config.compactor) === null || _c === void 0 ? void 0 : _c.working_directory, 'compactor.working_directory').to.be.a('string');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
// scripts/ is plain CommonJS that the export script also loads, so it is
|
|
5
|
+
// required rather than imported.
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
7
|
+
const site = require('../../scripts/lib/openapi-for-site');
|
|
8
|
+
const { forSite, VERSION_TOKEN, assertNoRawDocumentLink } = site;
|
|
9
|
+
/**
|
|
10
|
+
* The documentation site publishes the server's OpenAPI document. The spec
|
|
11
|
+
* names the version it was built from in `info`, so a committed copy would
|
|
12
|
+
* change on every release. forSite swaps that version for a token the site's
|
|
13
|
+
* build fills in, and points the "raw document" link at the user's own server,
|
|
14
|
+
* since the link would otherwise lead to the docs host.
|
|
15
|
+
*/
|
|
16
|
+
describe('openapi for the documentation site', () => {
|
|
17
|
+
const version = '2.13.1';
|
|
18
|
+
const makeSpec = () => ({
|
|
19
|
+
info: {
|
|
20
|
+
version,
|
|
21
|
+
description: `This reference describes version **${version}**. The raw OpenAPI document is at [\`/xenon/api-docs.json\`](/xenon/api-docs.json).`,
|
|
22
|
+
},
|
|
23
|
+
paths: { '/api/devices': {} },
|
|
24
|
+
});
|
|
25
|
+
it('puts the version token where the version was', () => {
|
|
26
|
+
const result = forSite(makeSpec(), version);
|
|
27
|
+
(0, chai_1.expect)(result.info.version).to.equal('__XENON_VERSION__');
|
|
28
|
+
(0, chai_1.expect)(VERSION_TOKEN).to.equal('__XENON_VERSION__');
|
|
29
|
+
(0, chai_1.expect)(result.info.description).to.contain('version **__XENON_VERSION__**');
|
|
30
|
+
});
|
|
31
|
+
it('leaves no trace of the version anywhere in the result', () => {
|
|
32
|
+
(0, chai_1.expect)(JSON.stringify(forSite(makeSpec(), version))).to.not.contain(version);
|
|
33
|
+
});
|
|
34
|
+
it('points the raw-document line at the reader’s own server, with no link', () => {
|
|
35
|
+
const { description } = forSite(makeSpec(), version).info;
|
|
36
|
+
(0, chai_1.expect)(description).to.contain('`/xenon/api-docs.json` on your server');
|
|
37
|
+
(0, chai_1.expect)(description).to.not.contain('](/xenon/api-docs.json)');
|
|
38
|
+
});
|
|
39
|
+
it('keeps the paths exactly as they were', () => {
|
|
40
|
+
(0, chai_1.expect)(forSite(makeSpec(), version).paths).to.deep.equal(makeSpec().paths);
|
|
41
|
+
});
|
|
42
|
+
it('does not change the object it is given', () => {
|
|
43
|
+
const spec = makeSpec();
|
|
44
|
+
forSite(spec, version);
|
|
45
|
+
(0, chai_1.expect)(spec).to.deep.equal(makeSpec());
|
|
46
|
+
});
|
|
47
|
+
it('refuses an empty or missing version', () => {
|
|
48
|
+
(0, chai_1.expect)(() => forSite(makeSpec(), '')).to.throw(/version/);
|
|
49
|
+
(0, chai_1.expect)(() => forSite(makeSpec(), undefined)).to.throw(/version/);
|
|
50
|
+
(0, chai_1.expect)(() => forSite(makeSpec(), 2.13)).to.throw(/version/);
|
|
51
|
+
});
|
|
52
|
+
it('only touches the version inside info', () => {
|
|
53
|
+
const spec = makeSpec();
|
|
54
|
+
spec.paths['/api/version'] = { get: { example: { pluginVersion: version } } };
|
|
55
|
+
const result = forSite(spec, version);
|
|
56
|
+
(0, chai_1.expect)(result.paths['/api/version'].get.example.pluginVersion).to.equal(version);
|
|
57
|
+
});
|
|
58
|
+
describe('the guard against a link to the raw document', () => {
|
|
59
|
+
const guarded = (description) => {
|
|
60
|
+
const spec = makeSpec();
|
|
61
|
+
spec.info.description = description;
|
|
62
|
+
return JSON.stringify(forSite(spec, version), null, 2);
|
|
63
|
+
};
|
|
64
|
+
it('passes the introduction as the server writes it, once forSite has rewritten the link', () => {
|
|
65
|
+
(0, chai_1.expect)(() => assertNoRawDocumentLink(guarded(makeSpec().info.description))).to.not.throw();
|
|
66
|
+
});
|
|
67
|
+
it('catches the link when the introduction is reworded', () => {
|
|
68
|
+
const text = guarded('Download the [raw document](/xenon/api-docs.json).');
|
|
69
|
+
(0, chai_1.expect)(() => assertNoRawDocumentLink(text)).to.throw(/still links to \/xenon\/api-docs\.json[\s\S]*src\/app\/swagger\.ts/);
|
|
70
|
+
});
|
|
71
|
+
it('catches a link that has a space, an angle bracket or a fragment in its target', () => {
|
|
72
|
+
(0, chai_1.expect)(() => assertNoRawDocumentLink('a [b]( /xenon/api-docs.json)')).to.throw();
|
|
73
|
+
(0, chai_1.expect)(() => assertNoRawDocumentLink('a [b](</xenon/api-docs.json>)')).to.throw();
|
|
74
|
+
(0, chai_1.expect)(() => assertNoRawDocumentLink('a [b](/xenon/api-docs.json#info "title")')).to.throw();
|
|
75
|
+
});
|
|
76
|
+
it('lets plain code and other links through', () => {
|
|
77
|
+
(0, chai_1.expect)(() => assertNoRawDocumentLink('at `/xenon/api-docs.json` on your server, see [the profile](/xenon/profile)')).to.not.throw();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|