@sasjs/cli 6.13.3 → 6.15.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.
@@ -35,6 +35,17 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
35
35
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
36
  }
37
37
  };
38
+ var __values = (this && this.__values) || function(o) {
39
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
40
+ if (m) return m.call(o);
41
+ if (o && typeof o.length === "number") return {
42
+ next: function () {
43
+ if (o && i >= o.length) o = void 0;
44
+ return { value: o && o[i++], done: !o };
45
+ }
46
+ };
47
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
48
+ };
38
49
  var __read = (this && this.__read) || function (o, n) {
39
50
  var m = typeof Symbol === "function" && o[Symbol.iterator];
40
51
  if (!m) return o;
@@ -51,23 +62,13 @@ var __read = (this && this.__read) || function (o, n) {
51
62
  }
52
63
  return ar;
53
64
  };
54
- var __values = (this && this.__values) || function(o) {
55
- var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
56
- if (m) return m.call(o);
57
- if (o && typeof o.length === "number") return {
58
- next: function () {
59
- if (o && i >= o.length) o = void 0;
60
- return { value: o && o[i++], done: !o };
61
- }
62
- };
63
- throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
64
- };
65
65
  var __importDefault = (this && this.__importDefault) || function (mod) {
66
66
  return (mod && mod.__esModule) ? mod : { "default": mod };
67
67
  };
68
68
  Object.defineProperty(exports, "__esModule", { value: true });
69
69
  var path_1 = __importDefault(require("path"));
70
70
  var api_1 = require("@sasjs/api");
71
+ var yazl_1 = __importDefault(require("yazl"));
71
72
  /**
72
73
  * The other half of the parity claim: the service worker is what turns an
73
74
  * /SASjsApi/ request into a response, so its framing (webout body, the _debug
@@ -190,6 +191,24 @@ var jsonResponse = function (body) { return ({
190
191
  return [2 /*return*/, body];
191
192
  }); }); }
192
193
  }); };
194
+ /** A zip archive of text members, written the way a deploy upload ships one.
195
+ * The first member is stored rather than deflated, so both methods the reader
196
+ * accepts are exercised by the one case. */
197
+ var zipOf = function (files) {
198
+ return new Promise(function (resolve, reject) {
199
+ var zip = new yazl_1.default.ZipFile();
200
+ Object.keys(files).forEach(function (name, index) {
201
+ return zip.addBuffer(Buffer.from(files[name]), name, {
202
+ compress: index !== 0
203
+ });
204
+ });
205
+ zip.end();
206
+ var chunks = [];
207
+ zip.outputStream.on('data', function (chunk) { return chunks.push(chunk); });
208
+ zip.outputStream.on('end', function () { return resolve(Buffer.concat(chunks)); });
209
+ zip.outputStream.on('error', reject);
210
+ });
211
+ };
193
212
  var runRequest = function (body) { return ({
194
213
  method: 'POST',
195
214
  headers: { 'content-type': 'application/json' },
@@ -551,31 +570,424 @@ describe('service worker host', function () {
551
570
  }
552
571
  });
553
572
  }); });
554
- it('answers the session state, and names a route it does not implement', function () { return __awaiter(void 0, void 0, void 0, function () {
555
- var dispatch, session, _a, unknown, _b;
556
- return __generator(this, function (_c) {
557
- switch (_c.label) {
573
+ it('answers the session route with the caller, and names a route it does not have', function () { return __awaiter(void 0, void 0, void 0, function () {
574
+ var dispatch, session, _a, refused, _b, unknown, _c;
575
+ return __generator(this, function (_d) {
576
+ switch (_d.label) {
558
577
  case 0:
559
578
  dispatch = seeded().dispatch;
560
579
  return [4 /*yield*/, dispatch('/SASjsApi/session')];
561
580
  case 1:
562
- session = _c.sent();
581
+ session = _d.sent();
563
582
  _a = expect;
564
583
  return [4 /*yield*/, session.json()];
565
584
  case 2:
566
- _a.apply(void 0, [_c.sent()]).toEqual({ state: 'completed' });
567
- return [4 /*yield*/, dispatch('/SASjsApi/unknown/route')];
585
+ _a.apply(void 0, [_d.sent()]).toEqual({
586
+ uid: 'user-admin',
587
+ username: 'sasjsadmin',
588
+ displayName: 'Demo Admin',
589
+ isAdmin: true,
590
+ needsToUpdatePassword: false
591
+ });
592
+ return [4 /*yield*/, dispatch('/SASjsApi/session', {
593
+ headers: { authorization: 'Bearer not-a-token' }
594
+ })];
568
595
  case 3:
569
- unknown = _c.sent();
570
- expect(unknown.status).toBe(404);
596
+ refused = _d.sent();
597
+ expect(refused.status).toBe(401);
571
598
  _b = expect;
572
- return [4 /*yield*/, unknown.json()];
599
+ return [4 /*yield*/, refused.json()];
573
600
  case 4:
574
- _b.apply(void 0, [(_c.sent()).message]).toContain('Not implemented');
601
+ _b.apply(void 0, [(_d.sent()).message]).toBe('Unauthorized');
602
+ return [4 /*yield*/, dispatch('/SASjsApi/unknown/route')];
603
+ case 5:
604
+ unknown = _d.sent();
605
+ expect(unknown.status).toBe(404);
606
+ _c = expect;
607
+ return [4 /*yield*/, unknown.json()];
608
+ case 6:
609
+ _c.apply(void 0, [(_d.sent()).message]).toContain('Not part of the API');
575
610
  return [2 /*return*/];
576
611
  }
577
612
  });
578
613
  }); });
614
+ describe('the API surface the package carries', function () {
615
+ it('answers the sign-in flow, and the record routes as that caller', function () { return __awaiter(void 0, void 0, void 0, function () {
616
+ var worker, signedIn, _a, refused, _b, authorized, code, exchanged, pair, users, _c;
617
+ return __generator(this, function (_d) {
618
+ switch (_d.label) {
619
+ case 0:
620
+ worker = seeded();
621
+ return [4 /*yield*/, worker.dispatch('/SASLogon/login', runRequest({ username: 'sasjsadmin', password: 'Orion123' }))];
622
+ case 1:
623
+ signedIn = _d.sent();
624
+ expect(signedIn.status).toBe(200);
625
+ _a = expect;
626
+ return [4 /*yield*/, signedIn.json()];
627
+ case 2:
628
+ _a.apply(void 0, [(_d.sent()).user.username]).toBe('sasjsadmin');
629
+ return [4 /*yield*/, worker.dispatch('/SASLogon/login', runRequest({ username: 'sasjsadmin', password: 'nope' }))];
630
+ case 3:
631
+ refused = _d.sent();
632
+ expect(refused.status).toBe(401);
633
+ _b = expect;
634
+ return [4 /*yield*/, refused.json()];
635
+ case 4:
636
+ _b.apply(void 0, [(_d.sent()).message]).toBe('Invalid Password.');
637
+ return [4 /*yield*/, worker.dispatch('/SASLogon/authorize', runRequest({ clientId: 'web_app' }))];
638
+ case 5:
639
+ authorized = _d.sent();
640
+ return [4 /*yield*/, authorized.json()];
641
+ case 6:
642
+ code = (_d.sent()).code;
643
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/auth/token', runRequest({ clientId: 'web_app', code: code }))];
644
+ case 7:
645
+ exchanged = _d.sent();
646
+ return [4 /*yield*/, exchanged.json()];
647
+ case 8:
648
+ pair = _d.sent();
649
+ expect(pair.accessToken).toBeTruthy();
650
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/user', {
651
+ headers: { authorization: "Bearer ".concat(pair.accessToken) }
652
+ })];
653
+ case 9:
654
+ users = _d.sent();
655
+ _c = expect;
656
+ return [4 /*yield*/, users.json()];
657
+ case 10:
658
+ _c.apply(void 0, [(_d.sent()).map(function (user) { return user.username; }).sort()]).toEqual(['sasjsadmin', 'sasjsuser']);
659
+ return [2 /*return*/];
660
+ }
661
+ });
662
+ }); });
663
+ it('answers the administration records the fixture seeds', function () { return __awaiter(void 0, void 0, void 0, function () {
664
+ var dispatch, groups, _a, permissions, _b, clients, _c, mine, _d, _e, _f;
665
+ return __generator(this, function (_g) {
666
+ switch (_g.label) {
667
+ case 0:
668
+ dispatch = seeded().dispatch;
669
+ return [4 /*yield*/, dispatch('/SASjsApi/group')];
670
+ case 1:
671
+ groups = _g.sent();
672
+ _a = expect;
673
+ return [4 /*yield*/, groups.json()];
674
+ case 2:
675
+ _a.apply(void 0, [(_g.sent()).map(function (group) { return group.name; }).sort()]).toEqual(['Admins', 'DCGroup']);
676
+ return [4 /*yield*/, dispatch('/SASjsApi/permission')];
677
+ case 3:
678
+ permissions = _g.sent();
679
+ _b = expect;
680
+ return [4 /*yield*/, permissions.json()];
681
+ case 4:
682
+ _b.apply(void 0, [(_g.sent()).length]).toBe(3);
683
+ return [4 /*yield*/, dispatch('/SASjsApi/client')];
684
+ case 5:
685
+ clients = _g.sent();
686
+ _c = expect;
687
+ return [4 /*yield*/, clients.json()];
688
+ case 6:
689
+ _c.apply(void 0, [(_g.sent()).map(function (client) { return client.clientId; }).sort()]).toEqual(['sasjs', 'web_app']);
690
+ return [4 /*yield*/, dispatch('/SASjsApi/info/myAuthorizedRoutes')];
691
+ case 7:
692
+ mine = _g.sent();
693
+ _d = expect;
694
+ _f = (_e = Object).keys;
695
+ return [4 /*yield*/, mine.json()];
696
+ case 8:
697
+ _d.apply(void 0, [_f.apply(_e, [_g.sent()]).length]).toBeGreaterThan(0);
698
+ return [2 /*return*/];
699
+ }
700
+ });
701
+ }); });
702
+ it('answers the documented 501 for the capabilities a static origin lacks', function () { return __awaiter(void 0, void 0, void 0, function () {
703
+ var dispatch, cases, cases_1, cases_1_1, _a, method, url, capability, response, _b, e_1_1, _c, _d, url, response, _e, e_2_1;
704
+ var e_1, _f, e_2, _g;
705
+ return __generator(this, function (_h) {
706
+ switch (_h.label) {
707
+ case 0:
708
+ dispatch = seeded().dispatch;
709
+ cases = [
710
+ ['GET', '/SASjsApi/admin/diagnostics', 'admin service'],
711
+ ['GET', '/SASjsApi/admin/logs', 'admin service'],
712
+ ['GET', '/SASjsApi/admin/logs/app.log', 'admin service'],
713
+ [
714
+ 'POST',
715
+ '/SASjsApi/authConfig/synchroniseWithLDAP',
716
+ 'directory service'
717
+ ]
718
+ ];
719
+ _h.label = 1;
720
+ case 1:
721
+ _h.trys.push([1, 7, 8, 9]);
722
+ cases_1 = __values(cases), cases_1_1 = cases_1.next();
723
+ _h.label = 2;
724
+ case 2:
725
+ if (!!cases_1_1.done) return [3 /*break*/, 6];
726
+ _a = __read(cases_1_1.value, 3), method = _a[0], url = _a[1], capability = _a[2];
727
+ return [4 /*yield*/, dispatch(url, { method: method })];
728
+ case 3:
729
+ response = _h.sent();
730
+ expect(response.status).toBe(501);
731
+ _b = expect;
732
+ return [4 /*yield*/, response.json()];
733
+ case 4:
734
+ _b.apply(void 0, [(_h.sent()).message]).toContain(capability);
735
+ _h.label = 5;
736
+ case 5:
737
+ cases_1_1 = cases_1.next();
738
+ return [3 /*break*/, 2];
739
+ case 6: return [3 /*break*/, 9];
740
+ case 7:
741
+ e_1_1 = _h.sent();
742
+ e_1 = { error: e_1_1 };
743
+ return [3 /*break*/, 9];
744
+ case 8:
745
+ try {
746
+ if (cases_1_1 && !cases_1_1.done && (_f = cases_1.return)) _f.call(cases_1);
747
+ }
748
+ finally { if (e_1) throw e_1.error; }
749
+ return [7 /*endfinally*/];
750
+ case 9:
751
+ _h.trys.push([9, 15, 16, 17]);
752
+ _c = __values(['/SASjsApi/code/lint', '/SASjsApi/code/format']), _d = _c.next();
753
+ _h.label = 10;
754
+ case 10:
755
+ if (!!_d.done) return [3 /*break*/, 14];
756
+ url = _d.value;
757
+ return [4 /*yield*/, dispatch(url, runRequest({ code: 'data a;' }))];
758
+ case 11:
759
+ response = _h.sent();
760
+ expect(response.status).toBe(501);
761
+ _e = expect;
762
+ return [4 /*yield*/, response.json()];
763
+ case 12:
764
+ _e.apply(void 0, [(_h.sent()).message]).toContain('code service');
765
+ _h.label = 13;
766
+ case 13:
767
+ _d = _c.next();
768
+ return [3 /*break*/, 10];
769
+ case 14: return [3 /*break*/, 17];
770
+ case 15:
771
+ e_2_1 = _h.sent();
772
+ e_2 = { error: e_2_1 };
773
+ return [3 /*break*/, 17];
774
+ case 16:
775
+ try {
776
+ if (_d && !_d.done && (_g = _c.return)) _g.call(_c);
777
+ }
778
+ finally { if (e_2) throw e_2.error; }
779
+ return [7 /*endfinally*/];
780
+ case 17: return [2 /*return*/];
781
+ }
782
+ });
783
+ }); });
784
+ it('matches a path whatever its casing, as the API does', function () { return __awaiter(void 0, void 0, void 0, function () {
785
+ var dispatch, response, _a;
786
+ return __generator(this, function (_b) {
787
+ switch (_b.label) {
788
+ case 0:
789
+ dispatch = seeded().dispatch;
790
+ return [4 /*yield*/, dispatch('/sasjsapi/INFO')];
791
+ case 1:
792
+ response = _b.sent();
793
+ expect(response.status).toBe(200);
794
+ _a = expect;
795
+ return [4 /*yield*/, response.json()];
796
+ case 2:
797
+ _a.apply(void 0, [(_b.sent()).mode]).toBe('desktop');
798
+ return [2 /*return*/];
799
+ }
800
+ });
801
+ }); });
802
+ it('starts a session, then reads its state, log and output', function () { return __awaiter(void 0, void 0, void 0, function () {
803
+ var dispatch, program, triggered, sessionId, state, _a, output, _b, log, missing;
804
+ return __generator(this, function (_c) {
805
+ switch (_c.label) {
806
+ case 0:
807
+ dispatch = seeded().dispatch;
808
+ program = encodeURIComponent("".concat(appLoc, "/services/public/getdata"));
809
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/trigger?_program=' + program, runRequest({ params: {} }))];
810
+ case 1:
811
+ triggered = _c.sent();
812
+ expect(triggered.status).toBe(200);
813
+ return [4 /*yield*/, triggered.json()];
814
+ case 2:
815
+ sessionId = (_c.sent()).sessionId;
816
+ expect(sessionId).toBeTruthy();
817
+ return [4 /*yield*/, dispatch("/SASjsApi/session/".concat(sessionId, "/state"))];
818
+ case 3:
819
+ state = _c.sent();
820
+ _a = expect;
821
+ return [4 /*yield*/, state.json()];
822
+ case 4:
823
+ _a.apply(void 0, [_c.sent()]).toEqual({ state: 'completed' });
824
+ return [4 /*yield*/, dispatch("/SASjsApi/session/".concat(sessionId, "/output"))];
825
+ case 5:
826
+ output = _c.sent();
827
+ expect(output.status).toBe(200);
828
+ _b = expect;
829
+ return [4 /*yield*/, output.text()];
830
+ case 6:
831
+ _b.apply(void 0, [_c.sent()]).toBe('seeded');
832
+ return [4 /*yield*/, dispatch("/SASjsApi/session/".concat(sessionId, "/log"))];
833
+ case 7:
834
+ log = _c.sent();
835
+ expect(log.status).toBe(200);
836
+ return [4 /*yield*/, dispatch('/SASjsApi/session/not-a-session/state')];
837
+ case 8:
838
+ missing = _c.sent();
839
+ expect(missing.status).toBe(404);
840
+ return [2 /*return*/];
841
+ }
842
+ });
843
+ }); });
844
+ it('refuses a deploy upload with no archive, and one that is not a zip', function () { return __awaiter(void 0, void 0, void 0, function () {
845
+ var dispatch, empty, _a, form, notZip, _b;
846
+ return __generator(this, function (_c) {
847
+ switch (_c.label) {
848
+ case 0:
849
+ dispatch = seeded().dispatch;
850
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
851
+ method: 'POST',
852
+ body: new FormData()
853
+ })];
854
+ case 1:
855
+ empty = _c.sent();
856
+ expect(empty.status).toBe(400);
857
+ _a = expect;
858
+ return [4 /*yield*/, empty.text()];
859
+ case 2:
860
+ _a.apply(void 0, [_c.sent()]).toBe('"file" is not present.');
861
+ form = new FormData();
862
+ form.set('file', new Blob([new Uint8Array([1, 2, 3])]), 'not-a-zip.zip');
863
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
864
+ method: 'POST',
865
+ body: form
866
+ })];
867
+ case 3:
868
+ notZip = _c.sent();
869
+ expect(notZip.status).toBe(400);
870
+ _b = expect;
871
+ return [4 /*yield*/, notZip.text()];
872
+ case 4:
873
+ _b.apply(void 0, [_c.sent()]).toBe('The archive is not a zip file.');
874
+ return [2 /*return*/];
875
+ }
876
+ });
877
+ }); });
878
+ it('takes a token from the query when a link cannot carry a header', function () { return __awaiter(void 0, void 0, void 0, function () {
879
+ var worker, authorized, code, exchanged, pair, users, _a;
880
+ return __generator(this, function (_b) {
881
+ switch (_b.label) {
882
+ case 0:
883
+ worker = seeded();
884
+ return [4 /*yield*/, worker.dispatch('/SASLogon/authorize', runRequest({ clientId: 'web_app' }))];
885
+ case 1:
886
+ authorized = _b.sent();
887
+ return [4 /*yield*/, authorized.json()];
888
+ case 2:
889
+ code = (_b.sent()).code;
890
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/auth/token', runRequest({ clientId: 'web_app', code: code }))];
891
+ case 3:
892
+ exchanged = _b.sent();
893
+ return [4 /*yield*/, exchanged.json()];
894
+ case 4:
895
+ pair = _b.sent();
896
+ return [4 /*yield*/, worker.dispatch("/SASjsApi/user?access_token=".concat(encodeURIComponent(pair.accessToken)))];
897
+ case 5:
898
+ users = _b.sent();
899
+ expect(users.status).toBe(200);
900
+ _a = expect;
901
+ return [4 /*yield*/, users.json()];
902
+ case 6:
903
+ _a.apply(void 0, [(_b.sent()).length]).toBe(2);
904
+ return [2 /*return*/];
905
+ }
906
+ });
907
+ }); });
908
+ it('names a table the page did not name, and takes an archive under any field', function () { return __awaiter(void 0, void 0, void 0, function () {
909
+ var worker, unnamed, _a, form, _b, _c, _d, _e, _f, upload;
910
+ var _g;
911
+ return __generator(this, function (_h) {
912
+ switch (_h.label) {
913
+ case 0:
914
+ worker = seeded();
915
+ worker.runtime.load((_g = {},
916
+ _g[drivePath('services/public/echo.js')] = '_webout = _WEBIN_NAME1 + "|" + _WEBIN_FILEREF1;',
917
+ _g));
918
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
919
+ encodeURIComponent("".concat(appLoc, "/services/public/echo")), runRequest({ params: {}, tables: [{ csv: 'A,B\r\n1,2' }] }))];
920
+ case 1:
921
+ unnamed = _h.sent();
922
+ _a = expect;
923
+ return [4 /*yield*/, unnamed.text()];
924
+ case 2:
925
+ _a.apply(void 0, [_h.sent()]).toBe('table1|A,B\r\n1,2');
926
+ form = new FormData();
927
+ form.set('appLoc', appLoc);
928
+ _c = (_b = form).set;
929
+ _d = ['archive'];
930
+ _e = Blob.bind;
931
+ _f = Uint8Array.bind;
932
+ return [4 /*yield*/, zipOf({ 'x.txt': 'x' })];
933
+ case 3:
934
+ _c.apply(_b, _d.concat([new (_e.apply(Blob, [void 0, [new (_f.apply(Uint8Array, [void 0, _h.sent()]))()]]))(),
935
+ 'deploy.zip']));
936
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/deploy/upload', {
937
+ method: 'POST',
938
+ body: form
939
+ })];
940
+ case 4:
941
+ upload = _h.sent();
942
+ expect(upload.status).toBe(200);
943
+ return [2 /*return*/];
944
+ }
945
+ });
946
+ }); });
947
+ it('expands an uploaded archive into the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
948
+ var dispatch, archive, form, response, tree, listing, _a, _b, file, _c;
949
+ return __generator(this, function (_d) {
950
+ switch (_d.label) {
951
+ case 0:
952
+ dispatch = seeded().dispatch;
953
+ return [4 /*yield*/, zipOf({
954
+ 'services/public/run.js': '_webout = "deployed";',
955
+ 'docs/readme.txt': 'hello'
956
+ })];
957
+ case 1:
958
+ archive = _d.sent();
959
+ form = new FormData();
960
+ form.set('appLoc', appLoc);
961
+ form.set('file', new Blob([new Uint8Array(archive)]), 'deploy.zip');
962
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
963
+ method: 'POST',
964
+ body: form
965
+ })];
966
+ case 2:
967
+ response = _d.sent();
968
+ expect(response.status).toBe(200);
969
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/filetree?_folderPath=' + encodeURIComponent(appLoc))];
970
+ case 3:
971
+ tree = _d.sent();
972
+ _b = (_a = JSON).stringify;
973
+ return [4 /*yield*/, tree.json()];
974
+ case 4:
975
+ listing = _b.apply(_a, [_d.sent()]);
976
+ expect(listing).toContain('run.js');
977
+ expect(listing).toContain('readme.txt');
978
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
979
+ encodeURIComponent("".concat(appLoc, "/docs/readme.txt")))];
980
+ case 5:
981
+ file = _d.sent();
982
+ _c = expect;
983
+ return [4 /*yield*/, file.text()];
984
+ case 6:
985
+ _c.apply(void 0, [_d.sent()]).toBe('hello');
986
+ return [2 /*return*/];
987
+ }
988
+ });
989
+ }); });
990
+ });
579
991
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
580
992
  var dispatch, response, _a;
581
993
  return __generator(this, function (_b) {
@@ -686,12 +1098,12 @@ describe('service worker host', function () {
686
1098
  _a = expect;
687
1099
  return [4 /*yield*/, response.json()];
688
1100
  case 2:
689
- _a.apply(void 0, [_b.sent()]).toEqual({ message: '_program is required' });
1101
+ _a.apply(void 0, [_b.sent()]).toEqual({ message: '"_program" is required' });
690
1102
  return [2 /*return*/];
691
1103
  }
692
1104
  });
693
1105
  }); });
694
- it('answers 500 with the failure shape when the service throws', function () { return __awaiter(void 0, void 0, void 0, function () {
1106
+ it('answers the documented failure envelope when the service throws', function () { return __awaiter(void 0, void 0, void 0, function () {
695
1107
  var _a, runtime, dispatch, response, body;
696
1108
  var _b;
697
1109
  return __generator(this, function (_c) {
@@ -705,11 +1117,17 @@ describe('service worker host', function () {
705
1117
  encodeURIComponent("".concat(appLoc, "/services/public/broken")))];
706
1118
  case 1:
707
1119
  response = _c.sent();
708
- return [4 /*yield*/, response.json()];
1120
+ return [4 /*yield*/, response.json()
1121
+ // the package's decision, and the server's: a run that failed is a 400
1122
+ // carrying the failure envelope, not a 500 that blames the transport
1123
+ ];
709
1124
  case 2:
710
1125
  body = _c.sent();
711
- expect(response.status).toBe(500);
1126
+ // the package's decision, and the server's: a run that failed is a 400
1127
+ // carrying the failure envelope, not a 500 that blames the transport
1128
+ expect(response.status).toBe(400);
712
1129
  expect(body.status).toBe('failure');
1130
+ expect(body.message).toBe('Job execution failed.');
713
1131
  expect(body.error).toContain('boom');
714
1132
  return [2 /*return*/];
715
1133
  }
@@ -721,13 +1139,58 @@ describe('service worker host', function () {
721
1139
  switch (_b.label) {
722
1140
  case 0:
723
1141
  dispatch = seeded().dispatch;
724
- return [4 /*yield*/, dispatch('/SASjsApi/code/execute', runRequest({ code: '_webout = "inline";', params: {} }))];
1142
+ return [4 /*yield*/, dispatch('/SASjsApi/code/execute', runRequest({ code: '_webout = "inline";', runTime: 'js', params: {} }))
1143
+ // a code route's documented body is the webout framed with the log, so the
1144
+ // run's output is the first section rather than the whole body
1145
+ ];
725
1146
  case 1:
726
1147
  response = _b.sent();
1148
+ // a code route's documented body is the webout framed with the log, so the
1149
+ // run's output is the first section rather than the whole body
1150
+ _a = expect;
1151
+ return [4 /*yield*/, response.text()];
1152
+ case 2:
1153
+ // a code route's documented body is the webout framed with the log, so the
1154
+ // run's output is the first section rather than the whole body
1155
+ _a.apply(void 0, [(_b.sent()).split(LOGS_SEPARATOR)[0].trim()]).toBe('inline');
1156
+ return [2 /*return*/];
1157
+ }
1158
+ });
1159
+ }); });
1160
+ it('reads the table envelope the page bridge posts for an XHR', function () { return __awaiter(void 0, void 0, void 0, function () {
1161
+ var worker, response, _a;
1162
+ var _b;
1163
+ return __generator(this, function (_c) {
1164
+ switch (_c.label) {
1165
+ case 0:
1166
+ worker = seeded();
1167
+ worker.runtime.load((_b = {},
1168
+ _b[drivePath('services/public/echo.js')] = '_webout = _WEBIN_NAME1 + "|" + _WEBIN_FILEREF1;',
1169
+ _b));
1170
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
1171
+ encodeURIComponent("".concat(appLoc, "/services/public/echo")), runRequest({
1172
+ params: {},
1173
+ tables: [
1174
+ {
1175
+ name: 'SASControlTable',
1176
+ filename: 'SASControlTable.csv',
1177
+ csv: 'A,B\r\n1,2'
1178
+ }
1179
+ ]
1180
+ }))
1181
+ // the bridge hands the worker `{params, tables}`; a stored program sees the
1182
+ // tables as its _WEBIN variables, not as one `tables` session variable
1183
+ ];
1184
+ case 1:
1185
+ response = _c.sent();
1186
+ // the bridge hands the worker `{params, tables}`; a stored program sees the
1187
+ // tables as its _WEBIN variables, not as one `tables` session variable
727
1188
  _a = expect;
728
1189
  return [4 /*yield*/, response.text()];
729
1190
  case 2:
730
- _a.apply(void 0, [_b.sent()]).toBe('inline');
1191
+ // the bridge hands the worker `{params, tables}`; a stored program sees the
1192
+ // tables as its _WEBIN variables, not as one `tables` session variable
1193
+ _a.apply(void 0, [_c.sent()]).toBe('SASControlTable|A,B\r\n1,2');
731
1194
  return [2 /*return*/];
732
1195
  }
733
1196
  });
@@ -1393,7 +1856,7 @@ describe('service worker host', function () {
1393
1856
  }); });
1394
1857
  });
1395
1858
  describe('conformance corpus', function () {
1396
- var e_1, _a;
1859
+ var e_3, _a;
1397
1860
  /**
1398
1861
  * The corpus is what "the same API" means: the cases @sasjs/api runs against
1399
1862
  * its own services, and against a live sasjs/server, run here against this
@@ -1402,7 +1865,7 @@ describe('service worker host', function () {
1402
1865
  * used to have - and it is a test rather than a review.
1403
1866
  */
1404
1867
  var httpFor = function (testCase) {
1405
- var e_2, _a;
1868
+ var e_4, _a;
1406
1869
  var _b;
1407
1870
  var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
1408
1871
  var query = new URLSearchParams();
@@ -1413,12 +1876,12 @@ describe('service worker host', function () {
1413
1876
  query.set(key, String(value));
1414
1877
  }
1415
1878
  }
1416
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
1879
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
1417
1880
  finally {
1418
1881
  try {
1419
1882
  if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
1420
1883
  }
1421
- finally { if (e_2) throw e_2.error; }
1884
+ finally { if (e_4) throw e_4.error; }
1422
1885
  }
1423
1886
  var suffix = query.toString() ? "?".concat(query) : '';
1424
1887
  var init = { method: method };
@@ -1449,8 +1912,8 @@ describe('service worker host', function () {
1449
1912
  * does.
1450
1913
  */
1451
1914
  var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
1452
- var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_3_1;
1453
- var e_4, _g, e_3, _h;
1915
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_5_1;
1916
+ var e_6, _g, e_5, _h;
1454
1917
  return __generator(this, function (_j) {
1455
1918
  switch (_j.label) {
1456
1919
  case 0:
@@ -1462,12 +1925,12 @@ describe('service worker host', function () {
1462
1925
  text["/sasjs/drive/files".concat(file)] = contents;
1463
1926
  }
1464
1927
  }
1465
- catch (e_4_1) { e_4 = { error: e_4_1 }; }
1928
+ catch (e_6_1) { e_6 = { error: e_6_1 }; }
1466
1929
  finally {
1467
1930
  try {
1468
1931
  if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
1469
1932
  }
1470
- finally { if (e_4) throw e_4.error; }
1933
+ finally { if (e_6) throw e_6.error; }
1471
1934
  }
1472
1935
  return [4 /*yield*/, runtime.load(text)];
1473
1936
  case 1:
@@ -1490,14 +1953,14 @@ describe('service worker host', function () {
1490
1953
  return [3 /*break*/, 3];
1491
1954
  case 6: return [3 /*break*/, 9];
1492
1955
  case 7:
1493
- e_3_1 = _j.sent();
1494
- e_3 = { error: e_3_1 };
1956
+ e_5_1 = _j.sent();
1957
+ e_5 = { error: e_5_1 };
1495
1958
  return [3 /*break*/, 9];
1496
1959
  case 8:
1497
1960
  try {
1498
1961
  if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
1499
1962
  }
1500
- finally { if (e_3) throw e_3.error; }
1963
+ finally { if (e_5) throw e_5.error; }
1501
1964
  return [7 /*endfinally*/];
1502
1965
  case 9: return [2 /*return*/];
1503
1966
  }
@@ -1505,42 +1968,55 @@ describe('service worker host', function () {
1505
1968
  }); };
1506
1969
  var _loop_1 = function (testCase) {
1507
1970
  it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
1508
- var worker, _a, requestPath, init, response, _b, want, _c, _d, _e;
1509
- return __generator(this, function (_f) {
1510
- switch (_f.label) {
1971
+ var worker, _a, requestPath, init, response, body, want, _b, _c, _d;
1972
+ return __generator(this, function (_e) {
1973
+ switch (_e.label) {
1511
1974
  case 0:
1512
1975
  worker = loadWorker();
1513
1976
  return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
1514
1977
  case 1:
1515
- _f.sent();
1978
+ _e.sent();
1516
1979
  worker.refuseWrites(Boolean(testCase.unwritable));
1517
1980
  _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
1518
1981
  return [4 /*yield*/, worker.dispatch(requestPath, init)];
1519
1982
  case 2:
1520
- response = _f.sent();
1983
+ response = _e.sent();
1521
1984
  expect(response.status).toBe(testCase.expect.status);
1522
1985
  if (!(testCase.expect.error !== undefined)) return [3 /*break*/, 4];
1523
- _b = expect;
1524
- return [4 /*yield*/, response.text()];
1986
+ return [4 /*yield*/, response.text()
1987
+ // the routes that answer an envelope - the two deploy routes - send
1988
+ // `{status, message}`; the rest send the message itself as text, which
1989
+ // is what the server's `res.status(code).send(err.message)` does
1990
+ ];
1525
1991
  case 3:
1526
- _b.apply(void 0, [_f.sent()]).toBe(testCase.expect.error);
1527
- _f.label = 4;
1992
+ body = _e.sent();
1993
+ // the routes that answer an envelope - the two deploy routes - send
1994
+ // `{status, message}`; the rest send the message itself as text, which
1995
+ // is what the server's `res.status(code).send(err.message)` does
1996
+ if ((response.headers.get('content-type') || '').includes('application/json'))
1997
+ expect(JSON.parse(body)).toEqual({
1998
+ status: 'failure',
1999
+ message: testCase.expect.error
2000
+ });
2001
+ else
2002
+ expect(body).toBe(testCase.expect.error);
2003
+ return [3 /*break*/, 8];
1528
2004
  case 4:
1529
2005
  want = testCase.expect.contents;
1530
2006
  if (!(typeof want === 'string')) return [3 /*break*/, 6];
1531
- _c = expect;
2007
+ _b = expect;
1532
2008
  return [4 /*yield*/, response.text()];
1533
2009
  case 5:
1534
- _c.apply(void 0, [_f.sent()]).toBe(want);
2010
+ _b.apply(void 0, [_e.sent()]).toBe(want);
1535
2011
  return [3 /*break*/, 8];
1536
2012
  case 6:
1537
2013
  if (!(want !== undefined)) return [3 /*break*/, 8];
1538
- _d = expect;
1539
- _e = Uint8Array.bind;
2014
+ _c = expect;
2015
+ _d = Uint8Array.bind;
1540
2016
  return [4 /*yield*/, response.arrayBuffer()];
1541
2017
  case 7:
1542
- _d.apply(void 0, [new (_e.apply(Uint8Array, [void 0, _f.sent()]))()]).toEqual(want);
1543
- _f.label = 8;
2018
+ _c.apply(void 0, [new (_d.apply(Uint8Array, [void 0, _e.sent()]))()]).toEqual(want);
2019
+ _e.label = 8;
1544
2020
  case 8: return [2 /*return*/];
1545
2021
  }
1546
2022
  });
@@ -1552,12 +2028,12 @@ describe('service worker host', function () {
1552
2028
  _loop_1(testCase);
1553
2029
  }
1554
2030
  }
1555
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
2031
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
1556
2032
  finally {
1557
2033
  try {
1558
2034
  if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
1559
2035
  }
1560
- finally { if (e_1) throw e_1.error; }
2036
+ finally { if (e_3) throw e_3.error; }
1561
2037
  }
1562
2038
  });
1563
2039
  });