@sasjs/cli 6.30.1 → 6.31.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.
@@ -0,0 +1,1371 @@
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 __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __read = (this && this.__read) || function (o, n) {
39
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
40
+ if (!m) return o;
41
+ var i = m.call(o), r, ar = [], e;
42
+ try {
43
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
44
+ }
45
+ catch (error) { e = { error: error }; }
46
+ finally {
47
+ try {
48
+ if (r && !r.done && (m = i["return"])) m.call(i);
49
+ }
50
+ finally { if (e) throw e.error; }
51
+ }
52
+ return ar;
53
+ };
54
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
55
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
56
+ if (ar || !(i in from)) {
57
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
58
+ ar[i] = from[i];
59
+ }
60
+ }
61
+ return to.concat(ar || Array.prototype.slice.call(from));
62
+ };
63
+ var __importDefault = (this && this.__importDefault) || function (mod) {
64
+ return (mod && mod.__esModule) ? mod : { "default": mod };
65
+ };
66
+ Object.defineProperty(exports, "__esModule", { value: true });
67
+ /**
68
+ * @jest-environment jsdom
69
+ */
70
+ var fs_1 = __importDefault(require("fs"));
71
+ var path_1 = __importDefault(require("path"));
72
+ /**
73
+ * The API console, as a page.
74
+ *
75
+ * The build's own specs prove the files are written and that the operations come
76
+ * from the route table; they cannot prove the page works. This one drives it in a
77
+ * DOM: it loads the real `api-index.html` (so a rename of an id fails here rather
78
+ * than in a browser), stubs the two files the page reads and the host bridge, and
79
+ * asserts the request a form sends - including the bearer token, which is the
80
+ * part that has to survive the reload `as()` performs.
81
+ *
82
+ * The service worker is deliberately absent: jsdom has none, and the page has to
83
+ * render without it (a site whose `host/persist.js` cannot load must still show
84
+ * the route list).
85
+ */
86
+ var page = function () {
87
+ return fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'assets', 'api-index.html'), 'utf8');
88
+ };
89
+ var bodyOf = function (html) {
90
+ var start = html.indexOf('<body>');
91
+ var end = html.indexOf('</body>');
92
+ return start < 0 || end < 0 ? html : html.slice(start + 6, end);
93
+ };
94
+ var operations = [
95
+ {
96
+ id: 'POST /SASjsApi/user',
97
+ method: 'POST',
98
+ path: '/SASjsApi/user',
99
+ tag: 'User',
100
+ summary: 'Create a user',
101
+ params: [
102
+ { name: 'username', in: 'body', required: true, type: 'string' },
103
+ { name: 'password', in: 'body', required: true, type: 'string' },
104
+ { name: 'isAdmin', in: 'body', required: false, type: 'boolean' }
105
+ ],
106
+ requestBodies: ['application/json'],
107
+ responses: [{ status: 200, description: 'Ok' }],
108
+ implemented: true
109
+ },
110
+ {
111
+ id: 'GET /SASjsApi/user',
112
+ method: 'GET',
113
+ path: '/SASjsApi/user',
114
+ tag: 'User',
115
+ summary: 'List all users',
116
+ params: [],
117
+ requestBodies: [],
118
+ responses: [{ status: 200, description: 'Ok' }],
119
+ implemented: true
120
+ }
121
+ ];
122
+ var fixture = {
123
+ format: 1,
124
+ users: [
125
+ {
126
+ uid: 'user-admin',
127
+ username: 'sasjsadmin',
128
+ password: 'Orion123',
129
+ isAdmin: true
130
+ },
131
+ {
132
+ uid: 'user-restricted',
133
+ username: 'sasjsuser',
134
+ password: 'Orion123',
135
+ isAdmin: false
136
+ }
137
+ ]
138
+ };
139
+ var jsonResponse = function (body) { return ({
140
+ ok: true,
141
+ status: 200,
142
+ statusText: 'OK',
143
+ headers: { get: function () { return 'application/json'; } },
144
+ json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
145
+ return [2 /*return*/, body];
146
+ }); }); },
147
+ text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
148
+ return [2 /*return*/, JSON.stringify(body)];
149
+ }); }); }
150
+ }); };
151
+ /** A service worker the page can register, and drive by hand. */
152
+ var withServiceWorker = function (options) {
153
+ if (options === void 0) { options = {}; }
154
+ var listeners = [];
155
+ var sw = {
156
+ controller: null,
157
+ register: jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () {
158
+ return __generator(this, function (_a) {
159
+ if (options.registerFails)
160
+ throw new Error('the browser refused');
161
+ return [2 /*return*/, {}];
162
+ });
163
+ }); }),
164
+ addEventListener: function (type, fn) {
165
+ if (type === 'controllerchange')
166
+ listeners.push(fn);
167
+ }
168
+ };
169
+ Object.defineProperty(navigator, 'serviceWorker', {
170
+ value: sw,
171
+ configurable: true
172
+ });
173
+ return {
174
+ sw: sw,
175
+ takeControl: function () {
176
+ sw.controller = {};
177
+ listeners.forEach(function (fn) { return fn(); });
178
+ }
179
+ };
180
+ };
181
+ /**
182
+ * Records the scripts the page asks for, and answers each one.
183
+ *
184
+ * jsdom does not fetch a script it is given, so the load event is dispatched
185
+ * here - otherwise the page would wait forever for `persist.js`.
186
+ */
187
+ var stubScripts = function () {
188
+ var realCreate = document.createElement.bind(document);
189
+ var requested = [];
190
+ jest.spyOn(document, 'createElement').mockImplementation(function (tag) {
191
+ var node = realCreate(tag);
192
+ if (String(tag).toLowerCase() === 'script') {
193
+ setTimeout(function () {
194
+ requested.push(node.src);
195
+ node.dispatchEvent(new Event('load'));
196
+ }, 0);
197
+ }
198
+ return node;
199
+ });
200
+ return { requested: requested };
201
+ };
202
+ /** Boots the page against a stubbed host and returns what it asked for. */
203
+ /** Waits for the render an assertion depends on, rather than for a wall clock. */
204
+ var settle = function (check) { return __awaiter(void 0, void 0, void 0, function () {
205
+ var i;
206
+ return __generator(this, function (_a) {
207
+ switch (_a.label) {
208
+ case 0:
209
+ i = 0;
210
+ _a.label = 1;
211
+ case 1:
212
+ if (!(i < 400)) return [3 /*break*/, 4];
213
+ if (check())
214
+ return [2 /*return*/];
215
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
216
+ case 2:
217
+ _a.sent();
218
+ _a.label = 3;
219
+ case 3:
220
+ i++;
221
+ return [3 /*break*/, 1];
222
+ case 4: throw new Error('the page did not reach the state the case expected');
223
+ }
224
+ });
225
+ }); };
226
+ var loadConsole = function () {
227
+ var args_1 = [];
228
+ for (var _i = 0; _i < arguments.length; _i++) {
229
+ args_1[_i] = arguments[_i];
230
+ }
231
+ return __awaiter(void 0, __spreadArray([], __read(args_1), false), void 0, function (before, table, identity) {
232
+ var calls, i;
233
+ if (before === void 0) { before = function () { }; }
234
+ if (table === void 0) { table = operations; }
235
+ if (identity === void 0) { identity = fixture; }
236
+ return __generator(this, function (_a) {
237
+ switch (_a.label) {
238
+ case 0:
239
+ jest.resetModules();
240
+ document.body.innerHTML = bodyOf(page());
241
+ // the version lives in the head, so the cases that care about it set it there
242
+ document.head.innerHTML = '';
243
+ before();
244
+ calls = [];
245
+ global.fetch = jest.fn(function (url_1) {
246
+ var args_2 = [];
247
+ for (var _i = 1; _i < arguments.length; _i++) {
248
+ args_2[_i - 1] = arguments[_i];
249
+ }
250
+ return __awaiter(void 0, __spreadArray([url_1], __read(args_2), false), void 0, function (url, init) {
251
+ if (init === void 0) { init = {}; }
252
+ return __generator(this, function (_a) {
253
+ if (String(url).includes('operations.json'))
254
+ return [2 /*return*/, jsonResponse(table)];
255
+ if (String(url).includes('identity.json'))
256
+ return [2 /*return*/, jsonResponse(identity)];
257
+ calls.push({ url: String(url), init: init });
258
+ return [2 /*return*/, jsonResponse({ created: true })];
259
+ });
260
+ });
261
+ });
262
+ require('../assets/api-console.js');
263
+ i = 0;
264
+ _a.label = 1;
265
+ case 1:
266
+ if (!(i < 200)) return [3 /*break*/, 4];
267
+ if (document.querySelectorAll('#routes .route').length)
268
+ return [3 /*break*/, 4];
269
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
270
+ case 2:
271
+ _a.sent();
272
+ _a.label = 3;
273
+ case 3:
274
+ i++;
275
+ return [3 /*break*/, 1];
276
+ case 4: return [2 /*return*/, { calls: calls }];
277
+ }
278
+ });
279
+ });
280
+ };
281
+ var selectRoute = function (id) { return __awaiter(void 0, void 0, void 0, function () {
282
+ var button;
283
+ return __generator(this, function (_a) {
284
+ switch (_a.label) {
285
+ case 0:
286
+ button = __spreadArray([], __read(document.querySelectorAll('#routes .route')), false).find(function (candidate) { return candidate.dataset.id === id; });
287
+ button.click();
288
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
289
+ case 1:
290
+ _a.sent();
291
+ return [2 /*return*/];
292
+ }
293
+ });
294
+ }); };
295
+ var fill = function (name, value) {
296
+ var input = document.querySelector("#form [data-name=\"".concat(name, "\"]"));
297
+ input.value = value;
298
+ };
299
+ var sendAndWait = function (calls_1) {
300
+ var args_1 = [];
301
+ for (var _i = 1; _i < arguments.length; _i++) {
302
+ args_1[_i - 1] = arguments[_i];
303
+ }
304
+ return __awaiter(void 0, __spreadArray([calls_1], __read(args_1), false), void 0, function (calls, want) {
305
+ var i;
306
+ if (want === void 0) { want = 1; }
307
+ return __generator(this, function (_a) {
308
+ switch (_a.label) {
309
+ case 0:
310
+ ;
311
+ document.getElementById('send').click();
312
+ i = 0;
313
+ _a.label = 1;
314
+ case 1:
315
+ if (!(i < 200)) return [3 /*break*/, 4];
316
+ if (calls.length >= want)
317
+ return [2 /*return*/];
318
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
319
+ case 2:
320
+ _a.sent();
321
+ _a.label = 3;
322
+ case 3:
323
+ i++;
324
+ return [3 /*break*/, 1];
325
+ case 4: return [2 /*return*/];
326
+ }
327
+ });
328
+ });
329
+ };
330
+ describe('the API console', function () {
331
+ afterEach(function () {
332
+ delete navigator.serviceWorker;
333
+ delete window.__sasjsMockReady;
334
+ });
335
+ beforeEach(function () {
336
+ // jsdom has no service worker, and the page says so on the console; that is
337
+ // the path under test, not a warning worth printing
338
+ jest.spyOn(console, 'warn').mockImplementation(function () { });
339
+ jest.spyOn(console, 'error').mockImplementation(function () { });
340
+ });
341
+ afterEach(function () {
342
+ jest.restoreAllMocks();
343
+ delete global.fetch;
344
+ delete global.SASjsMock;
345
+ sessionStorage.clear();
346
+ });
347
+ it('lists the operations it was built with, grouped by tag', function () { return __awaiter(void 0, void 0, void 0, function () {
348
+ var routes;
349
+ return __generator(this, function (_a) {
350
+ switch (_a.label) {
351
+ case 0: return [4 /*yield*/, loadConsole()];
352
+ case 1:
353
+ _a.sent();
354
+ routes = __spreadArray([], __read(document.querySelectorAll('#routes .route')), false);
355
+ expect(routes).toHaveLength(2);
356
+ expect(document.querySelector('#routes h3').textContent).toBe('User');
357
+ return [2 /*return*/];
358
+ }
359
+ });
360
+ }); });
361
+ it('sends the route it was given, with the parameters typed into the form', function () { return __awaiter(void 0, void 0, void 0, function () {
362
+ var calls, i;
363
+ return __generator(this, function (_a) {
364
+ switch (_a.label) {
365
+ case 0: return [4 /*yield*/, loadConsole()];
366
+ case 1:
367
+ calls = (_a.sent()).calls;
368
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/user')];
369
+ case 2:
370
+ _a.sent();
371
+ fill('username', 'demo_reviewer');
372
+ fill('password', 'Orion123');
373
+ return [4 /*yield*/, sendAndWait(calls)];
374
+ case 3:
375
+ _a.sent();
376
+ expect(calls[0].url).toMatch(/\/SASjsApi\/user$/);
377
+ expect(calls[0].init.method).toBe('POST');
378
+ // an untouched optional boolean is absent: the API reads `false` as a value
379
+ // it must act on, so sending it would change what was not asked for
380
+ expect(JSON.parse(calls[0].init.body)).toEqual({
381
+ username: 'demo_reviewer',
382
+ password: 'Orion123'
383
+ });
384
+ expect(calls[0].init.headers.authorization).toBeUndefined();
385
+ i = 0;
386
+ _a.label = 4;
387
+ case 4:
388
+ if (!(i < 200)) return [3 /*break*/, 7];
389
+ if ((document.getElementById('status').textContent || '').includes('200'))
390
+ return [3 /*break*/, 7];
391
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
392
+ case 5:
393
+ _a.sent();
394
+ _a.label = 6;
395
+ case 6:
396
+ i++;
397
+ return [3 /*break*/, 4];
398
+ case 7:
399
+ expect(document.getElementById('status').textContent).toContain('200');
400
+ return [2 /*return*/];
401
+ }
402
+ });
403
+ }); });
404
+ it('sends the token the host issued for the account it is acting as', function () { return __awaiter(void 0, void 0, void 0, function () {
405
+ var calls;
406
+ return __generator(this, function (_a) {
407
+ switch (_a.label) {
408
+ case 0:
409
+ // `as()` reloads the page, so the console reads the pair it stored before the
410
+ // navigation: a token held only in memory would be gone by the time the form
411
+ // is usable again
412
+ sessionStorage.setItem('mock-console-token', 'token-for-sasjsuser');
413
+ return [4 /*yield*/, loadConsole()];
414
+ case 1:
415
+ calls = (_a.sent()).calls;
416
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user')];
417
+ case 2:
418
+ _a.sent();
419
+ return [4 /*yield*/, sendAndWait(calls)];
420
+ case 3:
421
+ _a.sent();
422
+ expect(calls[0].url).toMatch(/\/SASjsApi\/user$/);
423
+ expect(calls[0].init.headers.authorization).toBe('Bearer token-for-sasjsuser');
424
+ return [2 /*return*/];
425
+ }
426
+ });
427
+ }); });
428
+ it('filters the list, and says so when nothing matches', function () { return __awaiter(void 0, void 0, void 0, function () {
429
+ var filter;
430
+ return __generator(this, function (_a) {
431
+ switch (_a.label) {
432
+ case 0: return [4 /*yield*/, loadConsole()];
433
+ case 1:
434
+ _a.sent();
435
+ filter = document.getElementById('filter');
436
+ filter.value = 'group';
437
+ filter.dispatchEvent(new Event('input'));
438
+ expect(document.querySelectorAll('#routes .route')).toHaveLength(0);
439
+ expect(document.getElementById('routes').textContent).toContain('No route matches that.');
440
+ filter.value = 'user';
441
+ filter.dispatchEvent(new Event('input'));
442
+ expect(document.querySelectorAll('#routes .route')).toHaveLength(2);
443
+ return [2 /*return*/];
444
+ }
445
+ });
446
+ }); });
447
+ it('marks a route the host does not implement, and one that takes an upload', function () { return __awaiter(void 0, void 0, void 0, function () {
448
+ var extra, i;
449
+ return __generator(this, function (_a) {
450
+ switch (_a.label) {
451
+ case 0:
452
+ extra = [
453
+ {
454
+ id: 'POST /SASjsApi/drive/deploy/upload',
455
+ method: 'POST',
456
+ path: '/SASjsApi/drive/deploy/upload',
457
+ tag: 'Drive',
458
+ summary: 'Upload a deployment',
459
+ params: [],
460
+ requestBodies: ['multipart/form-data'],
461
+ responses: [],
462
+ implemented: true
463
+ },
464
+ {
465
+ id: 'GET /SASjsApi/session/{sessionId}/log',
466
+ method: 'GET',
467
+ path: '/SASjsApi/session/{sessionId}/log',
468
+ tag: 'Session',
469
+ summary: 'A session log',
470
+ params: [
471
+ { name: 'sessionId', in: 'path', required: true, type: 'string' }
472
+ ],
473
+ requestBodies: [],
474
+ responses: [],
475
+ implemented: false,
476
+ reason: 'this host keeps no log file'
477
+ }
478
+ ];
479
+ jest.resetModules();
480
+ document.body.innerHTML = bodyOf(page());
481
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
482
+ return __generator(this, function (_a) {
483
+ return [2 /*return*/, String(url).includes('operations.json')
484
+ ? jsonResponse(extra)
485
+ : jsonResponse(fixture)];
486
+ });
487
+ }); });
488
+ require('../assets/api-console.js');
489
+ i = 0;
490
+ _a.label = 1;
491
+ case 1:
492
+ if (!(i < 200)) return [3 /*break*/, 4];
493
+ if (document.querySelectorAll('#routes .route').length)
494
+ return [3 /*break*/, 4];
495
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
496
+ case 2:
497
+ _a.sent();
498
+ _a.label = 3;
499
+ case 3:
500
+ i++;
501
+ return [3 /*break*/, 1];
502
+ case 4:
503
+ ;
504
+ global.fetch = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
505
+ return [2 /*return*/, jsonResponse({})];
506
+ }); }); });
507
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/drive/deploy/upload')];
508
+ case 5:
509
+ _a.sent();
510
+ expect(document.getElementById('form').textContent).toContain('does not send');
511
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/session/{sessionId}/log')];
512
+ case 6:
513
+ _a.sent();
514
+ expect(document.getElementById('summary').textContent).toContain('does not implement');
515
+ // a path parameter is rendered as a field, and the URL it builds carries it
516
+ expect(document.querySelector('#form [data-name="sessionId"]')).not.toBeNull();
517
+ return [2 /*return*/];
518
+ }
519
+ });
520
+ }); });
521
+ it('reports a body that is not the JSON it claims to be, without sending it', function () { return __awaiter(void 0, void 0, void 0, function () {
522
+ var calls, operations, field;
523
+ return __generator(this, function (_a) {
524
+ switch (_a.label) {
525
+ case 0: return [4 /*yield*/, loadConsole()
526
+ // a parameter the table describes as an object is sent as typed
527
+ ];
528
+ case 1:
529
+ calls = (_a.sent()).calls;
530
+ operations = __spreadArray([], __read(document.querySelectorAll('#routes .route')), false);
531
+ expect(operations.length).toBeGreaterThan(0);
532
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/user')
533
+ // the user route's fields are strings, so drive the object branch directly:
534
+ // an array parameter is the same code path
535
+ ];
536
+ case 2:
537
+ _a.sent();
538
+ field = document.querySelector('#form [data-name="username"]');
539
+ field.dataset.type = 'object';
540
+ field.value = '{not json';
541
+ document.getElementById('send').click();
542
+ return [4 /*yield*/, settle(function () {
543
+ return /must be JSON/.test(document.getElementById('body').textContent || '');
544
+ })];
545
+ case 3:
546
+ _a.sent();
547
+ expect(calls).toHaveLength(0);
548
+ expect(document.getElementById('body').textContent).toContain('must be JSON');
549
+ return [2 /*return*/];
550
+ }
551
+ });
552
+ }); });
553
+ it('shows a non-JSON answer as it came, and an empty one as empty', function () { return __awaiter(void 0, void 0, void 0, function () {
554
+ var calls;
555
+ return __generator(this, function (_a) {
556
+ switch (_a.label) {
557
+ case 0: return [4 /*yield*/, loadConsole()];
558
+ case 1:
559
+ calls = (_a.sent()).calls;
560
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user')];
561
+ case 2:
562
+ _a.sent();
563
+ global.fetch = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () {
564
+ return __generator(this, function (_a) {
565
+ return [2 /*return*/, ({
566
+ ok: true,
567
+ status: 200,
568
+ statusText: 'OK',
569
+ headers: { get: function () { return 'text/plain'; } },
570
+ text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
571
+ return [2 /*return*/, 'NOTE: hello'];
572
+ }); }); }
573
+ })];
574
+ });
575
+ }); });
576
+ return [4 /*yield*/, sendAndWait(calls)];
577
+ case 3:
578
+ _a.sent();
579
+ return [4 /*yield*/, settle(function () { return document.getElementById('body').textContent === 'NOTE: hello'; })];
580
+ case 4:
581
+ _a.sent();
582
+ expect(document.getElementById('body').textContent).toBe('NOTE: hello');
583
+ global.fetch = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () {
584
+ return __generator(this, function (_a) {
585
+ return [2 /*return*/, ({
586
+ ok: true,
587
+ status: 200,
588
+ statusText: 'OK',
589
+ headers: { get: function () { return 'application/json'; } },
590
+ text: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
591
+ return [2 /*return*/, ''];
592
+ }); }); }
593
+ })];
594
+ });
595
+ }); });
596
+ document.getElementById('send').click();
597
+ return [4 /*yield*/, settle(function () { return document.getElementById('body').textContent === '(empty body)'; })];
598
+ case 5:
599
+ _a.sent();
600
+ expect(document.getElementById('body').textContent).toBe('(empty body)');
601
+ return [2 /*return*/];
602
+ }
603
+ });
604
+ }); });
605
+ it('reports a request that never reached the host', function () { return __awaiter(void 0, void 0, void 0, function () {
606
+ return __generator(this, function (_a) {
607
+ switch (_a.label) {
608
+ case 0: return [4 /*yield*/, loadConsole()];
609
+ case 1:
610
+ _a.sent();
611
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user')];
612
+ case 2:
613
+ _a.sent();
614
+ global.fetch = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () {
615
+ return __generator(this, function (_a) {
616
+ throw new Error('the network is gone');
617
+ });
618
+ }); });
619
+ document.getElementById('send').click();
620
+ return [4 /*yield*/, settle(function () { return document.getElementById('status').textContent === 'request failed'; })];
621
+ case 3:
622
+ _a.sent();
623
+ expect(document.getElementById('status').textContent).toBe('request failed');
624
+ expect(document.getElementById('body').textContent).toContain('the network is gone');
625
+ return [2 /*return*/];
626
+ }
627
+ });
628
+ }); });
629
+ it('copies the answer, and survives a clipboard that refuses', function () { return __awaiter(void 0, void 0, void 0, function () {
630
+ var written;
631
+ return __generator(this, function (_a) {
632
+ switch (_a.label) {
633
+ case 0: return [4 /*yield*/, loadConsole()];
634
+ case 1:
635
+ _a.sent();
636
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user')];
637
+ case 2:
638
+ _a.sent();
639
+ written = [];
640
+ navigator.clipboard = {
641
+ writeText: function (text) { return __awaiter(void 0, void 0, void 0, function () {
642
+ return __generator(this, function (_a) {
643
+ written.push(text);
644
+ return [2 /*return*/];
645
+ });
646
+ }); }
647
+ };
648
+ document.getElementById('body').textContent = '{"a":1}';
649
+ document.getElementById('copy').click();
650
+ return [4 /*yield*/, settle(function () { return written.length === 1; })];
651
+ case 3:
652
+ _a.sent();
653
+ expect(written).toEqual(['{"a":1}']);
654
+ expect(document.getElementById('copy').textContent).toBe('Copied');
655
+ navigator.clipboard = {
656
+ writeText: function () { return __awaiter(void 0, void 0, void 0, function () {
657
+ return __generator(this, function (_a) {
658
+ throw new Error('denied');
659
+ });
660
+ }); }
661
+ };
662
+ document.getElementById('copy').click();
663
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 20); })];
664
+ case 4:
665
+ _a.sent();
666
+ expect(document.getElementById('copy').textContent).toBe('Copied');
667
+ return [2 /*return*/];
668
+ }
669
+ });
670
+ }); });
671
+ it('selects the route named in the URL, so a reload comes back to it', function () { return __awaiter(void 0, void 0, void 0, function () {
672
+ var i;
673
+ return __generator(this, function (_a) {
674
+ switch (_a.label) {
675
+ case 0:
676
+ jest.resetModules();
677
+ document.body.innerHTML = bodyOf(page());
678
+ history.replaceState({}, '', '/api/?route=' + encodeURIComponent('GET /SASjsApi/user'));
679
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
680
+ return __generator(this, function (_a) {
681
+ return [2 /*return*/, String(url).includes('operations.json')
682
+ ? jsonResponse(operations)
683
+ : jsonResponse(fixture)];
684
+ });
685
+ }); });
686
+ require('../assets/api-console.js');
687
+ i = 0;
688
+ _a.label = 1;
689
+ case 1:
690
+ if (!(i < 200)) return [3 /*break*/, 4];
691
+ if (document.getElementById('title').textContent)
692
+ return [3 /*break*/, 4];
693
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
694
+ case 2:
695
+ _a.sent();
696
+ _a.label = 3;
697
+ case 3:
698
+ i++;
699
+ return [3 /*break*/, 1];
700
+ case 4:
701
+ expect(document.getElementById('title').textContent).toBe('GET /SASjsApi/user');
702
+ history.replaceState({}, '', '/api/');
703
+ return [2 /*return*/];
704
+ }
705
+ });
706
+ }); });
707
+ it('stores the token acting as another account returns, and sends it', function () { return __awaiter(void 0, void 0, void 0, function () {
708
+ var calls, actAs;
709
+ return __generator(this, function (_a) {
710
+ switch (_a.label) {
711
+ case 0: return [4 /*yield*/, loadConsole(function () {
712
+ ;
713
+ window.SASjsMock = {
714
+ identity: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
715
+ return [2 /*return*/, ({ fixture: fixture, subject: null })];
716
+ }); }); },
717
+ as: function () { return __awaiter(void 0, void 0, void 0, function () {
718
+ return __generator(this, function (_a) {
719
+ return [2 /*return*/, ({
720
+ accessToken: 'token-for-sasjsuser',
721
+ refreshToken: 'refresh-for-sasjsuser'
722
+ })];
723
+ });
724
+ }); }
725
+ };
726
+ })];
727
+ case 1:
728
+ calls = (_a.sent()).calls;
729
+ actAs = __spreadArray([], __read(document.querySelectorAll('#accounts button')), false).find(function (candidate) { return candidate.dataset.user === 'sasjsuser'; });
730
+ expect(actAs).toBeTruthy();
731
+ actAs.click();
732
+ return [4 /*yield*/, settle(function () {
733
+ return sessionStorage.getItem('mock-console-token') === 'token-for-sasjsuser';
734
+ })];
735
+ case 2:
736
+ _a.sent();
737
+ expect(sessionStorage.getItem('mock-console-token')).toBe('token-for-sasjsuser');
738
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user')];
739
+ case 3:
740
+ _a.sent();
741
+ return [4 /*yield*/, sendAndWait(calls)];
742
+ case 4:
743
+ _a.sent();
744
+ expect(calls[0].init.headers.authorization).toBe('Bearer token-for-sasjsuser');
745
+ return [2 /*return*/];
746
+ }
747
+ });
748
+ }); });
749
+ it('registers the versioned worker, waits for control, and hands over the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
750
+ var requested, host;
751
+ return __generator(this, function (_a) {
752
+ switch (_a.label) {
753
+ case 0:
754
+ requested = stubScripts().requested;
755
+ host = withServiceWorker();
756
+ return [4 /*yield*/, loadConsole(function () {
757
+ document.head.innerHTML =
758
+ '<meta name="sasjs-mock-version" content="1.2.3" />';
759
+ window.__sasjsMockReady = Promise.resolve({ booted: true });
760
+ })];
761
+ case 1:
762
+ _a.sent();
763
+ return [4 /*yield*/, settle(function () { return host.sw.register.mock.calls.length === 1; })
764
+ // the app's loader registers this exact URL: two URLs for one scope replace
765
+ // each other, which is an install nobody asked for
766
+ ];
767
+ case 2:
768
+ _a.sent();
769
+ // the app's loader registers this exact URL: two URLs for one scope replace
770
+ // each other, which is an install nobody asked for
771
+ expect(host.sw.register.mock.calls[0][0]).toContain('sw.js?v=1.2.3');
772
+ expect(host.sw.register.mock.calls[0][1]).toEqual(expect.objectContaining({ scope: '/', updateViaCache: 'none' }));
773
+ // a registration completes before the navigation commits, so the page waits
774
+ host.takeControl();
775
+ return [4 /*yield*/, settle(function () { return requested.some(function (url) { return url.includes('host/persist.js'); }); })];
776
+ case 3:
777
+ _a.sent();
778
+ expect(requested.some(function (url) { return url.includes('host/persist.js'); })).toBe(true);
779
+ return [2 /*return*/];
780
+ }
781
+ });
782
+ }); });
783
+ it('registers the plain worker URL while the page carries the placeholder', function () { return __awaiter(void 0, void 0, void 0, function () {
784
+ var host;
785
+ return __generator(this, function (_a) {
786
+ switch (_a.label) {
787
+ case 0:
788
+ host = withServiceWorker();
789
+ // the placeholder is what a hand-copied asset carries: it is not a version
790
+ return [4 /*yield*/, loadConsole(function () {
791
+ document.head.innerHTML =
792
+ '<meta name="sasjs-mock-version" content="__MOCK_VERSION__" />';
793
+ })];
794
+ case 1:
795
+ // the placeholder is what a hand-copied asset carries: it is not a version
796
+ _a.sent();
797
+ return [4 /*yield*/, settle(function () { return host.sw.register.mock.calls.length === 1; })];
798
+ case 2:
799
+ _a.sent();
800
+ expect(host.sw.register.mock.calls[0][0]).not.toContain('?v=');
801
+ // let the page past its wait: a registration nobody claims leaves a 4s timer
802
+ // that fires during whatever test runs next, and its reload throws there
803
+ host.takeControl();
804
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 20); })];
805
+ case 3:
806
+ _a.sent();
807
+ return [2 /*return*/];
808
+ }
809
+ });
810
+ }); });
811
+ it('says so when the browser refuses the registration', function () { return __awaiter(void 0, void 0, void 0, function () {
812
+ var error, host;
813
+ return __generator(this, function (_a) {
814
+ switch (_a.label) {
815
+ case 0:
816
+ error = jest.spyOn(console, 'error').mockImplementation(function () { });
817
+ host = withServiceWorker({ registerFails: true });
818
+ return [4 /*yield*/, loadConsole()];
819
+ case 1:
820
+ _a.sent();
821
+ return [4 /*yield*/, settle(function () { return error.mock.calls.length > 0; })];
822
+ case 2:
823
+ _a.sent();
824
+ expect(error.mock.calls[0][0]).toContain('could not register');
825
+ expect(host.sw.register).toHaveBeenCalled();
826
+ error.mockRestore();
827
+ return [2 /*return*/];
828
+ }
829
+ });
830
+ }); });
831
+ it('says so when the drive could not be handed over', function () { return __awaiter(void 0, void 0, void 0, function () {
832
+ var error, requested, host;
833
+ return __generator(this, function (_a) {
834
+ switch (_a.label) {
835
+ case 0:
836
+ error = jest.spyOn(console, 'error').mockImplementation(function () { });
837
+ requested = stubScripts().requested;
838
+ host = withServiceWorker();
839
+ return [4 /*yield*/, loadConsole(function () {
840
+ ;
841
+ window.__sasjsMockReady = Promise.reject(new Error('no storage in this browser'));
842
+ })];
843
+ case 1:
844
+ _a.sent();
845
+ host.takeControl();
846
+ return [4 /*yield*/, settle(function () {
847
+ return error.mock.calls.some(function (call) { return /handed over/.test(String(call[0])); });
848
+ })];
849
+ case 2:
850
+ _a.sent();
851
+ expect(error.mock.calls.some(function (call) { return /handed over/.test(String(call[0])); })).toBe(true);
852
+ expect(requested.some(function (url) { return url.includes('host/persist.js'); })).toBe(true);
853
+ error.mockRestore();
854
+ return [2 /*return*/];
855
+ }
856
+ });
857
+ }); });
858
+ it('prefills the sign-in route from the demo administrator', function () { return __awaiter(void 0, void 0, void 0, function () {
859
+ var signIn, username, password;
860
+ return __generator(this, function (_a) {
861
+ switch (_a.label) {
862
+ case 0:
863
+ signIn = [
864
+ {
865
+ id: 'POST /SASLogon/login',
866
+ method: 'POST',
867
+ path: '/SASLogon/login',
868
+ tag: 'Auth',
869
+ summary: 'Sign in',
870
+ params: [
871
+ { name: 'username', in: 'body', required: true, type: 'string' },
872
+ { name: 'password', in: 'body', required: true, type: 'string' }
873
+ ],
874
+ requestBodies: ['application/json'],
875
+ responses: [{ status: 200, description: 'Ok' }],
876
+ implemented: true
877
+ }
878
+ ];
879
+ return [4 /*yield*/, loadConsole(function () { }, signIn)];
880
+ case 1:
881
+ _a.sent();
882
+ return [4 /*yield*/, selectRoute('POST /SASLogon/login')
883
+ // the credentials are published on purpose, and this is where a visitor needs
884
+ // them: the route they sign in with
885
+ ];
886
+ case 2:
887
+ _a.sent();
888
+ username = document.querySelector('#form [data-name="username"]');
889
+ password = document.querySelector('#form [data-name="password"]');
890
+ expect(username.value).toBe('sasjsadmin');
891
+ expect(password.value).toBe('Orion123');
892
+ return [2 /*return*/];
893
+ }
894
+ });
895
+ }); });
896
+ it('puts a query parameter in the URL, and leaves it off when empty', function () { return __awaiter(void 0, void 0, void 0, function () {
897
+ var withQuery, calls;
898
+ return __generator(this, function (_a) {
899
+ switch (_a.label) {
900
+ case 0:
901
+ withQuery = [
902
+ {
903
+ id: 'GET /SASjsApi/user/list',
904
+ method: 'GET',
905
+ path: '/SASjsApi/user/list',
906
+ tag: 'User',
907
+ summary: 'List with a filter',
908
+ params: [
909
+ { name: 'search', in: 'query', required: false, type: 'string' }
910
+ ],
911
+ requestBodies: [],
912
+ responses: [{ status: 200, description: 'Ok' }],
913
+ implemented: true
914
+ }
915
+ ];
916
+ return [4 /*yield*/, loadConsole(function () { }, withQuery)];
917
+ case 1:
918
+ calls = (_a.sent()).calls;
919
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user/list')];
920
+ case 2:
921
+ _a.sent();
922
+ return [4 /*yield*/, sendAndWait(calls)];
923
+ case 3:
924
+ _a.sent();
925
+ expect(calls[0].url).toMatch(/\/SASjsApi\/user\/list$/);
926
+ // the button is disabled while a request is in flight, and a disabled button
927
+ // ignores a click, so wait for the first answer to land
928
+ return [4 /*yield*/, settle(function () { return !document.getElementById('send').disabled; })];
929
+ case 4:
930
+ // the button is disabled while a request is in flight, and a disabled button
931
+ // ignores a click, so wait for the first answer to land
932
+ _a.sent();
933
+ fill('search', 'demo reviewer');
934
+ return [4 /*yield*/, sendAndWait(calls, 2)];
935
+ case 5:
936
+ _a.sent();
937
+ expect(calls[1].url).toBe('/SASjsApi/user/list?search=demo+reviewer');
938
+ return [2 /*return*/];
939
+ }
940
+ });
941
+ }); });
942
+ it('says so when the host refuses to act as an account', function () { return __awaiter(void 0, void 0, void 0, function () {
943
+ var alert, error, actAs;
944
+ return __generator(this, function (_a) {
945
+ switch (_a.label) {
946
+ case 0:
947
+ alert = jest.spyOn(window, 'alert').mockImplementation(function () { });
948
+ error = jest.spyOn(console, 'error').mockImplementation(function () { });
949
+ return [4 /*yield*/, loadConsole(function () {
950
+ ;
951
+ window.SASjsMock = {
952
+ identity: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
953
+ return [2 /*return*/, ({ fixture: fixture, subject: null })];
954
+ }); }); },
955
+ as: function () { return __awaiter(void 0, void 0, void 0, function () {
956
+ return __generator(this, function (_a) {
957
+ throw new Error('no such account');
958
+ });
959
+ }); }
960
+ };
961
+ })];
962
+ case 1:
963
+ _a.sent();
964
+ actAs = __spreadArray([], __read(document.querySelectorAll('#accounts button')), false).find(function (candidate) { return candidate.dataset.user === 'sasjsuser'; });
965
+ actAs.click();
966
+ return [4 /*yield*/, settle(function () { return alert.mock.calls.length > 0; })];
967
+ case 2:
968
+ _a.sent();
969
+ expect(String(alert.mock.calls[0][0])).toContain('no such account');
970
+ expect(document.getElementById('who').textContent).not.toContain('sasjsuser');
971
+ alert.mockRestore();
972
+ error.mockRestore();
973
+ return [2 /*return*/];
974
+ }
975
+ });
976
+ }); });
977
+ it('refuses a route whose required field is empty, instead of sending the placeholder', function () { return __awaiter(void 0, void 0, void 0, function () {
978
+ var calls;
979
+ return __generator(this, function (_a) {
980
+ switch (_a.label) {
981
+ case 0: return [4 /*yield*/, loadConsole()];
982
+ case 1:
983
+ calls = (_a.sent()).calls;
984
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/user')
985
+ // username is required, and nothing is typed into it
986
+ ];
987
+ case 2:
988
+ _a.sent();
989
+ document.getElementById('send').click();
990
+ return [4 /*yield*/, settle(function () { return document.getElementById('status').textContent === 'missing input'; })];
991
+ case 3:
992
+ _a.sent();
993
+ expect(calls).toHaveLength(0);
994
+ expect(document.getElementById('body').textContent).toContain('username');
995
+ return [2 /*return*/];
996
+ }
997
+ });
998
+ }); });
999
+ it('refuses a number that is not a number, rather than sending null', function () { return __awaiter(void 0, void 0, void 0, function () {
1000
+ var withNumber, calls;
1001
+ return __generator(this, function (_a) {
1002
+ switch (_a.label) {
1003
+ case 0:
1004
+ withNumber = [
1005
+ {
1006
+ id: 'POST /SASjsApi/code/trigger',
1007
+ method: 'POST',
1008
+ path: '/SASjsApi/code/trigger',
1009
+ tag: 'Code',
1010
+ summary: 'Trigger a scheduled run',
1011
+ params: [
1012
+ {
1013
+ name: 'expiresAfterMins',
1014
+ in: 'body',
1015
+ required: false,
1016
+ type: 'number'
1017
+ }
1018
+ ],
1019
+ requestBodies: ['application/json'],
1020
+ responses: [{ status: 200, description: 'Ok' }],
1021
+ implemented: true
1022
+ }
1023
+ ];
1024
+ return [4 /*yield*/, loadConsole(function () { }, withNumber)];
1025
+ case 1:
1026
+ calls = (_a.sent()).calls;
1027
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/code/trigger')];
1028
+ case 2:
1029
+ _a.sent();
1030
+ fill('expiresAfterMins', 'ten');
1031
+ document.getElementById('send').click();
1032
+ return [4 /*yield*/, settle(function () {
1033
+ return /must be a number/.test(document.getElementById('body').textContent || '');
1034
+ })
1035
+ // JSON.stringify writes NaN as null, which the host would read as "absent"
1036
+ ];
1037
+ case 3:
1038
+ _a.sent();
1039
+ // JSON.stringify writes NaN as null, which the host would read as "absent"
1040
+ expect(calls).toHaveLength(0);
1041
+ return [2 /*return*/];
1042
+ }
1043
+ });
1044
+ }); });
1045
+ it('does not demand an upload it cannot take', function () { return __awaiter(void 0, void 0, void 0, function () {
1046
+ var upload, calls;
1047
+ return __generator(this, function (_a) {
1048
+ switch (_a.label) {
1049
+ case 0:
1050
+ upload = [
1051
+ {
1052
+ id: 'POST /SASjsApi/drive/deploy/upload',
1053
+ method: 'POST',
1054
+ path: '/SASjsApi/drive/deploy/upload',
1055
+ tag: 'Drive',
1056
+ summary: 'Upload a deployment',
1057
+ params: [
1058
+ { name: 'file', in: 'formData', required: true, type: 'string' }
1059
+ ],
1060
+ requestBodies: ['multipart/form-data'],
1061
+ responses: [{ status: 200, description: 'Ok' }],
1062
+ implemented: true
1063
+ }
1064
+ ];
1065
+ return [4 /*yield*/, loadConsole(function () { }, upload)];
1066
+ case 1:
1067
+ calls = (_a.sent()).calls;
1068
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/drive/deploy/upload')];
1069
+ case 2:
1070
+ _a.sent();
1071
+ document.getElementById('send').click();
1072
+ return [4 /*yield*/, settle(function () {
1073
+ return calls.length > 0 ||
1074
+ /does not send/.test(document.getElementById('form').textContent || '');
1075
+ })];
1076
+ case 3:
1077
+ _a.sent();
1078
+ expect(document.getElementById('status').textContent).not.toBe('missing input');
1079
+ return [2 /*return*/];
1080
+ }
1081
+ });
1082
+ }); });
1083
+ it('clears the drive and starts again when the URL asks it to', function () { return __awaiter(void 0, void 0, void 0, function () {
1084
+ var reset;
1085
+ return __generator(this, function (_a) {
1086
+ switch (_a.label) {
1087
+ case 0:
1088
+ reset = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
1089
+ return [2 /*return*/];
1090
+ }); }); });
1091
+ sessionStorage.setItem('mock-console-token', 'stale');
1092
+ sessionStorage.setItem('mock-console-reloaded', '1');
1093
+ // jsdom does not navigate, and the redirect is the last thing the page does:
1094
+ // what matters here is that the drive was reset before it
1095
+ return [4 /*yield*/, loadConsole(function () {
1096
+ history.replaceState({}, '', '/api/?reset=1');
1097
+ window.SASjsMock = { reset: reset };
1098
+ })];
1099
+ case 1:
1100
+ // jsdom does not navigate, and the redirect is the last thing the page does:
1101
+ // what matters here is that the drive was reset before it
1102
+ _a.sent();
1103
+ return [4 /*yield*/, settle(function () { return reset.mock.calls.length > 0; })];
1104
+ case 2:
1105
+ _a.sent();
1106
+ expect(reset).toHaveBeenCalled();
1107
+ expect(sessionStorage.length).toBe(0);
1108
+ history.replaceState({}, '', '/api/');
1109
+ return [2 /*return*/];
1110
+ }
1111
+ });
1112
+ }); });
1113
+ it('waits for the delete when there is no host to ask, and says so if it is blocked', function () { return __awaiter(void 0, void 0, void 0, function () {
1114
+ var blocked;
1115
+ return __generator(this, function (_a) {
1116
+ switch (_a.label) {
1117
+ case 0:
1118
+ blocked = {};
1119
+ global.indexedDB = {
1120
+ deleteDatabase: jest.fn(function () {
1121
+ setTimeout(function () { return blocked.onblocked && blocked.onblocked(); }, 5);
1122
+ return blocked;
1123
+ })
1124
+ };
1125
+ return [4 /*yield*/, loadConsole(function () {
1126
+ // no host bridge on this page at all
1127
+ delete window.SASjsMock;
1128
+ history.replaceState({}, '', '/api/?reset=1');
1129
+ })];
1130
+ case 1:
1131
+ _a.sent();
1132
+ return [4 /*yield*/, settle(function () {
1133
+ return /could not be reset/.test(document.getElementById('routes').textContent || '');
1134
+ })];
1135
+ case 2:
1136
+ _a.sent();
1137
+ expect(global.indexedDB.deleteDatabase).toHaveBeenCalledWith('sasjs-mock-host');
1138
+ expect(document.getElementById('routes').textContent).toContain('still holds the drive');
1139
+ history.replaceState({}, '', '/api/');
1140
+ delete global.indexedDB;
1141
+ return [2 /*return*/];
1142
+ }
1143
+ });
1144
+ }); });
1145
+ it('leaves the drive alone for any other value of the reset flag', function () { return __awaiter(void 0, void 0, void 0, function () {
1146
+ var reset;
1147
+ return __generator(this, function (_a) {
1148
+ switch (_a.label) {
1149
+ case 0:
1150
+ reset = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
1151
+ return [2 /*return*/];
1152
+ }); }); });
1153
+ sessionStorage.setItem('mock-console-token', 'kept');
1154
+ return [4 /*yield*/, loadConsole(function () {
1155
+ history.replaceState({}, '', '/api/?reset=0');
1156
+ window.SASjsMock = { reset: reset };
1157
+ })];
1158
+ case 1:
1159
+ _a.sent();
1160
+ return [4 /*yield*/, settle(function () { return document.querySelectorAll('#routes .route').length > 0; })];
1161
+ case 2:
1162
+ _a.sent();
1163
+ expect(reset).not.toHaveBeenCalled();
1164
+ expect(sessionStorage.getItem('mock-console-token')).toBe('kept');
1165
+ history.replaceState({}, '', '/api/');
1166
+ return [2 /*return*/];
1167
+ }
1168
+ });
1169
+ }); });
1170
+ it('omits an untouched optional boolean on a patch, and sends it when chosen', function () { return __awaiter(void 0, void 0, void 0, function () {
1171
+ var patch, calls, untouched;
1172
+ return __generator(this, function (_a) {
1173
+ switch (_a.label) {
1174
+ case 0:
1175
+ patch = [
1176
+ {
1177
+ id: 'PATCH /SASjsApi/user/{uid}',
1178
+ method: 'PATCH',
1179
+ path: '/SASjsApi/user/{uid}',
1180
+ tag: 'User',
1181
+ summary: 'Update a user',
1182
+ params: [
1183
+ { name: 'uid', in: 'path', required: true, type: 'string' },
1184
+ {
1185
+ name: 'displayName',
1186
+ in: 'body',
1187
+ required: true,
1188
+ type: 'string'
1189
+ },
1190
+ { name: 'username', in: 'body', required: true, type: 'string' },
1191
+ { name: 'password', in: 'body', required: true, type: 'string' },
1192
+ { name: 'isAdmin', in: 'body', required: false, type: 'boolean' }
1193
+ ],
1194
+ requestBodies: ['application/json'],
1195
+ responses: [{ status: 200, description: 'Ok' }],
1196
+ implemented: true
1197
+ }
1198
+ ];
1199
+ return [4 /*yield*/, loadConsole(function () { }, patch)];
1200
+ case 1:
1201
+ calls = (_a.sent()).calls;
1202
+ return [4 /*yield*/, selectRoute('PATCH /SASjsApi/user/{uid}')];
1203
+ case 2:
1204
+ _a.sent();
1205
+ fill('uid', 'user-restricted');
1206
+ fill('displayName', 'Restricted user');
1207
+ fill('username', 'sasjsuser');
1208
+ fill('password', 'Orion123');
1209
+ return [4 /*yield*/, sendAndWait(calls)];
1210
+ case 3:
1211
+ _a.sent();
1212
+ untouched = JSON.parse(calls[0].init.body);
1213
+ expect(untouched.isAdmin).toBeUndefined();
1214
+ expect(calls[0].url).toMatch(/\/SASjsApi\/user\/user-restricted$/);
1215
+ // and choosing a value sends it, as a boolean rather than the string
1216
+ return [4 /*yield*/, settle(function () { return !document.getElementById('send').disabled; })];
1217
+ case 4:
1218
+ // and choosing a value sends it, as a boolean rather than the string
1219
+ _a.sent();
1220
+ document.querySelector('#form select[data-name="isAdmin"]').value = 'true';
1221
+ return [4 /*yield*/, sendAndWait(calls, 2)];
1222
+ case 5:
1223
+ _a.sent();
1224
+ expect(JSON.parse(calls[1].init.body).isAdmin).toBe(true);
1225
+ return [2 /*return*/];
1226
+ }
1227
+ });
1228
+ }); });
1229
+ it('sends a named shape as JSON, while an enum stays the word that was typed', function () { return __awaiter(void 0, void 0, void 0, function () {
1230
+ var deploy, calls, body;
1231
+ return __generator(this, function (_a) {
1232
+ switch (_a.label) {
1233
+ case 0:
1234
+ deploy = [
1235
+ {
1236
+ id: 'POST /SASjsApi/drive/deploy',
1237
+ method: 'POST',
1238
+ path: '/SASjsApi/drive/deploy',
1239
+ tag: 'Drive',
1240
+ summary: 'Deploy a bundle',
1241
+ params: [
1242
+ { name: 'appLoc', in: 'body', required: true, type: 'string' },
1243
+ { name: 'fileTree', in: 'body', required: true, type: 'FileTree' },
1244
+ { name: 'runTime', in: 'body', required: false, type: 'RunTimeType' }
1245
+ ],
1246
+ requestBodies: ['application/json'],
1247
+ responses: [{ status: 200, description: 'Ok' }],
1248
+ implemented: true
1249
+ }
1250
+ ];
1251
+ return [4 /*yield*/, loadConsole(function () { }, deploy)];
1252
+ case 1:
1253
+ calls = (_a.sent()).calls;
1254
+ return [4 /*yield*/, selectRoute('POST /SASjsApi/drive/deploy')];
1255
+ case 2:
1256
+ _a.sent();
1257
+ fill('appLoc', '/Public/app/demo');
1258
+ fill('fileTree', JSON.stringify({
1259
+ members: [{ name: 'services', type: 'folder', members: [] }]
1260
+ }));
1261
+ fill('runTime', 'sas');
1262
+ return [4 /*yield*/, sendAndWait(calls)];
1263
+ case 3:
1264
+ _a.sent();
1265
+ body = JSON.parse(calls[0].init.body);
1266
+ expect(typeof body.fileTree).toBe('object');
1267
+ expect(body.fileTree.members[0].name).toBe('services');
1268
+ // an enum is not JSON and must not be coerced
1269
+ expect(body.runTime).toBe('sas');
1270
+ return [2 /*return*/];
1271
+ }
1272
+ });
1273
+ }); });
1274
+ it('names the account it falls back to when the fixture ships no administrator', function () { return __awaiter(void 0, void 0, void 0, function () {
1275
+ var plain, note, back;
1276
+ return __generator(this, function (_a) {
1277
+ switch (_a.label) {
1278
+ case 0:
1279
+ plain = {
1280
+ format: 1,
1281
+ users: [
1282
+ { uid: 'u1', username: 'plain1', password: 'Orion123', isAdmin: false },
1283
+ { uid: 'u2', username: 'plain2', password: 'Orion123', isAdmin: false }
1284
+ ]
1285
+ };
1286
+ return [4 /*yield*/, loadConsole(function () {
1287
+ ;
1288
+ window.SASjsMock = {
1289
+ identity: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
1290
+ return [2 /*return*/, ({ fixture: plain, subject: 'plain1' })];
1291
+ }); }); },
1292
+ as: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
1293
+ return [2 /*return*/, ({ accessToken: 't', refreshToken: 'r' })];
1294
+ }); }); }
1295
+ };
1296
+ }, operations, plain)];
1297
+ case 1:
1298
+ _a.sent();
1299
+ note = document.getElementById('accounts').textContent || '';
1300
+ expect(note).toContain('no administrator');
1301
+ expect(note).toContain('plain1');
1302
+ __spreadArray([], __read(document.querySelectorAll('#accounts button')), false).find(function (button) { return button.dataset.user === 'plain2'; }).click();
1303
+ return [4 /*yield*/, (function () { return __awaiter(void 0, void 0, void 0, function () {
1304
+ var i, found;
1305
+ return __generator(this, function (_a) {
1306
+ switch (_a.label) {
1307
+ case 0:
1308
+ i = 0;
1309
+ _a.label = 1;
1310
+ case 1:
1311
+ if (!(i < 200)) return [3 /*break*/, 4];
1312
+ found = __spreadArray([], __read(document.querySelectorAll('#accounts button')), false).find(function (button) { return /^Back to/.test(button.textContent || ''); });
1313
+ if (found)
1314
+ return [2 /*return*/, found];
1315
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
1316
+ case 2:
1317
+ _a.sent();
1318
+ _a.label = 3;
1319
+ case 3:
1320
+ i++;
1321
+ return [3 /*break*/, 1];
1322
+ case 4: return [2 /*return*/, undefined];
1323
+ }
1324
+ });
1325
+ }); })()];
1326
+ case 2:
1327
+ back = _a.sent();
1328
+ expect(back).toBeDefined();
1329
+ expect(back.textContent).toBe('Back to plain1');
1330
+ return [2 /*return*/];
1331
+ }
1332
+ });
1333
+ }); });
1334
+ it('drops the token when the visitor goes back to the demo administrator', function () { return __awaiter(void 0, void 0, void 0, function () {
1335
+ var calls, back;
1336
+ return __generator(this, function (_a) {
1337
+ switch (_a.label) {
1338
+ case 0:
1339
+ sessionStorage.setItem('mock-console-token', 'token-for-sasjsuser');
1340
+ global.SASjsMock = {
1341
+ as: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
1342
+ return [2 /*return*/, ({ accessToken: 'admin-token' })];
1343
+ }); }); },
1344
+ identity: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
1345
+ return [2 /*return*/, ({ subject: 'sasjsuser' })];
1346
+ }); }); }
1347
+ };
1348
+ return [4 /*yield*/, loadConsole()
1349
+ // the page offers the way back only while it is acting as someone
1350
+ ];
1351
+ case 1:
1352
+ calls = (_a.sent()).calls;
1353
+ back = __spreadArray([], __read(document.querySelectorAll('#accounts button')), false).find(function (button) { return (button.textContent || '').includes('Back to the demo'); });
1354
+ expect(back).toBeDefined();
1355
+ back.click();
1356
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 10); })];
1357
+ case 2:
1358
+ _a.sent();
1359
+ expect(sessionStorage.getItem('mock-console-token')).toBeNull();
1360
+ return [4 /*yield*/, selectRoute('GET /SASjsApi/user')];
1361
+ case 3:
1362
+ _a.sent();
1363
+ return [4 /*yield*/, sendAndWait(calls)];
1364
+ case 4:
1365
+ _a.sent();
1366
+ expect(calls[0].init.headers.authorization).toBeUndefined();
1367
+ return [2 /*return*/];
1368
+ }
1369
+ });
1370
+ }); });
1371
+ });