@sasjs/cli 6.0.0 → 6.2.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,716 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ /**
40
+ * The other half of the parity claim: the service worker is what turns an
41
+ * /SASjsApi/ request into a response, so its framing (webout body, the _debug
42
+ * log shape, the 400 and the 500) is tested against the file the site ships.
43
+ */
44
+ var appLoc = '/Public/app/test';
45
+ var origin = 'https://mock.example.com';
46
+ var LOGS_SEPARATOR = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784';
47
+ var drivePath = function (relative) {
48
+ return "/sasjs/drive/files".concat(appLoc, "/").concat(relative);
49
+ };
50
+ var loadWorker = function () {
51
+ jest.resetModules();
52
+ var handlers = {};
53
+ var self = {
54
+ location: { origin: origin },
55
+ addEventListener: function (type, fn) {
56
+ handlers[type] = handlers[type] || [];
57
+ handlers[type].push(fn);
58
+ },
59
+ skipWaiting: function () { },
60
+ clients: { claim: function () { }, matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
61
+ return [2 /*return*/, []];
62
+ }); }); } }
63
+ };
64
+ global.self = self;
65
+ global.importScripts = function (path) {
66
+ if (path.includes('md5'))
67
+ return;
68
+ require('../assets/runtime-core.js');
69
+ };
70
+ require('../assets/sw.js');
71
+ global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
72
+ var _a;
73
+ return __generator(this, function (_b) {
74
+ return [2 /*return*/, String(url).includes('vfs-seed')
75
+ ? jsonResponse({
76
+ files: (_a = {},
77
+ _a[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
78
+ _a)
79
+ })
80
+ : jsonResponse({ appLoc: appLoc })];
81
+ });
82
+ }); });
83
+ var runtime = self.SASjsMockRuntime;
84
+ var dispatch = function (path, init) {
85
+ if (init === void 0) { init = {}; }
86
+ var request = new Request(path.startsWith('http') ? path : origin + path, init);
87
+ var event = {
88
+ request: request,
89
+ respondWith: function (response) {
90
+ event.response = response;
91
+ }
92
+ };
93
+ (handlers.fetch || [])[0](event);
94
+ return event.response;
95
+ };
96
+ var message = function (data) {
97
+ var replies = [];
98
+ var event = {
99
+ data: data,
100
+ source: { postMessage: function (m) { return replies.push(m); } },
101
+ waitUntil: function (promise) {
102
+ event.pending = promise;
103
+ }
104
+ };
105
+ (handlers.message || []).forEach(function (fn) { return fn(event); });
106
+ return { replies: replies, pending: event.pending };
107
+ };
108
+ var lifecycle = function (type) {
109
+ var event = { waitUntil: function () { } };
110
+ (handlers[type] || []).forEach(function (fn) { return fn(event); });
111
+ };
112
+ return { runtime: runtime, dispatch: dispatch, message: message, lifecycle: lifecycle, self: self };
113
+ };
114
+ var jsonResponse = function (body) { return ({
115
+ ok: true,
116
+ json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
117
+ return [2 /*return*/, body];
118
+ }); }); }
119
+ }); };
120
+ var runRequest = function (body) { return ({
121
+ method: 'POST',
122
+ headers: { 'content-type': 'application/json' },
123
+ body: JSON.stringify(body)
124
+ }); };
125
+ describe('service worker host', function () {
126
+ afterEach(function () {
127
+ delete global.self;
128
+ delete global.importScripts;
129
+ delete global.fetch;
130
+ });
131
+ var seeded = function () {
132
+ var _a;
133
+ var worker = loadWorker();
134
+ worker.runtime.load((_a = {},
135
+ _a[drivePath('services/public/getdata.js')] = 'console.log("hello"); _webout = "seeded";',
136
+ _a));
137
+ return worker;
138
+ };
139
+ it('returns the webout as the response body', function () { return __awaiter(void 0, void 0, void 0, function () {
140
+ var dispatch, response, _a;
141
+ return __generator(this, function (_b) {
142
+ switch (_b.label) {
143
+ case 0:
144
+ dispatch = seeded().dispatch;
145
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
146
+ encodeURIComponent("".concat(appLoc, "/services/public/getdata")))];
147
+ case 1:
148
+ response = _b.sent();
149
+ expect(response.status).toBe(200);
150
+ _a = expect;
151
+ return [4 /*yield*/, response.text()];
152
+ case 2:
153
+ _a.apply(void 0, [_b.sent()]).toBe('seeded');
154
+ return [2 /*return*/];
155
+ }
156
+ });
157
+ }); });
158
+ /**
159
+ * A worker whose drive holds one service that answers with a download, the
160
+ * way a streaming service does on the server: the body goes to the webout
161
+ * fileref and the headers file says what it is.
162
+ */
163
+ var streaming = function (code) {
164
+ var _a;
165
+ var worker = loadWorker();
166
+ worker.runtime.load((_a = {},
167
+ _a[drivePath('services/public/download.js')] = code,
168
+ _a));
169
+ return worker;
170
+ };
171
+ var downloadPath = function (query) {
172
+ if (query === void 0) { query = ''; }
173
+ return '/SASjsApi/stp/execute?_program=' +
174
+ encodeURIComponent("".concat(appLoc, "/services/public/download")) +
175
+ query;
176
+ };
177
+ it('applies the content type and disposition a service declared', function () { return __awaiter(void 0, void 0, void 0, function () {
178
+ var dispatch, response, _a;
179
+ return __generator(this, function (_b) {
180
+ switch (_b.label) {
181
+ case 0:
182
+ dispatch = streaming([
183
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'Content-type: text/csv \\n Content-Disposition: attachment;filename="users.csv"\')',
184
+ "_webout = 'a,b\\n1,2'"
185
+ ].join('\n')).dispatch;
186
+ return [4 /*yield*/, dispatch(downloadPath())];
187
+ case 1:
188
+ response = _b.sent();
189
+ expect(response.status).toBe(200);
190
+ expect(response.headers.get('content-type')).toBe('text/csv');
191
+ expect(response.headers.get('content-disposition')).toBe('attachment;filename="users.csv"');
192
+ _a = expect;
193
+ return [4 /*yield*/, response.text()];
194
+ case 2:
195
+ _a.apply(void 0, [_b.sent()]).toBe('a,b\n1,2');
196
+ return [2 /*return*/];
197
+ }
198
+ });
199
+ }); });
200
+ it('serves a byte body as bytes, under the name the service chose', function () { return __awaiter(void 0, void 0, void 0, function () {
201
+ var dispatch, response, _a, _b;
202
+ return __generator(this, function (_c) {
203
+ switch (_c.label) {
204
+ case 0:
205
+ dispatch = streaming([
206
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'Content-type: application/zip\\nContent-Disposition: attachment;filename="pack.zip"\')',
207
+ 'fs.writeFileSync(weboutPath, Uint8Array.from([80, 75, 3, 4]))'
208
+ ].join('\n')).dispatch;
209
+ return [4 /*yield*/, dispatch(downloadPath())];
210
+ case 1:
211
+ response = _c.sent();
212
+ expect(response.headers.get('content-type')).toBe('application/zip');
213
+ expect(response.headers.get('content-disposition')).toBe('attachment;filename="pack.zip"');
214
+ _a = expect;
215
+ _b = Uint8Array.bind;
216
+ return [4 /*yield*/, response.arrayBuffer()];
217
+ case 2:
218
+ _a.apply(void 0, [new (_b.apply(Uint8Array, [void 0, _c.sent()]))()]).toEqual(new Uint8Array([80, 75, 3, 4]));
219
+ return [2 /*return*/];
220
+ }
221
+ });
222
+ }); });
223
+ it('serves a byte body with no declared type as an opaque download', function () { return __awaiter(void 0, void 0, void 0, function () {
224
+ var dispatch, response, _a, _b;
225
+ return __generator(this, function (_c) {
226
+ switch (_c.label) {
227
+ case 0:
228
+ dispatch = streaming('fs.writeFileSync(weboutPath, Uint8Array.from([1, 2, 3]))').dispatch;
229
+ return [4 /*yield*/, dispatch(downloadPath())];
230
+ case 1:
231
+ response = _c.sent();
232
+ expect(response.headers.get('content-type')).toBe('application/octet-stream');
233
+ _a = expect;
234
+ _b = Uint8Array.bind;
235
+ return [4 /*yield*/, response.arrayBuffer()];
236
+ case 2:
237
+ _a.apply(void 0, [new (_b.apply(Uint8Array, [void 0, _c.sent()]))()]).toEqual(new Uint8Array([1, 2, 3]));
238
+ return [2 /*return*/];
239
+ }
240
+ });
241
+ }); });
242
+ it('keeps a debug reply text, as the body is the framed log', function () { return __awaiter(void 0, void 0, void 0, function () {
243
+ var dispatch, response, _a;
244
+ return __generator(this, function (_b) {
245
+ switch (_b.label) {
246
+ case 0:
247
+ dispatch = streaming([
248
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'Content-type: application/zip\\nContent-Disposition: attachment;filename="pack.zip"\')',
249
+ 'fs.writeFileSync(weboutPath, Uint8Array.from([80, 75, 3, 4]))'
250
+ ].join('\n')).dispatch;
251
+ return [4 /*yield*/, dispatch(downloadPath('&_debug=log'))];
252
+ case 1:
253
+ response = _b.sent();
254
+ expect(response.headers.get('content-type')).toBe('text/plain');
255
+ expect(response.headers.get('content-disposition')).toBeNull();
256
+ // the body is still bytes, framed with the log rather than corrupted by it
257
+ _a = expect;
258
+ return [4 /*yield*/, response.text()];
259
+ case 2:
260
+ // the body is still bytes, framed with the log rather than corrupted by it
261
+ _a.apply(void 0, [_b.sent()]).toBe(['UEsDBA==', LOGS_SEPARATOR, '', LOGS_SEPARATOR].join('\n'));
262
+ return [2 /*return*/];
263
+ }
264
+ });
265
+ }); });
266
+ it('applies nothing it was not asked for, and drops an unusable value', function () { return __awaiter(void 0, void 0, void 0, function () {
267
+ var dispatch, response, _a;
268
+ return __generator(this, function (_b) {
269
+ switch (_b.label) {
270
+ case 0:
271
+ dispatch = streaming([
272
+ "fs.writeFileSync(_SASJS_WEBOUT_HEADERS, 'Cache-Control: no-store\\nContent-type: text/html\\u0000')",
273
+ "_webout = 'plain'"
274
+ ].join('\n')).dispatch;
275
+ return [4 /*yield*/, dispatch(downloadPath())];
276
+ case 1:
277
+ response = _b.sent();
278
+ expect(response.status).toBe(200);
279
+ expect(response.headers.get('cache-control')).toBeNull();
280
+ // a NUL would make Response throw, so the header is dropped and the body
281
+ // keeps its default type
282
+ expect(response.headers.get('content-type')).toBe('text/plain;charset=UTF-8');
283
+ _a = expect;
284
+ return [4 /*yield*/, response.text()];
285
+ case 2:
286
+ _a.apply(void 0, [_b.sent()]).toBe('plain');
287
+ return [2 /*return*/];
288
+ }
289
+ });
290
+ }); });
291
+ it('serves a byte file off the drive as bytes', function () { return __awaiter(void 0, void 0, void 0, function () {
292
+ var _a, runtime, dispatch, response, _b, _c;
293
+ return __generator(this, function (_d) {
294
+ switch (_d.label) {
295
+ case 0:
296
+ _a = seeded(), runtime = _a.runtime, dispatch = _a.dispatch;
297
+ runtime.run({
298
+ code: "fs.writeFileSync('/sasjs/drive/files/data.bin', Uint8Array.from([1, 2, 3]))"
299
+ });
300
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data.bin'))];
301
+ case 1:
302
+ response = _d.sent();
303
+ expect(response.headers.get('content-type')).toBe('application/octet-stream');
304
+ _b = expect;
305
+ _c = Uint8Array.bind;
306
+ return [4 /*yield*/, response.arrayBuffer()];
307
+ case 2:
308
+ _b.apply(void 0, [new (_c.apply(Uint8Array, [void 0, _d.sent()]))()]).toEqual(new Uint8Array([1, 2, 3]));
309
+ return [2 /*return*/];
310
+ }
311
+ });
312
+ }); });
313
+ it('keeps a tab inside a header value, as HTTP allows it', function () { return __awaiter(void 0, void 0, void 0, function () {
314
+ var dispatch, response;
315
+ return __generator(this, function (_a) {
316
+ switch (_a.label) {
317
+ case 0:
318
+ dispatch = streaming([
319
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'Content-Disposition: attachment;\\tfilename="a.zip"\')',
320
+ '_webout = "x"'
321
+ ].join('\n')).dispatch;
322
+ return [4 /*yield*/, dispatch(downloadPath())];
323
+ case 1:
324
+ response = _a.sent();
325
+ expect(response.headers.get('content-disposition')).toBe('attachment;\tfilename="a.zip"');
326
+ return [2 /*return*/];
327
+ }
328
+ });
329
+ }); });
330
+ it('lets the last line for a header win, and an unusable one remove it', function () { return __awaiter(void 0, void 0, void 0, function () {
331
+ var replaced, _a, removed, _b;
332
+ return __generator(this, function (_c) {
333
+ switch (_c.label) {
334
+ case 0:
335
+ replaced = streaming([
336
+ "fs.writeFileSync(_SASJS_WEBOUT_HEADERS, 'Content-type: text/plain\\nContent-type: application/json')",
337
+ '_webout = "x"'
338
+ ].join('\n'));
339
+ _a = expect;
340
+ return [4 /*yield*/, replaced.dispatch(downloadPath())];
341
+ case 1:
342
+ _a.apply(void 0, [(_c.sent()).headers.get('content-type')]).toBe('application/json');
343
+ removed = streaming([
344
+ "fs.writeFileSync(_SASJS_WEBOUT_HEADERS, 'Content-type: text/plain\\nContent-type: text/html\\u0000')",
345
+ '_webout = "x"'
346
+ ].join('\n'));
347
+ _b = expect;
348
+ return [4 /*yield*/, removed.dispatch(downloadPath())];
349
+ case 2:
350
+ _b.apply(void 0, [(_c.sent()).headers.get('content-type')]).toBe('text/plain;charset=UTF-8');
351
+ return [2 /*return*/];
352
+ }
353
+ });
354
+ }); });
355
+ it('normalises header names and ignores a line that is not a header', function () { return __awaiter(void 0, void 0, void 0, function () {
356
+ var dispatch, response, _a;
357
+ return __generator(this, function (_b) {
358
+ switch (_b.label) {
359
+ case 0:
360
+ dispatch = streaming([
361
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'CONTENT-TYPE: text/plain\\nnot a header\\n: no name\\nContent-Disposition: attachment;filename="a.txt"\')',
362
+ "_webout = 'x'"
363
+ ].join('\n')).dispatch;
364
+ return [4 /*yield*/, dispatch(downloadPath())];
365
+ case 1:
366
+ response = _b.sent();
367
+ expect(response.headers.get('content-type')).toBe('text/plain');
368
+ expect(response.headers.get('content-disposition')).toBe('attachment;filename="a.txt"');
369
+ _a = expect;
370
+ return [4 /*yield*/, response.text()];
371
+ case 2:
372
+ _a.apply(void 0, [_b.sent()]).toBe('x');
373
+ return [2 /*return*/];
374
+ }
375
+ });
376
+ }); });
377
+ it('serves a service that declares a type but writes no body', function () { return __awaiter(void 0, void 0, void 0, function () {
378
+ var dispatch, response, _a;
379
+ return __generator(this, function (_b) {
380
+ switch (_b.label) {
381
+ case 0:
382
+ dispatch = streaming("fs.writeFileSync(_SASJS_WEBOUT_HEADERS, 'Content-type: application/json')").dispatch;
383
+ return [4 /*yield*/, dispatch(downloadPath())];
384
+ case 1:
385
+ response = _b.sent();
386
+ expect(response.headers.get('content-type')).toBe('application/json');
387
+ _a = expect;
388
+ return [4 /*yield*/, response.text()];
389
+ case 2:
390
+ _a.apply(void 0, [_b.sent()]).toBe('');
391
+ return [2 /*return*/];
392
+ }
393
+ });
394
+ }); });
395
+ it('answers the session state, and names a route it does not implement', function () { return __awaiter(void 0, void 0, void 0, function () {
396
+ var dispatch, session, _a, unknown, _b;
397
+ return __generator(this, function (_c) {
398
+ switch (_c.label) {
399
+ case 0:
400
+ dispatch = seeded().dispatch;
401
+ return [4 /*yield*/, dispatch('/SASjsApi/session')];
402
+ case 1:
403
+ session = _c.sent();
404
+ _a = expect;
405
+ return [4 /*yield*/, session.json()];
406
+ case 2:
407
+ _a.apply(void 0, [_c.sent()]).toEqual({ state: 'completed' });
408
+ return [4 /*yield*/, dispatch('/SASjsApi/unknown/route')];
409
+ case 3:
410
+ unknown = _c.sent();
411
+ expect(unknown.status).toBe(404);
412
+ _b = expect;
413
+ return [4 /*yield*/, unknown.json()];
414
+ case 4:
415
+ _b.apply(void 0, [(_c.sent()).message]).toContain('Not implemented');
416
+ return [2 /*return*/];
417
+ }
418
+ });
419
+ }); });
420
+ it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
421
+ var dispatch, response, _a;
422
+ return __generator(this, function (_b) {
423
+ switch (_b.label) {
424
+ case 0:
425
+ dispatch = seeded().dispatch;
426
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/missing.txt'))];
427
+ case 1:
428
+ response = _b.sent();
429
+ expect(response.status).toBe(404);
430
+ _a = expect;
431
+ return [4 /*yield*/, response.json()];
432
+ case 2:
433
+ _a.apply(void 0, [(_b.sent()).message]).toBe('File not found');
434
+ return [2 /*return*/];
435
+ }
436
+ });
437
+ }); });
438
+ it('leaves a request to another origin alone', function () {
439
+ var dispatch = seeded().dispatch;
440
+ expect(dispatch('https://other.example.com/SASjsApi/info')).toBeUndefined();
441
+ });
442
+ it('passes a latin1 header value through as written, and drops one Response cannot carry', function () { return __awaiter(void 0, void 0, void 0, function () {
443
+ var accented, kept, beyond, dropped;
444
+ return __generator(this, function (_a) {
445
+ switch (_a.label) {
446
+ case 0:
447
+ accented = streaming([
448
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'Content-Disposition: attachment;filename="r\\u00e9sum\\u00e9.csv"\')',
449
+ "_webout = 'x'"
450
+ ].join('\n'));
451
+ return [4 /*yield*/, accented.dispatch(downloadPath())];
452
+ case 1:
453
+ kept = _a.sent();
454
+ expect(kept.headers.get('content-disposition')).toBe('attachment;filename="r\u00e9sum\u00e9.csv"');
455
+ beyond = streaming([
456
+ 'fs.writeFileSync(_SASJS_WEBOUT_HEADERS, \'Content-Disposition: attachment;filename="\\u{1F600}.csv"\')',
457
+ "_webout = 'x'"
458
+ ].join('\n'));
459
+ return [4 /*yield*/, beyond.dispatch(downloadPath())];
460
+ case 2:
461
+ dropped = _a.sent();
462
+ expect(dropped.status).toBe(200);
463
+ expect(dropped.headers.get('content-disposition')).toBeNull();
464
+ return [2 /*return*/];
465
+ }
466
+ });
467
+ }); });
468
+ it('answers 500 when a byte entry on the drive is not valid base64', function () { return __awaiter(void 0, void 0, void 0, function () {
469
+ var worker, response, _a;
470
+ var _b;
471
+ return __generator(this, function (_c) {
472
+ switch (_c.label) {
473
+ case 0:
474
+ worker = loadWorker();
475
+ worker.runtime.load((_b = {},
476
+ _b[drivePath('data.bin')] = '\u0000base64:!!!not base64!!!',
477
+ _b));
478
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' +
479
+ encodeURIComponent("".concat(appLoc, "/data.bin")))];
480
+ case 1:
481
+ response = _c.sent();
482
+ expect(response.status).toBe(500);
483
+ _a = expect;
484
+ return [4 /*yield*/, response.json()];
485
+ case 2:
486
+ _a.apply(void 0, [(_c.sent()).message]).toContain('Corrupt binary entry');
487
+ return [2 /*return*/];
488
+ }
489
+ });
490
+ }); });
491
+ it('frames the log behind the separator when _debug is set', function () { return __awaiter(void 0, void 0, void 0, function () {
492
+ var dispatch, response, _a;
493
+ return __generator(this, function (_b) {
494
+ switch (_b.label) {
495
+ case 0:
496
+ dispatch = seeded().dispatch;
497
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
498
+ encodeURIComponent("".concat(appLoc, "/services/public/getdata")) +
499
+ '&_debug=log')
500
+ // the log lands between the separators, not the empty string the framing
501
+ // would produce if the lines were dropped
502
+ ];
503
+ case 1:
504
+ response = _b.sent();
505
+ // the log lands between the separators, not the empty string the framing
506
+ // would produce if the lines were dropped
507
+ _a = expect;
508
+ return [4 /*yield*/, response.text()];
509
+ case 2:
510
+ // the log lands between the separators, not the empty string the framing
511
+ // would produce if the lines were dropped
512
+ _a.apply(void 0, [_b.sent()]).toBe(['seeded', LOGS_SEPARATOR, 'hello', LOGS_SEPARATOR].join('\n'));
513
+ return [2 /*return*/];
514
+ }
515
+ });
516
+ }); });
517
+ it('answers 400 when a request names neither a program nor code', function () { return __awaiter(void 0, void 0, void 0, function () {
518
+ var dispatch, response, _a;
519
+ return __generator(this, function (_b) {
520
+ switch (_b.label) {
521
+ case 0:
522
+ dispatch = seeded().dispatch;
523
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/execute')];
524
+ case 1:
525
+ response = _b.sent();
526
+ expect(response.status).toBe(400);
527
+ _a = expect;
528
+ return [4 /*yield*/, response.json()];
529
+ case 2:
530
+ _a.apply(void 0, [_b.sent()]).toEqual({ message: '_program is required' });
531
+ return [2 /*return*/];
532
+ }
533
+ });
534
+ }); });
535
+ it('answers 500 with the failure shape when the service throws', function () { return __awaiter(void 0, void 0, void 0, function () {
536
+ var _a, runtime, dispatch, response, body;
537
+ var _b;
538
+ return __generator(this, function (_c) {
539
+ switch (_c.label) {
540
+ case 0:
541
+ _a = seeded(), runtime = _a.runtime, dispatch = _a.dispatch;
542
+ runtime.load((_b = {},
543
+ _b[drivePath('services/public/broken.js')] = 'throw new Error("boom")',
544
+ _b));
545
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/execute?_program=' +
546
+ encodeURIComponent("".concat(appLoc, "/services/public/broken")))];
547
+ case 1:
548
+ response = _c.sent();
549
+ return [4 /*yield*/, response.json()];
550
+ case 2:
551
+ body = _c.sent();
552
+ expect(response.status).toBe(500);
553
+ expect(body.status).toBe('failure');
554
+ expect(body.error).toContain('boom');
555
+ return [2 /*return*/];
556
+ }
557
+ });
558
+ }); });
559
+ it('runs code carried by a code/execute request', function () { return __awaiter(void 0, void 0, void 0, function () {
560
+ var dispatch, response, _a;
561
+ return __generator(this, function (_b) {
562
+ switch (_b.label) {
563
+ case 0:
564
+ dispatch = seeded().dispatch;
565
+ return [4 /*yield*/, dispatch('/SASjsApi/code/execute', runRequest({ code: '_webout = "inline";', params: {} }))];
566
+ case 1:
567
+ response = _b.sent();
568
+ _a = expect;
569
+ return [4 /*yield*/, response.text()];
570
+ case 2:
571
+ _a.apply(void 0, [_b.sent()]).toBe('inline');
572
+ return [2 /*return*/];
573
+ }
574
+ });
575
+ }); });
576
+ it('serves a file off the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
577
+ var dispatch, response, _a;
578
+ return __generator(this, function (_b) {
579
+ switch (_b.label) {
580
+ case 0:
581
+ dispatch = seeded().dispatch;
582
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
583
+ encodeURIComponent("".concat(appLoc, "/services/public/getdata.js")))];
584
+ case 1:
585
+ response = _b.sent();
586
+ _a = expect;
587
+ return [4 /*yield*/, response.text()];
588
+ case 2:
589
+ _a.apply(void 0, [_b.sent()]).toBe('console.log("hello"); _webout = "seeded";');
590
+ return [2 /*return*/];
591
+ }
592
+ });
593
+ }); });
594
+ it('answers the API info and drive routes', function () { return __awaiter(void 0, void 0, void 0, function () {
595
+ var dispatch, info, _a, folder, _b, tree, _c, _d, _e;
596
+ return __generator(this, function (_f) {
597
+ switch (_f.label) {
598
+ case 0:
599
+ dispatch = seeded().dispatch;
600
+ return [4 /*yield*/, dispatch('/SASjsApi/info')];
601
+ case 1:
602
+ info = _f.sent();
603
+ _a = expect;
604
+ return [4 /*yield*/, info.json()];
605
+ case 2:
606
+ _a.apply(void 0, [_f.sent()]).toEqual(expect.objectContaining({ protocol: 'browser', runTimes: ['js'] }));
607
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/folder?_folderPath=' +
608
+ encodeURIComponent("".concat(appLoc, "/services/public")))];
609
+ case 3:
610
+ folder = _f.sent();
611
+ _b = expect;
612
+ return [4 /*yield*/, folder.json()];
613
+ case 4:
614
+ _b.apply(void 0, [(_f.sent()).files]).toContain('getdata.js');
615
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/filetree')];
616
+ case 5:
617
+ tree = _f.sent();
618
+ _c = expect;
619
+ _e = (_d = JSON).stringify;
620
+ return [4 /*yield*/, tree.json()];
621
+ case 6:
622
+ _c.apply(void 0, [_e.apply(_d, [_f.sent()])]).toContain('getdata.js');
623
+ return [2 /*return*/];
624
+ }
625
+ });
626
+ }); });
627
+ it('accepts a multipart request body', function () { return __awaiter(void 0, void 0, void 0, function () {
628
+ var dispatch, form, response, _a;
629
+ return __generator(this, function (_b) {
630
+ switch (_b.label) {
631
+ case 0:
632
+ dispatch = seeded().dispatch;
633
+ form = new FormData();
634
+ form.append('_program', "".concat(appLoc, "/services/public/getdata"));
635
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/execute', {
636
+ method: 'POST',
637
+ body: form
638
+ })];
639
+ case 1:
640
+ response = _b.sent();
641
+ _a = expect;
642
+ return [4 /*yield*/, response.text()];
643
+ case 2:
644
+ _a.apply(void 0, [_b.sent()]).toBe('seeded');
645
+ return [2 /*return*/];
646
+ }
647
+ });
648
+ }); });
649
+ it('acknowledges a load, a seed, a reset and a snapshot from the page', function () { return __awaiter(void 0, void 0, void 0, function () {
650
+ var _a, message, runtime, files, load, seed, snapshot, reset, stats;
651
+ var _b;
652
+ return __generator(this, function (_c) {
653
+ switch (_c.label) {
654
+ case 0:
655
+ _a = seeded(), message = _a.message, runtime = _a.runtime;
656
+ files = (_b = {}, _b[drivePath('services/public/other.js')] = '_webout = "x";', _b);
657
+ load = message({ type: 'sasjs-load', files: files });
658
+ return [4 /*yield*/, load.pending];
659
+ case 1:
660
+ _c.sent();
661
+ expect(load.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-load-ack' }));
662
+ seed = message({ type: 'sasjs-seed' });
663
+ return [4 /*yield*/, seed.pending];
664
+ case 2:
665
+ _c.sent();
666
+ expect(seed.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-seed-ack' }));
667
+ runtime.load(files);
668
+ snapshot = message({ type: 'sasjs-snapshot' });
669
+ expect(snapshot.replies[0].type).toBe('sasjs-snapshot');
670
+ expect(Object.keys(snapshot.replies[0].files).length).toBe(1);
671
+ reset = message({ type: 'sasjs-reset' });
672
+ return [4 /*yield*/, reset.pending];
673
+ case 3:
674
+ _c.sent();
675
+ expect(reset.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-reset-ack' }));
676
+ stats = message({ type: 'sasjs-stats' });
677
+ return [4 /*yield*/, stats.pending];
678
+ case 4:
679
+ _c.sent();
680
+ expect(stats.replies[0]).toEqual(expect.objectContaining({ type: 'sasjs-stats' }));
681
+ return [2 /*return*/];
682
+ }
683
+ });
684
+ }); });
685
+ it('reports a corrupt entry in the stats reply rather than leaving the page waiting', function () { return __awaiter(void 0, void 0, void 0, function () {
686
+ var worker, stats;
687
+ var _a;
688
+ return __generator(this, function (_b) {
689
+ switch (_b.label) {
690
+ case 0:
691
+ worker = loadWorker();
692
+ worker.runtime.load((_a = {},
693
+ _a[drivePath('data.bin')] = '\u0000base64:!!!not base64!!!',
694
+ _a));
695
+ stats = worker.message({ type: 'sasjs-stats' });
696
+ return [4 /*yield*/, stats.pending];
697
+ case 1:
698
+ _b.sent();
699
+ expect(stats.replies[0]).toEqual(expect.objectContaining({
700
+ type: 'sasjs-stats',
701
+ error: expect.stringContaining('Corrupt binary entry')
702
+ }));
703
+ return [2 /*return*/];
704
+ }
705
+ });
706
+ }); });
707
+ it('claims the clients on install and activate', function () {
708
+ var lifecycle = seeded().lifecycle;
709
+ expect(function () { return lifecycle('install'); }).not.toThrow();
710
+ expect(function () { return lifecycle('activate'); }).not.toThrow();
711
+ });
712
+ it('leaves requests outside /SASjsApi/ to the static host', function () {
713
+ var dispatch = seeded().dispatch;
714
+ expect(dispatch('/index.html')).toBeUndefined();
715
+ });
716
+ });