@xenon-device-management/xenon 2.13.2 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BXruClDk.js → AnnotationOverlay-B_VNtKBH.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-qTS4LPLC.js → ApiKeyGate-BwuaMjkX.js} +1 -1
- package/lib/public/assets/{BugReportButton-BoGhlI7F.js → BugReportButton-BAt0_pzq.js} +1 -1
- package/lib/public/assets/DeviceMosaicView-BYaQHhTf.js +2 -0
- package/lib/public/assets/{EmptyState-Cr17PdDv.js → EmptyState-BSjhk_IV.js} +1 -1
- package/lib/public/assets/{FieldGroup-Bvn08svv.js → FieldGroup-Cb5QZsE1.js} +1 -1
- package/lib/public/assets/{FilterMenu-DDSO8aDf.js → FilterMenu-BAaSlWbW.js} +1 -1
- package/lib/public/assets/{Menu-B-Qn2X3T.js → Menu-JGGYrSQQ.js} +1 -1
- package/lib/public/assets/{Modal-Cg9V5JYU.js → Modal-CFCp3n9d.js} +1 -1
- package/lib/public/assets/{RecordingPage-BTUs9VTW.js → RecordingPage-CbJj_EBT.js} +1 -1
- package/lib/public/assets/{RecordingsPage-DYcfxITK.js → RecordingsPage-BJeYnygR.js} +2 -2
- package/lib/public/assets/{SegmentedControl-Zy7suhT5.js → SegmentedControl-Bg42Atjy.js} +1 -1
- package/lib/public/assets/{SettingCard-CWYmycoe.js → SettingCard-Njz1pUIJ.js} +1 -1
- package/lib/public/assets/{Table-io9fwa98.js → Table-BcTm5xV2.js} +1 -1
- package/lib/public/assets/{activity-Bdf_uIWl.js → activity-DOVdsfMQ.js} +1 -1
- package/lib/public/assets/{ai-settings-Bce5SJob.js → ai-settings-CsuJroRf.js} +2 -2
- package/lib/public/assets/{api-keys-CPqtVpm3.js → api-keys-BKnNSTw9.js} +1 -1
- package/lib/public/assets/{apps-BgZan0H9.js → apps-D0SC6uw6.js} +2 -2
- package/lib/public/assets/{arrow-left-B27ckB31.js → arrow-left-MMbTmgoE.js} +1 -1
- package/lib/public/assets/{arrow-right-DOF8ITCz.js → arrow-right-BMfnGmxW.js} +1 -1
- package/lib/public/assets/{arrow-up-right-eFG5KTFS.js → arrow-up-right-DuAftMWB.js} +1 -1
- package/lib/public/assets/{auth-shell-DlC3_27j.js → auth-shell-nZC2JNDO.js} +2 -2
- package/lib/public/assets/{builds-page-BuXpPfKo.js → builds-page-hDCgTwkS.js} +1 -1
- package/lib/public/assets/{button-fIW8owMI.js → button-Dda4irn_.js} +1 -1
- package/lib/public/assets/{calendar-WNjmrF-a.js → calendar-BMr0Q62v.js} +1 -1
- package/lib/public/assets/{check-BdTh1Sq7.js → check-CZ9iw9-e.js} +1 -1
- package/lib/public/assets/{chevron-right-B4BKlsqk.js → chevron-right-BRYJJVC9.js} +1 -1
- package/lib/public/assets/{circle-check-zJU80_EF.js → circle-check-CqAJjQ5O.js} +1 -1
- package/lib/public/assets/{circle-x-5Uwquo0h.js → circle-x-DlDJtnVz.js} +1 -1
- package/lib/public/assets/{clock-BtUprDsZ.js → clock-CpuDKysU.js} +1 -1
- package/lib/public/assets/{copy-DGSbZI3u.js → copy-CAQydNFe.js} +1 -1
- package/lib/public/assets/device-explorer-CdO6_nrn.js +282 -0
- package/lib/public/assets/{download-WG6l7BgD.js → download-C9XG6lLv.js} +1 -1
- package/lib/public/assets/{forgot-password-DnudqFFo.js → forgot-password-CGnkRgE9.js} +1 -1
- package/lib/public/assets/h264Stream-VQBphYhg.js +26 -0
- package/lib/public/assets/{index-DI1fGIyj.js → index-BbKEXUFH.js} +5 -5
- package/lib/public/assets/{index-HDQVe1gi.js → index-DJGc1M2f.js} +1 -1
- package/lib/public/assets/{input-BjDRKCea.js → input-Cm0tmSoc.js} +1 -1
- package/lib/public/assets/{line-chart-C0zdgd0v.js → line-chart-D2DyJCml.js} +1 -1
- package/lib/public/assets/{list-checks-BdW5CjM8.js → list-checks-UWf1GY99.js} +1 -1
- package/lib/public/assets/{lock-Dy3L4I8E.js → lock-CCJv9m_I.js} +1 -1
- package/lib/public/assets/{login-BHbY9CLG.js → login-f8vOVW-U.js} +1 -1
- package/lib/public/assets/maintenance-settings-C1PrkuAH.js +6 -0
- package/lib/public/assets/{monitor-8xjkeHHi.js → monitor-B2jDgwm7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CXdKMXQ8.js → mouse-pointer-2-DLI1RtzU.js} +1 -1
- package/lib/public/assets/{network-Cj8JOKON.js → network-ebT86qoi.js} +1 -1
- package/lib/public/assets/{overview-CTg7F0im.js → overview-C0aykVrd.js} +1 -1
- package/lib/public/assets/{page-header--aTyJXq6.js → page-header-BjzgWz-T.js} +1 -1
- package/lib/public/assets/{play-B-FodgHV.js → play-DLG9t8FU.js} +1 -1
- package/lib/public/assets/{plus-CsIk10Cq.js → plus-nBFSMu_X.js} +1 -1
- package/lib/public/assets/{profile-page-DkFKkMfl.js → profile-page-CSl0UYlv.js} +1 -1
- package/lib/public/assets/{reset-password-I2-zqLlI.js → reset-password-LeOuHqmB.js} +1 -1
- package/lib/public/assets/{runbook-page-DJTVCJX7.js → runbook-page-CMMeQml9.js} +1 -1
- package/lib/public/assets/{select-DvCSv25z.js → select-88jHcl-Z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BLVfMJs_.js → selector-detail-redirect-BbXKJjZM.js} +1 -1
- package/lib/public/assets/{selector-health-page-oqadRokn.js → selector-health-page-B4rA1Xkv.js} +2 -2
- package/lib/public/assets/session-detail-page-COL_RZxw.js +4 -0
- package/lib/public/assets/settings-GF6ZMSQ6.js +6 -0
- package/lib/public/assets/{stat-tile-DV9Ix-4O.js → stat-tile-BV98e_Sv.js} +1 -1
- package/lib/public/assets/{tablet-6b_SEN2X.js → tablet-BiAsnFDj.js} +1 -1
- package/lib/public/assets/{teams-BjZyfuHh.js → teams-B4-jrr-1.js} +1 -1
- package/lib/public/assets/{trash-2-CTZpPSdd.js → trash-2-C5RSuX0e.js} +1 -1
- package/lib/public/assets/{upload-P3XzwQUb.js → upload-BG7_rMXK.js} +1 -1
- package/lib/public/assets/{use-builds-data-1cOCMf-z.js → use-builds-data-J-riQYyC.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DgF9w088.js → use-password-reset-mode-C8-eH-UG.js} +1 -1
- package/lib/public/assets/{users-DMF9Mk5H.js → users-CFba5TSy.js} +1 -1
- package/lib/public/assets/{users-CiNTKUiz.js → users-Cp0beJm3.js} +1 -1
- package/lib/public/assets/{video-off-CHp1KU_T.js → video-off-DDsEISjV.js} +1 -1
- package/lib/public/assets/webhook-settings-BGu32vSB.js +1 -0
- package/lib/public/assets/{zap-DRz0tDq5.js → zap-l4VaXzq8.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +19 -21
- package/lib/src/XenonCapabilityManager.js +22 -5
- package/lib/src/app/openapi/control.yaml +71 -20
- package/lib/src/app/openapi/grid.yaml +23 -5
- package/lib/src/app/openapi/platform.yaml +136 -28
- package/lib/src/app/routers/control.js +12 -2
- package/lib/src/app/routers/dashboard.js +29 -1
- package/lib/src/app/routers/grid.js +32 -10
- package/lib/src/app/routers/streamType.js +7 -2
- package/lib/src/app/routers/webhook.js +8 -4
- package/lib/src/config.js +50 -5
- package/lib/src/dashboard/commands.js +185 -141
- package/lib/src/dashboard/event-manager.js +39 -6
- package/lib/src/data-service/appleFamily.js +43 -0
- package/lib/src/data-service/db.js +11 -0
- package/lib/src/data-service/device-service.js +6 -1
- package/lib/src/data-service/device-store.js +62 -2
- package/lib/src/data-service/deviceClaims.js +37 -0
- package/lib/src/data-service/deviceFieldOwners.js +25 -11
- package/lib/src/data-service/deviceSettings.js +79 -0
- package/lib/src/data-service/prisma-store.js +150 -8
- package/lib/src/data-service/web-config-service.js +43 -92
- package/lib/src/device-managers/AndroidDeviceManager.js +9 -31
- package/lib/src/device-managers/ios/WDAClient.js +44 -43
- package/lib/src/device-managers/shellCommands.js +96 -0
- package/lib/src/device-utils.js +96 -40
- package/lib/src/generated/client/edge.js +17 -3
- package/lib/src/generated/client/index-browser.js +14 -0
- package/lib/src/generated/client/index.d.ts +2054 -379
- package/lib/src/generated/client/index.js +17 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +35 -7
- package/lib/src/generated/client/wasm.js +14 -0
- package/lib/src/interceptors/CommandInterceptor.js +29 -8
- package/lib/src/interceptors/xenonScripts.js +55 -0
- package/lib/src/interfaces/IPluginArgs.js +1 -2
- package/lib/src/plugin.js +15 -1
- package/lib/src/prisma.js +1 -1
- package/lib/src/scripts/database-check.js +124 -0
- package/lib/src/services/AIService.js +99 -0
- package/lib/src/services/CleanupService.js +6 -1
- package/lib/src/services/InterceptorService.js +70 -13
- package/lib/src/services/NetworkConditioningService.js +85 -88
- package/lib/src/services/NotificationService.js +36 -29
- package/lib/src/services/OrphanSweeper.js +5 -0
- package/lib/src/services/PortAllocator.js +5 -0
- package/lib/src/services/ServerManager.js +94 -8
- package/lib/src/services/SessionLifecycleService.js +47 -49
- package/lib/src/services/ShutdownCoordinator.js +33 -0
- package/lib/src/services/TeamService.js +20 -3
- package/lib/src/services/healing/HealingOrchestrator.js +32 -1
- package/lib/src/services/interceptor/AndroidProxyAdapter.js +18 -0
- package/lib/src/services/interceptor/interceptorOptions.js +44 -0
- package/lib/src/services/network/AndroidRadios.js +71 -0
- package/lib/src/services/network/PhoneNetworkLedger.js +184 -0
- package/lib/src/services/network/PhoneNetworkRestore.js +330 -0
- package/lib/src/services/network/adbForPhone.js +60 -0
- package/lib/src/services/network/phoneNetworkLock.js +20 -0
- package/lib/src/services/network/xenonProxy.js +107 -0
- package/lib/src/services/omni-vision/OmniVisionService.js +53 -43
- package/lib/src/services/omni-vision/VisionAssertionService.js +28 -25
- package/lib/src/services/omni-vision/ocrTextMatch.js +105 -0
- package/lib/src/services/omni-vision/screenScale.js +68 -0
- package/lib/src/services/recording/concurrency-gate.js +12 -3
- package/lib/src/services/settings/SelfHealingSwitch.js +70 -0
- package/lib/src/services/settings/labSettings.js +149 -0
- package/lib/src/services/webhookEvents.js +173 -0
- package/lib/test/helpers/fake-adb.js +58 -0
- package/lib/test/helpers/fake-device-table.js +63 -24
- package/lib/test/helpers/scratch-database.js +1 -0
- package/lib/test/unit/TeamService.test.js +3 -0
- package/lib/test/unit/XenonCapabilityManager.spec.js +4 -2
- package/lib/test/unit/apple-family.spec.js +36 -0
- package/lib/test/unit/cleanup-settings-db.spec.js +103 -0
- package/lib/test/unit/cleanup-settings.spec.js +244 -0
- package/lib/test/unit/dashboard-api-paths.spec.js +101 -0
- package/lib/test/unit/dashboard-commands.spec.js +249 -0
- package/lib/test/unit/dashboard-events-team-scope.spec.js +9 -6
- package/lib/test/unit/database-check.spec.js +108 -0
- package/lib/test/unit/default-plugin-args.spec.js +33 -0
- package/lib/test/unit/device-offline-webhook.spec.js +81 -0
- package/lib/test/unit/device-setting-columns.spec.js +66 -0
- package/lib/test/unit/device-settings-loki.spec.js +87 -0
- package/lib/test/unit/device-settings-survive.spec.js +555 -0
- package/lib/test/unit/device-sync-lost-update.spec.js +5 -0
- package/lib/test/unit/emulator-boot.spec.js +91 -0
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +48 -0
- package/lib/test/unit/hub-node-same-host.spec.js +5 -0
- package/lib/test/unit/iphone-ipad-only.spec.js +132 -0
- package/lib/test/unit/json-logging-option.spec.js +74 -0
- package/lib/test/unit/lab-settings.spec.js +218 -0
- package/lib/test/unit/max-sessions.spec.js +240 -0
- package/lib/test/unit/ocr-text-match.spec.js +44 -0
- package/lib/test/unit/omni-vision-analyze-screen.spec.js +72 -0
- package/lib/test/unit/omni-vision-ios-points.spec.js +161 -0
- package/lib/test/unit/omni-vision-tap-text.spec.js +129 -0
- package/lib/test/unit/omni-vision.spec.js +1 -1
- package/lib/test/unit/partial-config-file.spec.js +14 -0
- package/lib/test/unit/phone-network/boot-clean-up.spec.js +64 -0
- package/lib/test/unit/phone-network/interceptor-options.spec.js +77 -0
- package/lib/test/unit/phone-network/interceptor-proxy-restore.spec.js +145 -0
- package/lib/test/unit/phone-network/network-conditioning.spec.js +145 -0
- package/lib/test/unit/phone-network/phone-network-restore.spec.js +246 -0
- package/lib/test/unit/phone-network/session-end-paths.spec.js +182 -0
- package/lib/test/unit/phone-network/session-start.spec.js +110 -0
- package/lib/test/unit/phone-network/xenon-proxy.spec.js +69 -0
- package/lib/test/unit/plugin.spec.js +3 -3
- package/lib/test/unit/recording-options.spec.js +181 -0
- package/lib/test/unit/self-healing-boot.spec.js +75 -0
- package/lib/test/unit/self-healing-setting.spec.js +272 -0
- package/lib/test/unit/session-failed-webhook.spec.js +170 -0
- package/lib/test/unit/shell-commands.spec.js +133 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +2 -0
- package/lib/test/unit/stream-start-pipelines.spec.js +210 -0
- package/lib/test/unit/stream-status-type.spec.js +4 -0
- package/lib/test/unit/vision-assertion.spec.js +118 -0
- package/lib/test/unit/wda-client-shell.spec.js +133 -0
- package/lib/test/unit/web-config-service.spec.js +79 -0
- package/lib/test/unit/webhook-delivery.spec.js +24 -0
- package/lib/test/unit/webhook-payloads.spec.js +273 -0
- package/lib/test/unit/xenon-execute-scripts.spec.js +138 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/web/src/components/webhook-settings/webhookEventVariables.json +30 -0
- package/package.json +1 -1
- package/prisma/migrations/20261004140000_keep_device_settings/migration.sql +19 -0
- package/prisma/schema.prisma +35 -7
- package/schema.json +19 -21
- package/scripts/generate-types-from-schema.js +1 -2
- package/scripts/lib/always-present-plugin-args.js +4 -1
- package/lib/public/assets/DeviceMosaicView-8J0oYqMM.js +0 -2
- package/lib/public/assets/device-explorer-C-KnhMHI.js +0 -279
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +0 -6
- package/lib/public/assets/recording-group-store-WVM3hoXn.js +0 -26
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +0 -4
- package/lib/public/assets/settings-MsVU2ukE.js +0 -6
- package/lib/public/assets/webhook-settings-DiS8geIm.js +0 -1
|
@@ -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 fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const typedi_1 = require("typedi");
|
|
22
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
23
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
24
|
+
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
25
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
26
|
+
const config_1 = require("../../src/config");
|
|
27
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
28
|
+
const device_utils_1 = require("../../src/device-utils");
|
|
29
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
30
|
+
/**
|
|
31
|
+
* The Maintenance page's path from end to end, against a real (scratch)
|
|
32
|
+
* database: what POST /config saves is what the next cleanup run acts on,
|
|
33
|
+
* whatever the server was started with. (Saving more than one setting used to
|
|
34
|
+
* fail outright, see web-config-service.spec.ts.)
|
|
35
|
+
*/
|
|
36
|
+
describe('a retention window saved on the Maintenance page', () => {
|
|
37
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
38
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
39
|
+
let context;
|
|
40
|
+
let savedArgs;
|
|
41
|
+
let savedRecordings;
|
|
42
|
+
let tmp;
|
|
43
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
45
|
+
savedArgs = context.pluginArgs;
|
|
46
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { buildCleanupDays: 30, buildCleanupMaxCount: 100 });
|
|
47
|
+
savedRecordings = config_1.config.recordingsAssetsPath;
|
|
48
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'xenon-cleanup-db-'));
|
|
49
|
+
config_1.config.recordingsAssetsPath = path_1.default.join(tmp, 'recordings');
|
|
50
|
+
yield scratch.db.webConfig.deleteMany({});
|
|
51
|
+
yield scratch.db.build.deleteMany({});
|
|
52
|
+
const ago = (days) => new Date(Date.now() - days * DAY_MS);
|
|
53
|
+
yield scratch.db.build.create({ data: { id: 'b-10-days', createdAt: ago(10) } });
|
|
54
|
+
yield scratch.db.build.create({ data: { id: 'b-1-day', createdAt: ago(1) } });
|
|
55
|
+
}));
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
(0, device_utils_1.stopAllTimers)();
|
|
58
|
+
context.pluginArgs = savedArgs;
|
|
59
|
+
config_1.config.recordingsAssetsPath = savedRecordings;
|
|
60
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
function app() {
|
|
63
|
+
const a = (0, express_1.default)();
|
|
64
|
+
a.use(express_1.default.json());
|
|
65
|
+
a.use((req, _res, next) => {
|
|
66
|
+
req.auth = {
|
|
67
|
+
kind: 'user-session',
|
|
68
|
+
userId: 'usr_x',
|
|
69
|
+
role: 'SUPER_ADMIN',
|
|
70
|
+
scopes: 'admin,devices,sessions,read',
|
|
71
|
+
rateLimit: 1000,
|
|
72
|
+
};
|
|
73
|
+
next();
|
|
74
|
+
});
|
|
75
|
+
dashboard_1.default.register(a);
|
|
76
|
+
return a;
|
|
77
|
+
}
|
|
78
|
+
const buildIds = () => __awaiter(void 0, void 0, void 0, function* () { return (yield scratch.db.build.findMany({ select: { id: true } })).map((b) => b.id).sort(); });
|
|
79
|
+
it('is what the next cleanup run purges by, over the 30 days the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
80
|
+
const saved = yield (0, loopbackRequest_1.default)(app())
|
|
81
|
+
.post('/config')
|
|
82
|
+
.send({ buildCleanupDays: 5, buildCleanupMaxCount: 50, deleteBuildAssets: false })
|
|
83
|
+
.timeout(5000);
|
|
84
|
+
(0, chai_1.expect)(saved.status).to.equal(200);
|
|
85
|
+
const shown = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
86
|
+
(0, chai_1.expect)(shown.body).to.include({
|
|
87
|
+
buildCleanupDays: 5,
|
|
88
|
+
buildCleanupMaxCount: 50,
|
|
89
|
+
deleteBuildAssets: false,
|
|
90
|
+
});
|
|
91
|
+
yield typedi_1.Container.get(CleanupService_1.CleanupService).runCleanup(context.pluginArgs);
|
|
92
|
+
(0, chai_1.expect)(yield buildIds()).to.deep.equal(['b-1-day']);
|
|
93
|
+
}));
|
|
94
|
+
it('leaves both builds when nothing was saved and the server started with 30 days', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
yield typedi_1.Container.get(CleanupService_1.CleanupService).runCleanup(context.pluginArgs);
|
|
96
|
+
(0, chai_1.expect)(yield buildIds()).to.deep.equal(['b-1-day', 'b-10-days']);
|
|
97
|
+
}));
|
|
98
|
+
it('purges by a cap saved over the 100 the server started with', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
yield (0, loopbackRequest_1.default)(app()).post('/config').send({ buildCleanupMaxCount: 1 }).timeout(5000);
|
|
100
|
+
yield typedi_1.Container.get(CleanupService_1.CleanupService).runCleanup(context.pluginArgs);
|
|
101
|
+
(0, chai_1.expect)(yield buildIds()).to.deep.equal(['b-1-day']);
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
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 sinon_1 = __importDefault(require("sinon"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const node_schedule_1 = __importDefault(require("node-schedule"));
|
|
21
|
+
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
22
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
23
|
+
const prisma_1 = require("../../src/prisma");
|
|
24
|
+
const CleanupService_1 = require("../../src/services/CleanupService");
|
|
25
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
26
|
+
const PluginContext_1 = require("../../src/PluginContext");
|
|
27
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
28
|
+
const device_utils_1 = require("../../src/device-utils");
|
|
29
|
+
/**
|
|
30
|
+
* The Maintenance page saves the cleanup values (POST /config -> WebConfig),
|
|
31
|
+
* but the cleanup job read only the plugin options the server started with,
|
|
32
|
+
* so a saved retention window, build cap, asset purge or schedule did
|
|
33
|
+
* nothing until it matched what the server was started with, and never took
|
|
34
|
+
* effect at all. These pin the rule that replaced it: a value saved in the
|
|
35
|
+
* dashboard wins over the startup option, applies at the next run without a
|
|
36
|
+
* restart, and a new schedule replaces the old timer at once.
|
|
37
|
+
*/
|
|
38
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
39
|
+
function startupArgs(over = {}) {
|
|
40
|
+
return Object.assign(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { buildCleanupDays: 30, buildCleanupMaxCount: 100, buildCleanupSchedule: '0 0 * * *', deleteBuildAssets: true }), over);
|
|
41
|
+
}
|
|
42
|
+
/** How many days back the first build query looks: the retention window the run used. */
|
|
43
|
+
function daysBack(findMany) {
|
|
44
|
+
const cutoff = findMany.firstCall.args[0].where.createdAt.lt;
|
|
45
|
+
return Math.round((Date.now() - cutoff.getTime()) / DAY_MS);
|
|
46
|
+
}
|
|
47
|
+
/** A WebConfig table in memory: what POST /config saves is what GET reads. */
|
|
48
|
+
function fakeWebConfig(initial = {}) {
|
|
49
|
+
const saved = Object.assign({}, initial);
|
|
50
|
+
const svc = typedi_1.Container.get(web_config_service_1.WebConfigService);
|
|
51
|
+
sinon_1.default.stub(svc, 'getConfig').callsFake(() => __awaiter(this, void 0, void 0, function* () { return (Object.assign({}, saved)); }));
|
|
52
|
+
sinon_1.default.stub(svc, 'setConfig').callsFake((c) => __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
for (const [k, v] of Object.entries(c))
|
|
54
|
+
if (v !== undefined)
|
|
55
|
+
saved[k] = v;
|
|
56
|
+
}));
|
|
57
|
+
return saved;
|
|
58
|
+
}
|
|
59
|
+
describe('cleanup settings saved in the dashboard', () => {
|
|
60
|
+
let context;
|
|
61
|
+
let savedArgs;
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
context = typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
64
|
+
savedArgs = context.pluginArgs;
|
|
65
|
+
});
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
(0, device_utils_1.stopAllTimers)();
|
|
68
|
+
context.pluginArgs = savedArgs;
|
|
69
|
+
sinon_1.default.restore();
|
|
70
|
+
});
|
|
71
|
+
describe('the cleanup run', () => {
|
|
72
|
+
function stubDatabase() {
|
|
73
|
+
const findMany = sinon_1.default.stub(prisma_1.prisma.build, 'findMany').resolves([]);
|
|
74
|
+
sinon_1.default.stub(prisma_1.prisma.build, 'count').resolves(0);
|
|
75
|
+
const service = typedi_1.Container.get(CleanupService_1.CleanupService);
|
|
76
|
+
const purgeBuild = sinon_1.default.stub(service, 'purgeBuild').resolves();
|
|
77
|
+
sinon_1.default.stub(service, 'purgeOrphanSessions').resolves();
|
|
78
|
+
sinon_1.default.stub(service, 'purgeRecordings').resolves();
|
|
79
|
+
return { service, findMany, purgeBuild };
|
|
80
|
+
}
|
|
81
|
+
it('uses the retention window saved in the dashboard, not the startup option', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
fakeWebConfig({ buildCleanupDays: 7 });
|
|
83
|
+
const { service, findMany } = stubDatabase();
|
|
84
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 30 }));
|
|
85
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(7);
|
|
86
|
+
}));
|
|
87
|
+
it('uses the build cap and the asset purge saved in the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
|
+
fakeWebConfig({ buildCleanupMaxCount: 2, deleteBuildAssets: false });
|
|
89
|
+
const { service, findMany, purgeBuild } = stubDatabase();
|
|
90
|
+
prisma_1.prisma.build.count.resolves(3);
|
|
91
|
+
findMany.callsFake(((args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
var _a;
|
|
93
|
+
if ((args === null || args === void 0 ? void 0 : args.take) === 2)
|
|
94
|
+
return [{ id: 'b3' }, { id: 'b2' }];
|
|
95
|
+
if ((_a = args === null || args === void 0 ? void 0 : args.where) === null || _a === void 0 ? void 0 : _a.createdAt)
|
|
96
|
+
return [];
|
|
97
|
+
return [{ id: 'b1' }, { id: 'b2' }, { id: 'b3' }];
|
|
98
|
+
})));
|
|
99
|
+
yield service.runCleanup(startupArgs({ buildCleanupMaxCount: 100, deleteBuildAssets: true }));
|
|
100
|
+
(0, chai_1.expect)(purgeBuild.calledOnce).to.equal(true);
|
|
101
|
+
(0, chai_1.expect)(purgeBuild.firstCall.args).to.deep.equal(['b1', false]);
|
|
102
|
+
}));
|
|
103
|
+
it('falls back to the startup options when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
fakeWebConfig({});
|
|
105
|
+
const { service, findMany } = stubDatabase();
|
|
106
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 12 }));
|
|
107
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(12);
|
|
108
|
+
}));
|
|
109
|
+
it('still runs on the startup options when the saved settings cannot be read', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig').rejects(new Error('db down'));
|
|
111
|
+
const { service, findMany } = stubDatabase();
|
|
112
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 12 }));
|
|
113
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(12);
|
|
114
|
+
}));
|
|
115
|
+
it('ignores a saved value that would purge everything', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
116
|
+
fakeWebConfig({ buildCleanupDays: 0, buildCleanupMaxCount: 0 });
|
|
117
|
+
const { service, findMany } = stubDatabase();
|
|
118
|
+
yield service.runCleanup(startupArgs({ buildCleanupDays: 12 }));
|
|
119
|
+
(0, chai_1.expect)(daysBack(findMany)).to.equal(12);
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
describe('the schedule', () => {
|
|
123
|
+
it('starts on the schedule saved in the dashboard', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
fakeWebConfig({ buildCleanupSchedule: '0 3 * * *' });
|
|
125
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
126
|
+
yield (0, device_utils_1.setupCronCleanupBuilds)(startupArgs({ buildCleanupSchedule: '0 0 * * *' }));
|
|
127
|
+
(0, chai_1.expect)(scheduleJob.firstCall.args[0]).to.equal('0 3 * * *');
|
|
128
|
+
}));
|
|
129
|
+
it('starts on the startup schedule when none was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
fakeWebConfig({});
|
|
131
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
132
|
+
yield (0, device_utils_1.setupCronCleanupBuilds)(startupArgs({ buildCleanupSchedule: '30 1 * * *' }));
|
|
133
|
+
(0, chai_1.expect)(scheduleJob.firstCall.args[0]).to.equal('30 1 * * *');
|
|
134
|
+
}));
|
|
135
|
+
});
|
|
136
|
+
describe('POST /config', () => {
|
|
137
|
+
function app() {
|
|
138
|
+
const a = (0, express_1.default)();
|
|
139
|
+
a.use(express_1.default.json());
|
|
140
|
+
a.use((req, _res, next) => {
|
|
141
|
+
req.auth = {
|
|
142
|
+
kind: 'user-session',
|
|
143
|
+
userId: 'usr_x',
|
|
144
|
+
role: 'SUPER_ADMIN',
|
|
145
|
+
scopes: 'admin,devices,sessions,read',
|
|
146
|
+
rateLimit: 1000,
|
|
147
|
+
};
|
|
148
|
+
next();
|
|
149
|
+
});
|
|
150
|
+
dashboard_1.default.register(a);
|
|
151
|
+
return a;
|
|
152
|
+
}
|
|
153
|
+
it('replaces the running cleanup timer when the schedule changes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
|
+
fakeWebConfig({});
|
|
155
|
+
context.pluginArgs = startupArgs({ buildCleanupSchedule: '0 0 * * *' });
|
|
156
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
157
|
+
yield (0, device_utils_1.setupCronCleanupBuilds)(context.pluginArgs);
|
|
158
|
+
const running = scheduleJob.firstCall.returnValue;
|
|
159
|
+
const cancelRunning = sinon_1.default.spy(running, 'cancel');
|
|
160
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
161
|
+
.post('/config')
|
|
162
|
+
.send({ buildCleanupSchedule: '0 4 * * *' })
|
|
163
|
+
.timeout(5000);
|
|
164
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
165
|
+
(0, chai_1.expect)(cancelRunning.calledOnce, 'the old timer was cancelled').to.equal(true);
|
|
166
|
+
(0, chai_1.expect)(scheduleJob.lastCall.args[0]).to.equal('0 4 * * *');
|
|
167
|
+
(0, chai_1.expect)(scheduleJob.lastCall.returnValue).to.not.equal(running);
|
|
168
|
+
}));
|
|
169
|
+
it('does not touch the timer when the schedule is not in the request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
170
|
+
fakeWebConfig({});
|
|
171
|
+
context.pluginArgs = startupArgs();
|
|
172
|
+
const scheduleJob = sinon_1.default.spy(node_schedule_1.default, 'scheduleJob');
|
|
173
|
+
const r = yield (0, loopbackRequest_1.default)(app()).post('/config').send({ buildCleanupDays: 14 }).timeout(5000);
|
|
174
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
175
|
+
(0, chai_1.expect)(scheduleJob.called).to.equal(false);
|
|
176
|
+
}));
|
|
177
|
+
for (const [label, body] of [
|
|
178
|
+
['a retention window of zero', { buildCleanupDays: 0 }],
|
|
179
|
+
['a negative retention window', { buildCleanupDays: -3 }],
|
|
180
|
+
['a fractional build cap', { buildCleanupMaxCount: 2.5 }],
|
|
181
|
+
['a build cap that is not a number', { buildCleanupMaxCount: null }],
|
|
182
|
+
['a schedule that is not a cron expression', { buildCleanupSchedule: 'every night' }],
|
|
183
|
+
['an asset purge that is not a boolean', { deleteBuildAssets: 'yes' }],
|
|
184
|
+
]) {
|
|
185
|
+
it(`refuses ${label} and saves nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
const saved = fakeWebConfig({});
|
|
187
|
+
context.pluginArgs = startupArgs();
|
|
188
|
+
const r = yield (0, loopbackRequest_1.default)(app()).post('/config').send(body).timeout(5000);
|
|
189
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
190
|
+
(0, chai_1.expect)(r.body.error).to.equal('invalid_setting');
|
|
191
|
+
(0, chai_1.expect)(r.body.message).to.be.a('string').and.not.equal('');
|
|
192
|
+
(0, chai_1.expect)(saved).to.deep.equal({});
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
describe('GET /config', () => {
|
|
197
|
+
function app() {
|
|
198
|
+
const a = (0, express_1.default)();
|
|
199
|
+
a.use((req, _res, next) => {
|
|
200
|
+
req.auth = {
|
|
201
|
+
kind: 'user-session',
|
|
202
|
+
userId: 'usr_x',
|
|
203
|
+
role: 'ADMIN',
|
|
204
|
+
scopes: 'admin,read',
|
|
205
|
+
rateLimit: 1000,
|
|
206
|
+
};
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
209
|
+
dashboard_1.default.register(a);
|
|
210
|
+
return a;
|
|
211
|
+
}
|
|
212
|
+
it('shows the value the server runs with when nothing was saved', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
213
|
+
fakeWebConfig({});
|
|
214
|
+
context.pluginArgs = startupArgs({
|
|
215
|
+
buildCleanupDays: 12,
|
|
216
|
+
buildCleanupSchedule: '30 1 * * *',
|
|
217
|
+
});
|
|
218
|
+
const r = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
219
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
220
|
+
(0, chai_1.expect)(r.body.buildCleanupDays).to.equal(12);
|
|
221
|
+
(0, chai_1.expect)(r.body.buildCleanupSchedule).to.equal('30 1 * * *');
|
|
222
|
+
}));
|
|
223
|
+
it('shows the saved value over the startup one', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
224
|
+
fakeWebConfig({ buildCleanupDays: 7 });
|
|
225
|
+
context.pluginArgs = startupArgs({ buildCleanupDays: 12 });
|
|
226
|
+
const r = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
227
|
+
(0, chai_1.expect)(r.body.buildCleanupDays).to.equal(7);
|
|
228
|
+
}));
|
|
229
|
+
it("sends the server's own defaults, so no page needs a number of its own", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
230
|
+
fakeWebConfig({});
|
|
231
|
+
context.pluginArgs = startupArgs();
|
|
232
|
+
const r = yield (0, loopbackRequest_1.default)(app()).get('/config').timeout(5000);
|
|
233
|
+
(0, chai_1.expect)(r.body.defaults).to.deep.equal({
|
|
234
|
+
enableSelfHealing: true,
|
|
235
|
+
healthCheckIntervalMs: 300000,
|
|
236
|
+
buildCleanupDays: 30,
|
|
237
|
+
buildCleanupMaxCount: 100,
|
|
238
|
+
buildCleanupSchedule: '0 0 * * *',
|
|
239
|
+
deleteBuildAssets: true,
|
|
240
|
+
});
|
|
241
|
+
(0, chai_1.expect)(r.body.healthCheckIntervalMs).to.equal(300000);
|
|
242
|
+
}));
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
require("reflect-metadata");
|
|
7
|
+
const chai_1 = require("chai");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const index_1 = require("../../src/app/index");
|
|
11
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
12
|
+
const expressRoutes_1 = require("../helpers/expressRoutes");
|
|
13
|
+
/**
|
|
14
|
+
* Every address the dashboard calls must be one the server serves.
|
|
15
|
+
*
|
|
16
|
+
* "Assign team" called `PUT /xenon/api/grid/device/:udid/team` while the
|
|
17
|
+
* server mounts that route at the API root (`/xenon/api/device/:udid/team`).
|
|
18
|
+
* Nothing mounts `/grid`, so every assignment from the Devices page was a
|
|
19
|
+
* 404, and the only test mocked the whole api-service, so it never saw it.
|
|
20
|
+
*
|
|
21
|
+
* This reads the paths the dashboard's source writes down (`/xenon/api/...`
|
|
22
|
+
* literals and the `makeGETRequest('/...')` family, which the api client
|
|
23
|
+
* prefixes with `/xenon/api`) and checks each against the routes the router
|
|
24
|
+
* really serves.
|
|
25
|
+
*/
|
|
26
|
+
const WEB_SRC = path_1.default.resolve(__dirname, '../../web/src');
|
|
27
|
+
const API = '/xenon/api';
|
|
28
|
+
/** "/xenon/api/control/:udid/tap" and "/xenon/api/control/${udid}/tap" both -> ".../control/{}/tap" */
|
|
29
|
+
const shape = (p) => p
|
|
30
|
+
.replace(/\$\{[^}]*\}/g, '{}')
|
|
31
|
+
.replace(/:[A-Za-z_]\w*/g, '{}')
|
|
32
|
+
.replace(/\/+$/, '');
|
|
33
|
+
// Addresses written in the dashboard that are not served, each with why.
|
|
34
|
+
const KNOWN_NOT_SERVED = {
|
|
35
|
+
[`${API}/session/{}/xenon/omni-scan`]: 'XenonApiService.omniScan: an Appium plugin route, not under /xenon/api; no caller',
|
|
36
|
+
[`${API}/session/{}/xenon/test-locator`]: 'XenonApiService.testAiLocator: an Appium plugin route, not under /xenon/api; no caller',
|
|
37
|
+
[`${API}/sessions`]: "ApiKeyGate's sign-in probe: only the 401 matters, and the login check answers before routing",
|
|
38
|
+
};
|
|
39
|
+
// Written as a base that the same file extends (`const BASE = '/xenon/api/auth'`,
|
|
40
|
+
// then `${BASE}/me`), so the base alone is not a route.
|
|
41
|
+
const BASES = [`${API}/auth`, `${API}/interceptor`, `${API}/profile`];
|
|
42
|
+
function sources(dir, out = []) {
|
|
43
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
44
|
+
const full = path_1.default.join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
if (entry.name !== 'node_modules')
|
|
47
|
+
sources(full, out);
|
|
48
|
+
}
|
|
49
|
+
else if (/\.tsx?$/.test(entry.name) && !/\.(test|spec)\.tsx?$/.test(entry.name)) {
|
|
50
|
+
out.push(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Each address a source file writes down, with the file it is in. */
|
|
56
|
+
function dashboardAddresses() {
|
|
57
|
+
const found = [];
|
|
58
|
+
for (const file of sources(WEB_SRC)) {
|
|
59
|
+
const text = fs_1.default.readFileSync(file, 'utf8');
|
|
60
|
+
const rel = path_1.default.relative(WEB_SRC, file);
|
|
61
|
+
// '/xenon/api/...' in a string or template; not '/xenon/api-docs', nor the
|
|
62
|
+
// api client's bare `/xenon/api${url}` prefix.
|
|
63
|
+
for (const m of text.matchAll(/['"`]\/xenon\/api(\/[^'"`\s?]*)/g)) {
|
|
64
|
+
found.push({ address: `${API}${m[1]}`, file: rel });
|
|
65
|
+
}
|
|
66
|
+
// apiClient.makeGETRequest('/queue') -> the client adds the prefix.
|
|
67
|
+
for (const m of text.matchAll(/make(?:GET|POST|PUT|DELETE|PATCH)Request\(\s*['"`](\/[^'"`?]*)/g)) {
|
|
68
|
+
found.push({ address: `${API}${m[1]}`, file: rel });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
describe('the addresses the dashboard calls', () => {
|
|
74
|
+
const served = [
|
|
75
|
+
...(0, expressRoutes_1.routesOf)((0, index_1.createRouter)(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: 'http://hub.example:4723' })), '/xenon'),
|
|
76
|
+
]
|
|
77
|
+
.filter((r) => r.includes(' /xenon/api'))
|
|
78
|
+
.map((r) => shape(r.slice(r.indexOf(' ') + 1)));
|
|
79
|
+
const isServed = (address) => served.includes(shape(address));
|
|
80
|
+
const isBase = (address) => BASES.includes(shape(address));
|
|
81
|
+
const underBase = (base) => served.some((s) => s.startsWith(`${base}/`));
|
|
82
|
+
it("finds the dashboard's addresses (the scan is not reading nothing)", () => {
|
|
83
|
+
const all = dashboardAddresses();
|
|
84
|
+
(0, chai_1.expect)(all.length).to.be.greaterThan(60);
|
|
85
|
+
(0, chai_1.expect)(all.map((a) => shape(a.address))).to.include(`${API}/device/{}/team`);
|
|
86
|
+
});
|
|
87
|
+
it('are all served by the API router', () => {
|
|
88
|
+
const missing = dashboardAddresses()
|
|
89
|
+
.filter(({ address }) => !isServed(address) && !isBase(address) && !(shape(address) in KNOWN_NOT_SERVED))
|
|
90
|
+
.map(({ address, file }) => `${address} (web/src/${file})`);
|
|
91
|
+
(0, chai_1.expect)([...new Set(missing)].sort(), 'called by the dashboard but not served').to.deep.equal([]);
|
|
92
|
+
});
|
|
93
|
+
it('has no allowance for an address that is now served or no longer written', () => {
|
|
94
|
+
const written = new Set(dashboardAddresses().map(({ address }) => shape(address)));
|
|
95
|
+
const stale = [
|
|
96
|
+
...Object.keys(KNOWN_NOT_SERVED).filter((a) => isServed(a) || !written.has(a)),
|
|
97
|
+
...BASES.filter((b) => !written.has(b) || !underBase(b)),
|
|
98
|
+
];
|
|
99
|
+
(0, chai_1.expect)(stale, 'remove these from KNOWN_NOT_SERVED / BASES').to.deep.equal([]);
|
|
100
|
+
});
|
|
101
|
+
});
|