@sasjs/cli 6.1.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.
@@ -83,7 +83,7 @@ var loadWorker = function () {
83
83
  var runtime = self.SASjsMockRuntime;
84
84
  var dispatch = function (path, init) {
85
85
  if (init === void 0) { init = {}; }
86
- var request = new Request(origin + path, init);
86
+ var request = new Request(path.startsWith('http') ? path : origin + path, init);
87
87
  var event = {
88
88
  request: request,
89
89
  respondWith: function (response) {
@@ -155,6 +155,339 @@ describe('service worker host', function () {
155
155
  }
156
156
  });
157
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
+ }); });
158
491
  it('frames the log behind the separator when _debug is set', function () { return __awaiter(void 0, void 0, void 0, function () {
159
492
  var dispatch, response, _a;
160
493
  return __generator(this, function (_b) {
@@ -349,6 +682,28 @@ describe('service worker host', function () {
349
682
  }
350
683
  });
351
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
+ }); });
352
707
  it('claims the clients on install and activate', function () {
353
708
  var lifecycle = seeded().lifecycle;
354
709
  expect(function () { return lifecycle('install'); }).not.toThrow();
package/package.json CHANGED
@@ -126,5 +126,5 @@
126
126
  "peerDependencies": {
127
127
  "@sasjs/lint": "^4.3.0"
128
128
  },
129
- "version": "6.1.0"
129
+ "version": "6.2.0"
130
130
  }