@sasjs/cli 6.27.1 → 6.27.3

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.
@@ -90,6 +90,7 @@ var path_1 = __importDefault(require("path"));
90
90
  var api_1 = require("@sasjs/api");
91
91
  var os_1 = __importDefault(require("os"));
92
92
  var yazl_1 = __importDefault(require("yazl"));
93
+ var utils_1 = require("@sasjs/utils");
93
94
  var mockSite_1 = require("../mockSite");
94
95
  /**
95
96
  * The other half of the parity claim: the service worker is what turns an
@@ -122,8 +123,9 @@ var lintBundle = function () {
122
123
  }
123
124
  return lintBundlePath;
124
125
  };
125
- var loadWorker = function (base) {
126
+ var loadWorker = function (base, options) {
126
127
  if (base === void 0) { base = ''; }
128
+ if (options === void 0) { options = {}; }
127
129
  jest.resetModules();
128
130
  var handlers = {};
129
131
  var refusingWrites = false;
@@ -205,6 +207,22 @@ var loadWorker = function (base) {
205
207
  fixture = global.__sasTranscript;
206
208
  return [2 /*return*/, fixture ? jsonResponse(fixture) : new Response('', { status: 404 })];
207
209
  }
210
+ // the webout() block the build writes beside the worker, from the same
211
+ // @sasjs/utils generator sasjs/server uses. Absent unless a test asks for
212
+ // it: a site built before the block existed keeps the `_webout`-only path.
213
+ if (options.prelude && target.includes('webout-prelude'))
214
+ return [2 /*return*/, jsonResponse({
215
+ sentinels: mockSite_1.WEBOUT_SENTINELS,
216
+ template: (0, utils_1.getWeboutPrelude)({
217
+ weboutPath: mockSite_1.WEBOUT_SENTINELS.weboutPath,
218
+ headersPath: mockSite_1.WEBOUT_SENTINELS.headersPath,
219
+ program: mockSite_1.WEBOUT_SENTINELS.program,
220
+ debug: mockSite_1.WEBOUT_SENTINELS.debug,
221
+ username: mockSite_1.WEBOUT_SENTINELS.username,
222
+ userId: mockSite_1.WEBOUT_SENTINELS.userId,
223
+ apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
224
+ })
225
+ })];
208
226
  return [2 /*return*/, target.includes('vfs-seed')
209
227
  ? jsonResponse({
210
228
  files: (_a = {},
@@ -260,7 +278,8 @@ var jsonResponse = function (body) { return ({
260
278
  return [2 /*return*/, body];
261
279
  }); }); }
262
280
  }); };
263
- /** A zip archive of text members, written the way a deploy upload ships one.
281
+ /** A zip archive of members - text, or bytes for a base64 member - written the way
282
+ * a deploy upload ships one.
264
283
  * The first member is stored rather than deflated, so both methods the reader
265
284
  * accepts are exercised by the one case. */
266
285
  var zipOf = function (files) {
@@ -392,6 +411,164 @@ describe('service worker host', function () {
392
411
  }
393
412
  });
394
413
  }); });
414
+ it('runs a JS program written the way sasjs/server runs one', function () { return __awaiter(void 0, void 0, void 0, function () {
415
+ var worker, response, body, _a, _b;
416
+ var _c;
417
+ return __generator(this, function (_d) {
418
+ switch (_d.label) {
419
+ case 0:
420
+ worker = loadWorker('', { prelude: true });
421
+ worker.runtime.load((_c = {},
422
+ _c[drivePath('services/public/run.js')] = [
423
+ "const rows = [{ name: 'Alice', age: 14 }, { name: 'Bob', age: 15 }]",
424
+ 'console.log(`Read ${rows.length} rows`)',
425
+ "webout('OPEN')",
426
+ "webout('OBJ', 'adults', rows.filter((row) => row.age > 13))",
427
+ "webout('CLOSE')"
428
+ ].join('\n'),
429
+ _c));
430
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
431
+ encodeURIComponent("".concat(appLoc, "/services/public/run")))];
432
+ case 1:
433
+ response = _d.sent();
434
+ expect(response.status).toBe(200);
435
+ _b = (_a = JSON).parse;
436
+ return [4 /*yield*/, response.text()];
437
+ case 2:
438
+ body = _b.apply(_a, [_d.sent()]);
439
+ expect(body.adults).toEqual([
440
+ { name: 'Alice', age: 14 },
441
+ { name: 'Bob', age: 15 }
442
+ ]);
443
+ return [2 /*return*/];
444
+ }
445
+ });
446
+ }); });
447
+ it('answers a Run in Studio on the recorded program, as a code run', function () { return __awaiter(void 0, void 0, void 0, function () {
448
+ var worker, response, parts;
449
+ var _a, _b;
450
+ return __generator(this, function (_c) {
451
+ switch (_c.label) {
452
+ case 0:
453
+ // Press Run in Studio posts the buffer to /code/execute with the file's own
454
+ // path prepended as `%let _program` - the path is not a query parameter, as
455
+ // it is for stp/execute. The recording is what answers it: without this the
456
+ // demo's .sas program has a Run button (the host states `sas`) and no way to
457
+ // answer it, because a browser cannot compile SAS.
458
+ ;
459
+ global.__sasTranscript = (_a = {},
460
+ _a["".concat(appLoc, "/services/public/program.sas")] = {
461
+ webout: '{"SYSSITE":"00000000"}',
462
+ log: 'NOTE: recorded',
463
+ print: 'The SAS System'
464
+ },
465
+ _a);
466
+ worker = loadWorker();
467
+ worker.runtime.load((_b = {},
468
+ _b[drivePath('services/public/program.sas')] = 'data work.class; run;',
469
+ _b));
470
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({
471
+ code: "%let _program = ".concat(appLoc, "/services/public/program.sas;\ndata work.class; run;"),
472
+ runTime: 'sas'
473
+ }))];
474
+ case 1:
475
+ response = _c.sent();
476
+ expect(response.status).toBe(200);
477
+ return [4 /*yield*/, response.text()];
478
+ case 2:
479
+ parts = (_c.sent()).split(LOGS_SEPARATOR);
480
+ expect(parts[0].trim()).toBe('{"SYSSITE":"00000000"}');
481
+ expect(parts[1]).toContain('NOTE: recorded');
482
+ expect(parts[2]).toContain('The SAS System');
483
+ delete global.__sasTranscript;
484
+ return [2 /*return*/];
485
+ }
486
+ });
487
+ }); });
488
+ it('answers only the line the interface writes, not one further down', function () { return __awaiter(void 0, void 0, void 0, function () {
489
+ var worker, response, _a;
490
+ var _b, _c;
491
+ return __generator(this, function (_d) {
492
+ switch (_d.label) {
493
+ case 0:
494
+ // The interface puts `%let _program` first; a line further down belongs to
495
+ // the program. Answered, it would hand a caller's own SAS program the session
496
+ // recorded for another file - the "inventing a session" the refusal exists to
497
+ // avoid, with a different file's log.
498
+ ;
499
+ global.__sasTranscript = (_b = {},
500
+ _b["".concat(appLoc, "/services/public/program.sas")] = {
501
+ webout: '{"SYSSITE":"00000000"}',
502
+ log: 'NOTE: recorded',
503
+ print: 'The SAS System'
504
+ },
505
+ _b);
506
+ worker = loadWorker();
507
+ worker.runtime.load((_c = {},
508
+ _c[drivePath('services/public/mine.sas')] = 'data work.class; run;',
509
+ _c));
510
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({
511
+ code: "data _null_;\n%let _program = ".concat(appLoc, "/services/public/program.sas;\nrun;"),
512
+ runTime: 'sas'
513
+ }))];
514
+ case 1:
515
+ response = _d.sent();
516
+ expect(response.status).toBe(400);
517
+ _a = expect;
518
+ return [4 /*yield*/, response.text()];
519
+ case 2:
520
+ _a.apply(void 0, [_d.sent()]).toContain('SAS is not available');
521
+ delete global.__sasTranscript;
522
+ return [2 /*return*/];
523
+ }
524
+ });
525
+ }); });
526
+ it('refuses SAS code that names no recorded program', function () { return __awaiter(void 0, void 0, void 0, function () {
527
+ var worker, _a, _b, code, response, body, e_1_1;
528
+ var e_1, _c;
529
+ return __generator(this, function (_d) {
530
+ switch (_d.label) {
531
+ case 0:
532
+ worker = loadWorker();
533
+ _d.label = 1;
534
+ case 1:
535
+ _d.trys.push([1, 7, 8, 9]);
536
+ _a = __values([
537
+ 'data work.class; run;',
538
+ "%let _program = ".concat(appLoc, "/services/public/other.sas;\ndata work.class; run;")
539
+ ]), _b = _a.next();
540
+ _d.label = 2;
541
+ case 2:
542
+ if (!!_b.done) return [3 /*break*/, 6];
543
+ code = _b.value;
544
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({ code: code, runTime: 'sas' }))];
545
+ case 3:
546
+ response = _d.sent();
547
+ expect(response.status).toBe(400);
548
+ return [4 /*yield*/, response.text()];
549
+ case 4:
550
+ body = _d.sent();
551
+ expect(body).toContain('SAS is not available');
552
+ expect(body).not.toContain('SyntaxError');
553
+ _d.label = 5;
554
+ case 5:
555
+ _b = _a.next();
556
+ return [3 /*break*/, 2];
557
+ case 6: return [3 /*break*/, 9];
558
+ case 7:
559
+ e_1_1 = _d.sent();
560
+ e_1 = { error: e_1_1 };
561
+ return [3 /*break*/, 9];
562
+ case 8:
563
+ try {
564
+ if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
565
+ }
566
+ finally { if (e_1) throw e_1.error; }
567
+ return [7 /*endfinally*/];
568
+ case 9: return [2 /*return*/];
569
+ }
570
+ });
571
+ }); });
395
572
  it('refuses a .sas program no recording names, as it always did', function () { return __awaiter(void 0, void 0, void 0, function () {
396
573
  var worker, response, body;
397
574
  var _a;
@@ -410,7 +587,8 @@ describe('service worker host', function () {
410
587
  return [4 /*yield*/, response.text()];
411
588
  case 2:
412
589
  body = _b.sent();
413
- expect(body).toContain('runTime');
590
+ expect(body).toContain('SAS is not available');
591
+ expect(body).toContain('recorded session');
414
592
  expect(body).not.toContain('SyntaxError');
415
593
  return [2 /*return*/];
416
594
  }
@@ -1546,8 +1724,8 @@ describe('service worker host', function () {
1546
1724
  });
1547
1725
  }); });
1548
1726
  it('publishes, lists, serves and removes an app through the package surface', function () { return __awaiter(void 0, void 0, void 0, function () {
1549
- var dispatch, _a, _b, path_2, listing, _c, e_1_1, _d, _e, path_3, landing, e_2_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_3_1, _j, _k, path_5, page, _l, e_4_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
1550
- var e_1, _q, e_2, _r, e_3, _s, e_4, _t;
1727
+ var dispatch, _a, _b, path_2, listing, _c, e_2_1, _d, _e, path_3, landing, e_3_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_4_1, _j, _k, path_5, page, _l, e_5_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
1728
+ var e_2, _q, e_3, _r, e_4, _s, e_5, _t;
1551
1729
  return __generator(this, function (_u) {
1552
1730
  switch (_u.label) {
1553
1731
  case 0:
@@ -1574,14 +1752,14 @@ describe('service worker host', function () {
1574
1752
  return [3 /*break*/, 2];
1575
1753
  case 6: return [3 /*break*/, 9];
1576
1754
  case 7:
1577
- e_1_1 = _u.sent();
1578
- e_1 = { error: e_1_1 };
1755
+ e_2_1 = _u.sent();
1756
+ e_2 = { error: e_2_1 };
1579
1757
  return [3 /*break*/, 9];
1580
1758
  case 8:
1581
1759
  try {
1582
1760
  if (_b && !_b.done && (_q = _a.return)) _q.call(_a);
1583
1761
  }
1584
- finally { if (e_1) throw e_1.error; }
1762
+ finally { if (e_2) throw e_2.error; }
1585
1763
  return [7 /*endfinally*/];
1586
1764
  case 9:
1587
1765
  _u.trys.push([9, 14, 15, 16]);
@@ -1601,14 +1779,14 @@ describe('service worker host', function () {
1601
1779
  return [3 /*break*/, 10];
1602
1780
  case 13: return [3 /*break*/, 16];
1603
1781
  case 14:
1604
- e_2_1 = _u.sent();
1605
- e_2 = { error: e_2_1 };
1782
+ e_3_1 = _u.sent();
1783
+ e_3 = { error: e_3_1 };
1606
1784
  return [3 /*break*/, 16];
1607
1785
  case 15:
1608
1786
  try {
1609
1787
  if (_e && !_e.done && (_r = _d.return)) _r.call(_d);
1610
1788
  }
1611
- finally { if (e_2) throw e_2.error; }
1789
+ finally { if (e_3) throw e_3.error; }
1612
1790
  return [7 /*endfinally*/];
1613
1791
  case 16: return [4 /*yield*/, dispatch('/AppStream')];
1614
1792
  case 17:
@@ -1661,14 +1839,14 @@ describe('service worker host', function () {
1661
1839
  return [3 /*break*/, 22];
1662
1840
  case 26: return [3 /*break*/, 29];
1663
1841
  case 27:
1664
- e_3_1 = _u.sent();
1665
- e_3 = { error: e_3_1 };
1842
+ e_4_1 = _u.sent();
1843
+ e_4 = { error: e_4_1 };
1666
1844
  return [3 /*break*/, 29];
1667
1845
  case 28:
1668
1846
  try {
1669
1847
  if (_g && !_g.done && (_s = _f.return)) _s.call(_f);
1670
1848
  }
1671
- finally { if (e_3) throw e_3.error; }
1849
+ finally { if (e_4) throw e_4.error; }
1672
1850
  return [7 /*endfinally*/];
1673
1851
  case 29:
1674
1852
  _u.trys.push([29, 35, 36, 37]);
@@ -1695,14 +1873,14 @@ describe('service worker host', function () {
1695
1873
  return [3 /*break*/, 30];
1696
1874
  case 34: return [3 /*break*/, 37];
1697
1875
  case 35:
1698
- e_4_1 = _u.sent();
1699
- e_4 = { error: e_4_1 };
1876
+ e_5_1 = _u.sent();
1877
+ e_5 = { error: e_5_1 };
1700
1878
  return [3 /*break*/, 37];
1701
1879
  case 36:
1702
1880
  try {
1703
1881
  if (_k && !_k.done && (_t = _j.return)) _t.call(_j);
1704
1882
  }
1705
- finally { if (e_4) throw e_4.error; }
1883
+ finally { if (e_5) throw e_5.error; }
1706
1884
  return [7 /*endfinally*/];
1707
1885
  case 37: return [4 /*yield*/, dispatch("/app/".concat(app.id))];
1708
1886
  case 38:
@@ -2005,30 +2183,33 @@ describe('service worker host', function () {
2005
2183
  case 0:
2006
2184
  dispatch = seeded().dispatch;
2007
2185
  return [4 /*yield*/, dispatch('/SASjsApi/info')
2008
- // the documented body, field for field, in the values @sasjs/api's
2009
- // BROWSER_INFO states: a property the document does not state, or a login
2010
- // form told there are accounts when there are none, is this host describing
2011
- // an API the server does not serve
2186
+ // the documented body, field for field: a property the document does not
2187
+ // state, or a login form told there are accounts when there are none, is
2188
+ // this host describing an API the server does not serve. `runTimes` is the
2189
+ // host's own, not the package's browser fixture: `sas` is what makes the
2190
+ // Studio offer Run on a .sas file, and the recording is how it is answered.
2012
2191
  ];
2013
2192
  case 1:
2014
2193
  info = _f.sent();
2015
- // the documented body, field for field, in the values @sasjs/api's
2016
- // BROWSER_INFO states: a property the document does not state, or a login
2017
- // form told there are accounts when there are none, is this host describing
2018
- // an API the server does not serve
2194
+ // the documented body, field for field: a property the document does not
2195
+ // state, or a login form told there are accounts when there are none, is
2196
+ // this host describing an API the server does not serve. `runTimes` is the
2197
+ // host's own, not the package's browser fixture: `sas` is what makes the
2198
+ // Studio offer Run on a .sas file, and the recording is how it is answered.
2019
2199
  _a = expect;
2020
2200
  return [4 /*yield*/, info.json()];
2021
2201
  case 2:
2022
- // the documented body, field for field, in the values @sasjs/api's
2023
- // BROWSER_INFO states: a property the document does not state, or a login
2024
- // form told there are accounts when there are none, is this host describing
2025
- // an API the server does not serve
2202
+ // the documented body, field for field: a property the document does not
2203
+ // state, or a login form told there are accounts when there are none, is
2204
+ // this host describing an API the server does not serve. `runTimes` is the
2205
+ // host's own, not the package's browser fixture: `sas` is what makes the
2206
+ // Studio offer Run on a .sas file, and the recording is how it is answered.
2026
2207
  _a.apply(void 0, [_f.sent()]).toEqual({
2027
2208
  mode: 'desktop',
2028
2209
  cors: 'disable',
2029
2210
  httpPort: null,
2030
2211
  httpsPort: null,
2031
- runTimes: ['js'],
2212
+ runTimes: ['js', 'sas'],
2032
2213
  authProviders: [],
2033
2214
  localLoginEnabled: false,
2034
2215
  version: 'dev'
@@ -2641,7 +2822,7 @@ describe('service worker host', function () {
2641
2822
  }); });
2642
2823
  });
2643
2824
  describe('conformance corpus', function () {
2644
- var e_5, _a, e_6, _b;
2825
+ var e_6, _a, e_7, _b;
2645
2826
  /**
2646
2827
  * The corpus is what "the same API" means: the cases @sasjs/api runs against
2647
2828
  * its own services, and against a live sasjs/server, run here against this
@@ -2650,7 +2831,7 @@ describe('service worker host', function () {
2650
2831
  * used to have - and it is a test rather than a review.
2651
2832
  */
2652
2833
  var httpFor = function (testCase) {
2653
- var e_7, _a;
2834
+ var e_8, _a;
2654
2835
  var _b;
2655
2836
  var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
2656
2837
  var query = new URLSearchParams();
@@ -2661,12 +2842,12 @@ describe('service worker host', function () {
2661
2842
  query.set(key, String(value));
2662
2843
  }
2663
2844
  }
2664
- catch (e_7_1) { e_7 = { error: e_7_1 }; }
2845
+ catch (e_8_1) { e_8 = { error: e_8_1 }; }
2665
2846
  finally {
2666
2847
  try {
2667
2848
  if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
2668
2849
  }
2669
- finally { if (e_7) throw e_7.error; }
2850
+ finally { if (e_8) throw e_8.error; }
2670
2851
  }
2671
2852
  var suffix = query.toString() ? "?".concat(query) : '';
2672
2853
  var init = { method: method };
@@ -2697,8 +2878,8 @@ describe('service worker host', function () {
2697
2878
  * does.
2698
2879
  */
2699
2880
  var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
2700
- var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_8_1;
2701
- var e_9, _g, e_8, _h;
2881
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_9_1;
2882
+ var e_10, _g, e_9, _h;
2702
2883
  return __generator(this, function (_j) {
2703
2884
  switch (_j.label) {
2704
2885
  case 0:
@@ -2710,12 +2891,12 @@ describe('service worker host', function () {
2710
2891
  text["/sasjs/drive/files".concat(file)] = contents;
2711
2892
  }
2712
2893
  }
2713
- catch (e_9_1) { e_9 = { error: e_9_1 }; }
2894
+ catch (e_10_1) { e_10 = { error: e_10_1 }; }
2714
2895
  finally {
2715
2896
  try {
2716
2897
  if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
2717
2898
  }
2718
- finally { if (e_9) throw e_9.error; }
2899
+ finally { if (e_10) throw e_10.error; }
2719
2900
  }
2720
2901
  return [4 /*yield*/, runtime.load(text)];
2721
2902
  case 1:
@@ -2738,14 +2919,14 @@ describe('service worker host', function () {
2738
2919
  return [3 /*break*/, 3];
2739
2920
  case 6: return [3 /*break*/, 9];
2740
2921
  case 7:
2741
- e_8_1 = _j.sent();
2742
- e_8 = { error: e_8_1 };
2922
+ e_9_1 = _j.sent();
2923
+ e_9 = { error: e_9_1 };
2743
2924
  return [3 /*break*/, 9];
2744
2925
  case 8:
2745
2926
  try {
2746
2927
  if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
2747
2928
  }
2748
- finally { if (e_8) throw e_8.error; }
2929
+ finally { if (e_9) throw e_9.error; }
2749
2930
  return [7 /*endfinally*/];
2750
2931
  case 9: return [2 /*return*/];
2751
2932
  }
@@ -2856,29 +3037,13 @@ describe('service worker host', function () {
2856
3037
  _loop_1(testCase);
2857
3038
  }
2858
3039
  }
2859
- catch (e_5_1) { e_5 = { error: e_5_1 }; }
3040
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
2860
3041
  finally {
2861
3042
  try {
2862
3043
  if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
2863
3044
  }
2864
- finally { if (e_5) throw e_5.error; }
3045
+ finally { if (e_6) throw e_6.error; }
2865
3046
  }
2866
- /**
2867
- * The deploy route's JSON form and its removal, which is what `sasjs deploy`
2868
- * sends and what this host's transport carries unchanged.
2869
- *
2870
- * The upload route's cases hand over the expanded tree in place of the
2871
- * archive, which is a host's transport detail rather than an answer: this
2872
- * worker takes the archive and expands it, so those cases would pin the
2873
- * corpus's assumption instead of this host's behaviour. The zip cases above
2874
- * pin that route. The removal cases need an app to remove, so the index is
2875
- * seeded with the corpus's own - the worker starts with nothing published,
2876
- * which is the server's state too.
2877
- *
2878
- * The state a case leaves is checked too - `expectDrive` - because both deploy
2879
- * answers are `200` and the difference between replacing a folder and keeping
2880
- * what is there is only visible in the drive afterwards.
2881
- */
2882
3047
  /** The app a removal case names, as the corpus states it. */
2883
3048
  var CORPUS_APP = {
2884
3049
  appLoc: '/Public/myApp',
@@ -2932,10 +3097,92 @@ describe('service worker host', function () {
2932
3097
  }
2933
3098
  });
2934
3099
  }); };
3100
+ /**
3101
+ * The archive a client uploads, built from the tree the case states.
3102
+ *
3103
+ * The corpus is transport-agnostic: an upload case gives the tree the archive
3104
+ * has to expand to, and the folder in the query where the document puts it.
3105
+ * This host takes an archive, so the harness makes one - which is the only way
3106
+ * the mock's own expansion, and the tree it produces, gets exercised by the
3107
+ * cases that describe it.
3108
+ */
3109
+ var archiveOf = function (tree) { return __awaiter(void 0, void 0, void 0, function () {
3110
+ var files, walk;
3111
+ return __generator(this, function (_a) {
3112
+ files = {};
3113
+ walk = function (members, prefix) {
3114
+ var e_11, _a;
3115
+ var _b;
3116
+ try {
3117
+ for (var _c = __values(members || []), _d = _c.next(); !_d.done; _d = _c.next()) {
3118
+ var member = _d.value;
3119
+ var at = prefix ? "".concat(prefix, "/").concat(member.name) : member.name;
3120
+ if (member.type === 'folder')
3121
+ walk(member.members, at);
3122
+ else
3123
+ files[at] =
3124
+ member.encoding === 'base64'
3125
+ ? new Uint8Array(Buffer.from(String(member.code), 'base64'))
3126
+ : String((_b = member.code) !== null && _b !== void 0 ? _b : '');
3127
+ }
3128
+ }
3129
+ catch (e_11_1) { e_11 = { error: e_11_1 }; }
3130
+ finally {
3131
+ try {
3132
+ if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
3133
+ }
3134
+ finally { if (e_11) throw e_11.error; }
3135
+ }
3136
+ };
3137
+ walk(tree === null || tree === void 0 ? void 0 : tree.members, '');
3138
+ return [2 /*return*/, zipOf(files)];
3139
+ });
3140
+ }); };
3141
+ /**
3142
+ * The request a deploy case describes, as HTTP.
3143
+ *
3144
+ * The JSON and removal routes carry their fields as they are stated. The upload
3145
+ * route's body is an archive, so the tree becomes one and the fields the case
3146
+ * states beside it travel as form fields - a host's multipart body, with the
3147
+ * folder also in the query where the document puts it.
3148
+ */
3149
+ var deployRequest = function (testCase) { return __awaiter(void 0, void 0, void 0, function () {
3150
+ var _a, requestPath, init, body, archive, form, _b, _c, _d, field, value;
3151
+ var e_12, _e;
3152
+ return __generator(this, function (_f) {
3153
+ switch (_f.label) {
3154
+ case 0:
3155
+ _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
3156
+ if (!testCase.route.endsWith('/deploy/upload'))
3157
+ return [2 /*return*/, { requestPath: requestPath, init: init }];
3158
+ body = (testCase.request.body || {});
3159
+ return [4 /*yield*/, archiveOf(body.fileTree)];
3160
+ case 1:
3161
+ archive = _f.sent();
3162
+ form = new FormData();
3163
+ try {
3164
+ for (_b = __values(Object.entries(body)), _c = _b.next(); !_c.done; _c = _b.next()) {
3165
+ _d = __read(_c.value, 2), field = _d[0], value = _d[1];
3166
+ if (field !== 'fileTree' && value !== undefined)
3167
+ form.set(field, String(value));
3168
+ }
3169
+ }
3170
+ catch (e_12_1) { e_12 = { error: e_12_1 }; }
3171
+ finally {
3172
+ try {
3173
+ if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
3174
+ }
3175
+ finally { if (e_12) throw e_12.error; }
3176
+ }
3177
+ form.set('file', new Blob([new Uint8Array(archive)]), 'deploy.zip');
3178
+ return [2 /*return*/, { requestPath: requestPath, init: { method: 'POST', body: form } }];
3179
+ }
3180
+ });
3181
+ }); };
2935
3182
  var _loop_2 = function (testCase) {
2936
3183
  it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
2937
- var worker, _a, requestPath, init, response, listing, ids, _b, _c, path_6, file, e_10_1, _d, _e, path_7, file, e_11_1;
2938
- var e_10, _f, e_11, _g;
3184
+ var worker, _a, requestPath, init, response, listing, ids, _b, _c, path_6, file, e_13_1, _d, _e, path_7, file, e_14_1;
3185
+ var e_13, _f, e_14, _g;
2939
3186
  var _h, _j, _k;
2940
3187
  return __generator(this, function (_l) {
2941
3188
  switch (_l.label) {
@@ -2944,102 +3191,121 @@ describe('service worker host', function () {
2944
3191
  return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
2945
3192
  case 1:
2946
3193
  _l.sent();
2947
- return [4 /*yield*/, seedIndex(worker.runtime, testCase)
2948
- // `unwritable` is not passed on here: the case that states it is the
2949
- // upload route's, which the filter above keeps out, and the deploy route's
2950
- // own `500` is covered by `deploy-refused-by-store` in the drive loop.
2951
- ];
3194
+ return [4 /*yield*/, seedIndex(worker.runtime, testCase)];
2952
3195
  case 2:
2953
3196
  _l.sent();
2954
- _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
2955
- return [4 /*yield*/, worker.dispatch(requestPath, init)];
3197
+ worker.refuseWrites(Boolean(testCase.unwritable));
3198
+ return [4 /*yield*/, deployRequest(testCase)];
2956
3199
  case 3:
3200
+ _a = _l.sent(), requestPath = _a.requestPath, init = _a.init;
3201
+ return [4 /*yield*/, worker.dispatch(requestPath, init)];
3202
+ case 4:
2957
3203
  response = _l.sent();
2958
3204
  return [4 /*yield*/, assertExpectations(response, testCase)
2959
3205
  // a removal takes the app the id names, and leaves the rest published
2960
3206
  ];
2961
- case 4:
3207
+ case 5:
2962
3208
  _l.sent();
2963
- if (!(testCase.route === 'DELETE /SASjsApi/drive/deploy')) return [3 /*break*/, 7];
3209
+ if (!(testCase.route === 'DELETE /SASjsApi/drive/deploy')) return [3 /*break*/, 8];
2964
3210
  return [4 /*yield*/, worker.dispatch('/app/apps.json')];
2965
- case 5:
3211
+ case 6:
2966
3212
  listing = _l.sent();
2967
3213
  return [4 /*yield*/, listing.json()];
2968
- case 6:
3214
+ case 7:
2969
3215
  ids = (_l.sent()).apps.map(function (app) { return app.id; });
2970
3216
  expect(ids).toContain(BYSTANDER.id);
2971
3217
  if (response.status === 200)
2972
3218
  expect(ids).not.toContain((_h = testCase.request.body) === null || _h === void 0 ? void 0 : _h.id);
2973
- _l.label = 7;
2974
- case 7:
2975
- _l.trys.push([7, 12, 13, 14]);
2976
- _b = __values(((_j = testCase.expectDrive) === null || _j === void 0 ? void 0 : _j.present) || []), _c = _b.next();
2977
3219
  _l.label = 8;
2978
3220
  case 8:
2979
- if (!!_c.done) return [3 /*break*/, 11];
3221
+ _l.trys.push([8, 13, 14, 15]);
3222
+ _b = __values(((_j = testCase.expectDrive) === null || _j === void 0 ? void 0 : _j.present) || []), _c = _b.next();
3223
+ _l.label = 9;
3224
+ case 9:
3225
+ if (!!_c.done) return [3 /*break*/, 12];
2980
3226
  path_6 = _c.value;
2981
3227
  return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent(path_6))];
2982
- case 9:
3228
+ case 10:
2983
3229
  file = _l.sent();
2984
3230
  expect({ path: path_6, status: file.status }).toEqual({ path: path_6, status: 200 });
2985
- _l.label = 10;
2986
- case 10:
3231
+ _l.label = 11;
3232
+ case 11:
2987
3233
  _c = _b.next();
2988
- return [3 /*break*/, 8];
2989
- case 11: return [3 /*break*/, 14];
2990
- case 12:
2991
- e_10_1 = _l.sent();
2992
- e_10 = { error: e_10_1 };
2993
- return [3 /*break*/, 14];
3234
+ return [3 /*break*/, 9];
3235
+ case 12: return [3 /*break*/, 15];
2994
3236
  case 13:
3237
+ e_13_1 = _l.sent();
3238
+ e_13 = { error: e_13_1 };
3239
+ return [3 /*break*/, 15];
3240
+ case 14:
2995
3241
  try {
2996
3242
  if (_c && !_c.done && (_f = _b.return)) _f.call(_b);
2997
3243
  }
2998
- finally { if (e_10) throw e_10.error; }
3244
+ finally { if (e_13) throw e_13.error; }
2999
3245
  return [7 /*endfinally*/];
3000
- case 14:
3001
- _l.trys.push([14, 19, 20, 21]);
3002
- _d = __values(((_k = testCase.expectDrive) === null || _k === void 0 ? void 0 : _k.missing) || []), _e = _d.next();
3003
- _l.label = 15;
3004
3246
  case 15:
3005
- if (!!_e.done) return [3 /*break*/, 18];
3247
+ _l.trys.push([15, 20, 21, 22]);
3248
+ _d = __values(((_k = testCase.expectDrive) === null || _k === void 0 ? void 0 : _k.missing) || []), _e = _d.next();
3249
+ _l.label = 16;
3250
+ case 16:
3251
+ if (!!_e.done) return [3 /*break*/, 19];
3006
3252
  path_7 = _e.value;
3007
3253
  return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent(path_7))];
3008
- case 16:
3254
+ case 17:
3009
3255
  file = _l.sent();
3010
3256
  expect({ path: path_7, status: file.status }).toEqual({ path: path_7, status: 404 });
3011
- _l.label = 17;
3012
- case 17:
3257
+ _l.label = 18;
3258
+ case 18:
3013
3259
  _e = _d.next();
3014
- return [3 /*break*/, 15];
3015
- case 18: return [3 /*break*/, 21];
3016
- case 19:
3017
- e_11_1 = _l.sent();
3018
- e_11 = { error: e_11_1 };
3019
- return [3 /*break*/, 21];
3260
+ return [3 /*break*/, 16];
3261
+ case 19: return [3 /*break*/, 22];
3020
3262
  case 20:
3263
+ e_14_1 = _l.sent();
3264
+ e_14 = { error: e_14_1 };
3265
+ return [3 /*break*/, 22];
3266
+ case 21:
3021
3267
  try {
3022
3268
  if (_e && !_e.done && (_g = _d.return)) _g.call(_d);
3023
3269
  }
3024
- finally { if (e_11) throw e_11.error; }
3270
+ finally { if (e_14) throw e_14.error; }
3025
3271
  return [7 /*endfinally*/];
3026
- case 21: return [2 /*return*/];
3272
+ case 22: return [2 /*return*/];
3027
3273
  }
3028
3274
  });
3029
3275
  }); });
3030
3276
  };
3031
3277
  try {
3032
- for (var _c = __values(api_1.DEPLOY_CORPUS.filter(function (entry) { return entry.route !== 'POST /SASjsApi/drive/deploy/upload'; })), _d = _c.next(); !_d.done; _d = _c.next()) {
3278
+ /**
3279
+ * The deploy routes: the JSON form and the removal, plus the upload route
3280
+ * through an archive this harness builds from the tree the case states.
3281
+ *
3282
+ * The corpus describes an upload by what the archive has to expand to, so a
3283
+ * host that takes bytes can run the same case as one that is handed an
3284
+ * expansion - `deployRequest` makes the archive, and what it asserts is this
3285
+ * worker's own unzipping and tree-building rather than the corpus's
3286
+ * assumption. The one case that cannot travel this way is the expansion that is
3287
+ * not a tree, which the corpus marks `transport: 'expansion'`.
3288
+ *
3289
+ * The removal cases need an app to remove, so the index is seeded with the one
3290
+ * the case names and a bystander that must survive it. The state a case leaves
3291
+ * is checked too - `expectDrive` - because both deploy answers are `200` and
3292
+ * replacing a folder only differs from keeping it in what the drive holds
3293
+ * afterwards.
3294
+ */
3295
+ for (var _c = __values(api_1.DEPLOY_CORPUS.filter(
3296
+ // a case a host that takes an archive cannot produce, which the corpus marks
3297
+ // on the case itself rather than leaving each host to name it
3298
+ function (entry) { return entry.transport !== 'expansion'; })), _d = _c.next(); !_d.done; _d = _c.next()) {
3033
3299
  var testCase = _d.value;
3034
3300
  _loop_2(testCase);
3035
3301
  }
3036
3302
  }
3037
- catch (e_6_1) { e_6 = { error: e_6_1 }; }
3303
+ catch (e_7_1) { e_7 = { error: e_7_1 }; }
3038
3304
  finally {
3039
3305
  try {
3040
3306
  if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
3041
3307
  }
3042
- finally { if (e_6) throw e_6.error; }
3308
+ finally { if (e_7) throw e_7.error; }
3043
3309
  }
3044
3310
  });
3045
3311
  /**
@@ -3051,7 +3317,7 @@ describe('service worker host', function () {
3051
3317
  * are the package's.
3052
3318
  */
3053
3319
  describe('identity and record conformance', function () {
3054
- var e_12, _a;
3320
+ var e_15, _a;
3055
3321
  /**
3056
3322
  * Boots the worker from a fixture, the way the page does: the case's own
3057
3323
  * identity, and `subject: null` for no implicit caller, so a case that
@@ -3084,7 +3350,7 @@ describe('service worker host', function () {
3084
3350
  };
3085
3351
  /** The request a case describes, as HTTP: path parameters filled, token sent. */
3086
3352
  var httpForCase = function (testCase) {
3087
- var e_13, _a;
3353
+ var e_16, _a;
3088
3354
  var _b = __read(testCase.route.split(' '), 2), method = _b[0], routePath = _b[1];
3089
3355
  var filled = routePath.replace(/\{(\w+)\}/g, function (_match, name) {
3090
3356
  return encodeURIComponent(String((testCase.request.params || {})[name]));
@@ -3097,12 +3363,12 @@ describe('service worker host', function () {
3097
3363
  query.set(key, String(value));
3098
3364
  }
3099
3365
  }
3100
- catch (e_13_1) { e_13 = { error: e_13_1 }; }
3366
+ catch (e_16_1) { e_16 = { error: e_16_1 }; }
3101
3367
  finally {
3102
3368
  try {
3103
3369
  if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
3104
3370
  }
3105
- finally { if (e_13) throw e_13.error; }
3371
+ finally { if (e_16) throw e_16.error; }
3106
3372
  }
3107
3373
  var suffix = query.toString() ? "?".concat(query) : '';
3108
3374
  var headers = {};
@@ -3224,12 +3490,12 @@ describe('service worker host', function () {
3224
3490
  _loop_3(testCase);
3225
3491
  }
3226
3492
  }
3227
- catch (e_12_1) { e_12 = { error: e_12_1 }; }
3493
+ catch (e_15_1) { e_15 = { error: e_15_1 }; }
3228
3494
  finally {
3229
3495
  try {
3230
3496
  if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
3231
3497
  }
3232
- finally { if (e_12) throw e_12.error; }
3498
+ finally { if (e_15) throw e_15.error; }
3233
3499
  }
3234
3500
  it('answers as the fixture administrator when no subject was chosen', function () { return __awaiter(void 0, void 0, void 0, function () {
3235
3501
  var worker, response, rules;