@xenon-device-management/xenon 2.12.0 → 2.13.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/lib/package.json +2 -2
- package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
- package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
- package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
- package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
- package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
- package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
- package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
- package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
- package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
- package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
- package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
- package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
- package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
- package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
- package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
- package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
- package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
- package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
- package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
- package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
- package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
- package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
- package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
- package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
- package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
- package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
- package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
- package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
- package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
- package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
- package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
- package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
- package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
- package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
- package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
- package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
- package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
- package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
- package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
- package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
- package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
- package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
- package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
- package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
- package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
- package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
- package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
- package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
- package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
- package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
- package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
- package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
- package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
- package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
- package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
- package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
- package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
- package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
- package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
- package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
- package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/apiErrors.js +118 -0
- package/lib/src/app/index.js +6 -1
- package/lib/src/app/openapi/control.yaml +3129 -0
- package/lib/src/app/openapi/grid.yaml +2295 -0
- package/lib/src/app/openapi/identity.yaml +2168 -0
- package/lib/src/app/openapi/platform.yaml +2885 -0
- package/lib/src/app/openapi/sessions.yaml +3784 -0
- package/lib/src/app/routers/bug-report.js +4 -1
- package/lib/src/app/routers/config.js +6 -107
- package/lib/src/app/routers/control.js +117 -59
- package/lib/src/app/routers/dashboard.js +13 -7
- package/lib/src/app/routers/grid.js +54 -14
- package/lib/src/app/routers/profile.js +27 -13
- package/lib/src/app/routers/recordings.js +11 -5
- package/lib/src/app/routers/reservation.js +63 -15
- package/lib/src/app/routers/users.js +4 -0
- package/lib/src/app/routers/webhook.js +17 -8
- package/lib/src/app/swagger.js +259 -177
- package/lib/src/data-service/device-service.js +4 -1
- package/lib/src/data-service/deviceFieldOwners.js +1 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
- package/lib/src/device-managers/ios/WDAClient.js +32 -32
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +38 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +2 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/middleware/csrfMiddleware.js +13 -5
- package/lib/src/middleware/rateLimitMiddleware.js +19 -5
- package/lib/src/middleware/roleGuard.js +20 -0
- package/lib/src/services/AIService.js +13 -3
- package/lib/src/services/NotificationService.js +28 -28
- package/lib/src/services/bug-report/BugReportService.js +8 -2
- package/lib/src/services/lease/LeaseService.js +71 -20
- package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
- package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
- package/lib/test/helpers/expressRoutes.js +41 -0
- package/lib/test/integration/team-visibility-control.spec.js +2 -2
- package/lib/test/unit/access-scopes.spec.js +227 -0
- package/lib/test/unit/api-error-handling.spec.js +179 -0
- package/lib/test/unit/bug-report/route.spec.js +29 -0
- package/lib/test/unit/bug-report/service.spec.js +30 -0
- package/lib/test/unit/control-honest-answers.spec.js +134 -0
- package/lib/test/unit/device-allocation-routes.spec.js +232 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
- package/lib/test/unit/install-repository-app-team.spec.js +4 -1
- package/lib/test/unit/lease/LeaseService.spec.js +5 -4
- package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
- package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
- package/lib/test/unit/omni-vision-failures.spec.js +81 -0
- package/lib/test/unit/openapi-coverage.spec.js +114 -0
- package/lib/test/unit/profile-router.test.js +42 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
- package/lib/test/unit/recording-orchestrator.spec.js +80 -0
- package/lib/test/unit/recordings-library-routes.spec.js +37 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
- package/lib/test/unit/reset-link.test.js +2 -1
- package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
- package/lib/test/unit/users-router.test.js +14 -1
- package/lib/test/unit/wda-client-failures.spec.js +90 -0
- package/lib/test/unit/webhook-delivery.spec.js +142 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
- package/lib/src/app/swagger-docs.js +0 -1701
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
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 loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
21
|
+
const webhook_1 = __importDefault(require("../../src/app/routers/webhook"));
|
|
22
|
+
const recordings_1 = __importDefault(require("../../src/app/routers/recordings"));
|
|
23
|
+
const NotificationService_1 = require("../../src/services/NotificationService");
|
|
24
|
+
const dashboard_1 = __importDefault(require("../../src/app/routers/dashboard"));
|
|
25
|
+
const config_1 = __importDefault(require("../../src/app/routers/config"));
|
|
26
|
+
const web_config_service_1 = require("../../src/data-service/web-config-service");
|
|
27
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
28
|
+
const grid_1 = __importDefault(require("../../src/app/routers/grid"));
|
|
29
|
+
const SessionManager_1 = require("../../src/sessions/SessionManager");
|
|
30
|
+
const prisma_1 = require("../../src/prisma");
|
|
31
|
+
const csrfMiddleware_1 = require("../../src/middleware/csrfMiddleware");
|
|
32
|
+
const config_2 = require("../../src/config");
|
|
33
|
+
function appAs(caller, mount) {
|
|
34
|
+
const app = (0, express_1.default)();
|
|
35
|
+
app.use(express_1.default.json());
|
|
36
|
+
app.use((req, _res, next) => {
|
|
37
|
+
req.auth = Object.assign({ kind: 'api-key', userId: 'usr_x', rateLimit: 1000 }, caller);
|
|
38
|
+
next();
|
|
39
|
+
});
|
|
40
|
+
const router = express_1.default.Router();
|
|
41
|
+
mount(router);
|
|
42
|
+
app.use(router);
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
describe('access scopes', () => {
|
|
46
|
+
afterEach(() => sinon_1.default.restore());
|
|
47
|
+
describe('GET /webhook', () => {
|
|
48
|
+
const app = (scopes) => appAs({ role: 'ADMIN', scopes }, (r) => webhook_1.default.register(r));
|
|
49
|
+
it("refuses an admin's key without the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
50
|
+
const list = sinon_1.default.stub(typedi_1.Container.get(NotificationService_1.NotificationService), 'getConfigs').resolves([]);
|
|
51
|
+
const r = yield (0, loopbackRequest_1.default)(app('read')).get('/webhook').timeout(5000);
|
|
52
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
53
|
+
(0, chai_1.expect)(list.called).to.equal(false);
|
|
54
|
+
}));
|
|
55
|
+
it('answers an admin with the admin scope', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
sinon_1.default.stub(typedi_1.Container.get(NotificationService_1.NotificationService), 'getConfigs').resolves([]);
|
|
57
|
+
const r = yield (0, loopbackRequest_1.default)(app('admin')).get('/webhook').timeout(5000);
|
|
58
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
describe('recording writes', () => {
|
|
62
|
+
const app = (scopes) => appAs({ role: 'MEMBER', scopes }, (r) => recordings_1.default.register(r));
|
|
63
|
+
for (const path of [
|
|
64
|
+
'/recordings',
|
|
65
|
+
'/recordings/g1/add-device',
|
|
66
|
+
'/recordings/g1/stop',
|
|
67
|
+
'/recordings/g1/bookmark',
|
|
68
|
+
'/recordings/g1/annotation',
|
|
69
|
+
'/recordings/g1/annotations/clear',
|
|
70
|
+
]) {
|
|
71
|
+
it(`refuses POST ${path} without the devices scope`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
const r = yield (0, loopbackRequest_1.default)(app('sessions,read')).post(path).send({}).timeout(5000);
|
|
73
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
74
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'insufficient scope' });
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
it('lets the same key read recordings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
const r = yield (0, loopbackRequest_1.default)(app('sessions,read')).get('/recordings?limit=x').timeout(5000);
|
|
79
|
+
(0, chai_1.expect)(r.status).to.not.equal(403);
|
|
80
|
+
}));
|
|
81
|
+
it('leaves routes mounted after the recordings router alone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
82
|
+
const a = appAs({ role: 'MEMBER', scopes: 'read' }, (r) => {
|
|
83
|
+
recordings_1.default.register(r);
|
|
84
|
+
r.post('/elsewhere', (_req, res) => res.json({ ok: true }));
|
|
85
|
+
});
|
|
86
|
+
const r = yield (0, loopbackRequest_1.default)(a).post('/elsewhere').send({}).timeout(5000);
|
|
87
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
describe('/config', () => {
|
|
91
|
+
const ALL = 'admin,devices,sessions,read';
|
|
92
|
+
const app = (role, scopes = ALL) => appAs({ role, scopes }, (r) => {
|
|
93
|
+
dashboard_1.default.register(r);
|
|
94
|
+
config_1.default.register(r);
|
|
95
|
+
});
|
|
96
|
+
const SETTINGS_REFUSED = {
|
|
97
|
+
error: "Only a super admin can change the lab's settings.",
|
|
98
|
+
message: "Only a super admin can change the lab's settings.",
|
|
99
|
+
};
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'getConfig').resolves({});
|
|
102
|
+
});
|
|
103
|
+
it('refuses a member the settings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app('MEMBER')).get('/config').timeout(5000)).status).to.equal(403);
|
|
105
|
+
}));
|
|
106
|
+
it('shows an admin the settings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app('ADMIN')).get('/config').timeout(5000)).status).to.equal(200);
|
|
108
|
+
}));
|
|
109
|
+
for (const path of ['/config', '/config/reset-metrics']) {
|
|
110
|
+
it(`refuses an admin POST ${path}, saying why`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
111
|
+
const set = sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'setConfig').resolves();
|
|
112
|
+
const r = yield (0, loopbackRequest_1.default)(app('ADMIN'))
|
|
113
|
+
.post(path)
|
|
114
|
+
.send({ buildCleanupDays: 7 })
|
|
115
|
+
.timeout(5000);
|
|
116
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
117
|
+
(0, chai_1.expect)(r.body).to.deep.equal(SETTINGS_REFUSED);
|
|
118
|
+
(0, chai_1.expect)(set.called).to.equal(false);
|
|
119
|
+
}));
|
|
120
|
+
it(`refuses a super admin's key without the admin scope POST ${path}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
+
const r = yield (0, loopbackRequest_1.default)(app('SUPER_ADMIN', 'read')).post(path).send({}).timeout(5000);
|
|
122
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
it('lets a super admin change the settings', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const set = sinon_1.default.stub(typedi_1.Container.get(web_config_service_1.WebConfigService), 'setConfig').resolves();
|
|
127
|
+
const r = yield (0, loopbackRequest_1.default)(app('SUPER_ADMIN'))
|
|
128
|
+
.post('/config')
|
|
129
|
+
.send({ buildCleanupDays: 7 })
|
|
130
|
+
.timeout(5000);
|
|
131
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
132
|
+
(0, chai_1.expect)(set.calledOnce).to.equal(true);
|
|
133
|
+
}));
|
|
134
|
+
it('refuses an admin an AI provider test, saying why', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
135
|
+
const test = sinon_1.default.stub(AIService_1.AI_SERVICE, 'testConnection').resolves({ success: true });
|
|
136
|
+
const r = yield (0, loopbackRequest_1.default)(app('ADMIN')).post('/config/test-ai').send({}).timeout(5000);
|
|
137
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
138
|
+
(0, chai_1.expect)(r.body.message).to.equal('Only a super admin can test an AI provider.');
|
|
139
|
+
(0, chai_1.expect)(test.called).to.equal(false);
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
describe('GET /sessions/active', () => {
|
|
143
|
+
const session = (id, udid, type) => ({
|
|
144
|
+
getId: () => id,
|
|
145
|
+
getType: () => type,
|
|
146
|
+
getDevice: () => ({ udid, name: udid, platform: 'android' }),
|
|
147
|
+
});
|
|
148
|
+
function app(teamIds) {
|
|
149
|
+
const a = (0, express_1.default)();
|
|
150
|
+
a.use((req, _res, next) => {
|
|
151
|
+
req.auth = {
|
|
152
|
+
kind: 'user-session',
|
|
153
|
+
userId: 'usr_x',
|
|
154
|
+
role: teamIds ? 'MEMBER' : 'ADMIN',
|
|
155
|
+
scopes: 'devices,sessions,read',
|
|
156
|
+
rateLimit: 1000,
|
|
157
|
+
teamIds,
|
|
158
|
+
};
|
|
159
|
+
next();
|
|
160
|
+
});
|
|
161
|
+
const router = express_1.default.Router();
|
|
162
|
+
grid_1.default.register(router, { bindHostOrIp: '127.0.0.1' });
|
|
163
|
+
a.use(router);
|
|
164
|
+
return a;
|
|
165
|
+
}
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
sinon_1.default
|
|
168
|
+
.stub(typedi_1.Container.get(SessionManager_1.SessionManager), 'getAllSessions')
|
|
169
|
+
.returns([
|
|
170
|
+
session('s-shared', 'SHARED', 'LOCAL'),
|
|
171
|
+
session('s-b1', 'PHONE-B', 'LOCAL'),
|
|
172
|
+
session('s-b2', 'PHONE-B2', 'REMOTE'),
|
|
173
|
+
]);
|
|
174
|
+
sinon_1.default.stub(prisma_1.prisma.device, 'findMany').resolves([
|
|
175
|
+
{ udid: 'SHARED', teamId: null },
|
|
176
|
+
{ udid: 'PHONE-B', teamId: 'team-b' },
|
|
177
|
+
{ udid: 'PHONE-B2', teamId: 'team-b' },
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
it("counts only the sessions a member can see, not other teams'", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
181
|
+
const r = yield (0, loopbackRequest_1.default)(app(['team-a']))
|
|
182
|
+
.get('/sessions/active')
|
|
183
|
+
.timeout(5000);
|
|
184
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
185
|
+
(0, chai_1.expect)(r.body.sessions.map((s) => s.id)).to.deep.equal(['s-shared']);
|
|
186
|
+
(0, chai_1.expect)(r.body.stats).to.deep.equal({ total: 1, byType: { local: 1, remote: 0, cloud: 0 } });
|
|
187
|
+
}));
|
|
188
|
+
it('counts every session for an admin', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
189
|
+
const r = yield (0, loopbackRequest_1.default)(app(undefined)).get('/sessions/active').timeout(5000);
|
|
190
|
+
(0, chai_1.expect)(r.body.stats).to.deep.equal({ total: 3, byType: { local: 2, remote: 1, cloud: 0 } });
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
193
|
+
describe('the same-origin check', () => {
|
|
194
|
+
let saved;
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
saved = config_2.config.authDisabled;
|
|
197
|
+
config_2.config.authDisabled = false;
|
|
198
|
+
});
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
config_2.config.authDisabled = saved;
|
|
201
|
+
});
|
|
202
|
+
function app() {
|
|
203
|
+
const a = (0, express_1.default)();
|
|
204
|
+
a.use(csrfMiddleware_1.csrfMiddleware);
|
|
205
|
+
a.post('/x', (_req, res) => res.json({ ok: true }));
|
|
206
|
+
return a;
|
|
207
|
+
}
|
|
208
|
+
it('lets a bearer request through without Origin or Referer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
209
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
210
|
+
.post('/x')
|
|
211
|
+
.set('Authorization', 'Bearer eyJhbGciOiJSUzI1NiJ9.e30.sig')
|
|
212
|
+
.timeout(5000);
|
|
213
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
214
|
+
}));
|
|
215
|
+
it('still refuses a cookie request without Origin or Referer', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
217
|
+
.post('/x')
|
|
218
|
+
.set('Cookie', 'xenon_dashboard_session=abc')
|
|
219
|
+
.timeout(5000);
|
|
220
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
221
|
+
}));
|
|
222
|
+
it('does not take another Authorization scheme for a bearer token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
223
|
+
const r = yield (0, loopbackRequest_1.default)(app()).post('/x').set('Authorization', 'Basic dTpw').timeout(5000);
|
|
224
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
225
|
+
}));
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
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 loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
20
|
+
const apiErrors_1 = require("../../src/app/apiErrors");
|
|
21
|
+
const index_1 = require("../../src/app/index");
|
|
22
|
+
const config_1 = require("../../src/config");
|
|
23
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
24
|
+
/**
|
|
25
|
+
* Express 4 ignores the promise an async handler returns. A handler that
|
|
26
|
+
* threw after an `await` left the request without an answer until the
|
|
27
|
+
* client gave up, and logged an unhandled rejection: deleting an unknown API
|
|
28
|
+
* key, creating a user whose email exists, a selector lookup with a `%`,
|
|
29
|
+
* another node's status. A synchronous throw got Express's own HTML page,
|
|
30
|
+
* with a stack trace. Every error under /xenon/api now gets a JSON answer.
|
|
31
|
+
*/
|
|
32
|
+
const prismaError = (code, meta = {}) => Object.assign(new Error(`prisma ${code}`), { code, meta, clientVersion: '5.4.0' });
|
|
33
|
+
function appWith(route, method = 'get') {
|
|
34
|
+
(0, apiErrors_1.forwardAsyncErrors)();
|
|
35
|
+
const app = (0, express_1.default)();
|
|
36
|
+
app.use(express_1.default.json());
|
|
37
|
+
app[method]('/x', route);
|
|
38
|
+
app.use(apiErrors_1.apiErrorHandler);
|
|
39
|
+
return app;
|
|
40
|
+
}
|
|
41
|
+
describe('API errors', () => {
|
|
42
|
+
describe('the error handling', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
sinon_1.default.stub(console, 'error');
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => sinon_1.default.restore());
|
|
47
|
+
it('answers an async handler that throws, with JSON', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
const app = appWith(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
|
+
yield Promise.resolve();
|
|
50
|
+
throw new Error('boom: secret detail');
|
|
51
|
+
}));
|
|
52
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
53
|
+
(0, chai_1.expect)(r.status).to.equal(500);
|
|
54
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'internal', message: 'Internal server error' });
|
|
55
|
+
(0, chai_1.expect)(r.text).not.to.include('secret detail');
|
|
56
|
+
}));
|
|
57
|
+
it('answers a synchronous throw with JSON, not an HTML stack trace', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
+
const app = appWith(() => {
|
|
59
|
+
throw new Error('sync boom');
|
|
60
|
+
});
|
|
61
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
62
|
+
(0, chai_1.expect)(r.status).to.equal(500);
|
|
63
|
+
(0, chai_1.expect)(r.type).to.equal('application/json');
|
|
64
|
+
(0, chai_1.expect)(r.text).not.to.include('at ');
|
|
65
|
+
}));
|
|
66
|
+
it('answers a record that does not exist with 404', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
67
|
+
const app = appWith(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
|
+
throw prismaError('P2025', { cause: 'Record to update not found.' });
|
|
69
|
+
}));
|
|
70
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
71
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
72
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'not_found', message: 'Not found' });
|
|
73
|
+
}));
|
|
74
|
+
it('answers a duplicate with 409, naming the field', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const app = appWith(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
76
|
+
throw prismaError('P2002', { target: ['email'] });
|
|
77
|
+
}));
|
|
78
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
79
|
+
(0, chai_1.expect)(r.status).to.equal(409);
|
|
80
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'conflict', message: 'email is already in use' });
|
|
81
|
+
}));
|
|
82
|
+
it('answers a malformed % escape with 400', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
const app = appWith(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
decodeURIComponent('%E0%A4%A');
|
|
85
|
+
}));
|
|
86
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
87
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
88
|
+
(0, chai_1.expect)(r.body.error).to.equal('bad_request');
|
|
89
|
+
}));
|
|
90
|
+
it('answers a body that is not JSON with 400', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
const app = appWith((_req, res) => res.json({ ok: true }), 'post');
|
|
92
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
93
|
+
.post('/x')
|
|
94
|
+
.set('Content-Type', 'application/json')
|
|
95
|
+
.send('{"broken":')
|
|
96
|
+
.timeout(3000);
|
|
97
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
98
|
+
(0, chai_1.expect)(r.body).to.deep.equal({
|
|
99
|
+
error: 'bad_request',
|
|
100
|
+
message: 'The request body is not valid JSON',
|
|
101
|
+
});
|
|
102
|
+
}));
|
|
103
|
+
it('keeps a status an error carries (http-errors style)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const app = appWith(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
throw Object.assign(new Error('Payload too large'), { status: 413, expose: true });
|
|
106
|
+
}));
|
|
107
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
108
|
+
(0, chai_1.expect)(r.status).to.equal(413);
|
|
109
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'Payload too large' });
|
|
110
|
+
}));
|
|
111
|
+
it('ends a response that had already started instead of leaving it open', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
112
|
+
const app = appWith((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
113
|
+
res.status(200).write('partial');
|
|
114
|
+
yield Promise.resolve();
|
|
115
|
+
throw new Error('after headers');
|
|
116
|
+
}));
|
|
117
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000).buffer(true);
|
|
118
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
119
|
+
}));
|
|
120
|
+
it('leaves a handler that answered normally alone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
121
|
+
const app = appWith((_req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
+
yield Promise.resolve();
|
|
123
|
+
res.json({ fine: true });
|
|
124
|
+
}));
|
|
125
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/x').timeout(3000);
|
|
126
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
127
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ fine: true });
|
|
128
|
+
}));
|
|
129
|
+
});
|
|
130
|
+
describe('the routes that hung', () => {
|
|
131
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
132
|
+
let app;
|
|
133
|
+
let savedAuthDisabled;
|
|
134
|
+
before(() => {
|
|
135
|
+
app = (0, express_1.default)();
|
|
136
|
+
app.use('/xenon', (0, index_1.createRouter)({ bindHostOrIp: '127.0.0.1', enableDashboard: true }));
|
|
137
|
+
});
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
savedAuthDisabled = config_1.config.authDisabled;
|
|
140
|
+
// Every caller is a synthetic SUPER_ADMIN, and no same-origin check.
|
|
141
|
+
config_1.config.authDisabled = true;
|
|
142
|
+
sinon_1.default.stub(console, 'error');
|
|
143
|
+
});
|
|
144
|
+
afterEach(() => {
|
|
145
|
+
config_1.config.authDisabled = savedAuthDisabled;
|
|
146
|
+
sinon_1.default.restore();
|
|
147
|
+
});
|
|
148
|
+
it('DELETE /api/apikeys/:id with an unknown id answers 404', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
149
|
+
const r = yield (0, loopbackRequest_1.default)(app).delete('/xenon/api/apikeys/no-such-key').timeout(5000);
|
|
150
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
151
|
+
(0, chai_1.expect)(r.body.error).to.equal('not_found');
|
|
152
|
+
}));
|
|
153
|
+
it('POST /api/users with an email already in use answers 409', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
|
+
const body = {
|
|
155
|
+
email: 'dup@example.com',
|
|
156
|
+
name: 'Dup',
|
|
157
|
+
role: 'MEMBER',
|
|
158
|
+
password: 'longenough',
|
|
159
|
+
};
|
|
160
|
+
const first = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/users').send(body).timeout(5000);
|
|
161
|
+
(0, chai_1.expect)(first.status).to.equal(201);
|
|
162
|
+
const again = yield (0, loopbackRequest_1.default)(app).post('/xenon/api/users').send(body).timeout(5000);
|
|
163
|
+
(0, chai_1.expect)(again.status).to.equal(409);
|
|
164
|
+
(0, chai_1.expect)(again.body).to.deep.equal({ error: 'conflict', message: 'email is already in use' });
|
|
165
|
+
}));
|
|
166
|
+
it('GET /api/healing/state/:strategy/:value reads a selector with a % once-encoded', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
167
|
+
const value = "//*[@text='50% off']";
|
|
168
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
169
|
+
.get(`/xenon/api/healing/state/xpath/${encodeURIComponent(value)}`)
|
|
170
|
+
.timeout(5000);
|
|
171
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
172
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ state: null });
|
|
173
|
+
}));
|
|
174
|
+
it('GET /api/node/:host/status for another host with no devices answers 404', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
175
|
+
const r = yield (0, loopbackRequest_1.default)(app).get('/xenon/api/node/10.9.9.9:4723/status').timeout(5000);
|
|
176
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
177
|
+
}));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -81,6 +81,35 @@ describe('bug-report route', () => {
|
|
|
81
81
|
const res = yield (0, loopbackRequest_1.default)(makeApp({ admin: true })).post('/sessions/sess-b/bug-report?mode=full');
|
|
82
82
|
(0, chai_1.expect)(res.status).to.equal(200);
|
|
83
83
|
}));
|
|
84
|
+
for (const [role, included] of [
|
|
85
|
+
['MEMBER', false],
|
|
86
|
+
['ADMIN', true],
|
|
87
|
+
['SUPER_ADMIN', true],
|
|
88
|
+
]) {
|
|
89
|
+
it(`${included ? 'adds' : 'leaves out'} the network capture for a ${role}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
const assemble = sinon_1.default.stub(BugReportService_1.BugReportService.prototype, 'assemble').resolves({
|
|
91
|
+
filename: 'bugreport-sess-1.zip',
|
|
92
|
+
manifest: { warnings: [] },
|
|
93
|
+
entries: [{ name: 'a.txt', source: { kind: 'buffer', data: Buffer.from('hi') } }],
|
|
94
|
+
cleanup: () => __awaiter(void 0, void 0, void 0, function* () { }),
|
|
95
|
+
});
|
|
96
|
+
const app = (0, express_1.default)();
|
|
97
|
+
app.use((req, _res, next) => {
|
|
98
|
+
req.auth = {
|
|
99
|
+
kind: 'user-session',
|
|
100
|
+
userId: 'u1',
|
|
101
|
+
role,
|
|
102
|
+
scopes: 'sessions,read',
|
|
103
|
+
teamIds: [],
|
|
104
|
+
};
|
|
105
|
+
next();
|
|
106
|
+
});
|
|
107
|
+
bug_report_1.default.register(app);
|
|
108
|
+
const res = yield (0, loopbackRequest_1.default)(app).post('/sessions/sess-1/bug-report?mode=full');
|
|
109
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
110
|
+
(0, chai_1.expect)(assemble.firstCall.args[0].includeNetwork).to.equal(included);
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
84
113
|
it('401 when the caller is not signed in', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
114
|
const res = yield (0, loopbackRequest_1.default)(makeApp({ signedIn: false })).post('/sessions/sess-1/bug-report?mode=full');
|
|
86
115
|
(0, chai_1.expect)(res.status).to.equal(401);
|
|
@@ -18,6 +18,7 @@ const sinon_1 = __importDefault(require("sinon"));
|
|
|
18
18
|
const typedi_1 = require("typedi");
|
|
19
19
|
const BugReportService_1 = require("../../../src/services/bug-report/BugReportService");
|
|
20
20
|
const prisma_1 = require("../../../src/prisma");
|
|
21
|
+
const InterceptorService_1 = require("../../../src/services/InterceptorService");
|
|
21
22
|
const FIXTURE_SESSION = {
|
|
22
23
|
id: 'sess-1',
|
|
23
24
|
status: 'failed',
|
|
@@ -60,4 +61,33 @@ describe('BugReportService', () => {
|
|
|
60
61
|
(0, chai_1.expect)(bundle.entries.find((e) => e.name === 'ai-summary.txt')).to.exist;
|
|
61
62
|
yield bundle.cleanup();
|
|
62
63
|
}));
|
|
64
|
+
describe('the network capture', () => {
|
|
65
|
+
// A live capture: InterceptorService answers for the session.
|
|
66
|
+
const HAR = { log: { version: '1.2', entries: [] } };
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
sinon_1.default.stub(prisma_1.prisma.session, 'findUnique').resolves(FIXTURE_SESSION);
|
|
69
|
+
sinon_1.default.stub(prisma_1.prisma.sessionLog, 'findMany').resolves([]);
|
|
70
|
+
const interceptor = typedi_1.Container.get(InterceptorService_1.InterceptorService);
|
|
71
|
+
sinon_1.default.stub(interceptor, 'isActive').returns(true);
|
|
72
|
+
sinon_1.default.stub(interceptor, 'exportHar').returns(HAR);
|
|
73
|
+
});
|
|
74
|
+
it('is left out unless asked for, and the manifest says why', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const bundle = yield typedi_1.Container.get(BugReportService_1.BugReportService).assemble({
|
|
76
|
+
sessionId: 'sess-1',
|
|
77
|
+
mode: 'full',
|
|
78
|
+
});
|
|
79
|
+
(0, chai_1.expect)(bundle.entries.find((e) => e.name === 'network.har')).to.equal(undefined);
|
|
80
|
+
(0, chai_1.expect)(bundle.manifest.warnings).to.include('network capture left out: only admins can download it');
|
|
81
|
+
yield bundle.cleanup();
|
|
82
|
+
}));
|
|
83
|
+
it('is added when asked for', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const bundle = yield typedi_1.Container.get(BugReportService_1.BugReportService).assemble({
|
|
85
|
+
sessionId: 'sess-1',
|
|
86
|
+
mode: 'full',
|
|
87
|
+
includeNetwork: true,
|
|
88
|
+
});
|
|
89
|
+
(0, chai_1.expect)(bundle.entries.find((e) => e.name === 'network.har')).to.exist;
|
|
90
|
+
yield bundle.cleanup();
|
|
91
|
+
}));
|
|
92
|
+
});
|
|
63
93
|
});
|
|
@@ -0,0 +1,134 @@
|
|
|
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 loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
19
|
+
const index_1 = require("../../src/app/index");
|
|
20
|
+
const config_1 = require("../../src/config");
|
|
21
|
+
const scratch_database_1 = require("../helpers/scratch-database");
|
|
22
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
23
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
24
|
+
const own_node_id_1 = require("../helpers/own-node-id");
|
|
25
|
+
const AndroidDeviceManager_1 = __importDefault(require("../../src/device-managers/AndroidDeviceManager"));
|
|
26
|
+
/**
|
|
27
|
+
* Device control's answers say what happened: a missing field is a 400, not
|
|
28
|
+
* a 500 from deep inside the server.
|
|
29
|
+
*/
|
|
30
|
+
describe('device control answers', () => {
|
|
31
|
+
(0, scratch_database_1.useScratchDatabase)();
|
|
32
|
+
(0, own_node_id_1.useOwnNodeId)();
|
|
33
|
+
const UDID = 'R5CT32ABCDE';
|
|
34
|
+
let app;
|
|
35
|
+
let saved;
|
|
36
|
+
before(() => {
|
|
37
|
+
app = (0, express_1.default)();
|
|
38
|
+
app.use('/xenon', (0, index_1.createRouter)({ bindHostOrIp: '127.0.0.1', enableDashboard: true }));
|
|
39
|
+
});
|
|
40
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
|
+
saved = config_1.config.authDisabled;
|
|
42
|
+
config_1.config.authDisabled = true;
|
|
43
|
+
// The store, not a row: which store the process uses depends on the specs
|
|
44
|
+
// that ran before this one.
|
|
45
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
46
|
+
findDevice: (filter) => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
return (filter === null || filter === void 0 ? void 0 : filter.udid) === UDID
|
|
48
|
+
? {
|
|
49
|
+
udid: UDID,
|
|
50
|
+
host: 'http://127.0.0.1:4723',
|
|
51
|
+
nodeId: own_node_id_1.OWN_NODE_ID,
|
|
52
|
+
platform: 'android',
|
|
53
|
+
busy: false,
|
|
54
|
+
session_id: null,
|
|
55
|
+
teamId: null,
|
|
56
|
+
}
|
|
57
|
+
: undefined;
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
}));
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
config_1.config.authDisabled = saved;
|
|
63
|
+
sinon_1.default.restore();
|
|
64
|
+
});
|
|
65
|
+
// Most routes answered an unknown udid with the text "Device not found",
|
|
66
|
+
// which a JSON client couldn't parse; two answered a different JSON shape.
|
|
67
|
+
for (const [method, action] of [
|
|
68
|
+
['get', 'screenshot'],
|
|
69
|
+
['post', 'tap'],
|
|
70
|
+
['get', 'display'],
|
|
71
|
+
['get', 'appium-session'],
|
|
72
|
+
['get', 'logs'],
|
|
73
|
+
]) {
|
|
74
|
+
it(`${method.toUpperCase()} ${action} answers an unknown device with JSON`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
75
|
+
const r = yield (0, loopbackRequest_1.default)(app)[method](`/xenon/api/control/NO-SUCH-PHONE/${action}`)
|
|
76
|
+
.send({ x: 1, y: 2 })
|
|
77
|
+
.timeout(5000);
|
|
78
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
79
|
+
(0, chai_1.expect)(r.type).to.equal('application/json');
|
|
80
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'not_found', message: 'Device not found' });
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
describe('POST /api/control/:udid/install-repository-app', () => {
|
|
84
|
+
for (const [what, body] of [
|
|
85
|
+
['no body', {}],
|
|
86
|
+
['an empty appId', { appId: '' }],
|
|
87
|
+
['an appId that is not a string', { appId: 42 }],
|
|
88
|
+
]) {
|
|
89
|
+
it(`answers 400 for ${what}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
91
|
+
.post(`/xenon/api/control/${UDID}/install-repository-app`)
|
|
92
|
+
.send(body)
|
|
93
|
+
.timeout(5000);
|
|
94
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
95
|
+
(0, chai_1.expect)(r.body).to.deep.equal({ error: 'bad_request', message: 'appId is required' });
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
it('answers 404 for an app the library does not have', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
100
|
+
.post(`/xenon/api/control/${UDID}/install-repository-app`)
|
|
101
|
+
.send({ appId: '6f1d2c3b-0000-4000-8000-000000000000' })
|
|
102
|
+
.timeout(5000);
|
|
103
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* Android's live logs answered 200 when adb failed, with the error inside
|
|
109
|
+
* `logs` as if it were a log line, and the session log stored it as one.
|
|
110
|
+
*/
|
|
111
|
+
describe('AndroidDeviceManager.getLogs', () => {
|
|
112
|
+
afterEach(() => sinon_1.default.restore());
|
|
113
|
+
it('fails when adb fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
const manager = new AndroidDeviceManager_1.default({});
|
|
115
|
+
sinon_1.default.stub(manager, 'getAdb').resolves({
|
|
116
|
+
adbInstance: { adbExec: sinon_1.default.stub().rejects(new Error('device offline')) },
|
|
117
|
+
});
|
|
118
|
+
const err = yield manager.getLogs('R5CT32ABCDE').then(() => null, (e) => e);
|
|
119
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('device offline');
|
|
120
|
+
}));
|
|
121
|
+
it('fails when adb is not available', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
122
|
+
const manager = new AndroidDeviceManager_1.default({});
|
|
123
|
+
sinon_1.default.stub(manager, 'getAdb').resolves({ adbInstance: undefined });
|
|
124
|
+
const err = yield manager.getLogs('R5CT32ABCDE').then(() => null, (e) => e);
|
|
125
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('ADB is not available');
|
|
126
|
+
}));
|
|
127
|
+
it('returns the lines when adb answers', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
|
+
const manager = new AndroidDeviceManager_1.default({});
|
|
129
|
+
sinon_1.default.stub(manager, 'getAdb').resolves({
|
|
130
|
+
adbInstance: { adbExec: sinon_1.default.stub().resolves('10-04 09:00:00.000 1 1 I Tag: hello') },
|
|
131
|
+
});
|
|
132
|
+
(0, chai_1.expect)(yield manager.getLogs('R5CT32ABCDE')).to.include('hello');
|
|
133
|
+
}));
|
|
134
|
+
});
|