@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,246 @@
|
|
|
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 net_1 = __importDefault(require("net"));
|
|
19
|
+
const typedi_1 = require("typedi");
|
|
20
|
+
const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
|
|
21
|
+
const PhoneNetworkLedger_1 = require("../../../src/services/network/PhoneNetworkLedger");
|
|
22
|
+
const NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
|
|
23
|
+
const InterceptorService_1 = require("../../../src/services/InterceptorService");
|
|
24
|
+
const PortAllocator_1 = require("../../../src/services/PortAllocator");
|
|
25
|
+
const prisma_1 = require("../../../src/prisma");
|
|
26
|
+
const container_registration_1 = require("../../helpers/container-registration");
|
|
27
|
+
const scratch_database_1 = require("../../helpers/scratch-database");
|
|
28
|
+
const fake_adb_1 = require("../../helpers/fake-adb");
|
|
29
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
30
|
+
function deferred() {
|
|
31
|
+
let resolve;
|
|
32
|
+
const promise = new Promise((r) => (resolve = r));
|
|
33
|
+
return { promise, resolve };
|
|
34
|
+
}
|
|
35
|
+
/** Stand-ins for the two services that change a phone's network for a session. */
|
|
36
|
+
function fakeServices(live) {
|
|
37
|
+
const net = {
|
|
38
|
+
has: (id) => live.net.has(id),
|
|
39
|
+
sessionIds: () => [...live.net],
|
|
40
|
+
reset: sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () { return void live.net.delete(id); })),
|
|
41
|
+
};
|
|
42
|
+
const icp = {
|
|
43
|
+
isActive: (id) => live.icp.has(id),
|
|
44
|
+
sessionIds: () => [...live.icp],
|
|
45
|
+
stop: sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () { return void live.icp.delete(id); })),
|
|
46
|
+
};
|
|
47
|
+
return { net, icp };
|
|
48
|
+
}
|
|
49
|
+
describe('PhoneNetworkRestore', function () {
|
|
50
|
+
this.timeout(60000);
|
|
51
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
52
|
+
let adb;
|
|
53
|
+
(0, fake_adb_1.useFakeAdb)(() => adb);
|
|
54
|
+
let restoreRegs;
|
|
55
|
+
let live;
|
|
56
|
+
let fakes;
|
|
57
|
+
let ledger;
|
|
58
|
+
let range;
|
|
59
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
adb = (0, fake_adb_1.fakeAdb)();
|
|
61
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(NetworkConditioningService_1.NetworkConditioningService, InterceptorService_1.InterceptorService, PortAllocator_1.PortAllocator, PhoneNetworkLedger_1.PhoneNetworkLedger);
|
|
62
|
+
live = { net: new Set(), icp: new Set() };
|
|
63
|
+
fakes = fakeServices(live);
|
|
64
|
+
typedi_1.Container.set(NetworkConditioningService_1.NetworkConditioningService, fakes.net);
|
|
65
|
+
typedi_1.Container.set(InterceptorService_1.InterceptorService, fakes.icp);
|
|
66
|
+
range = [11100, 11199];
|
|
67
|
+
typedi_1.Container.set(PortAllocator_1.PortAllocator, { rangeOf: () => range });
|
|
68
|
+
ledger = new PhoneNetworkLedger_1.PhoneNetworkLedger();
|
|
69
|
+
typedi_1.Container.set(PhoneNetworkLedger_1.PhoneNetworkLedger, ledger);
|
|
70
|
+
for (const entry of yield ledger.list())
|
|
71
|
+
yield ledger.forget(entry.sessionId);
|
|
72
|
+
}));
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
sinon_1.default.restore();
|
|
75
|
+
restoreRegs();
|
|
76
|
+
});
|
|
77
|
+
describe('restoreSession', () => {
|
|
78
|
+
it('resets the profile and stops the interceptor of a session', () => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
live.net.add('s1');
|
|
80
|
+
live.icp.add('s1');
|
|
81
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreSession('s1', 'test');
|
|
82
|
+
(0, chai_1.expect)(fakes.net.reset.calledOnceWith('s1')).to.equal(true);
|
|
83
|
+
(0, chai_1.expect)(fakes.icp.stop.calledOnceWith('s1')).to.equal(true);
|
|
84
|
+
}));
|
|
85
|
+
it('does nothing for a session that changed nothing, or no session', () => __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
const restore = new PhoneNetworkRestore_1.PhoneNetworkRestore();
|
|
87
|
+
yield restore.restoreSession('s-plain', 'test');
|
|
88
|
+
yield restore.restoreSession(undefined, 'test');
|
|
89
|
+
yield restore.restoreSession(null, 'test');
|
|
90
|
+
(0, chai_1.expect)(fakes.net.reset.called).to.equal(false);
|
|
91
|
+
(0, chai_1.expect)(fakes.icp.stop.called).to.equal(false);
|
|
92
|
+
}));
|
|
93
|
+
it('runs once when two endings race, and both wait until it is done', () => __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
live.net.add('s1');
|
|
95
|
+
const gate = deferred();
|
|
96
|
+
fakes.net.reset.callsFake((id) => __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
yield gate.promise;
|
|
98
|
+
live.net.delete(id);
|
|
99
|
+
}));
|
|
100
|
+
const restore = new PhoneNetworkRestore_1.PhoneNetworkRestore();
|
|
101
|
+
let firstDone = false;
|
|
102
|
+
let secondDone = false;
|
|
103
|
+
const first = restore.restoreSession('s1', 'deleted').then(() => (firstDone = true));
|
|
104
|
+
const second = restore.restoreSession('s1', 'idle').then(() => (secondDone = true));
|
|
105
|
+
yield new Promise((r) => setImmediate(r));
|
|
106
|
+
(0, chai_1.expect)(firstDone || secondDone, 'nobody may go on before the network is back').to.equal(false);
|
|
107
|
+
gate.resolve();
|
|
108
|
+
yield Promise.all([first, second]);
|
|
109
|
+
yield restore.restoreSession('s1', 'later');
|
|
110
|
+
(0, chai_1.expect)(fakes.net.reset.callCount).to.equal(1);
|
|
111
|
+
}));
|
|
112
|
+
it("never throws: one service's failure doesn't stop the other's restore", () => __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
live.net.add('s1');
|
|
114
|
+
live.icp.add('s1');
|
|
115
|
+
fakes.net.reset.rejects(new Error('boom'));
|
|
116
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreSession('s1', 'test');
|
|
117
|
+
(0, chai_1.expect)(fakes.icp.stop.calledOnceWith('s1')).to.equal(true);
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
it('restoreAll restores every session either service still has', () => __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
live.net.add('a');
|
|
122
|
+
live.icp.add('a');
|
|
123
|
+
live.icp.add('b');
|
|
124
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreAll('shutdown');
|
|
125
|
+
(0, chai_1.expect)(fakes.net.reset.args.map((a) => a[0])).to.deep.equal(['a']);
|
|
126
|
+
(0, chai_1.expect)(fakes.icp.stop.args.map((a) => a[0]).sort()).to.deep.equal(['a', 'b']);
|
|
127
|
+
}));
|
|
128
|
+
describe('restoreLeftovers: what an ended run wrote down', () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
adb.answers['P1 shell settings get global http_proxy'] = '127.0.0.1:11150';
|
|
131
|
+
});
|
|
132
|
+
it('turns back on what was on, and puts back the proxy it set', () => __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
yield ledger.note('old', 'P1', { offline: { wifiOn: true, dataOn: false } });
|
|
134
|
+
yield ledger.note('old', 'P1', {
|
|
135
|
+
proxy: { set: '127.0.0.1:11150', previous: null, reversePort: 11150 },
|
|
136
|
+
});
|
|
137
|
+
const done = yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
|
|
138
|
+
(0, chai_1.expect)(done).to.equal(1);
|
|
139
|
+
(0, chai_1.expect)(adb.commands('P1')).to.deep.equal([
|
|
140
|
+
'shell svc wifi enable',
|
|
141
|
+
'shell settings get global http_proxy',
|
|
142
|
+
'shell settings put global http_proxy :0',
|
|
143
|
+
'reverse --remove tcp:11150',
|
|
144
|
+
]);
|
|
145
|
+
(0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
|
|
146
|
+
}));
|
|
147
|
+
it("puts back the phone's own proxy when it had one", () => __awaiter(this, void 0, void 0, function* () {
|
|
148
|
+
yield ledger.note('old', 'P1', {
|
|
149
|
+
proxy: { set: '127.0.0.1:11150', previous: 'proxy.corp:3128' },
|
|
150
|
+
});
|
|
151
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
|
|
152
|
+
(0, chai_1.expect)(adb.commands('P1')).to.include('shell settings put global http_proxy proxy.corp:3128');
|
|
153
|
+
}));
|
|
154
|
+
it('leaves a proxy alone that someone changed since, and forgets it', () => __awaiter(this, void 0, void 0, function* () {
|
|
155
|
+
adb.answers['P1 shell settings get global http_proxy'] = 'proxy.corp:3128';
|
|
156
|
+
yield ledger.note('old', 'P1', { proxy: { set: '127.0.0.1:11150', previous: null } });
|
|
157
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
|
|
158
|
+
(0, chai_1.expect)(adb.commands('P1')).to.deep.equal(['shell settings get global http_proxy']);
|
|
159
|
+
(0, chai_1.expect)(yield ledger.list()).to.deep.equal([]);
|
|
160
|
+
}));
|
|
161
|
+
it('skips a session this server still runs', () => __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
live.net.add('running');
|
|
163
|
+
yield ledger.note('running', 'P1', { offline: { wifiOn: true, dataOn: true } });
|
|
164
|
+
(0, chai_1.expect)(yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({})).to.equal(0);
|
|
165
|
+
(0, chai_1.expect)(adb.calls).to.deep.equal([]);
|
|
166
|
+
(0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['running']);
|
|
167
|
+
}));
|
|
168
|
+
it('touches only the phone it is asked about', () => __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
yield ledger.note('old-1', 'P1', { offline: { wifiOn: true, dataOn: true } });
|
|
170
|
+
yield ledger.note('old-2', 'P2', { offline: { wifiOn: true, dataOn: true } });
|
|
171
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({ udid: 'P2' });
|
|
172
|
+
(0, chai_1.expect)(adb.commands('P1')).to.deep.equal([]);
|
|
173
|
+
(0, chai_1.expect)(adb.commands('P2')).to.deep.equal(['shell svc data enable', 'shell svc wifi enable']);
|
|
174
|
+
(0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['old-1']);
|
|
175
|
+
}));
|
|
176
|
+
it('keeps what it could not undo for next time, and gives up after a week', () => __awaiter(this, void 0, void 0, function* () {
|
|
177
|
+
var _a;
|
|
178
|
+
adb.answers['P1 shell svc data enable'] = new Error("device 'P1' not found");
|
|
179
|
+
adb.answers['P9 shell svc data enable'] = new Error("device 'P9' not found");
|
|
180
|
+
yield ledger.note('recent', 'P1', { offline: { wifiOn: true, dataOn: true } });
|
|
181
|
+
yield ledger.note('ancient', 'P9', { offline: { wifiOn: true, dataOn: true } });
|
|
182
|
+
const row = yield prisma_1.prisma.webConfig.findUnique({ where: { name: 'phone-network:ancient' } });
|
|
183
|
+
(0, chai_1.expect)(row, 'the record').to.not.equal(null);
|
|
184
|
+
const value = JSON.parse((_a = row === null || row === void 0 ? void 0 : row.value) !== null && _a !== void 0 ? _a : '{}');
|
|
185
|
+
yield prisma_1.prisma.webConfig.update({
|
|
186
|
+
where: { name: 'phone-network:ancient' },
|
|
187
|
+
data: { value: JSON.stringify(Object.assign(Object.assign({}, value), { at: Date.now() - 8 * DAY })) },
|
|
188
|
+
});
|
|
189
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().restoreLeftovers({});
|
|
190
|
+
(0, chai_1.expect)((yield ledger.list()).map((e) => e.sessionId)).to.deep.equal(['recent']);
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
describe('cleanUpAtBoot: a proxy left pointing at a dead capture port', () => {
|
|
194
|
+
let server;
|
|
195
|
+
let livePort;
|
|
196
|
+
let deadPort;
|
|
197
|
+
beforeEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
server = net_1.default.createServer((s) => s.destroy());
|
|
199
|
+
yield new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
|
|
200
|
+
livePort = server.address().port;
|
|
201
|
+
const closed = net_1.default.createServer();
|
|
202
|
+
yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
|
|
203
|
+
deadPort = closed.address().port;
|
|
204
|
+
yield new Promise((r) => closed.close(() => r()));
|
|
205
|
+
range = [Math.min(livePort, deadPort), Math.max(livePort, deadPort)];
|
|
206
|
+
}));
|
|
207
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
208
|
+
yield new Promise((r) => server.close(() => r()));
|
|
209
|
+
}));
|
|
210
|
+
it("clears only a proxy that points at this machine's capture ports where nothing answers", () => __awaiter(this, void 0, void 0, function* () {
|
|
211
|
+
adb.answers['DEAD shell settings get global http_proxy'] = `127.0.0.1:${deadPort}`;
|
|
212
|
+
adb.answers['EMU shell settings get global http_proxy'] = `10.0.2.2:${deadPort}`;
|
|
213
|
+
adb.answers['LIVE shell settings get global http_proxy'] = `127.0.0.1:${livePort}`;
|
|
214
|
+
adb.answers['CORP shell settings get global http_proxy'] = 'proxy.corp:3128';
|
|
215
|
+
adb.answers['NONE shell settings get global http_proxy'] = ':0';
|
|
216
|
+
adb.answers['FAR shell settings get global http_proxy'] = '127.0.0.1:1';
|
|
217
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().cleanUpAtBoot(['DEAD', 'EMU', 'LIVE', 'CORP', 'NONE', 'FAR']);
|
|
218
|
+
(0, chai_1.expect)(adb.commands('DEAD')).to.deep.equal([
|
|
219
|
+
'shell settings get global http_proxy',
|
|
220
|
+
'shell settings put global http_proxy :0',
|
|
221
|
+
`reverse --remove tcp:${deadPort}`,
|
|
222
|
+
]);
|
|
223
|
+
(0, chai_1.expect)(adb.commands('EMU')).to.deep.equal([
|
|
224
|
+
'shell settings get global http_proxy',
|
|
225
|
+
'shell settings put global http_proxy :0',
|
|
226
|
+
]);
|
|
227
|
+
for (const kept of ['LIVE', 'CORP', 'NONE', 'FAR']) {
|
|
228
|
+
(0, chai_1.expect)(adb.commands(kept), kept).to.deep.equal(['shell settings get global http_proxy']);
|
|
229
|
+
}
|
|
230
|
+
}));
|
|
231
|
+
it('undoes what the ledger holds first, then checks the phones', () => __awaiter(this, void 0, void 0, function* () {
|
|
232
|
+
yield ledger.note('old', 'P1', { offline: { wifiOn: true, dataOn: true } });
|
|
233
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().cleanUpAtBoot([]);
|
|
234
|
+
(0, chai_1.expect)(adb.commands('P1')).to.deep.equal(['shell svc data enable', 'shell svc wifi enable']);
|
|
235
|
+
}));
|
|
236
|
+
it('never throws, even when a phone cannot be read', () => __awaiter(this, void 0, void 0, function* () {
|
|
237
|
+
adb.answers['shell settings get global http_proxy'] = new Error('unauthorized');
|
|
238
|
+
yield new PhoneNetworkRestore_1.PhoneNetworkRestore().cleanUpAtBoot(['X']);
|
|
239
|
+
}));
|
|
240
|
+
});
|
|
241
|
+
it('can be built by the container', () => {
|
|
242
|
+
restoreRegs();
|
|
243
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore);
|
|
244
|
+
(0, chai_1.expect)(() => typedi_1.Container.get(PhoneNetworkRestore_1.PhoneNetworkRestore)).not.to.throw();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
require("reflect-metadata");
|
|
49
|
+
const chai_1 = require("chai");
|
|
50
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
51
|
+
const typedi_1 = require("typedi");
|
|
52
|
+
const plugin_1 = require("../../../src/plugin");
|
|
53
|
+
const NodeDevices_1 = __importDefault(require("../../../src/device-managers/NodeDevices"));
|
|
54
|
+
const DeviceService = __importStar(require("../../../src/data-service/device-service"));
|
|
55
|
+
const ActiveLeases = __importStar(require("../../../src/services/lease/activeLeases"));
|
|
56
|
+
const device_utils_1 = require("../../../src/device-utils");
|
|
57
|
+
const SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
|
|
58
|
+
const SessionMetricsService_1 = require("../../../src/services/metrics/SessionMetricsService");
|
|
59
|
+
const OrphanSweeper_1 = require("../../../src/services/OrphanSweeper");
|
|
60
|
+
const ShutdownCoordinator_1 = require("../../../src/services/ShutdownCoordinator");
|
|
61
|
+
const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
|
|
62
|
+
const event_manager_1 = require("../../../src/dashboard/event-manager");
|
|
63
|
+
const SessionManager_1 = require("../../../src/sessions/SessionManager");
|
|
64
|
+
const prisma_1 = require("../../../src/prisma");
|
|
65
|
+
const container_registration_1 = require("../../helpers/container-registration");
|
|
66
|
+
const scratch_database_1 = require("../../helpers/scratch-database");
|
|
67
|
+
/**
|
|
68
|
+
* A network profile or the interceptor changes the whole phone. Its network
|
|
69
|
+
* was put back only when the test deleted a session Xenon kept in memory.
|
|
70
|
+
* Every other ending (Appium's new-command timeout, Xenon's idle release, a
|
|
71
|
+
* shutdown, a stale heartbeat) left an Offline phone offline and an
|
|
72
|
+
* intercepted phone pointing at a proxy that no longer exists.
|
|
73
|
+
*
|
|
74
|
+
* Each ending now asks PhoneNetworkRestore, keyed by the session id, and
|
|
75
|
+
* before the phone is released: a phone handed to the next session while its
|
|
76
|
+
* network is still being put back could have that session's own profile
|
|
77
|
+
* undone.
|
|
78
|
+
*/
|
|
79
|
+
describe('a session’s phone network is put back however the session ends', function () {
|
|
80
|
+
this.timeout(30000);
|
|
81
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
82
|
+
let restoreRegs;
|
|
83
|
+
let order;
|
|
84
|
+
let restoreSession;
|
|
85
|
+
let restoreAll;
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(PhoneNetworkRestore_1.PhoneNetworkRestore, SessionMetricsService_1.SessionMetricsService);
|
|
88
|
+
order = [];
|
|
89
|
+
restoreSession = sinon_1.default.stub().callsFake((id) => __awaiter(this, void 0, void 0, function* () {
|
|
90
|
+
order.push(`restore ${id}`);
|
|
91
|
+
}));
|
|
92
|
+
restoreAll = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
order.push('restore all');
|
|
94
|
+
}));
|
|
95
|
+
typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreSession, restoreAll });
|
|
96
|
+
typedi_1.Container.set(SessionMetricsService_1.SessionMetricsService, { stop: sinon_1.default.stub().resolves() });
|
|
97
|
+
sinon_1.default.stub(DeviceService, 'releaseSessionDevices').callsFake((id) => __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
order.push(`release ${id}`);
|
|
99
|
+
}));
|
|
100
|
+
sinon_1.default.stub(event_manager_1.DASHBORD_EVENT_MANAGER, 'onSessionStopped').resolves();
|
|
101
|
+
});
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
sinon_1.default.restore();
|
|
104
|
+
restoreRegs();
|
|
105
|
+
});
|
|
106
|
+
it('when the test deletes a session Xenon keeps no record of', () => __awaiter(this, void 0, void 0, function* () {
|
|
107
|
+
(0, chai_1.expect)(SessionManager_1.SESSION_MANAGER.getSession('s-untracked')).to.equal(undefined);
|
|
108
|
+
const next = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
order.push('driver quit');
|
|
110
|
+
}));
|
|
111
|
+
yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).deleteSession(next, 's-untracked');
|
|
112
|
+
(0, chai_1.expect)(restoreSession.callCount).to.equal(1);
|
|
113
|
+
(0, chai_1.expect)(order.indexOf('restore s-untracked')).to.be.lessThan(order.indexOf('release s-untracked'));
|
|
114
|
+
}));
|
|
115
|
+
it("when Appium ends the session itself (new-command timeout), on this server's own phone", () => __awaiter(this, void 0, void 0, function* () {
|
|
116
|
+
const plugin = { pluginArgs: {}, xenonLog: { withSession: () => ({ info() { } }) } };
|
|
117
|
+
yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-timeout', caps: { udid: 'R5CT' } }, new Error('New command timeout'));
|
|
118
|
+
(0, chai_1.expect)(restoreSession.callCount).to.equal(1);
|
|
119
|
+
(0, chai_1.expect)(restoreSession.firstCall.args[0]).to.equal('s-timeout');
|
|
120
|
+
(0, chai_1.expect)(order).to.deep.equal(['restore s-timeout', 'release s-timeout']);
|
|
121
|
+
}));
|
|
122
|
+
it('when Appium ends the session on a node', () => __awaiter(this, void 0, void 0, function* () {
|
|
123
|
+
const unblock = sinon_1.default.stub(NodeDevices_1.default.prototype, 'unblockDevice').callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
order.push('hub unblock');
|
|
125
|
+
return undefined;
|
|
126
|
+
}));
|
|
127
|
+
const plugin = { pluginArgs: { hub: 'http://hub:4723' } };
|
|
128
|
+
yield plugin_1.XenonPlugin.prototype.onUnexpectedShutdown.call(plugin, { sessionId: 's-node', caps: {} }, null);
|
|
129
|
+
(0, chai_1.expect)(unblock.calledOnce).to.equal(true);
|
|
130
|
+
(0, chai_1.expect)(order).to.deep.equal(['restore s-node', 'hub unblock']);
|
|
131
|
+
}));
|
|
132
|
+
it("when Xenon's idle check releases the phone", () => __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
const idle = {
|
|
134
|
+
udid: 'R5CT',
|
|
135
|
+
host: 'http://127.0.0.1:4723',
|
|
136
|
+
busy: true,
|
|
137
|
+
userBlocked: false,
|
|
138
|
+
session_id: 's-idle',
|
|
139
|
+
claimSessionId: 's-idle',
|
|
140
|
+
lastCmdExecutedAt: Date.now() - 10 * 60 * 1000,
|
|
141
|
+
};
|
|
142
|
+
sinon_1.default.stub(DeviceService, 'getAllDevices').resolves([idle]);
|
|
143
|
+
sinon_1.default.stub(ActiveLeases, 'activeLeasesByDevice').resolves(new Map());
|
|
144
|
+
sinon_1.default.stub(DeviceService, 'releaseSessionDevice').callsFake((_u, _h, id) => __awaiter(this, void 0, void 0, function* () {
|
|
145
|
+
order.push(`release ${id}`);
|
|
146
|
+
return true;
|
|
147
|
+
}));
|
|
148
|
+
yield (0, device_utils_1.releaseBlockedDevices)(60);
|
|
149
|
+
(0, chai_1.expect)(restoreSession.callCount).to.equal(1);
|
|
150
|
+
(0, chai_1.expect)(order).to.deep.equal(['restore s-idle', 'release s-idle']);
|
|
151
|
+
}));
|
|
152
|
+
it('when a graceful shutdown drains a session', () => __awaiter(this, void 0, void 0, function* () {
|
|
153
|
+
yield typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).stopSessionForShutdown('s-down', 'shutdown');
|
|
154
|
+
(0, chai_1.expect)(order.slice(0, 2)).to.deep.equal(['restore s-down', 'release s-down']);
|
|
155
|
+
}));
|
|
156
|
+
it('when a graceful shutdown ends, for sessions Xenon keeps no record of', () => __awaiter(this, void 0, void 0, function* () {
|
|
157
|
+
sinon_1.default.stub(SessionManager_1.SESSION_MANAGER, 'getAllSessions').returns([]);
|
|
158
|
+
yield new ShutdownCoordinator_1.ShutdownCoordinator().drain(1000, 500);
|
|
159
|
+
(0, chai_1.expect)(restoreAll.calledOnce).to.equal(true);
|
|
160
|
+
}));
|
|
161
|
+
it("when a session's heartbeat goes stale", () => __awaiter(this, void 0, void 0, function* () {
|
|
162
|
+
var _a;
|
|
163
|
+
yield scratch.db.session.deleteMany({});
|
|
164
|
+
yield scratch.db.session.create({
|
|
165
|
+
data: {
|
|
166
|
+
id: 's-stale',
|
|
167
|
+
status: 'running',
|
|
168
|
+
desired_capabilities: '{}',
|
|
169
|
+
session_capabilities: '{}',
|
|
170
|
+
node_id: 'n',
|
|
171
|
+
has_live_video: false,
|
|
172
|
+
device_udid: 'R5CT',
|
|
173
|
+
device_platform: 'android',
|
|
174
|
+
device_version: '14',
|
|
175
|
+
last_heartbeat_at: new Date(Date.now() - 60 * 60 * 1000),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
yield new OrphanSweeper_1.OrphanSweeper().sweep({ heartbeatIntervalMs: 1000 });
|
|
179
|
+
(0, chai_1.expect)(order.slice(0, 2)).to.deep.equal(['restore s-stale', 'release s-stale']);
|
|
180
|
+
(0, chai_1.expect)((_a = (yield prisma_1.prisma.session.findUnique({ where: { id: 's-stale' } }))) === null || _a === void 0 ? void 0 : _a.status).to.equal('failed');
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
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 typedi_1 = require("typedi");
|
|
19
|
+
const SessionLifecycleService_1 = require("../../../src/services/SessionLifecycleService");
|
|
20
|
+
const NetworkConditioningService_1 = require("../../../src/services/NetworkConditioningService");
|
|
21
|
+
const InterceptorService_1 = require("../../../src/services/InterceptorService");
|
|
22
|
+
const PhoneNetworkRestore_1 = require("../../../src/services/network/PhoneNetworkRestore");
|
|
23
|
+
const PluginContext_1 = require("../../../src/PluginContext");
|
|
24
|
+
const IPluginArgs_1 = require("../../../src/interfaces/IPluginArgs");
|
|
25
|
+
const XenonCapabilityManager_1 = require("../../../src/XenonCapabilityManager");
|
|
26
|
+
const SessionManager_1 = require("../../../src/sessions/SessionManager");
|
|
27
|
+
const SessionType_1 = __importDefault(require("../../../src/enums/SessionType"));
|
|
28
|
+
const container_registration_1 = require("../../helpers/container-registration");
|
|
29
|
+
const scratch_database_1 = require("../../helpers/scratch-database");
|
|
30
|
+
const android = { udid: 'R5CT', platform: 'android', deviceType: 'real', host: 'h' };
|
|
31
|
+
const sessionOf = (type) => ({
|
|
32
|
+
getId: () => 's1',
|
|
33
|
+
getType: () => type,
|
|
34
|
+
getCapabilities: () => ({}),
|
|
35
|
+
apiKeyId: null,
|
|
36
|
+
userId: null,
|
|
37
|
+
startVideoRecording: sinon_1.default.stub().resolves(),
|
|
38
|
+
});
|
|
39
|
+
const capsWith = (extra) => (Object.assign({ [XenonCapabilityManager_1.XENON_CAPABILITIES.VIDEO_RECORDING]: false, [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_MOCKS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_INCLUDE_HOSTS]: [], [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_EXCLUDE_HOSTS]: [] }, extra));
|
|
40
|
+
/**
|
|
41
|
+
* Where a session's network changes are made. Only the server that drives the
|
|
42
|
+
* phone makes them: a hub used to run its own adb against a node's phone (on
|
|
43
|
+
* one Mac, the very same phone), and start a second proxy for it.
|
|
44
|
+
*/
|
|
45
|
+
describe('a session’s network changes at its start', function () {
|
|
46
|
+
this.timeout(30000);
|
|
47
|
+
// The row a hub writes for a session another server runs (recordRoutedSession).
|
|
48
|
+
const scratch = (0, scratch_database_1.useScratchDatabase)();
|
|
49
|
+
let restoreRegs;
|
|
50
|
+
let order;
|
|
51
|
+
let applyProfile;
|
|
52
|
+
let start;
|
|
53
|
+
let restoreLeftovers;
|
|
54
|
+
let context;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
restoreRegs = (0, container_registration_1.saveRegistrations)(NetworkConditioningService_1.NetworkConditioningService, InterceptorService_1.InterceptorService, PhoneNetworkRestore_1.PhoneNetworkRestore, PluginContext_1.PluginContext);
|
|
57
|
+
order = [];
|
|
58
|
+
applyProfile = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('profile'); }));
|
|
59
|
+
start = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return void order.push('interceptor'); }));
|
|
60
|
+
restoreLeftovers = sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
order.push('leftovers');
|
|
62
|
+
return 0;
|
|
63
|
+
}));
|
|
64
|
+
typedi_1.Container.set(NetworkConditioningService_1.NetworkConditioningService, { applyProfile });
|
|
65
|
+
typedi_1.Container.set(InterceptorService_1.InterceptorService, { start });
|
|
66
|
+
typedi_1.Container.set(PhoneNetworkRestore_1.PhoneNetworkRestore, { restoreLeftovers });
|
|
67
|
+
context = new PluginContext_1.PluginContext();
|
|
68
|
+
context.pluginArgs = Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { enableDashboard: false });
|
|
69
|
+
typedi_1.Container.set(PluginContext_1.PluginContext, context);
|
|
70
|
+
});
|
|
71
|
+
afterEach(() => __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
sinon_1.default.restore();
|
|
73
|
+
SessionManager_1.SESSION_MANAGER.removeSession('s1');
|
|
74
|
+
restoreRegs();
|
|
75
|
+
yield scratch.db.session.deleteMany({});
|
|
76
|
+
}));
|
|
77
|
+
const apply = (type, caps) => typedi_1.Container.get(SessionLifecycleService_1.SessionLifecycleService).applyPostSessionLogic(sessionOf(type), caps, android);
|
|
78
|
+
it('puts back what an earlier session left on the phone, then applies its own', () => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
yield apply(SessionType_1.default.LOCAL, capsWith({
|
|
80
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
|
|
81
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
|
|
82
|
+
}));
|
|
83
|
+
(0, chai_1.expect)(restoreLeftovers.calledOnceWith({ udid: 'R5CT' })).to.equal(true);
|
|
84
|
+
(0, chai_1.expect)(order).to.deep.equal(['leftovers', 'profile', 'interceptor']);
|
|
85
|
+
}));
|
|
86
|
+
it("doesn't touch a phone another server drives", () => __awaiter(this, void 0, void 0, function* () {
|
|
87
|
+
for (const type of [SessionType_1.default.REMOTE, SessionType_1.default.CLOUD]) {
|
|
88
|
+
yield apply(type, capsWith({
|
|
89
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.NETWORK_PROFILE]: 'Offline',
|
|
90
|
+
[XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: true,
|
|
91
|
+
}));
|
|
92
|
+
SessionManager_1.SESSION_MANAGER.removeSession('s1');
|
|
93
|
+
}
|
|
94
|
+
(0, chai_1.expect)(order).to.deep.equal([]);
|
|
95
|
+
}));
|
|
96
|
+
it("starts the interceptor by the server's option when the session doesn't say", () => __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true, bufferSize: 9 } });
|
|
98
|
+
yield apply(SessionType_1.default.LOCAL, capsWith({}));
|
|
99
|
+
(0, chai_1.expect)(start.calledOnce).to.equal(true);
|
|
100
|
+
const [sessionId, device, opts] = start.firstCall.args;
|
|
101
|
+
(0, chai_1.expect)(sessionId).to.equal('s1');
|
|
102
|
+
(0, chai_1.expect)(device).to.equal(android);
|
|
103
|
+
(0, chai_1.expect)(opts).to.include({ enabled: true, bufferSize: 9, captureBodies: true });
|
|
104
|
+
}));
|
|
105
|
+
it("leaves the interceptor off when the session turns it off over the server's option", () => __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
context.pluginArgs = Object.assign(Object.assign({}, context.pluginArgs), { interceptor: { enabled: true } });
|
|
107
|
+
yield apply(SessionType_1.default.LOCAL, capsWith({ [XenonCapabilityManager_1.XENON_CAPABILITIES.INTERCEPTOR_ENABLED]: false }));
|
|
108
|
+
(0, chai_1.expect)(start.called).to.equal(false);
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 net_1 = __importDefault(require("net"));
|
|
18
|
+
const xenonProxy_1 = require("../../../src/services/network/xenonProxy");
|
|
19
|
+
describe('a phone proxy setting, read', () => {
|
|
20
|
+
it('parses host:port', () => {
|
|
21
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('127.0.0.1:11100')).to.deep.equal({ host: '127.0.0.1', port: 11100 });
|
|
22
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(' 10.0.2.2:11105 ')).to.deep.equal({ host: '10.0.2.2', port: 11105 });
|
|
23
|
+
});
|
|
24
|
+
it('reads no proxy from what Android stores for none', () => {
|
|
25
|
+
for (const none of [null, undefined, '', 'null', ':0', ' ']) {
|
|
26
|
+
(0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)(none), String(none)).to.equal(true);
|
|
27
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)(none)).to.equal(null);
|
|
28
|
+
}
|
|
29
|
+
(0, chai_1.expect)((0, xenonProxy_1.isUnsetProxy)('proxy.corp:3128')).to.equal(false);
|
|
30
|
+
});
|
|
31
|
+
it('refuses a value that is not host:port', () => {
|
|
32
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('proxy.corp')).to.equal(null);
|
|
33
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:b')).to.equal(null);
|
|
34
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:0')).to.equal(null);
|
|
35
|
+
(0, chai_1.expect)((0, xenonProxy_1.parseProxySetting)('a:70000')).to.equal(null);
|
|
36
|
+
});
|
|
37
|
+
it('knows the addresses a phone uses to reach this machine', () => {
|
|
38
|
+
const own = ['192.168.1.20'];
|
|
39
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('127.0.0.1', own)).to.equal(true);
|
|
40
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('localhost', own)).to.equal(true);
|
|
41
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('10.0.2.2', own)).to.equal(true);
|
|
42
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.20', own)).to.equal(true);
|
|
43
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('192.168.1.21', own)).to.equal(false);
|
|
44
|
+
(0, chai_1.expect)((0, xenonProxy_1.pointsAtThisMachine)('proxy.corp', own)).to.equal(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('isPortListening', function () {
|
|
48
|
+
let server;
|
|
49
|
+
let livePort;
|
|
50
|
+
let deadPort;
|
|
51
|
+
before(() => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
server = net_1.default.createServer((s) => s.destroy());
|
|
53
|
+
yield new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
|
|
54
|
+
livePort = server.address().port;
|
|
55
|
+
const closed = net_1.default.createServer();
|
|
56
|
+
yield new Promise((r) => closed.listen(0, '127.0.0.1', () => r()));
|
|
57
|
+
deadPort = closed.address().port;
|
|
58
|
+
yield new Promise((r) => closed.close(() => r()));
|
|
59
|
+
}));
|
|
60
|
+
after(() => __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
yield new Promise((r) => server.close(() => r()));
|
|
62
|
+
}));
|
|
63
|
+
it('says a port with a listener answers', () => __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
(0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(livePort)).to.equal(true);
|
|
65
|
+
}));
|
|
66
|
+
it('says a closed port does not', () => __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
(0, chai_1.expect)(yield (0, xenonProxy_1.isPortListening)(deadPort)).to.equal(false);
|
|
68
|
+
}));
|
|
69
|
+
});
|
|
@@ -81,7 +81,7 @@ describe('Device filter tests', () => {
|
|
|
81
81
|
(0, chai_1.expect)(filter).to.deep.equal({
|
|
82
82
|
platform: 'ios',
|
|
83
83
|
platformVersion: '14.0',
|
|
84
|
-
|
|
84
|
+
appleFamily: 'iphone',
|
|
85
85
|
deviceType: 'real',
|
|
86
86
|
udid: ['21112-1111-1111-111'],
|
|
87
87
|
minSDK: undefined,
|
|
@@ -113,7 +113,7 @@ describe('Device filter tests', () => {
|
|
|
113
113
|
(0, chai_1.expect)(filter).to.deep.equal({
|
|
114
114
|
platform: 'ios',
|
|
115
115
|
platformVersion: '14.0',
|
|
116
|
-
|
|
116
|
+
appleFamily: 'iphone',
|
|
117
117
|
filterByHost: undefined,
|
|
118
118
|
deviceType: 'simulator',
|
|
119
119
|
udid: [],
|
|
@@ -140,7 +140,7 @@ describe('Device filter tests', () => {
|
|
|
140
140
|
platform: 'ios',
|
|
141
141
|
filterByHost: undefined,
|
|
142
142
|
platformVersion: undefined,
|
|
143
|
-
|
|
143
|
+
appleFamily: 'iphone',
|
|
144
144
|
deviceType: 'simulator',
|
|
145
145
|
udid: [],
|
|
146
146
|
minSDK: '10.2.0',
|