@xenon-device-management/xenon 2.13.1 → 2.13.2
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 +1 -1
- package/lib/public/assets/{AnnotationOverlay-Cvpkk4r9.js → AnnotationOverlay-BXruClDk.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-Bhn5ZtVM.js → ApiKeyGate-qTS4LPLC.js} +1 -1
- package/lib/public/assets/{BugReportButton-BA6aEjTC.js → BugReportButton-BoGhlI7F.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-ChhHnpFQ.js → DeviceMosaicView-8J0oYqMM.js} +1 -1
- package/lib/public/assets/{EmptyState-CgzbuUvO.js → EmptyState-Cr17PdDv.js} +1 -1
- package/lib/public/assets/{FieldGroup-BHc_5qNc.js → FieldGroup-Bvn08svv.js} +1 -1
- package/lib/public/assets/{FilterMenu-D7OMGuRc.js → FilterMenu-DDSO8aDf.js} +1 -1
- package/lib/public/assets/{Menu-C173JJ0l.js → Menu-B-Qn2X3T.js} +1 -1
- package/lib/public/assets/{Modal-ChvVUI3B.js → Modal-Cg9V5JYU.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dhw4d_O6.js → RecordingPage-BTUs9VTW.js} +1 -1
- package/lib/public/assets/{RecordingsPage-z97ftGK-.js → RecordingsPage-DYcfxITK.js} +1 -1
- package/lib/public/assets/{SegmentedControl-DbXhBGZC.js → SegmentedControl-Zy7suhT5.js} +1 -1
- package/lib/public/assets/{SettingCard-C908GgiI.js → SettingCard-CWYmycoe.js} +1 -1
- package/lib/public/assets/{Table-B1DB_ZN-.js → Table-io9fwa98.js} +1 -1
- package/lib/public/assets/{activity-Css0xNvp.js → activity-Bdf_uIWl.js} +1 -1
- package/lib/public/assets/{ai-settings-BMQK3-yN.js → ai-settings-Bce5SJob.js} +1 -1
- package/lib/public/assets/{api-keys-CbJ42eDQ.js → api-keys-CPqtVpm3.js} +1 -1
- package/lib/public/assets/{apps-DW1pw2re.js → apps-BgZan0H9.js} +1 -1
- package/lib/public/assets/{arrow-left-Dpog9L0F.js → arrow-left-B27ckB31.js} +1 -1
- package/lib/public/assets/{arrow-right-CsBIlSIw.js → arrow-right-DOF8ITCz.js} +1 -1
- package/lib/public/assets/{arrow-up-right-z1tRQE4R.js → arrow-up-right-eFG5KTFS.js} +1 -1
- package/lib/public/assets/{auth-shell-mXiX5vNk.js → auth-shell-DlC3_27j.js} +2 -2
- package/lib/public/assets/{builds-page-BcFn23bE.js → builds-page-BuXpPfKo.js} +1 -1
- package/lib/public/assets/{button-Cq_zHETv.js → button-fIW8owMI.js} +1 -1
- package/lib/public/assets/{calendar-CKxABX9q.js → calendar-WNjmrF-a.js} +1 -1
- package/lib/public/assets/{check-C7xu-hGU.js → check-BdTh1Sq7.js} +1 -1
- package/lib/public/assets/{chevron-right-BiqfdCGA.js → chevron-right-B4BKlsqk.js} +1 -1
- package/lib/public/assets/{circle-check-CyBjjPxR.js → circle-check-zJU80_EF.js} +1 -1
- package/lib/public/assets/{circle-x-6gMRcx6z.js → circle-x-5Uwquo0h.js} +1 -1
- package/lib/public/assets/{clock-U1rDTT1h.js → clock-BtUprDsZ.js} +1 -1
- package/lib/public/assets/{copy-BIM0ZX0L.js → copy-DGSbZI3u.js} +1 -1
- package/lib/public/assets/{device-explorer-DSpAA_t9.js → device-explorer-C-KnhMHI.js} +1 -1
- package/lib/public/assets/{download-DDBRXnAZ.js → download-WG6l7BgD.js} +1 -1
- package/lib/public/assets/{forgot-password-Dpg7yxM8.js → forgot-password-DnudqFFo.js} +1 -1
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-BGBqohfQ.js → index-DI1fGIyj.js} +2 -2
- package/lib/public/assets/{index-BQ2luT7L.js → index-HDQVe1gi.js} +1 -1
- package/lib/public/assets/{input-BvB1cjHE.js → input-BjDRKCea.js} +1 -1
- package/lib/public/assets/{line-chart-m78VggnW.js → line-chart-C0zdgd0v.js} +1 -1
- package/lib/public/assets/{list-checks-cY7o1QTL.js → list-checks-BdW5CjM8.js} +1 -1
- package/lib/public/assets/{lock-loz8txQm.js → lock-Dy3L4I8E.js} +1 -1
- package/lib/public/assets/{login-BYOvz4lx.js → login-BHbY9CLG.js} +1 -1
- package/lib/public/assets/{maintenance-settings-BxbA7SkB.js → maintenance-settings-zDVlOMVK.js} +1 -1
- package/lib/public/assets/{monitor-C4HPHb6s.js → monitor-8xjkeHHi.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BkdKtCzi.js → mouse-pointer-2-CXdKMXQ8.js} +1 -1
- package/lib/public/assets/{network-C5e0MIWo.js → network-Cj8JOKON.js} +1 -1
- package/lib/public/assets/{overview-tlu8ijGd.js → overview-CTg7F0im.js} +1 -1
- package/lib/public/assets/{page-header-FXPlo6az.js → page-header--aTyJXq6.js} +1 -1
- package/lib/public/assets/{play-CWdk0_Sy.js → play-B-FodgHV.js} +1 -1
- package/lib/public/assets/{plus-B2xCnntV.js → plus-CsIk10Cq.js} +1 -1
- package/lib/public/assets/{profile-page-D080qzr4.js → profile-page-DkFKkMfl.js} +1 -1
- package/lib/public/assets/{recording-group-store-B75435xR.js → recording-group-store-WVM3hoXn.js} +1 -1
- package/lib/public/assets/{reset-password-D3u4Mbvn.js → reset-password-I2-zqLlI.js} +1 -1
- package/lib/public/assets/{runbook-page-hPRZMy7s.js → runbook-page-DJTVCJX7.js} +1 -1
- package/lib/public/assets/{select-12RThAXc.js → select-DvCSv25z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-uQwvV7EQ.js → selector-detail-redirect-BLVfMJs_.js} +1 -1
- package/lib/public/assets/{selector-health-page-CThoxDFO.js → selector-health-page-oqadRokn.js} +1 -1
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +4 -0
- package/lib/public/assets/{settings-C1VIluuT.js → settings-MsVU2ukE.js} +1 -1
- package/lib/public/assets/{stat-tile-B02WEte8.js → stat-tile-DV9Ix-4O.js} +1 -1
- package/lib/public/assets/{tablet-Dj30VGZl.js → tablet-6b_SEN2X.js} +1 -1
- package/lib/public/assets/{teams-uL4fjTDR.js → teams-BjZyfuHh.js} +1 -1
- package/lib/public/assets/{trash-2-B54e-TfY.js → trash-2-CTZpPSdd.js} +1 -1
- package/lib/public/assets/{upload-B3k8WCSP.js → upload-P3XzwQUb.js} +1 -1
- package/lib/public/assets/{use-builds-data-BFn90NH3.js → use-builds-data-1cOCMf-z.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DL2TmWZ0.js → use-password-reset-mode-DgF9w088.js} +1 -1
- package/lib/public/assets/{users-DjQSucKb.js → users-CiNTKUiz.js} +1 -1
- package/lib/public/assets/{users-CqBuxMlX.js → users-DMF9Mk5H.js} +1 -1
- package/lib/public/assets/{video-off-T0rlt0GA.js → video-off-CHp1KU_T.js} +1 -1
- package/lib/public/assets/{webhook-settings-DAHgnJyZ.js → webhook-settings-DiS8geIm.js} +1 -1
- package/lib/public/assets/{zap-gBHqutZV.js → zap-DRz0tDq5.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +0 -25
- package/lib/src/app/routers/control.js +19 -4
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- package/lib/src/data-service/device-service.js +40 -0
- package/lib/src/data-service/device-store.js +4 -1
- package/lib/src/data-service/prisma-store.js +2 -1
- package/lib/src/device-utils.js +63 -13
- package/lib/src/middleware/deviceAccessGuard.js +63 -9
- package/lib/src/services/ServerManager.js +3 -0
- package/lib/src/services/SessionLifecycleService.js +16 -4
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- package/lib/src/services/lease/activeLeases.js +37 -0
- package/lib/src/services/recording/busy-precheck.js +24 -3
- package/lib/test/unit/device-access-lease.spec.js +270 -0
- package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -0
- package/lib/test/unit/partial-config-file.spec.js +100 -0
- package/lib/test/unit/schema-required-args.spec.js +21 -16
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +0 -25
- package/scripts/generate-types-from-schema.js +6 -0
- package/scripts/lib/always-present-plugin-args.js +39 -0
- package/lib/public/assets/session-detail-page-Bc02YjMm.js +0 -4
|
@@ -13,6 +13,7 @@ exports.makeTicketActorAuthorizer = makeTicketActorAuthorizer;
|
|
|
13
13
|
const manualLock_1 = require("../recording/manualLock");
|
|
14
14
|
const deviceAccessPolicy_1 = require("./deviceAccessPolicy");
|
|
15
15
|
const deviceClaims_1 = require("../../data-service/deviceClaims");
|
|
16
|
+
const leaseHold_1 = require("./leaseHold");
|
|
16
17
|
/**
|
|
17
18
|
* Build the ownership check for a caller authenticated by a stream ticket.
|
|
18
19
|
*
|
|
@@ -53,6 +54,10 @@ function makeTicketActorAuthorizer(deps) {
|
|
|
53
54
|
const sessionOwnerUserId = device.busy && device.session_id && !(0, manualLock_1.isManualLock)(device.session_id)
|
|
54
55
|
? yield deps.resolveSessionOwner(device.session_id)
|
|
55
56
|
: null;
|
|
57
|
+
// As deviceAccessGuard: a leased phone is its lease holder's.
|
|
58
|
+
const lease = deps.findActiveLease && deps.resolveLeaseHolder && !actor.isAdmin
|
|
59
|
+
? yield (0, leaseHold_1.leaseHoldFor)({ udid, host: device.host }, deps.findActiveLease, deps.resolveLeaseHolder)
|
|
60
|
+
: null;
|
|
56
61
|
return (0, deviceAccessPolicy_1.evaluateDeviceAccess)({
|
|
57
62
|
udid,
|
|
58
63
|
// heldHere, as deviceAccessGuard reads it.
|
|
@@ -63,6 +68,7 @@ function makeTicketActorAuthorizer(deps) {
|
|
|
63
68
|
actorUserId: actor.actorId,
|
|
64
69
|
actorApiKeyId: actor.apiKeyId,
|
|
65
70
|
isAdmin: !!actor.isAdmin,
|
|
71
|
+
lease,
|
|
66
72
|
}).allow;
|
|
67
73
|
});
|
|
68
74
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.leaseKey = void 0;
|
|
13
|
+
exports.activeLeasesByDevice = activeLeasesByDevice;
|
|
14
|
+
exports.activeLeaseOn = activeLeaseOn;
|
|
15
|
+
const prisma_1 = require("../../prisma");
|
|
16
|
+
const leaseKey = (udid, host) => `${udid}@${host}`;
|
|
17
|
+
exports.leaseKey = leaseKey;
|
|
18
|
+
/** Every live lease, keyed by `leaseKey(udid, host)`. */
|
|
19
|
+
function activeLeasesByDevice() {
|
|
20
|
+
return __awaiter(this, arguments, void 0, function* (db = prisma_1.prisma, now = Date.now()) {
|
|
21
|
+
const rows = yield db.lease.findMany({
|
|
22
|
+
where: { status: 'active', expiresAt: { gte: now } },
|
|
23
|
+
select: { id: true, deviceUdid: true, deviceHost: true, actorId: true },
|
|
24
|
+
});
|
|
25
|
+
return new Map(rows.map((l) => [(0, exports.leaseKey)(l.deviceUdid, l.deviceHost), l]));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** The live lease on one phone, or null. */
|
|
29
|
+
function activeLeaseOn(udid_1, host_1) {
|
|
30
|
+
return __awaiter(this, arguments, void 0, function* (udid, host, db = prisma_1.prisma, now = Date.now()) {
|
|
31
|
+
var _a;
|
|
32
|
+
return ((_a = (yield db.lease.findFirst({
|
|
33
|
+
where: { deviceUdid: udid, deviceHost: host, status: 'active', expiresAt: { gte: now } },
|
|
34
|
+
select: { id: true, deviceUdid: true, deviceHost: true, actorId: true },
|
|
35
|
+
}))) !== null && _a !== void 0 ? _a : null);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -23,6 +23,9 @@ const typedi_1 = require("typedi");
|
|
|
23
23
|
const device_store_1 = require("../../data-service/device-store");
|
|
24
24
|
const manualLock_1 = require("./manualLock");
|
|
25
25
|
const deviceAccessPolicy_1 = require("../device-access/deviceAccessPolicy");
|
|
26
|
+
const leaseHold_1 = require("../device-access/leaseHold");
|
|
27
|
+
const SessionOwnerResolver_1 = require("../device-access/SessionOwnerResolver");
|
|
28
|
+
const activeLeases_1 = require("../lease/activeLeases");
|
|
26
29
|
const recording_store_1 = require("./recording-store");
|
|
27
30
|
/**
|
|
28
31
|
* Atomic multi-UDID busy detection. Used by RecordingOrchestrator before
|
|
@@ -39,14 +42,18 @@ const recording_store_1 = require("./recording-store");
|
|
|
39
42
|
* - missing/null → unknown (defensive fallback).
|
|
40
43
|
*/
|
|
41
44
|
let BusyPrecheck = class BusyPrecheck {
|
|
42
|
-
//
|
|
45
|
+
// Every parameter must emit `Object` metadata: TypeDI injects constructor
|
|
43
46
|
// parameters by type, and a function type made it look up `Function` in the
|
|
44
47
|
// container, failing every recording start.
|
|
45
|
-
constructor(store, recordings) {
|
|
48
|
+
constructor(store, recordings, leases) {
|
|
46
49
|
this.storeProvider = store ? () => store : () => device_store_1.DeviceStoreFactory.getStore();
|
|
47
50
|
this.recordings = recordings !== null && recordings !== void 0 ? recordings : {
|
|
48
51
|
isRecording: (udid) => typedi_1.Container.get(recording_store_1.RecordingStore).isRecording(udid),
|
|
49
52
|
};
|
|
53
|
+
this.leases = leases !== null && leases !== void 0 ? leases : {
|
|
54
|
+
find: (udid, host) => (0, activeLeases_1.activeLeaseOn)(udid, host),
|
|
55
|
+
holderOf: (actorId) => typedi_1.Container.get(SessionOwnerResolver_1.SessionOwnerResolver).leaseHolderOf(actorId),
|
|
56
|
+
};
|
|
50
57
|
}
|
|
51
58
|
/**
|
|
52
59
|
* @param udids Devices to inspect.
|
|
@@ -79,6 +86,20 @@ let BusyPrecheck = class BusyPrecheck {
|
|
|
79
86
|
out.push({ udid, reason: 'recording_other_group' });
|
|
80
87
|
continue;
|
|
81
88
|
}
|
|
89
|
+
// A leased phone is its lease holder's, whatever `busy` says (a lease
|
|
90
|
+
// locks with `busy` alone, which other writes can clear). Anyone else
|
|
91
|
+
// is refused; for the holder the lease itself is no conflict, though a
|
|
92
|
+
// session or hold on the phone still is, below. A failed lookup throws:
|
|
93
|
+
// the start fails rather than records someone's leased phone.
|
|
94
|
+
const lease = yield (0, leaseHold_1.leaseHoldFor)(device, this.leases.find, this.leases.holderOf);
|
|
95
|
+
if (lease) {
|
|
96
|
+
if (!(0, deviceAccessPolicy_1.isLeaseHolder)(lease, actorId, actorApiKeyId)) {
|
|
97
|
+
out.push({ udid, reason: 'leased' });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!device.session_id)
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
82
103
|
if (!device.busy)
|
|
83
104
|
continue;
|
|
84
105
|
// This server's own hold, else, on a hub, a node phone's preview hold
|
|
@@ -112,5 +133,5 @@ let BusyPrecheck = class BusyPrecheck {
|
|
|
112
133
|
exports.BusyPrecheck = BusyPrecheck;
|
|
113
134
|
exports.BusyPrecheck = BusyPrecheck = __decorate([
|
|
114
135
|
(0, typedi_1.Service)(),
|
|
115
|
-
__metadata("design:paramtypes", [Object, Object])
|
|
136
|
+
__metadata("design:paramtypes", [Object, Object, Object])
|
|
116
137
|
], BusyPrecheck);
|
|
@@ -0,0 +1,270 @@
|
|
|
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 deviceAccessPolicy_1 = require("../../src/services/device-access/deviceAccessPolicy");
|
|
20
|
+
const deviceAccessGuard_1 = require("../../src/middleware/deviceAccessGuard");
|
|
21
|
+
const ticketActorAccess_1 = require("../../src/services/device-access/ticketActorAccess");
|
|
22
|
+
const streamStartConflict_1 = require("../../src/app/routers/streamStartConflict");
|
|
23
|
+
const busy_precheck_1 = require("../../src/services/recording/busy-precheck");
|
|
24
|
+
// A phone an SDK lease holds is its lease holder's. The guard used to read
|
|
25
|
+
// only `busy` and `session_id`: a freshly leased phone (busy, no session) was
|
|
26
|
+
// refused to everyone but admins, its holder included, and a leased phone
|
|
27
|
+
// whose `busy` had been cleared (a lease-bound session's release, the idle
|
|
28
|
+
// sweeper) was open to anyone.
|
|
29
|
+
const UDID = 'DEV-1';
|
|
30
|
+
const HOST = 'http://hub:4723';
|
|
31
|
+
const ALICE = 'usr_alice';
|
|
32
|
+
const ALICE_KEY = 'key_alice';
|
|
33
|
+
const BOB = 'usr_bob';
|
|
34
|
+
const BOB_KEY = 'key_bob';
|
|
35
|
+
const aliceLease = { actorId: ALICE_KEY, holderUserId: ALICE };
|
|
36
|
+
function input(over = {}) {
|
|
37
|
+
return Object.assign({ udid: UDID, busy: true, sessionId: null, sessionOwnerUserId: null, actorUserId: BOB, actorApiKeyId: BOB_KEY, isAdmin: false, lease: aliceLease }, over);
|
|
38
|
+
}
|
|
39
|
+
describe('device access on a leased phone', () => {
|
|
40
|
+
describe('evaluateDeviceAccess', () => {
|
|
41
|
+
it('lets the lease holder through by the key that created it', () => {
|
|
42
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ actorUserId: 'usr_other', actorApiKeyId: ALICE_KEY }))).to.deep.equal({ allow: true });
|
|
43
|
+
});
|
|
44
|
+
it('lets the lease holder through with any credential of theirs', () => {
|
|
45
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ actorUserId: ALICE, actorApiKeyId: undefined }))).to.deep.equal({ allow: true });
|
|
46
|
+
});
|
|
47
|
+
it('lets a user through on a lease made with their bearer token (actorId is the user id)', () => {
|
|
48
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({
|
|
49
|
+
actorUserId: ALICE,
|
|
50
|
+
actorApiKeyId: undefined,
|
|
51
|
+
lease: { actorId: ALICE, holderUserId: ALICE },
|
|
52
|
+
}))).to.deep.equal({ allow: true });
|
|
53
|
+
});
|
|
54
|
+
it('refuses anyone else, naming the lease holder', () => {
|
|
55
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input())).to.deep.equal({
|
|
56
|
+
allow: false,
|
|
57
|
+
code: 'device_held_by_another_user',
|
|
58
|
+
holderId: ALICE,
|
|
59
|
+
heldBy: 'lease',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
it("refuses anyone else even when the phone's busy flag was cleared", () => {
|
|
63
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ busy: false })).allow).to.equal(false);
|
|
64
|
+
});
|
|
65
|
+
it('lets the owner of the session running on the leased phone drive it', () => {
|
|
66
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ sessionId: 'sess-1', sessionOwnerUserId: BOB }))).to.deep.equal({ allow: true });
|
|
67
|
+
});
|
|
68
|
+
it("doesn't let a manual hold on a leased phone stand in for the lease", () => {
|
|
69
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ sessionId: `manual_${BOB}_${UDID}` })).allow).to.equal(false);
|
|
70
|
+
});
|
|
71
|
+
it('fails closed when the holder cannot be resolved', () => {
|
|
72
|
+
const d = (0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ lease: { actorId: 'key_gone', holderUserId: null } }));
|
|
73
|
+
(0, chai_1.expect)(d).to.deep.equal({
|
|
74
|
+
allow: false,
|
|
75
|
+
code: 'device_held_by_another_user',
|
|
76
|
+
holderId: '',
|
|
77
|
+
heldBy: 'lease',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('still allows an admin', () => {
|
|
81
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ isAdmin: true }))).to.deep.equal({ allow: true });
|
|
82
|
+
});
|
|
83
|
+
it('leaves an unleased phone to the rules it had', () => {
|
|
84
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ lease: null, busy: false }))).to.deep.equal({
|
|
85
|
+
allow: true,
|
|
86
|
+
});
|
|
87
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.evaluateDeviceAccess)(input({ lease: null, sessionId: 'sess-1', sessionOwnerUserId: ALICE }))
|
|
88
|
+
.allow).to.equal(false);
|
|
89
|
+
});
|
|
90
|
+
it('isLeaseHolder needs a lease', () => {
|
|
91
|
+
(0, chai_1.expect)((0, deviceAccessPolicy_1.isLeaseHolder)(null, ALICE, ALICE_KEY)).to.equal(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('deviceAccessGuard', () => {
|
|
95
|
+
function appFor(opts) {
|
|
96
|
+
var _a;
|
|
97
|
+
const app = (0, express_1.default)();
|
|
98
|
+
app.use(express_1.default.json());
|
|
99
|
+
app.use((req, _res, next) => {
|
|
100
|
+
var _a;
|
|
101
|
+
req.auth = {
|
|
102
|
+
kind: 'api-key',
|
|
103
|
+
userId: opts.actorUserId,
|
|
104
|
+
role: (_a = opts.role) !== null && _a !== void 0 ? _a : 'MEMBER',
|
|
105
|
+
scopes: 'devices',
|
|
106
|
+
rateLimit: 100,
|
|
107
|
+
};
|
|
108
|
+
if (opts.apiKeyId)
|
|
109
|
+
req.apiKey = { id: opts.apiKeyId };
|
|
110
|
+
next();
|
|
111
|
+
});
|
|
112
|
+
const router = express_1.default.Router();
|
|
113
|
+
router.use((0, deviceAccessGuard_1.deviceAccessGuard)({
|
|
114
|
+
findDevice: () => __awaiter(this, void 0, void 0, function* () {
|
|
115
|
+
var _a;
|
|
116
|
+
return ({
|
|
117
|
+
udid: UDID,
|
|
118
|
+
host: HOST,
|
|
119
|
+
busy: (_a = opts.busy) !== null && _a !== void 0 ? _a : true,
|
|
120
|
+
session_id: null,
|
|
121
|
+
});
|
|
122
|
+
}),
|
|
123
|
+
resolveSessionOwner: () => __awaiter(this, void 0, void 0, function* () { return null; }),
|
|
124
|
+
describeHolder: (id) => __awaiter(this, void 0, void 0, function* () { return (id === ALICE ? 'alice@example.com' : null); }),
|
|
125
|
+
findActiveLease: (_a = opts.leaseLookup) !== null && _a !== void 0 ? _a : ((udid, host) => __awaiter(this, void 0, void 0, function* () { var _a; return (udid === UDID && host === HOST ? ((_a = opts.lease) !== null && _a !== void 0 ? _a : null) : null); })),
|
|
126
|
+
resolveLeaseHolder: () => __awaiter(this, void 0, void 0, function* () { var _a; return (_a = opts.leaseHolder) !== null && _a !== void 0 ? _a : null; }),
|
|
127
|
+
}));
|
|
128
|
+
router.post('/:udid/tap', (_req, res) => res.json({ reached: true }));
|
|
129
|
+
router.get('/:udid/clipboard', (_req, res) => res.json({ reached: true }));
|
|
130
|
+
app.use('/control', router);
|
|
131
|
+
return app;
|
|
132
|
+
}
|
|
133
|
+
it('lets the lease holder control the phone they just leased', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
const res = yield (0, loopbackRequest_1.default)(appFor({ actorUserId: ALICE, lease: { actorId: ALICE_KEY }, leaseHolder: ALICE })).post(`/control/${UDID}/tap`);
|
|
135
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
136
|
+
(0, chai_1.expect)(res.body.reached).to.equal(true);
|
|
137
|
+
}));
|
|
138
|
+
it('refuses another member with a message about the lease', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
139
|
+
const res = yield (0, loopbackRequest_1.default)(appFor({ actorUserId: BOB, lease: { actorId: ALICE_KEY }, leaseHolder: ALICE })).post(`/control/${UDID}/tap`);
|
|
140
|
+
(0, chai_1.expect)(res.status).to.equal(409);
|
|
141
|
+
(0, chai_1.expect)(res.body.error).to.equal('device_held_by_another_user');
|
|
142
|
+
(0, chai_1.expect)(res.body.message).to.match(/leased by alice@example\.com/);
|
|
143
|
+
(0, chai_1.expect)(res.body.holder.userId).to.equal(ALICE);
|
|
144
|
+
}));
|
|
145
|
+
it('refuses another member on a leased phone whose busy flag was cleared', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
146
|
+
const res = yield (0, loopbackRequest_1.default)(appFor({
|
|
147
|
+
actorUserId: BOB,
|
|
148
|
+
busy: false,
|
|
149
|
+
lease: { actorId: ALICE_KEY },
|
|
150
|
+
leaseHolder: ALICE,
|
|
151
|
+
})).get(`/control/${UDID}/clipboard`);
|
|
152
|
+
(0, chai_1.expect)(res.status).to.equal(409);
|
|
153
|
+
}));
|
|
154
|
+
it('answers 503 when it cannot tell whether a lease holds the phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
155
|
+
const res = yield (0, loopbackRequest_1.default)(appFor({
|
|
156
|
+
actorUserId: BOB,
|
|
157
|
+
leaseLookup: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
158
|
+
throw new Error('db down');
|
|
159
|
+
}),
|
|
160
|
+
})).post(`/control/${UDID}/tap`);
|
|
161
|
+
(0, chai_1.expect)(res.status).to.equal(503);
|
|
162
|
+
(0, chai_1.expect)(res.body.error).to.equal('device_ownership_unavailable');
|
|
163
|
+
}));
|
|
164
|
+
it('does not look leases up for an admin', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
165
|
+
const res = yield (0, loopbackRequest_1.default)(appFor({
|
|
166
|
+
actorUserId: BOB,
|
|
167
|
+
role: 'SUPER_ADMIN',
|
|
168
|
+
leaseLookup: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
169
|
+
throw new Error('an admin should not need this');
|
|
170
|
+
}),
|
|
171
|
+
})).post(`/control/${UDID}/tap`);
|
|
172
|
+
(0, chai_1.expect)(res.status).to.equal(200);
|
|
173
|
+
}));
|
|
174
|
+
});
|
|
175
|
+
describe('the logcat ticket authorizer', () => {
|
|
176
|
+
const authorize = (0, ticketActorAccess_1.makeTicketActorAuthorizer)({
|
|
177
|
+
findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return ({ udid: UDID, host: HOST, busy: true, session_id: null }); }),
|
|
178
|
+
resolveSessionOwner: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
|
|
179
|
+
findActiveLease: () => __awaiter(void 0, void 0, void 0, function* () { return ({ actorId: ALICE_KEY }); }),
|
|
180
|
+
resolveLeaseHolder: () => __awaiter(void 0, void 0, void 0, function* () { return ALICE; }),
|
|
181
|
+
});
|
|
182
|
+
it('lets the lease holder read the leased phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
183
|
+
(0, chai_1.expect)(yield authorize(UDID, { actorId: ALICE })).to.equal(true);
|
|
184
|
+
}));
|
|
185
|
+
it('refuses anyone else', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
186
|
+
(0, chai_1.expect)(yield authorize(UDID, { actorId: BOB })).to.equal(false);
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
189
|
+
describe('stream/start', () => {
|
|
190
|
+
const start = (over = {}) => (0, streamStartConflict_1.decideStreamStartConflict)(Object.assign({ udid: UDID, busy: true, sessionId: null, hasLiveManualStream: false, sessionOwnerUserId: null, actorUserId: ALICE, isAdmin: false }, over));
|
|
191
|
+
it('lets the lease holder preview their leased phone (busy, no session)', () => {
|
|
192
|
+
(0, chai_1.expect)(start({ leaseHolder: true })).to.deep.equal({ action: 'proceed' });
|
|
193
|
+
});
|
|
194
|
+
it('still refuses a busy phone with no holder on record to anyone else', () => {
|
|
195
|
+
(0, chai_1.expect)(start().action).to.equal('deny');
|
|
196
|
+
});
|
|
197
|
+
it("doesn't let a lease stand in for someone else's session on the phone", () => {
|
|
198
|
+
(0, chai_1.expect)(start({ leaseHolder: true, sessionId: 'sess-9', sessionOwnerUserId: BOB }).action).to.equal('deny');
|
|
199
|
+
});
|
|
200
|
+
function appFor(actorUserId, role = 'MEMBER', leased = true) {
|
|
201
|
+
const app = (0, express_1.default)();
|
|
202
|
+
app.use((req, _res, next) => {
|
|
203
|
+
req.auth = { kind: 'api-key', userId: actorUserId, role, scopes: 'devices' };
|
|
204
|
+
next();
|
|
205
|
+
});
|
|
206
|
+
const router = express_1.default.Router();
|
|
207
|
+
router.use((0, deviceAccessGuard_1.deviceAccessGuard)({
|
|
208
|
+
findDevice: () => __awaiter(this, void 0, void 0, function* () { return ({ udid: UDID, host: HOST, busy: false, session_id: null }); }),
|
|
209
|
+
resolveSessionOwner: () => __awaiter(this, void 0, void 0, function* () { return null; }),
|
|
210
|
+
describeHolder: () => __awaiter(this, void 0, void 0, function* () { return 'alice@example.com'; }),
|
|
211
|
+
findActiveLease: () => __awaiter(this, void 0, void 0, function* () {
|
|
212
|
+
if (role === 'SUPER_ADMIN')
|
|
213
|
+
throw new Error('an admin should not need this');
|
|
214
|
+
return leased ? { actorId: ALICE_KEY } : null;
|
|
215
|
+
}),
|
|
216
|
+
resolveLeaseHolder: () => __awaiter(this, void 0, void 0, function* () { return ALICE; }),
|
|
217
|
+
}));
|
|
218
|
+
router.post('/:udid/stream/start', (_req, res) => res.json({ reached: true }));
|
|
219
|
+
app.use('/control', router);
|
|
220
|
+
return app;
|
|
221
|
+
}
|
|
222
|
+
it('refuses a stranger before the handler (and before a hub forwards it)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
223
|
+
const res = yield (0, loopbackRequest_1.default)(appFor(BOB)).post(`/control/${UDID}/stream/start`);
|
|
224
|
+
(0, chai_1.expect)(res.status).to.equal(409);
|
|
225
|
+
(0, chai_1.expect)(res.body.message).to.match(/leased by/);
|
|
226
|
+
}));
|
|
227
|
+
it('passes the lease holder on to the handler', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
228
|
+
const res = yield (0, loopbackRequest_1.default)(appFor(ALICE)).post(`/control/${UDID}/stream/start`);
|
|
229
|
+
(0, chai_1.expect)(res.body.reached).to.equal(true);
|
|
230
|
+
}));
|
|
231
|
+
it('leaves an unleased phone to stream/start, as before', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
232
|
+
const res = yield (0, loopbackRequest_1.default)(appFor(BOB, 'MEMBER', false)).post(`/control/${UDID}/stream/start`);
|
|
233
|
+
(0, chai_1.expect)(res.body.reached).to.equal(true);
|
|
234
|
+
}));
|
|
235
|
+
it('passes an admin without looking leases up', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
236
|
+
const res = yield (0, loopbackRequest_1.default)(appFor(BOB, 'SUPER_ADMIN')).post(`/control/${UDID}/stream/start`);
|
|
237
|
+
(0, chai_1.expect)(res.body.reached).to.equal(true);
|
|
238
|
+
}));
|
|
239
|
+
});
|
|
240
|
+
describe('recordings (BusyPrecheck)', () => {
|
|
241
|
+
const precheck = (row, lease) => new busy_precheck_1.BusyPrecheck({ findDevice: () => __awaiter(void 0, void 0, void 0, function* () { return (Object.assign({ udid: UDID, host: HOST }, row)); }) }, { isRecording: () => __awaiter(void 0, void 0, void 0, function* () { return false; }) }, {
|
|
242
|
+
find: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
243
|
+
if (lease instanceof Error)
|
|
244
|
+
throw lease;
|
|
245
|
+
return lease;
|
|
246
|
+
}),
|
|
247
|
+
holderOf: () => __awaiter(void 0, void 0, void 0, function* () { return ALICE; }),
|
|
248
|
+
});
|
|
249
|
+
it("refuses a stranger's recording of a leased phone, busy or not", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
250
|
+
(0, chai_1.expect)(yield precheck({ busy: false }, { actorId: ALICE_KEY }).findBusy([UDID], BOB)).to.deep.equal([{ udid: UDID, reason: 'leased' }]);
|
|
251
|
+
}));
|
|
252
|
+
it('lets the lease holder record their leased phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
253
|
+
(0, chai_1.expect)(yield precheck({ busy: true }, { actorId: ALICE_KEY }).findBusy([UDID], ALICE)).to.deep.equal([]);
|
|
254
|
+
}));
|
|
255
|
+
it('still refuses the holder while a session runs on it, as for any session', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
256
|
+
const busy = yield precheck({ busy: true, session_id: 'sess-1' }, { actorId: ALICE_KEY }).findBusy([UDID], ALICE);
|
|
257
|
+
(0, chai_1.expect)(busy).to.deep.equal([{ udid: UDID, reason: 'automation', sessionId: 'sess-1' }]);
|
|
258
|
+
}));
|
|
259
|
+
it('fails the start when it cannot tell whether a lease holds the phone', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
260
|
+
let thrown;
|
|
261
|
+
try {
|
|
262
|
+
yield precheck({ busy: false }, new Error('db down')).findBusy([UDID], BOB);
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
thrown = e;
|
|
266
|
+
}
|
|
267
|
+
(0, chai_1.expect)(thrown === null || thrown === void 0 ? void 0 : thrown.message).to.equal('db down');
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -41,6 +41,7 @@ describe('allocateDeviceForSession — lease-bound session', () => {
|
|
|
41
41
|
let device;
|
|
42
42
|
let lease;
|
|
43
43
|
let restore;
|
|
44
|
+
let updateDevice;
|
|
44
45
|
const allocate = (proof, callerTeamIds) => (0, device_utils_1.allocateDeviceForSession)(leaseCaps(), 1000, 100, IPluginArgs_1.DefaultPluginArgs, callerTeamIds, proof);
|
|
45
46
|
const refusedWith = (proof, callerTeamIds) => __awaiter(void 0, void 0, void 0, function* () {
|
|
46
47
|
try {
|
|
@@ -71,8 +72,10 @@ describe('allocateDeviceForSession — lease-bound session', () => {
|
|
|
71
72
|
const noAuth = { nodePairAuth: () => __awaiter(void 0, void 0, void 0, function* () { return ({ accessKey: '', token: '' }); }) };
|
|
72
73
|
restore = (0, container_registration_1.saveRegistrations)(LeaseService_1.LeaseService);
|
|
73
74
|
typedi_1.Container.set(LeaseService_1.LeaseService, new LeaseService_1.LeaseService(db, {}, {}, noAuth));
|
|
75
|
+
updateDevice = sinon_1.default.stub().resolves();
|
|
74
76
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
75
77
|
findDevice: sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () { return device; })),
|
|
78
|
+
updateDevice,
|
|
76
79
|
});
|
|
77
80
|
});
|
|
78
81
|
afterEach(() => {
|
|
@@ -94,6 +97,25 @@ describe('allocateDeviceForSession — lease-bound session', () => {
|
|
|
94
97
|
(0, chai_1.expect)(got.udid).to.equal('u1');
|
|
95
98
|
}));
|
|
96
99
|
});
|
|
100
|
+
describe('the idle sweeper reads the session, not the lease', () => {
|
|
101
|
+
// It used to read the server-wide newCommandTimeoutSec and the lease's
|
|
102
|
+
// creation time, so a leased session was ended a minute after the lease
|
|
103
|
+
// was taken, whatever the session asked for.
|
|
104
|
+
it("records the session's newCommandTimeout and its start", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
105
|
+
const caps = leaseCaps();
|
|
106
|
+
caps.alwaysMatch['appium:newCommandTimeout'] = 120;
|
|
107
|
+
const before = Date.now();
|
|
108
|
+
yield (0, device_utils_1.allocateDeviceForSession)(caps, 1000, 100, IPluginArgs_1.DefaultPluginArgs, undefined, Object.assign(Object.assign({}, NOBODY), { leaseToken: TOKEN }));
|
|
109
|
+
const [udid, host, written] = updateDevice.firstCall.args;
|
|
110
|
+
(0, chai_1.expect)([udid, host]).to.deep.equal(['u1', 'h1']);
|
|
111
|
+
(0, chai_1.expect)(written.newCommandTimeout).to.equal(120);
|
|
112
|
+
(0, chai_1.expect)(written.lastCmdExecutedAt).to.be.at.least(before);
|
|
113
|
+
}));
|
|
114
|
+
it("falls back to the server's newCommandTimeoutSec", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
115
|
+
yield allocate(Object.assign(Object.assign({}, NOBODY), { leaseToken: TOKEN }));
|
|
116
|
+
(0, chai_1.expect)(updateDevice.firstCall.args[2].newCommandTimeout).to.equal(IPluginArgs_1.DefaultPluginArgs.newCommandTimeoutSec);
|
|
117
|
+
}));
|
|
118
|
+
});
|
|
97
119
|
describe("someone else's lease id is refused, as if it were not active", () => {
|
|
98
120
|
it('with no credentials and no token', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
99
121
|
(0, chai_1.expect)(yield refusal(NOBODY)).to.equal(REFUSED);
|
|
@@ -179,6 +179,8 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
179
179
|
teamRows = sinon_1.default.stub(prisma_1.prisma.teamMember, 'findMany').resolves([]);
|
|
180
180
|
sinon_1.default.stub(device_store_1.DeviceStoreFactory, 'getStore').returns({
|
|
181
181
|
findDevice: sinon_1.default.stub().callsFake(() => __awaiter(void 0, void 0, void 0, function* () { return device; })),
|
|
182
|
+
// The lease branch records the session's newCommandTimeout on the row.
|
|
183
|
+
updateDevice: sinon_1.default.stub().resolves(),
|
|
182
184
|
});
|
|
183
185
|
sinon_1.default.stub(pendingSessions, 'addNewPendingSession').callsFake((c) => __awaiter(void 0, void 0, void 0, function* () {
|
|
184
186
|
pendingCopy = JSON.parse(JSON.stringify(c));
|
|
@@ -257,6 +259,63 @@ describe('createSession — a lease-bound session proves it holds the lease', ()
|
|
|
257
259
|
(0, chai_1.expect)(findById.calledOnce).to.equal(true);
|
|
258
260
|
(0, chai_1.expect)(teamRows.calledOnce).to.equal(true);
|
|
259
261
|
}));
|
|
262
|
+
describe('the platform allocation lock', () => {
|
|
263
|
+
// Ordinary creates queue on it, first come first served, each with its
|
|
264
|
+
// full wait from the head of the queue. A lease-bound create allocates
|
|
265
|
+
// nothing and used to queue behind them anyway, for up to their wait.
|
|
266
|
+
const plainCaps = () => {
|
|
267
|
+
const caps = sessionCaps(ownerKey);
|
|
268
|
+
delete caps.alwaysMatch['xe:options'].leaseId;
|
|
269
|
+
return caps;
|
|
270
|
+
};
|
|
271
|
+
const tick = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
272
|
+
it("doesn't make a lease-bound create queue behind one waiting for a phone", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
273
|
+
const real = deviceUtils.allocateDeviceForSession;
|
|
274
|
+
let releaseWaiter = () => undefined;
|
|
275
|
+
sinon_1.default.stub(deviceUtils, 'allocateDeviceForSession').callsFake(((c, ...rest) => {
|
|
276
|
+
var _a;
|
|
277
|
+
return ((_a = c.alwaysMatch['xe:options']) === null || _a === void 0 ? void 0 : _a.leaseId)
|
|
278
|
+
? real(c, ...rest)
|
|
279
|
+
: new Promise((resolve) => {
|
|
280
|
+
releaseWaiter = resolve;
|
|
281
|
+
});
|
|
282
|
+
}));
|
|
283
|
+
const waiting = create(plainCaps());
|
|
284
|
+
let leased = Promise.resolve();
|
|
285
|
+
try {
|
|
286
|
+
yield tick(50);
|
|
287
|
+
let leasedDone = false;
|
|
288
|
+
leased = create(sessionCaps(ownerKey)).then(() => {
|
|
289
|
+
leasedDone = true;
|
|
290
|
+
});
|
|
291
|
+
yield Promise.race([leased, tick(2000)]);
|
|
292
|
+
(0, chai_1.expect)(leasedDone, 'the lease-bound create waited on the lock').to.equal(true);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
// Free the platform lock even when this fails, and let both creates
|
|
296
|
+
// finish here, or they run on into the next test.
|
|
297
|
+
releaseWaiter(device);
|
|
298
|
+
yield waiting;
|
|
299
|
+
yield leased;
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
it('still queues ordinary creates one at a time', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
303
|
+
const waiters = [];
|
|
304
|
+
const allocate = sinon_1.default
|
|
305
|
+
.stub(deviceUtils, 'allocateDeviceForSession')
|
|
306
|
+
.callsFake((() => new Promise((resolve) => waiters.push(resolve))));
|
|
307
|
+
const first = create(plainCaps());
|
|
308
|
+
const second = create(plainCaps());
|
|
309
|
+
yield tick(100);
|
|
310
|
+
(0, chai_1.expect)(allocate.callCount, 'the second create allocated before the first finished').to.equal(1);
|
|
311
|
+
waiters[0](device);
|
|
312
|
+
yield first;
|
|
313
|
+
yield tick(50);
|
|
314
|
+
(0, chai_1.expect)(allocate.callCount).to.equal(2);
|
|
315
|
+
waiters[1](device);
|
|
316
|
+
yield second;
|
|
317
|
+
}));
|
|
318
|
+
});
|
|
260
319
|
it('a session that names a lease reads them once too, for the scope and the lease', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
261
320
|
const findById = sinon_1.default.spy(typedi_1.Container.get(UserService_1.UserService), 'findById');
|
|
262
321
|
yield allowed(sessionCaps(ownerKey));
|