@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,232 @@
|
|
|
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 grid_1 = __importDefault(require("../../src/app/routers/grid"));
|
|
21
|
+
const reservation_1 = __importDefault(require("../../src/app/routers/reservation"));
|
|
22
|
+
const device_store_1 = require("../../src/data-service/device-store");
|
|
23
|
+
/**
|
|
24
|
+
* Blocking and reserving a phone act on exactly the phone named, by udid and
|
|
25
|
+
* host, and a reservation is changed only by whoever took it, or an admin.
|
|
26
|
+
*/
|
|
27
|
+
const UDID = 'R5CT32ABCDE';
|
|
28
|
+
const HOST = 'http://192.168.1.100:4723';
|
|
29
|
+
const OTHER_HOST = 'http://192.168.1.101:4723';
|
|
30
|
+
const HOUR = 60 * 60 * 1000;
|
|
31
|
+
const DANA = { userId: 'usr_dana', role: 'MEMBER', scopes: 'devices,sessions,read' };
|
|
32
|
+
const LEE = { userId: 'usr_lee', role: 'MEMBER', scopes: 'devices,sessions,read' };
|
|
33
|
+
const ADMIN = { userId: 'usr_admin', role: 'ADMIN', scopes: 'admin,devices,sessions,read' };
|
|
34
|
+
/** A store with the phone on HOST, and each phone write recorded. */
|
|
35
|
+
function fakeStore(phone = {}) {
|
|
36
|
+
const rows = [
|
|
37
|
+
Object.assign({ udid: UDID, host: HOST, name: 'Pixel 7', platform: 'android', teamId: null }, phone),
|
|
38
|
+
{ udid: UDID, host: OTHER_HOST, name: 'Pixel 7', platform: 'android', teamId: null },
|
|
39
|
+
];
|
|
40
|
+
const writes = [];
|
|
41
|
+
const store = {
|
|
42
|
+
findDevice: (filter) => __awaiter(this, void 0, void 0, function* () { var _a; return (_a = rows.find((r) => r.udid === filter.udid && r.host === filter.host)) !== null && _a !== void 0 ? _a : null; }),
|
|
43
|
+
updateDevice: (udid, host, data) => __awaiter(this, void 0, void 0, function* () {
|
|
44
|
+
writes.push({ udid, host, data });
|
|
45
|
+
const row = rows.find((r) => r.udid === udid && r.host === host);
|
|
46
|
+
if (row)
|
|
47
|
+
Object.assign(row, data);
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns(store);
|
|
51
|
+
return { rows, writes };
|
|
52
|
+
}
|
|
53
|
+
function appAs(caller, mount) {
|
|
54
|
+
const app = (0, express_1.default)();
|
|
55
|
+
app.use(express_1.default.json());
|
|
56
|
+
app.use((req, _res, next) => {
|
|
57
|
+
req.auth = Object.assign({ kind: 'user-session', rateLimit: 1000 }, caller);
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
mount(app);
|
|
61
|
+
return app;
|
|
62
|
+
}
|
|
63
|
+
describe('device allocation routes', () => {
|
|
64
|
+
afterEach(() => sinon_1.default.restore());
|
|
65
|
+
describe('POST /block and /unblock', () => {
|
|
66
|
+
const app = () => appAs(ADMIN, (a) => {
|
|
67
|
+
const router = express_1.default.Router();
|
|
68
|
+
grid_1.default.register(router, { bindHostOrIp: '127.0.0.1' });
|
|
69
|
+
a.use(router);
|
|
70
|
+
});
|
|
71
|
+
for (const path of ['/block', '/unblock']) {
|
|
72
|
+
for (const [what, body] of [
|
|
73
|
+
['an empty body', {}],
|
|
74
|
+
['no host', { udid: UDID }],
|
|
75
|
+
['no udid', { host: HOST }],
|
|
76
|
+
['a udid that is not a string', { udid: 42, host: HOST }],
|
|
77
|
+
]) {
|
|
78
|
+
it(`${path} answers 400 for ${what}, and changes nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
79
|
+
const { writes } = fakeStore();
|
|
80
|
+
const r = yield (0, loopbackRequest_1.default)(app()).post(path).send(body).timeout(5000);
|
|
81
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
82
|
+
(0, chai_1.expect)(r.body).to.deep.equal({
|
|
83
|
+
success: false,
|
|
84
|
+
error: 'bad_request',
|
|
85
|
+
message: 'udid and host are required',
|
|
86
|
+
});
|
|
87
|
+
(0, chai_1.expect)(writes).to.deep.equal([]);
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
it(`${path} answers 404 for a host the phone is not on`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
const { writes } = fakeStore();
|
|
92
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
93
|
+
.post(path)
|
|
94
|
+
.send({ udid: UDID, host: 'http://10.0.0.9:4723' })
|
|
95
|
+
.timeout(5000);
|
|
96
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
97
|
+
(0, chai_1.expect)(writes).to.deep.equal([]);
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
it('blocks the phone on the host named, not another row of its udid', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
101
|
+
const { writes } = fakeStore();
|
|
102
|
+
const r = yield (0, loopbackRequest_1.default)(app())
|
|
103
|
+
.post('/block')
|
|
104
|
+
.send({ udid: UDID, host: OTHER_HOST })
|
|
105
|
+
.timeout(5000);
|
|
106
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
107
|
+
(0, chai_1.expect)(writes).to.deep.equal([{ udid: UDID, host: OTHER_HOST, data: { userBlocked: true } }]);
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
describe('/reservation', () => {
|
|
111
|
+
const app = (caller) => appAs(caller, (a) => a.use('/reservation', reservation_1.default));
|
|
112
|
+
const reserved = (userId) => ({
|
|
113
|
+
reservedBy: 'Dana',
|
|
114
|
+
reservedByUserId: userId,
|
|
115
|
+
reservedUntil: Date.now() + HOUR,
|
|
116
|
+
reservationReason: 'release testing',
|
|
117
|
+
});
|
|
118
|
+
const hostPath = encodeURIComponent(HOST);
|
|
119
|
+
it('answers 404 when the phone is not on the host named, and reserves nothing', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
120
|
+
const { writes } = fakeStore();
|
|
121
|
+
const r = yield (0, loopbackRequest_1.default)(app(DANA))
|
|
122
|
+
.post('/reservation')
|
|
123
|
+
.send({ udid: UDID, host: 'http://10.0.0.9:4723', reservedBy: 'Dana', duration: '1h' })
|
|
124
|
+
.timeout(5000);
|
|
125
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
126
|
+
(0, chai_1.expect)(writes).to.deep.equal([]);
|
|
127
|
+
}));
|
|
128
|
+
it('records who reserved the phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
129
|
+
const { rows } = fakeStore();
|
|
130
|
+
const r = yield (0, loopbackRequest_1.default)(app(DANA))
|
|
131
|
+
.post('/reservation')
|
|
132
|
+
.send({ udid: UDID, host: HOST, reservedBy: 'Dana', duration: '1h' })
|
|
133
|
+
.timeout(5000);
|
|
134
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
135
|
+
(0, chai_1.expect)(rows[0]).to.include({ reservedBy: 'Dana', reservedByUserId: DANA.userId });
|
|
136
|
+
}));
|
|
137
|
+
it("refuses someone else's reservation even under the same name", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
138
|
+
fakeStore(reserved(DANA.userId));
|
|
139
|
+
const r = yield (0, loopbackRequest_1.default)(app(LEE))
|
|
140
|
+
.post('/reservation')
|
|
141
|
+
.send({ udid: UDID, host: HOST, reservedBy: 'Dana', duration: '1h' })
|
|
142
|
+
.timeout(5000);
|
|
143
|
+
(0, chai_1.expect)(r.status).to.equal(409);
|
|
144
|
+
}));
|
|
145
|
+
it('lets the holder renew their own reservation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
fakeStore(reserved(DANA.userId));
|
|
147
|
+
const r = yield (0, loopbackRequest_1.default)(app(DANA))
|
|
148
|
+
.post('/reservation')
|
|
149
|
+
.send({ udid: UDID, host: HOST, reservedBy: 'Dana', duration: '2h' })
|
|
150
|
+
.timeout(5000);
|
|
151
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
152
|
+
}));
|
|
153
|
+
for (const [what, call] of [
|
|
154
|
+
['release', (a) => (0, loopbackRequest_1.default)(a).delete(`/reservation/${UDID}/${hostPath}`)],
|
|
155
|
+
[
|
|
156
|
+
'extend',
|
|
157
|
+
(a) => (0, loopbackRequest_1.default)(a).post(`/reservation/${UDID}/${hostPath}/extend`).send({ duration: '1h' }),
|
|
158
|
+
],
|
|
159
|
+
]) {
|
|
160
|
+
it(`refuses to let another member ${what} it, and changes nothing`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
+
const { writes } = fakeStore(reserved(DANA.userId));
|
|
162
|
+
const r = yield call(app(LEE)).timeout(5000);
|
|
163
|
+
(0, chai_1.expect)(r.status).to.equal(403);
|
|
164
|
+
(0, chai_1.expect)(r.body).to.deep.equal({
|
|
165
|
+
success: false,
|
|
166
|
+
error: 'not_reservation_holder',
|
|
167
|
+
message: 'Only the person who reserved this device, or an admin, can change the reservation.',
|
|
168
|
+
});
|
|
169
|
+
(0, chai_1.expect)(writes).to.deep.equal([]);
|
|
170
|
+
}));
|
|
171
|
+
it(`lets the holder ${what} it`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
172
|
+
fakeStore(reserved(DANA.userId));
|
|
173
|
+
(0, chai_1.expect)((yield call(app(DANA)).timeout(5000)).status).to.equal(200);
|
|
174
|
+
}));
|
|
175
|
+
it(`lets an admin ${what} it`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
176
|
+
fakeStore(reserved(DANA.userId));
|
|
177
|
+
(0, chai_1.expect)((yield call(app(ADMIN)).timeout(5000)).status).to.equal(200);
|
|
178
|
+
}));
|
|
179
|
+
it(`lets anyone ${what} a reservation that names no user (taken before 2.13)`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
180
|
+
fakeStore(reserved(null));
|
|
181
|
+
(0, chai_1.expect)((yield call(app(LEE)).timeout(5000)).status).to.equal(200);
|
|
182
|
+
}));
|
|
183
|
+
it(`answers 404 for ${what} on a host the phone is not on`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
|
+
fakeStore(reserved(DANA.userId));
|
|
185
|
+
const a = app(DANA);
|
|
186
|
+
const r = what === 'release'
|
|
187
|
+
? yield (0, loopbackRequest_1.default)(a).delete(`/reservation/${UDID}/${encodeURIComponent('http://10.0.0.9:4723')}`)
|
|
188
|
+
: yield (0, loopbackRequest_1.default)(a)
|
|
189
|
+
.post(`/reservation/${UDID}/${encodeURIComponent('http://10.0.0.9:4723')}/extend`)
|
|
190
|
+
.send({ duration: '1h' });
|
|
191
|
+
(0, chai_1.expect)(r.status).to.equal(404);
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
it('keeps the holder when a reservation is extended', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
const { rows } = fakeStore(reserved(DANA.userId));
|
|
196
|
+
yield (0, loopbackRequest_1.default)(app(ADMIN))
|
|
197
|
+
.post(`/reservation/${UDID}/${hostPath}/extend`)
|
|
198
|
+
.send({ duration: '1h' })
|
|
199
|
+
.timeout(5000);
|
|
200
|
+
(0, chai_1.expect)(rows[0].reservedByUserId).to.equal(DANA.userId);
|
|
201
|
+
}));
|
|
202
|
+
for (const [what, duration] of [
|
|
203
|
+
['past 24 hours from now', 24 * HOUR],
|
|
204
|
+
['by a negative duration', -HOUR],
|
|
205
|
+
['by less than a minute', 1000],
|
|
206
|
+
]) {
|
|
207
|
+
it(`refuses to extend it ${what}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
208
|
+
const { writes } = fakeStore(reserved(DANA.userId));
|
|
209
|
+
const r = yield (0, loopbackRequest_1.default)(app(DANA))
|
|
210
|
+
.post(`/reservation/${UDID}/${hostPath}/extend`)
|
|
211
|
+
.send({ duration })
|
|
212
|
+
.timeout(5000);
|
|
213
|
+
(0, chai_1.expect)(r.status).to.equal(400);
|
|
214
|
+
(0, chai_1.expect)(writes).to.deep.equal([]);
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
it('extends it up to 24 hours from now', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
218
|
+
const { rows } = fakeStore(reserved(DANA.userId));
|
|
219
|
+
const r = yield (0, loopbackRequest_1.default)(app(DANA))
|
|
220
|
+
.post(`/reservation/${UDID}/${hostPath}/extend`)
|
|
221
|
+
.send({ duration: 22 * HOUR })
|
|
222
|
+
.timeout(5000);
|
|
223
|
+
(0, chai_1.expect)(r.status).to.equal(200);
|
|
224
|
+
(0, chai_1.expect)(rows[0].reservedUntil).to.be.at.most(Date.now() + 24 * HOUR);
|
|
225
|
+
}));
|
|
226
|
+
it('clears the holder when the reservation is released', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
227
|
+
const { rows } = fakeStore(reserved(DANA.userId));
|
|
228
|
+
yield (0, loopbackRequest_1.default)(app(DANA)).delete(`/reservation/${UDID}/${hostPath}`).timeout(5000);
|
|
229
|
+
(0, chai_1.expect)(rows[0]).to.include({ reservedBy: null, reservedByUserId: null, reservedUntil: null });
|
|
230
|
+
}));
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -284,16 +284,19 @@ describe('GET /healing/state/:strategy/:value — getSelectorStateByTuple', () =
|
|
|
284
284
|
findUniqueStub = sinon_1.default.stub(prisma_1.prisma.selectorState, 'findUnique');
|
|
285
285
|
});
|
|
286
286
|
afterEach(() => sinon_1.default.restore());
|
|
287
|
-
|
|
287
|
+
// Express has decoded the path params before the handler runs, so they
|
|
288
|
+
// arrive here plain. The handler used to decode them again, which made a
|
|
289
|
+
// selector containing `%` need encoding twice.
|
|
290
|
+
it('returns the row for the tuple Express decoded, a `%` included', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
288
291
|
findUniqueStub.resolves({
|
|
289
292
|
original_strategy: 'accessibility id',
|
|
290
|
-
original_selector: '
|
|
293
|
+
original_selector: "//*[@text='50% off']",
|
|
291
294
|
status: 'pending',
|
|
292
295
|
});
|
|
293
296
|
const { req, res, jsonStub } = mockReqRes({
|
|
294
297
|
params: {
|
|
295
|
-
strategy:
|
|
296
|
-
value:
|
|
298
|
+
strategy: 'accessibility id',
|
|
299
|
+
value: "//*[@text='50% off']",
|
|
297
300
|
},
|
|
298
301
|
});
|
|
299
302
|
yield (0, dashboard_1.getSelectorStateByTuple)(req, res);
|
|
@@ -302,7 +305,7 @@ describe('GET /healing/state/:strategy/:value — getSelectorStateByTuple', () =
|
|
|
302
305
|
where: {
|
|
303
306
|
original_strategy_original_selector: {
|
|
304
307
|
original_strategy: 'accessibility id',
|
|
305
|
-
original_selector: '
|
|
308
|
+
original_selector: "//*[@text='50% off']",
|
|
306
309
|
},
|
|
307
310
|
},
|
|
308
311
|
});
|
|
@@ -107,7 +107,10 @@ describe('POST /control/:udid/install-repository-app follows the app team rule',
|
|
|
107
107
|
const hidden = yield install(['team-a'], 'app-b');
|
|
108
108
|
const unknown = yield install(['team-a'], 'no-such-app');
|
|
109
109
|
(0, chai_1.expect)(hidden.status).to.equal(404);
|
|
110
|
-
(0, chai_1.expect)(hidden.
|
|
110
|
+
(0, chai_1.expect)(hidden.body).to.deep.equal({
|
|
111
|
+
error: 'not_found',
|
|
112
|
+
message: 'App not found in repository',
|
|
113
|
+
});
|
|
111
114
|
(0, chai_1.expect)(unknown.status).to.equal(hidden.status);
|
|
112
115
|
(0, chai_1.expect)(unknown.text).to.equal(hidden.text);
|
|
113
116
|
(0, chai_1.expect)(manager.installApp.called).to.equal(false);
|
|
@@ -67,13 +67,14 @@ describe('LeaseService', () => {
|
|
|
67
67
|
deleteMany: sinon_1.default.stub().resolves({ count: 0 }),
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
|
+
const phone = {
|
|
71
|
+
udid: 'u1', host: 'h1', platform: 'android', sdk: '14', name: 'Pixel 7', teamId: null,
|
|
72
|
+
};
|
|
70
73
|
storeStub = {
|
|
71
|
-
findAndLockDevice: sinon_1.default.stub().resolves(
|
|
72
|
-
udid: 'u1', host: 'h1', platform: 'android', sdk: '14', name: 'Pixel 7', teamId: null,
|
|
73
|
-
}),
|
|
74
|
+
findAndLockDevice: sinon_1.default.stub().resolves(phone),
|
|
74
75
|
updateDevice: sinon_1.default.stub().resolves(),
|
|
75
76
|
releaseLeaseLock: sinon_1.default.stub().resolves(true),
|
|
76
|
-
getDevices: sinon_1.default.stub().resolves([]),
|
|
77
|
+
getDevices: sinon_1.default.stub().resolves([phone]),
|
|
77
78
|
};
|
|
78
79
|
portClientStub = {
|
|
79
80
|
allocate: sinon_1.default.stub().resolves({ systemPort: 9001, chromedriverPort: 9002, mjpegServerPort: 9003 }),
|
|
@@ -0,0 +1,161 @@
|
|
|
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 express_1 = __importDefault(require("express"));
|
|
19
|
+
const loopbackRequest_1 = __importDefault(require("../../helpers/loopbackRequest"));
|
|
20
|
+
const sdk_leases_1 = require("../../../src/app/routers/sdk-leases");
|
|
21
|
+
const LeaseService_1 = require("../../../src/services/lease/LeaseService");
|
|
22
|
+
const device_store_1 = require("../../../src/data-service/device-store");
|
|
23
|
+
const prisma_1 = require("../../../src/prisma");
|
|
24
|
+
/**
|
|
25
|
+
* Which device a lease takes. Through 2.12 the lease took the first free
|
|
26
|
+
* device of the platform: one an admin had blocked, one reserved for someone
|
|
27
|
+
* else, or one failing its health check, and it dropped the `sdk` and
|
|
28
|
+
* `deviceName` filters. An unknown udid answered 409 "all busy" whenever any
|
|
29
|
+
* device of the platform existed.
|
|
30
|
+
*
|
|
31
|
+
* The router, LeaseService and the in-memory device store are real here.
|
|
32
|
+
* Only the port RPC and the lease rows are faked.
|
|
33
|
+
*/
|
|
34
|
+
// Unique, so the match is held to this suite's phones: other specs in the
|
|
35
|
+
// same process leave their own devices in the in-memory store.
|
|
36
|
+
const HOST = `lease-match-${Date.now()}`;
|
|
37
|
+
const FREE = 'MATCH-FREE';
|
|
38
|
+
const BLOCKED = 'MATCH-BLOCKED';
|
|
39
|
+
const RESERVED = 'MATCH-RESERVED';
|
|
40
|
+
const UNHEALTHY = 'MATCH-UNHEALTHY';
|
|
41
|
+
const OLD = 'MATCH-OLD';
|
|
42
|
+
const ALL = [FREE, BLOCKED, RESERVED, UNHEALTHY, OLD];
|
|
43
|
+
describe('SDK leases take only a device that is free to use', () => {
|
|
44
|
+
let store;
|
|
45
|
+
let previousStore;
|
|
46
|
+
let previousEnv;
|
|
47
|
+
let svc;
|
|
48
|
+
before(() => {
|
|
49
|
+
previousEnv = process.env.NODE_ENV;
|
|
50
|
+
process.env.NODE_ENV = 'test';
|
|
51
|
+
previousStore = device_store_1.DeviceStoreFactory._deviceStore;
|
|
52
|
+
device_store_1.DeviceStoreFactory._deviceStore = undefined;
|
|
53
|
+
store = device_store_1.DeviceStoreFactory.getStore();
|
|
54
|
+
(0, chai_1.expect)(store.constructor.name).to.equal('LokiDeviceStore');
|
|
55
|
+
});
|
|
56
|
+
after(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
for (const udid of ALL)
|
|
58
|
+
yield store.removeDevices({ udid });
|
|
59
|
+
device_store_1.DeviceStoreFactory._deviceStore = previousStore;
|
|
60
|
+
process.env.NODE_ENV = previousEnv;
|
|
61
|
+
}));
|
|
62
|
+
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
const phone = (udid, extra = {}) => (Object.assign({ udid, host: HOST, name: 'Pixel 7', platform: 'android', sdk: '14', teamId: null, busy: false, offline: false, userBlocked: false, healthStatus: 'Healthy' }, extra));
|
|
64
|
+
for (const udid of ALL)
|
|
65
|
+
yield store.removeDevices({ udid });
|
|
66
|
+
yield store.addDevices([
|
|
67
|
+
phone(BLOCKED, { userBlocked: true }),
|
|
68
|
+
phone(RESERVED, { reservedBy: 'Dana', reservedUntil: Date.now() + 60 * 60 * 1000 }),
|
|
69
|
+
phone(UNHEALTHY, { healthStatus: 'Unhealthy' }),
|
|
70
|
+
phone(OLD, { sdk: '12', name: 'Galaxy S9+' }),
|
|
71
|
+
phone(FREE),
|
|
72
|
+
]);
|
|
73
|
+
sinon_1.default.stub(prisma_1.prisma.lease, 'findMany').resolves([]);
|
|
74
|
+
let n = 0;
|
|
75
|
+
const db = {
|
|
76
|
+
lease: {
|
|
77
|
+
create: (_a) => __awaiter(void 0, [_a], void 0, function* ({ data }) { return (Object.assign(Object.assign({}, data), { id: `lse_${++n}` })); }),
|
|
78
|
+
update: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); }),
|
|
79
|
+
delete: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); }),
|
|
80
|
+
},
|
|
81
|
+
portLease: {
|
|
82
|
+
updateMany: () => __awaiter(void 0, void 0, void 0, function* () { return ({ count: 0 }); }),
|
|
83
|
+
deleteMany: () => __awaiter(void 0, void 0, void 0, function* () { return ({ count: 0 }); }),
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const ports = {
|
|
87
|
+
allocate: () => __awaiter(void 0, void 0, void 0, function* () { return ({ systemPort: 9001, chromedriverPort: 9002, mjpegServerPort: 9003 }); }),
|
|
88
|
+
};
|
|
89
|
+
svc = new LeaseService_1.LeaseService(db, store, ports, {
|
|
90
|
+
nodePairAuth: () => __awaiter(void 0, void 0, void 0, function* () { return ({ accessKey: 'k', token: 't' }); }),
|
|
91
|
+
});
|
|
92
|
+
}));
|
|
93
|
+
afterEach(() => sinon_1.default.restore());
|
|
94
|
+
function lease(filters) {
|
|
95
|
+
const app = (0, express_1.default)();
|
|
96
|
+
app.use(express_1.default.json());
|
|
97
|
+
app.use((req, _res, next) => {
|
|
98
|
+
req.auth = {
|
|
99
|
+
kind: 'bearer',
|
|
100
|
+
userId: 'usr_admin',
|
|
101
|
+
role: 'ADMIN',
|
|
102
|
+
scopes: 'admin,devices,sessions,read',
|
|
103
|
+
rateLimit: 100,
|
|
104
|
+
};
|
|
105
|
+
next();
|
|
106
|
+
});
|
|
107
|
+
app.use('/sdk/leases', (0, sdk_leases_1.makeRouter)({ leaseService: svc }));
|
|
108
|
+
return (0, loopbackRequest_1.default)(app)
|
|
109
|
+
.post('/sdk/leases')
|
|
110
|
+
.send({ filters: Object.assign({ platform: 'android', filterByHost: HOST }, filters) });
|
|
111
|
+
}
|
|
112
|
+
const busy = (udid) => __awaiter(void 0, void 0, void 0, function* () { var _a; return !!((_a = (yield store.findDevice({ udid, host: HOST }))) === null || _a === void 0 ? void 0 : _a.busy); });
|
|
113
|
+
it('skips blocked, reserved and unhealthy devices', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
114
|
+
const got = [];
|
|
115
|
+
for (let i = 0; i < 5; i++) {
|
|
116
|
+
const res = yield lease({ sdk: '14' });
|
|
117
|
+
if (res.status === 201)
|
|
118
|
+
got.push(res.body.device.udid);
|
|
119
|
+
}
|
|
120
|
+
(0, chai_1.expect)(got).to.deep.equal([FREE]);
|
|
121
|
+
for (const udid of [BLOCKED, RESERVED, UNHEALTHY])
|
|
122
|
+
(0, chai_1.expect)(yield busy(udid)).to.equal(false);
|
|
123
|
+
}));
|
|
124
|
+
for (const [what, udid] of [
|
|
125
|
+
['blocked', BLOCKED],
|
|
126
|
+
['reserved', RESERVED],
|
|
127
|
+
['unhealthy', UNHEALTHY],
|
|
128
|
+
]) {
|
|
129
|
+
it(`answers 409 for a device named by udid that is ${what}, and leaves it free`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
130
|
+
const res = yield lease({ udid });
|
|
131
|
+
(0, chai_1.expect)(res.status).to.equal(409);
|
|
132
|
+
(0, chai_1.expect)(res.body.error).to.equal('all_matching_busy');
|
|
133
|
+
(0, chai_1.expect)(yield busy(udid)).to.equal(false);
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
it('answers 404 for an unknown udid, though other devices of the platform exist', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
137
|
+
const res = yield lease({ udid: 'MATCH-NO-SUCH-PHONE' });
|
|
138
|
+
(0, chai_1.expect)(res.status).to.equal(404);
|
|
139
|
+
(0, chai_1.expect)(res.body).to.deep.equal({
|
|
140
|
+
error: 'no_matching_device',
|
|
141
|
+
message: 'No device matches the filters',
|
|
142
|
+
});
|
|
143
|
+
}));
|
|
144
|
+
it('applies the sdk filter', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
145
|
+
const res = yield lease({ sdk: '12' });
|
|
146
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(201);
|
|
147
|
+
(0, chai_1.expect)(res.body.device.udid).to.equal(OLD);
|
|
148
|
+
(0, chai_1.expect)((yield lease({ sdk: '9' })).status).to.equal(404);
|
|
149
|
+
}));
|
|
150
|
+
it('applies the deviceName filter, ignoring case', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
151
|
+
const res = yield lease({ deviceName: 'galaxy s9+' });
|
|
152
|
+
(0, chai_1.expect)(res.status, JSON.stringify(res.body)).to.equal(201);
|
|
153
|
+
(0, chai_1.expect)(res.body.device.udid).to.equal(OLD);
|
|
154
|
+
(0, chai_1.expect)((yield lease({ deviceName: 'Pixel 9' })).status).to.equal(404);
|
|
155
|
+
}));
|
|
156
|
+
it('ignores availability fields the client puts in the filter', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
157
|
+
const res = yield lease({ udid: BLOCKED, userBlocked: true, busy: true });
|
|
158
|
+
(0, chai_1.expect)(res.status).to.equal(409);
|
|
159
|
+
(0, chai_1.expect)(yield busy(BLOCKED)).to.equal(false);
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
@@ -45,17 +45,18 @@ describe('LeaseService — who may use a lease for a session', () => {
|
|
|
45
45
|
deleteMany: sinon_1.default.stub().resolves({ count: 0 }),
|
|
46
46
|
},
|
|
47
47
|
};
|
|
48
|
+
const phone = {
|
|
49
|
+
udid: 'u1',
|
|
50
|
+
host: 'h1',
|
|
51
|
+
platform: 'android',
|
|
52
|
+
sdk: '14',
|
|
53
|
+
name: 'Pixel 7',
|
|
54
|
+
teamId: null,
|
|
55
|
+
};
|
|
48
56
|
store = {
|
|
49
|
-
findAndLockDevice: sinon_1.default.stub().resolves(
|
|
50
|
-
udid: 'u1',
|
|
51
|
-
host: 'h1',
|
|
52
|
-
platform: 'android',
|
|
53
|
-
sdk: '14',
|
|
54
|
-
name: 'Pixel 7',
|
|
55
|
-
teamId: null,
|
|
56
|
-
}),
|
|
57
|
+
findAndLockDevice: sinon_1.default.stub().resolves(phone),
|
|
57
58
|
updateDevice: sinon_1.default.stub().resolves(),
|
|
58
|
-
getDevices: sinon_1.default.stub().resolves([]),
|
|
59
|
+
getDevices: sinon_1.default.stub().resolves([phone]),
|
|
59
60
|
};
|
|
60
61
|
const ports = { allocate: sinon_1.default.stub().resolves({ systemPort: 9001, mjpegServerPort: 9003 }) };
|
|
61
62
|
svc = new LeaseService_1.LeaseService(db, store, ports, {
|
|
@@ -0,0 +1,81 @@
|
|
|
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 OmniVisionService_1 = require("../../src/services/omni-vision/OmniVisionService");
|
|
19
|
+
const AIService_1 = require("../../src/services/AIService");
|
|
20
|
+
/**
|
|
21
|
+
* Device control's "Test locator" answered 200 with no matches when OCR or
|
|
22
|
+
* the AI call failed, which reads as "the locator finds nothing". The search
|
|
23
|
+
* now fails for a caller that asks it to (`throwOnError`). An Appium
|
|
24
|
+
* findElement still gets an empty result, which it turns into
|
|
25
|
+
* NoSuchElement, as before.
|
|
26
|
+
*/
|
|
27
|
+
describe('OmniVisionService search failures', () => {
|
|
28
|
+
const driver = { sessionId: 'manual_x', getScreenshot: sinon_1.default.stub() };
|
|
29
|
+
let service;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
service = new OmniVisionService_1.OmniVisionService();
|
|
32
|
+
driver.getScreenshot = sinon_1.default.stub().resolves('aGVsbG8=');
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => sinon_1.default.restore());
|
|
35
|
+
describe('findByText', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
sinon_1.default.stub(service, 'performOcr').rejects(new Error('OCR worker crashed'));
|
|
38
|
+
});
|
|
39
|
+
it('keeps answering an empty list for Appium', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
40
|
+
(0, chai_1.expect)(yield service.findByText(driver, 'Login')).to.deep.equal([]);
|
|
41
|
+
}));
|
|
42
|
+
it('fails when asked to', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
const err = yield service.findByText(driver, 'Login', { throwOnError: true }).then(() => null, (e) => e);
|
|
44
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('OCR worker crashed');
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
describe('findByIcon', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'visualFind').rejects(new Error('AI provider timed out'));
|
|
50
|
+
});
|
|
51
|
+
it('keeps answering null for Appium', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
(0, chai_1.expect)(yield service.findByIcon(driver, 'gear icon')).to.equal(null);
|
|
53
|
+
}));
|
|
54
|
+
it('fails when asked to', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
55
|
+
const err = yield service.findByIcon(driver, 'gear icon', { throwOnError: true }).then(() => null, (e) => e);
|
|
56
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('AI provider timed out');
|
|
57
|
+
}));
|
|
58
|
+
it('still answers null, without failing, when nothing matches', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
59
|
+
AIService_1.AI_SERVICE.visualFind.resolves(null);
|
|
60
|
+
(0, chai_1.expect)(yield service.findByIcon(driver, 'gear icon', { throwOnError: true })).to.equal(null);
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
describe('AIService.visualFind', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'initializeProvider');
|
|
66
|
+
});
|
|
67
|
+
it('answers null without a provider, or fails when asked to', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
68
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(false);
|
|
69
|
+
(0, chai_1.expect)(yield AIService_1.AI_SERVICE.visualFind('aGVsbG8=', 'gear icon')).to.equal(null);
|
|
70
|
+
const err = yield AIService_1.AI_SERVICE.visualFind('aGVsbG8=', 'gear icon', { throwOnError: true }).then(() => null, (e) => e);
|
|
71
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('No AI provider is configured');
|
|
72
|
+
}));
|
|
73
|
+
it('answers null when the provider fails, or fails when asked to', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
74
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'isEnabled').returns(true);
|
|
75
|
+
sinon_1.default.stub(AIService_1.AI_SERVICE, 'callProvider').rejects(new Error('429 quota exceeded'));
|
|
76
|
+
(0, chai_1.expect)(yield AIService_1.AI_SERVICE.visualFind('aGVsbG8=', 'gear icon')).to.equal(null);
|
|
77
|
+
const err = yield AIService_1.AI_SERVICE.visualFind('aGVsbG8=', 'gear icon', { throwOnError: true }).then(() => null, (e) => e);
|
|
78
|
+
(0, chai_1.expect)(err === null || err === void 0 ? void 0 : err.message).to.equal('429 quota exceeded');
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
});
|