@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,114 @@
|
|
|
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 swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
|
|
18
|
+
const index_1 = require("../../src/app/index");
|
|
19
|
+
const swagger_1 = require("../../src/app/swagger");
|
|
20
|
+
const IPluginArgs_1 = require("../../src/interfaces/IPluginArgs");
|
|
21
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
22
|
+
const expressRoutes_1 = require("../helpers/expressRoutes");
|
|
23
|
+
/**
|
|
24
|
+
* The API reference at /xenon/api-docs is the spec in src/app/openapi/*.yaml.
|
|
25
|
+
* It had drifted: 61 routes the server served were missing from it, 10 it
|
|
26
|
+
* described were gone, and the page showed only 46 of its 100 paths,
|
|
27
|
+
* because tsc dropped the JSDoc comments the spec was written in.
|
|
28
|
+
*
|
|
29
|
+
* These checks keep it whole: every route served under /xenon/api is
|
|
30
|
+
* documented and every documented route is served, the spec is valid
|
|
31
|
+
* OpenAPI 3, and each operation carries what a reader needs.
|
|
32
|
+
*/
|
|
33
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
34
|
+
// Served but deliberately outside the API reference.
|
|
35
|
+
const NOT_API = new Set([
|
|
36
|
+
'GET /xenon/*', // the dashboard's own pages (client-side routing)
|
|
37
|
+
'GET /xenon/api-docs.json', // this spec itself
|
|
38
|
+
]);
|
|
39
|
+
/** "GET /xenon/api/control/:udid/tap" -> "GET /xenon/api/control/{}/tap" */
|
|
40
|
+
const shape = (route) => route.replace(/:[A-Za-z_][\w]*/g, '{}').replace(/\{[^}]+\}/g, '{}');
|
|
41
|
+
function operations() {
|
|
42
|
+
var _a;
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const [path, item] of Object.entries((_a = swagger_1.swaggerSpec.paths) !== null && _a !== void 0 ? _a : {})) {
|
|
45
|
+
for (const method of METHODS) {
|
|
46
|
+
const op = item[method];
|
|
47
|
+
if (op)
|
|
48
|
+
out.push({ key: `${method.toUpperCase()} /xenon${path}`, op });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
describe('the API reference (OpenAPI)', () => {
|
|
54
|
+
// A hub's node-only routes (/api/node/sessions/...) exist only with `hub`.
|
|
55
|
+
const served = new Set([
|
|
56
|
+
// The plugin mounts this router at /xenon.
|
|
57
|
+
...(0, expressRoutes_1.routesOf)((0, index_1.createRouter)(Object.assign(Object.assign({}, IPluginArgs_1.DefaultPluginArgs), { hub: 'http://hub.example:4723' })), '/xenon'),
|
|
58
|
+
]
|
|
59
|
+
.filter((r) => r.includes(' /xenon/') && !NOT_API.has(r))
|
|
60
|
+
.map(shape));
|
|
61
|
+
const documented = new Set(operations().map(({ key }) => shape(key)));
|
|
62
|
+
it('documents every route the server serves', () => {
|
|
63
|
+
const missing = [...served].filter((r) => !documented.has(r)).sort();
|
|
64
|
+
(0, chai_1.expect)(missing, `served but not documented:\n ${missing.join('\n ')}`).to.deep.equal([]);
|
|
65
|
+
});
|
|
66
|
+
it('documents no route the server no longer serves', () => {
|
|
67
|
+
const stale = [...documented].filter((r) => !served.has(r)).sort();
|
|
68
|
+
(0, chai_1.expect)(stale, `documented but not served:\n ${stale.join('\n ')}`).to.deep.equal([]);
|
|
69
|
+
});
|
|
70
|
+
it('is valid OpenAPI 3', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
+
yield swagger_parser_1.default.validate(JSON.parse(JSON.stringify(swagger_1.swaggerSpec)));
|
|
72
|
+
}));
|
|
73
|
+
it("carries the package's version", () => {
|
|
74
|
+
(0, chai_1.expect)(swagger_1.swaggerSpec.info.version).to.equal(package_json_1.default.version);
|
|
75
|
+
});
|
|
76
|
+
it('gives every operation a summary, a description, a known tag and a unique id', () => {
|
|
77
|
+
var _a, _b, _c, _d;
|
|
78
|
+
const tags = new Set(((_a = swagger_1.swaggerSpec.tags) !== null && _a !== void 0 ? _a : []).map((t) => t.name));
|
|
79
|
+
const ids = new Map();
|
|
80
|
+
const problems = [];
|
|
81
|
+
for (const { key, op } of operations()) {
|
|
82
|
+
if (!op.summary)
|
|
83
|
+
problems.push(`${key}: no summary`);
|
|
84
|
+
if (!op.description)
|
|
85
|
+
problems.push(`${key}: no description`);
|
|
86
|
+
if (!((_b = op.tags) === null || _b === void 0 ? void 0 : _b.length))
|
|
87
|
+
problems.push(`${key}: no tag`);
|
|
88
|
+
for (const t of (_c = op.tags) !== null && _c !== void 0 ? _c : [])
|
|
89
|
+
if (!tags.has(t))
|
|
90
|
+
problems.push(`${key}: unknown tag ${t}`);
|
|
91
|
+
if (!op.operationId)
|
|
92
|
+
problems.push(`${key}: no operationId`);
|
|
93
|
+
else if (ids.has(op.operationId))
|
|
94
|
+
problems.push(`${key}: operationId ${op.operationId} also on ${ids.get(op.operationId)}`);
|
|
95
|
+
else
|
|
96
|
+
ids.set(op.operationId, key);
|
|
97
|
+
const codes = Object.keys((_d = op.responses) !== null && _d !== void 0 ? _d : {});
|
|
98
|
+
if (!codes.some((c) => /^[23]\d\d$/.test(c) || c === '101'))
|
|
99
|
+
problems.push(`${key}: no success response`);
|
|
100
|
+
const isPublic = Array.isArray(op.security) && op.security.length === 0;
|
|
101
|
+
if (!isPublic && !codes.includes('401'))
|
|
102
|
+
problems.push(`${key}: authenticated but no 401`);
|
|
103
|
+
}
|
|
104
|
+
(0, chai_1.expect)(problems, problems.join('\n')).to.deep.equal([]);
|
|
105
|
+
});
|
|
106
|
+
it('uses every tag it declares', () => {
|
|
107
|
+
var _a;
|
|
108
|
+
const used = new Set(operations().flatMap(({ op }) => { var _a; return (_a = op.tags) !== null && _a !== void 0 ? _a : []; }));
|
|
109
|
+
const unused = ((_a = swagger_1.swaggerSpec.tags) !== null && _a !== void 0 ? _a : [])
|
|
110
|
+
.map((t) => t.name)
|
|
111
|
+
.filter((t) => !used.has(t));
|
|
112
|
+
(0, chai_1.expect)(unused).to.deep.equal([]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -67,6 +67,48 @@ describe('profile router', () => {
|
|
|
67
67
|
(0, chai_1.expect)(r.status).to.equal(400);
|
|
68
68
|
(0, chai_1.expect)(r.body.error).to.match(/cannot widen/);
|
|
69
69
|
}));
|
|
70
|
+
it("POST /profile/tokens can't widen past the credential creating it", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
+
const create = sinon_1.default
|
|
72
|
+
.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
|
|
73
|
+
.resolves({ id: 'k3', raw: 'x' });
|
|
74
|
+
// An admin's read-only key: the role would allow devices, the key does not.
|
|
75
|
+
const app = appWithAuth({ userId: 'u1', role: 'ADMIN', scopes: 'read' });
|
|
76
|
+
const widened = yield (0, loopbackRequest_1.default)(app)
|
|
77
|
+
.post('/profile/tokens')
|
|
78
|
+
.send({ name: 'CI', scopes: ['devices', 'read'] });
|
|
79
|
+
(0, chai_1.expect)(widened.status).to.equal(400);
|
|
80
|
+
// The role's default (devices, sessions, read) is wider than the key too.
|
|
81
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).post('/profile/tokens').send({ name: 'CI' })).status).to.equal(400);
|
|
82
|
+
const narrow = yield (0, loopbackRequest_1.default)(app)
|
|
83
|
+
.post('/profile/tokens')
|
|
84
|
+
.send({ name: 'CI', scopes: ['read'] });
|
|
85
|
+
(0, chai_1.expect)(narrow.status).to.equal(201);
|
|
86
|
+
(0, chai_1.expect)(create.calledOnce).to.equal(true);
|
|
87
|
+
(0, chai_1.expect)(create.firstCall.args[0].scopes).to.deep.equal(['read']);
|
|
88
|
+
}));
|
|
89
|
+
it('POST /profile/tokens lets a super admin ask for a narrower token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
90
|
+
const create = sinon_1.default
|
|
91
|
+
.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
|
|
92
|
+
.resolves({ id: 'k4', raw: 'x' });
|
|
93
|
+
const app = appWithAuth({
|
|
94
|
+
userId: 'u1',
|
|
95
|
+
role: 'SUPER_ADMIN',
|
|
96
|
+
scopes: 'admin,devices,sessions,read',
|
|
97
|
+
});
|
|
98
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
99
|
+
.post('/profile/tokens')
|
|
100
|
+
.send({ name: 'CI', scopes: ['devices'] });
|
|
101
|
+
(0, chai_1.expect)(r.status).to.equal(201);
|
|
102
|
+
(0, chai_1.expect)(create.firstCall.args[0].scopes).to.deep.equal(['devices']);
|
|
103
|
+
}));
|
|
104
|
+
it('POST /profile/tokens refuses a scope that does not exist', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const app = appWithAuth({ userId: 'u1', role: 'SUPER_ADMIN', scopes: 'admin' });
|
|
106
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
107
|
+
.post('/profile/tokens')
|
|
108
|
+
.send({ name: 'CI', scopes: ['root'] });
|
|
109
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
110
|
+
(0, chai_1.expect)(r.body.error).to.equal('scopes must be some of read, sessions, devices, admin');
|
|
111
|
+
}));
|
|
70
112
|
it('POST /profile/tokens accepts a future expiresAt and forwards it to the service', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
113
|
var _a;
|
|
72
114
|
const create = sinon_1.default.stub(typedi_1.Container.get(ApiKeyService_1.ApiKeyService), 'create')
|
|
@@ -36,4 +36,53 @@ describe('rateLimitMiddleware', () => {
|
|
|
36
36
|
mw(req, res, next);
|
|
37
37
|
(0, chai_1.expect)(res.status.calledWith(429)).to.be.true;
|
|
38
38
|
});
|
|
39
|
+
describe('signed-in users and bearer tokens', () => {
|
|
40
|
+
// Through 2.12 only API keys were limited: a cookie session or a bearer
|
|
41
|
+
// token, which carry no key, could send without limit.
|
|
42
|
+
const userReq = (kind, userId, method = 'POST', path = '/control/u1/tap') => ({ auth: { kind, userId, rateLimit: 3 }, method, path });
|
|
43
|
+
for (const kind of ['user-session', 'bearer']) {
|
|
44
|
+
it(`limits a ${kind} caller's changes to its budget`, () => {
|
|
45
|
+
const next = sinon_1.default.stub();
|
|
46
|
+
const mw = (0, rateLimitMiddleware_1.rateLimitMiddleware)();
|
|
47
|
+
const res = mockRes();
|
|
48
|
+
for (let i = 0; i < 4; i++)
|
|
49
|
+
mw(userReq(kind, `u-${kind}`), res, next);
|
|
50
|
+
(0, chai_1.expect)(next.callCount).to.equal(3);
|
|
51
|
+
(0, chai_1.expect)(res.status.calledWith(429)).to.equal(true);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
it("shares one budget across a user's sessions and tokens", () => {
|
|
55
|
+
const next = sinon_1.default.stub();
|
|
56
|
+
const mw = (0, rateLimitMiddleware_1.rateLimitMiddleware)();
|
|
57
|
+
const res = mockRes();
|
|
58
|
+
mw(userReq('user-session', 'dana'), res, next);
|
|
59
|
+
mw(userReq('bearer', 'dana'), res, next);
|
|
60
|
+
mw(userReq('user-session', 'dana'), res, next);
|
|
61
|
+
mw(userReq('bearer', 'dana'), res, next);
|
|
62
|
+
(0, chai_1.expect)(next.callCount).to.equal(3);
|
|
63
|
+
});
|
|
64
|
+
it('keeps users apart', () => {
|
|
65
|
+
const next = sinon_1.default.stub();
|
|
66
|
+
const mw = (0, rateLimitMiddleware_1.rateLimitMiddleware)();
|
|
67
|
+
for (let i = 0; i < 3; i++)
|
|
68
|
+
mw(userReq('user-session', 'dana'), mockRes(), next);
|
|
69
|
+
mw(userReq('user-session', 'lee'), mockRes(), next);
|
|
70
|
+
(0, chai_1.expect)(next.callCount).to.equal(4);
|
|
71
|
+
});
|
|
72
|
+
it('gives their reads ten times the budget', () => {
|
|
73
|
+
const next = sinon_1.default.stub();
|
|
74
|
+
const mw = (0, rateLimitMiddleware_1.rateLimitMiddleware)();
|
|
75
|
+
const res = mockRes();
|
|
76
|
+
for (let i = 0; i < 31; i++)
|
|
77
|
+
mw(userReq('user-session', 'reader', 'GET', '/devices'), res, next);
|
|
78
|
+
(0, chai_1.expect)(next.callCount).to.equal(30);
|
|
79
|
+
});
|
|
80
|
+
it('leaves single-use tickets unlimited', () => {
|
|
81
|
+
const next = sinon_1.default.stub();
|
|
82
|
+
const mw = (0, rateLimitMiddleware_1.rateLimitMiddleware)();
|
|
83
|
+
for (let i = 0; i < 5; i++)
|
|
84
|
+
mw(userReq('stream-ticket', 'dana', 'GET', '/control/u1/stream'), mockRes(), next);
|
|
85
|
+
(0, chai_1.expect)(next.callCount).to.equal(5);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
39
88
|
});
|
|
@@ -493,6 +493,86 @@ describe('RecordingOrchestrator.stop', () => {
|
|
|
493
493
|
(0, chai_1.expect)(store.finalize.firstCall.args[1].status).to.equal('FAILED');
|
|
494
494
|
(0, chai_1.expect)(leaveDeviceFn.calledOnceWith('U1')).to.equal(true);
|
|
495
495
|
}));
|
|
496
|
+
// A second stop, or a stop after one phone's source ended, finalized every
|
|
497
|
+
// row again: it overwrote ended_at, the duration and fail_reason
|
|
498
|
+
// (`source_ended` was lost), and let go of the phone a second time.
|
|
499
|
+
it('leaves phones whose recording already ended as they are', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
500
|
+
const finished = {
|
|
501
|
+
id: 'r1',
|
|
502
|
+
device_udid: 'U1',
|
|
503
|
+
device_host: '127.0.0.1',
|
|
504
|
+
file_path: '/nonexistent/r1.mp4',
|
|
505
|
+
status: 'STOPPED',
|
|
506
|
+
duration_ms: 26000,
|
|
507
|
+
size_bytes: 4200000,
|
|
508
|
+
fail_reason: 'source_ended',
|
|
509
|
+
started_at: new Date(Date.now() - 60000),
|
|
510
|
+
};
|
|
511
|
+
const store = {
|
|
512
|
+
listGroup: sinon_1.default.stub().resolves([
|
|
513
|
+
finished,
|
|
514
|
+
{
|
|
515
|
+
id: 'r2',
|
|
516
|
+
device_udid: 'U2',
|
|
517
|
+
device_host: '127.0.0.1',
|
|
518
|
+
file_path: '/nonexistent/r2.mp4',
|
|
519
|
+
status: 'RECORDING',
|
|
520
|
+
started_at: new Date(Date.now() - 60000),
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
id: 'r3',
|
|
524
|
+
device_udid: 'U3',
|
|
525
|
+
device_host: '127.0.0.1',
|
|
526
|
+
file_path: '/nonexistent/r3.mp4',
|
|
527
|
+
status: 'FAILED',
|
|
528
|
+
fail_reason: 'ffmpeg_stop_failed',
|
|
529
|
+
started_at: new Date(Date.now() - 60000),
|
|
530
|
+
},
|
|
531
|
+
]),
|
|
532
|
+
finalize: sinon_1.default.stub().resolves({}),
|
|
533
|
+
};
|
|
534
|
+
const videoPipeline = {
|
|
535
|
+
startRecording: sinon_1.default.stub(),
|
|
536
|
+
stopRecording: sinon_1.default.stub().resolves('/tmp/x.mp4'),
|
|
537
|
+
stopComposite: sinon_1.default.stub().resolves(null),
|
|
538
|
+
};
|
|
539
|
+
const { orch, leaveDeviceFn } = makeOrch({ store, videoPipeline });
|
|
540
|
+
const result = yield orch.stop('grp-1');
|
|
541
|
+
(0, chai_1.expect)(store.finalize.getCalls().map((c) => c.args[0])).to.deep.equal(['r2']);
|
|
542
|
+
(0, chai_1.expect)(videoPipeline.stopRecording.getCalls().map((c) => c.args[0])).to.deep.equal(['r2']);
|
|
543
|
+
(0, chai_1.expect)(leaveDeviceFn.getCalls().map((c) => c.args[0])).to.deep.equal(['U2']);
|
|
544
|
+
// The answer still names every phone, the finished ones as they were.
|
|
545
|
+
(0, chai_1.expect)(result.recordings.map((r) => [r.id, r.status])).to.deep.equal([
|
|
546
|
+
['r1', 'STOPPED'],
|
|
547
|
+
['r2', 'FAILED'], // nothing on disk in this test, so it can't be STOPPED
|
|
548
|
+
['r3', 'FAILED'],
|
|
549
|
+
]);
|
|
550
|
+
(0, chai_1.expect)(result.recordings[0]).to.include({ durationMs: 26000, sizeBytes: 4200000 });
|
|
551
|
+
}));
|
|
552
|
+
it('leaves a recording another path is finalizing right now to that path', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
553
|
+
const store = {
|
|
554
|
+
listGroup: sinon_1.default.stub().resolves([
|
|
555
|
+
{
|
|
556
|
+
id: 'r1',
|
|
557
|
+
device_udid: 'U1',
|
|
558
|
+
device_host: '127.0.0.1',
|
|
559
|
+
file_path: '/nonexistent/r1.mp4',
|
|
560
|
+
status: 'RECORDING',
|
|
561
|
+
started_at: new Date(Date.now() - 1000),
|
|
562
|
+
},
|
|
563
|
+
]),
|
|
564
|
+
finalize: sinon_1.default.stub().resolves({}),
|
|
565
|
+
};
|
|
566
|
+
const videoPipeline = {
|
|
567
|
+
startRecording: sinon_1.default.stub(),
|
|
568
|
+
stopRecording: sinon_1.default.stub().resolves('/tmp/x.mp4'),
|
|
569
|
+
stopComposite: sinon_1.default.stub().resolves(null),
|
|
570
|
+
};
|
|
571
|
+
const { orch } = makeOrch({ store, videoPipeline });
|
|
572
|
+
orch.finalizing.add('r1'); // its source ended a moment ago
|
|
573
|
+
yield orch.stop('grp-1');
|
|
574
|
+
(0, chai_1.expect)(store.finalize.called).to.equal(false);
|
|
575
|
+
}));
|
|
496
576
|
});
|
|
497
577
|
describe('RecordingOrchestrator.recoverOnBoot', () => {
|
|
498
578
|
// compositeOutputPath and per-device paths resolve through ArtifactStore.
|
|
@@ -57,6 +57,7 @@ const stream_1 = require("stream");
|
|
|
57
57
|
const typedi_1 = require("typedi");
|
|
58
58
|
const recordings_1 = __importStar(require("../../src/app/routers/recordings"));
|
|
59
59
|
const recording_store_1 = require("../../src/services/recording/recording-store");
|
|
60
|
+
const proof_bundle_1 = require("../../src/services/recording/proof-bundle");
|
|
60
61
|
const annotation_render_1 = require("../../src/services/recording/annotation-render");
|
|
61
62
|
const deviceService = __importStar(require("../../src/data-service/device-service"));
|
|
62
63
|
const recordingFiles = __importStar(require("../../src/services/recording/recordingFiles"));
|
|
@@ -1017,4 +1018,40 @@ describe('recordings library routes', () => {
|
|
|
1017
1018
|
}));
|
|
1018
1019
|
});
|
|
1019
1020
|
});
|
|
1021
|
+
// composite.mp4 said `Accept-Ranges: bytes` and then sent the whole file,
|
|
1022
|
+
// with 200, for every request: a player couldn't seek in a long composite.
|
|
1023
|
+
describe('GET /recordings/:groupId/composite.mp4', () => {
|
|
1024
|
+
let file;
|
|
1025
|
+
beforeEach(() => {
|
|
1026
|
+
rows = [rec({ id: 'a', group_id: 'g1' })];
|
|
1027
|
+
file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-composite-')), 'composite.mp4');
|
|
1028
|
+
fs.writeFileSync(file, '0123456789');
|
|
1029
|
+
const bundle = typedi_1.Container.get(proof_bundle_1.ProofBundleService);
|
|
1030
|
+
sinon_1.default.stub(bundle, 'compositeAllowed').returns(true);
|
|
1031
|
+
sinon_1.default.stub(bundle, 'resolveCompositeFile').resolves(file);
|
|
1032
|
+
});
|
|
1033
|
+
it('sends the whole video without a Range', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1034
|
+
const res = yield (0, loopbackRequest_1.default)(buildApp(alice)).get('/xenon/api/recordings/g1/composite.mp4');
|
|
1035
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
1036
|
+
(0, chai_1.expect)(res.headers['content-type']).to.match(/^video\/mp4/);
|
|
1037
|
+
(0, chai_1.expect)(res.headers['accept-ranges']).to.equal('bytes');
|
|
1038
|
+
(0, chai_1.expect)(String(res.body)).to.equal('0123456789');
|
|
1039
|
+
}));
|
|
1040
|
+
it('answers a Range with 206 and only those bytes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1041
|
+
const res = yield (0, loopbackRequest_1.default)(buildApp(alice))
|
|
1042
|
+
.get('/xenon/api/recordings/g1/composite.mp4')
|
|
1043
|
+
.set('Range', 'bytes=2-5');
|
|
1044
|
+
(0, chai_1.expect)(res.status).to.equal(206);
|
|
1045
|
+
(0, chai_1.expect)(res.headers['content-range']).to.equal('bytes 2-5/10');
|
|
1046
|
+
(0, chai_1.expect)(String(res.body)).to.equal('2345');
|
|
1047
|
+
}));
|
|
1048
|
+
it('answers a Range past the end with 416, as JSON', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1049
|
+
const res = yield (0, loopbackRequest_1.default)(buildApp(alice))
|
|
1050
|
+
.get('/xenon/api/recordings/g1/composite.mp4')
|
|
1051
|
+
.set('Range', 'bytes=50-60');
|
|
1052
|
+
(0, chai_1.expect)(res.status).to.equal(416);
|
|
1053
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ error: 'range_not_satisfiable' });
|
|
1054
|
+
(0, chai_1.expect)(res.headers['content-range']).to.equal('bytes */10');
|
|
1055
|
+
}));
|
|
1056
|
+
});
|
|
1020
1057
|
});
|
|
@@ -52,6 +52,7 @@ const express_1 = __importDefault(require("express"));
|
|
|
52
52
|
const loopbackRequest_1 = __importDefault(require("../helpers/loopbackRequest"));
|
|
53
53
|
const reservation_1 = __importDefault(require("../../src/app/routers/reservation"));
|
|
54
54
|
const deviceService = __importStar(require("../../src/data-service/device-service"));
|
|
55
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
55
56
|
const prisma_1 = require("../../src/prisma");
|
|
56
57
|
const authMiddleware_1 = require("../../src/middleware/authMiddleware");
|
|
57
58
|
/**
|
|
@@ -116,9 +117,9 @@ describe('reservations respect the team boundary', () => {
|
|
|
116
117
|
let reserveDevice;
|
|
117
118
|
let releaseReservation;
|
|
118
119
|
beforeEach(() => {
|
|
119
|
-
sinon_1.default
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
121
|
+
findDevice: (f) => __awaiter(void 0, void 0, void 0, function* () { return f.udid in TEAM_OF && f.host === HOST ? device(f.udid) : null; }),
|
|
122
|
+
});
|
|
122
123
|
sinon_1.default
|
|
123
124
|
.stub(deviceService, 'getReservedDevices')
|
|
124
125
|
.resolves([SHARED, PHONE_A, PHONE_B].map(device));
|
|
@@ -33,7 +33,8 @@ function usersApp(auth) {
|
|
|
33
33
|
const app = (0, express_1.default)();
|
|
34
34
|
app.use(express_1.default.json());
|
|
35
35
|
app.use((req, _res, next) => {
|
|
36
|
-
|
|
36
|
+
// A dashboard admin's scopes: /users also needs the admin scope.
|
|
37
|
+
req.auth = Object.assign({ scopes: 'admin,devices,sessions,read' }, auth);
|
|
37
38
|
next();
|
|
38
39
|
});
|
|
39
40
|
app.use('/users', (0, users_1.usersRouter)());
|
|
@@ -172,7 +172,7 @@ describe('stream/ticket carries the user identity, not the credential', () => {
|
|
|
172
172
|
it("404s a udid that isn't a device, and mints nothing", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
173
173
|
const res = yield (0, loopbackRequest_1.default)(yield loopback.serve(buildApp({ userId: ALICE_USER }))).post('/xenon/api/control/NO-SUCH-DEVICE/stream/ticket');
|
|
174
174
|
(0, chai_1.expect)(res.status).to.equal(404);
|
|
175
|
-
(0, chai_1.expect)(res.
|
|
175
|
+
(0, chai_1.expect)(res.body).to.deep.equal({ error: 'not_found', message: 'Device not found' });
|
|
176
176
|
}));
|
|
177
177
|
it('401s an unauthenticated mint', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
178
|
const res = yield mint({});
|
|
@@ -25,7 +25,8 @@ function appWithAuth(auth) {
|
|
|
25
25
|
const app = (0, express_1.default)();
|
|
26
26
|
app.use(express_1.default.json());
|
|
27
27
|
app.use((req, _res, next) => {
|
|
28
|
-
|
|
28
|
+
// A dashboard admin's scopes, unless the test names its own.
|
|
29
|
+
req.auth = Object.assign({ scopes: 'admin,devices,sessions,read' }, auth);
|
|
29
30
|
next();
|
|
30
31
|
});
|
|
31
32
|
app.use('/users', (0, users_1.usersRouter)());
|
|
@@ -33,6 +34,18 @@ function appWithAuth(auth) {
|
|
|
33
34
|
}
|
|
34
35
|
describe('/users router', () => {
|
|
35
36
|
afterEach(() => sinon_1.default.restore());
|
|
37
|
+
it("refuses an admin's key that lacks the admin scope", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
38
|
+
const list = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'listUsers').resolves([]);
|
|
39
|
+
const create = sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'createUser');
|
|
40
|
+
const app = appWithAuth({ userId: 'admin', role: 'ADMIN', scopes: 'read' });
|
|
41
|
+
(0, chai_1.expect)((yield (0, loopbackRequest_1.default)(app).get('/users')).status).to.equal(403);
|
|
42
|
+
const r = yield (0, loopbackRequest_1.default)(app)
|
|
43
|
+
.post('/users')
|
|
44
|
+
.send({ email: 'new@x.local', name: 'New', role: 'MEMBER' });
|
|
45
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
46
|
+
(0, chai_1.expect)(list.called).to.equal(false);
|
|
47
|
+
(0, chai_1.expect)(create.called).to.equal(false);
|
|
48
|
+
}));
|
|
36
49
|
it('GET /users returns the listUsers result, filtered by caller role', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
50
|
sinon_1.default.stub(typedi_1.Container.get(UserService_1.UserService), 'listUsers').resolves([{ id: 'u1' }]);
|
|
38
51
|
const app = appWithAuth({ userId: 'admin', role: 'ADMIN' });
|
|
@@ -0,0 +1,90 @@
|
|
|
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 WDAClient_1 = require("../../src/device-managers/ios/WDAClient");
|
|
19
|
+
/**
|
|
20
|
+
* Device control on an iPhone answered 200 for a clipboard write, a lock or
|
|
21
|
+
* an unlock that WebDriverAgent refused: WDAClient caught the error and only
|
|
22
|
+
* logged it at debug level. A key the phone doesn't have was also "pressed"
|
|
23
|
+
* successfully. The failures now reach the caller.
|
|
24
|
+
*/
|
|
25
|
+
describe('WDAClient reports what WebDriverAgent refused', () => {
|
|
26
|
+
const PHONE = '00008110-00084CE80E51401E';
|
|
27
|
+
let send;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
send = sinon_1.default.stub(WDAClient_1.WDAClient.prototype, 'sendWDACommand');
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => sinon_1.default.restore());
|
|
32
|
+
// WDA answered and refused: axios puts its answer on `response`.
|
|
33
|
+
const refuse = (path) => send.withArgs(PHONE, 'post', path).rejects(Object.assign(new Error(`WDA request failed: ${path} 500`), {
|
|
34
|
+
response: { status: 500, data: { value: { error: 'unknown error' } } },
|
|
35
|
+
}));
|
|
36
|
+
for (const [name, path, call] of [
|
|
37
|
+
['setClipboard', '/wda/setPasteboard', (c) => c.setClipboard(PHONE, 'hello')],
|
|
38
|
+
['lock', '/wda/lock', (c) => c.lock(PHONE)],
|
|
39
|
+
['unlock', '/wda/unlock', (c) => c.unlock(PHONE)],
|
|
40
|
+
]) {
|
|
41
|
+
it(`${name} fails when WebDriverAgent refuses`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
42
|
+
refuse(path);
|
|
43
|
+
const err = yield call(new WDAClient_1.WDAClient()).then(() => null, (e) => e);
|
|
44
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.include(path);
|
|
45
|
+
}));
|
|
46
|
+
it(`${name} succeeds when WebDriverAgent accepts`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
47
|
+
send.resolves({});
|
|
48
|
+
yield call(new WDAClient_1.WDAClient());
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
it('pressKey refuses a key the iPhone does not have', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
refuse('/wda/pressButton');
|
|
53
|
+
const err = yield new WDAClient_1.WDAClient().pressKey(PHONE, 'F13').then(() => null, (e) => e);
|
|
54
|
+
(0, chai_1.expect)(err).to.be.instanceOf(WDAClient_1.UnsupportedKeyError);
|
|
55
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('The key "F13" isn\'t available on an iPhone.');
|
|
56
|
+
}));
|
|
57
|
+
it('pressKey reports WebDriverAgent being unreachable, not an unsupported key', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
58
|
+
send
|
|
59
|
+
.withArgs(PHONE, 'post', '/wda/pressButton')
|
|
60
|
+
.rejects(Object.assign(new Error('connect ECONNREFUSED 127.0.0.1:8100'), { code: 'ECONNREFUSED' }));
|
|
61
|
+
const err = yield new WDAClient_1.WDAClient().pressKey(PHONE, 'f13').then(() => null, (e) => e);
|
|
62
|
+
(0, chai_1.expect)(err).not.to.be.instanceOf(WDAClient_1.UnsupportedKeyError);
|
|
63
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.include('ECONNREFUSED');
|
|
64
|
+
}));
|
|
65
|
+
it('pressKey fails when a hardware button and its fallback both fail', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
66
|
+
refuse('/wda/pressButton');
|
|
67
|
+
refuse('/wda/homescreen');
|
|
68
|
+
const err = yield new WDAClient_1.WDAClient().pressKey(PHONE, 'home').then(() => null, (e) => e);
|
|
69
|
+
(0, chai_1.expect)(err).to.be.an('error');
|
|
70
|
+
(0, chai_1.expect)(err).not.to.be.instanceOf(WDAClient_1.UnsupportedKeyError);
|
|
71
|
+
}));
|
|
72
|
+
it('pressKey still falls back to the home screen on an old WebDriverAgent', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
73
|
+
refuse('/wda/pressButton');
|
|
74
|
+
send.withArgs(PHONE, 'post', '/wda/homescreen').resolves({});
|
|
75
|
+
yield new WDAClient_1.WDAClient().pressKey(PHONE, 'home');
|
|
76
|
+
}));
|
|
77
|
+
it('pressKey fails a keyboard key only when both ways of typing it fail', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
refuse('/wda/keys');
|
|
79
|
+
send.withArgs(PHONE, 'post', '/wda/type').resolves({});
|
|
80
|
+
yield new WDAClient_1.WDAClient().pressKey(PHONE, 'enter');
|
|
81
|
+
send.withArgs(PHONE, 'post', '/wda/type').rejects(new Error('WDA request failed: /wda/type'));
|
|
82
|
+
const err = yield new WDAClient_1.WDAClient().pressKey(PHONE, 'enter').then(() => null, (e) => e);
|
|
83
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.include('/wda/type');
|
|
84
|
+
}));
|
|
85
|
+
it('pressKey fails a volume button WebDriverAgent refuses', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
86
|
+
refuse('/wda/pressButton');
|
|
87
|
+
const err = yield new WDAClient_1.WDAClient().pressKey(PHONE, 'volume_up').then(() => null, (e) => e);
|
|
88
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.include('/wda/pressButton');
|
|
89
|
+
}));
|
|
90
|
+
});
|