@sasjs/cli 6.14.0 → 6.16.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.
@@ -68,6 +68,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
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 os_1 = __importDefault(require("os"));
72
+ var yazl_1 = __importDefault(require("yazl"));
73
+ var mockSite_1 = require("../mockSite");
71
74
  /**
72
75
  * The other half of the parity claim: the service worker is what turns an
73
76
  * /SASjsApi/ request into a response, so its framing (webout body, the _debug
@@ -86,6 +89,19 @@ var drivePath = function (relative) {
86
89
  var apiBrowserBuild = function () {
87
90
  return path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
88
91
  };
92
+ /**
93
+ * The lint bundle the site ships, built once for the suite from the installed
94
+ * packages - the same call `sasjs build` makes, so the worker under test answers
95
+ * the code routes with the code the site carries.
96
+ */
97
+ var lintBundlePath;
98
+ var lintBundle = function () {
99
+ if (!lintBundlePath) {
100
+ lintBundlePath = path_1.default.join(os_1.default.tmpdir(), "sasjs-lint-".concat(process.pid, ".js"));
101
+ (0, mockSite_1.buildLintBundle)(lintBundlePath);
102
+ }
103
+ return lintBundlePath;
104
+ };
89
105
  var loadWorker = function () {
90
106
  jest.resetModules();
91
107
  var handlers = {};
@@ -107,6 +123,13 @@ var loadWorker = function () {
107
123
  return;
108
124
  if (path.includes('api-browser'))
109
125
  return require(apiBrowserBuild());
126
+ if (path.includes('lint-browser')) {
127
+ // the bundle assigns globalThis.SASjsCode, and this harness keeps the
128
+ // worker's `self` apart from the module global
129
+ require(lintBundle());
130
+ self.SASjsCode = global.SASjsCode;
131
+ return;
132
+ }
110
133
  require('../assets/runtime-core.js');
111
134
  // The corpus needs the same drive with its writes refused, without building
112
135
  // a second site. The flag is flipped after the case has seeded, because a
@@ -132,10 +155,20 @@ var loadWorker = function () {
132
155
  };
133
156
  };
134
157
  require('../assets/sw.js');
135
- global.fetch = jest.fn(function (url) { return __awaiter(void 0, void 0, void 0, function () {
158
+ global.fetch = jest.fn(function (input) { return __awaiter(void 0, void 0, void 0, function () {
159
+ var target;
136
160
  var _a;
137
161
  return __generator(this, function (_b) {
138
- return [2 /*return*/, String(url).includes('vfs-seed')
162
+ target = typeof input === 'string' ? input : (input && input.url) || String(input);
163
+ // a navigation back to the site root: the static host's copy of the page
164
+ if (new URL(target, origin).pathname === '/')
165
+ return [2 /*return*/, new Response('<html><body>static page</body></html>', {
166
+ status: 200,
167
+ headers: { 'content-type': 'text/html' }
168
+ })];
169
+ if (target.includes('index.html'))
170
+ return [2 /*return*/, new Response('<!doctype html><html><head><title>App</title></head><body>home</body></html>', { status: 200, headers: { 'content-type': 'text/html' } })];
171
+ return [2 /*return*/, target.includes('vfs-seed')
139
172
  ? jsonResponse({
140
173
  files: (_a = {},
141
174
  _a[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
@@ -190,6 +223,24 @@ var jsonResponse = function (body) { return ({
190
223
  return [2 /*return*/, body];
191
224
  }); }); }
192
225
  }); };
226
+ /** A zip archive of text members, written the way a deploy upload ships one.
227
+ * The first member is stored rather than deflated, so both methods the reader
228
+ * accepts are exercised by the one case. */
229
+ var zipOf = function (files) {
230
+ return new Promise(function (resolve, reject) {
231
+ var zip = new yazl_1.default.ZipFile();
232
+ Object.keys(files).forEach(function (name, index) {
233
+ return zip.addBuffer(Buffer.from(files[name]), name, {
234
+ compress: index !== 0
235
+ });
236
+ });
237
+ zip.end();
238
+ var chunks = [];
239
+ zip.outputStream.on('data', function (chunk) { return chunks.push(chunk); });
240
+ zip.outputStream.on('end', function () { return resolve(Buffer.concat(chunks)); });
241
+ zip.outputStream.on('error', reject);
242
+ });
243
+ };
193
244
  var runRequest = function (body) { return ({
194
245
  method: 'POST',
195
246
  headers: { 'content-type': 'application/json' },
@@ -551,31 +602,543 @@ describe('service worker host', function () {
551
602
  }
552
603
  });
553
604
  }); });
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) {
605
+ 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 () {
606
+ var dispatch, session, _a, refused, _b, unknown, _c;
607
+ return __generator(this, function (_d) {
608
+ switch (_d.label) {
558
609
  case 0:
559
610
  dispatch = seeded().dispatch;
560
611
  return [4 /*yield*/, dispatch('/SASjsApi/session')];
561
612
  case 1:
562
- session = _c.sent();
613
+ session = _d.sent();
563
614
  _a = expect;
564
615
  return [4 /*yield*/, session.json()];
565
616
  case 2:
566
- _a.apply(void 0, [_c.sent()]).toEqual({ state: 'completed' });
567
- return [4 /*yield*/, dispatch('/SASjsApi/unknown/route')];
617
+ _a.apply(void 0, [_d.sent()]).toEqual({
618
+ uid: 'user-admin',
619
+ username: 'sasjsadmin',
620
+ displayName: 'Demo Admin',
621
+ isAdmin: true,
622
+ needsToUpdatePassword: false
623
+ });
624
+ return [4 /*yield*/, dispatch('/SASjsApi/session', {
625
+ headers: { authorization: 'Bearer not-a-token' }
626
+ })];
568
627
  case 3:
569
- unknown = _c.sent();
570
- expect(unknown.status).toBe(404);
628
+ refused = _d.sent();
629
+ expect(refused.status).toBe(401);
571
630
  _b = expect;
572
- return [4 /*yield*/, unknown.json()];
631
+ return [4 /*yield*/, refused.json()];
573
632
  case 4:
574
- _b.apply(void 0, [(_c.sent()).message]).toContain('Not implemented');
633
+ _b.apply(void 0, [(_d.sent()).message]).toBe('Unauthorized');
634
+ return [4 /*yield*/, dispatch('/SASjsApi/unknown/route')];
635
+ case 5:
636
+ unknown = _d.sent();
637
+ expect(unknown.status).toBe(404);
638
+ _c = expect;
639
+ return [4 /*yield*/, unknown.json()];
640
+ case 6:
641
+ _c.apply(void 0, [(_d.sent()).message]).toContain('Not part of the API');
575
642
  return [2 /*return*/];
576
643
  }
577
644
  });
578
645
  }); });
646
+ describe('the API surface the package carries', function () {
647
+ it('answers the sign-in flow, and the record routes as that caller', function () { return __awaiter(void 0, void 0, void 0, function () {
648
+ var worker, signedIn, _a, refused, _b, authorized, code, exchanged, pair, users, _c;
649
+ return __generator(this, function (_d) {
650
+ switch (_d.label) {
651
+ case 0:
652
+ worker = seeded();
653
+ return [4 /*yield*/, worker.dispatch('/SASLogon/login', runRequest({ username: 'sasjsadmin', password: 'Orion123' }))];
654
+ case 1:
655
+ signedIn = _d.sent();
656
+ expect(signedIn.status).toBe(200);
657
+ _a = expect;
658
+ return [4 /*yield*/, signedIn.json()];
659
+ case 2:
660
+ _a.apply(void 0, [(_d.sent()).user.username]).toBe('sasjsadmin');
661
+ return [4 /*yield*/, worker.dispatch('/SASLogon/login', runRequest({ username: 'sasjsadmin', password: 'nope' }))];
662
+ case 3:
663
+ refused = _d.sent();
664
+ expect(refused.status).toBe(401);
665
+ _b = expect;
666
+ return [4 /*yield*/, refused.json()];
667
+ case 4:
668
+ _b.apply(void 0, [(_d.sent()).message]).toBe('Invalid Password.');
669
+ return [4 /*yield*/, worker.dispatch('/SASLogon/authorize', runRequest({ clientId: 'web_app' }))];
670
+ case 5:
671
+ authorized = _d.sent();
672
+ return [4 /*yield*/, authorized.json()];
673
+ case 6:
674
+ code = (_d.sent()).code;
675
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/auth/token', runRequest({ clientId: 'web_app', code: code }))];
676
+ case 7:
677
+ exchanged = _d.sent();
678
+ return [4 /*yield*/, exchanged.json()];
679
+ case 8:
680
+ pair = _d.sent();
681
+ expect(pair.accessToken).toBeTruthy();
682
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/user', {
683
+ headers: { authorization: "Bearer ".concat(pair.accessToken) }
684
+ })];
685
+ case 9:
686
+ users = _d.sent();
687
+ _c = expect;
688
+ return [4 /*yield*/, users.json()];
689
+ case 10:
690
+ _c.apply(void 0, [(_d.sent()).map(function (user) { return user.username; }).sort()]).toEqual(['sasjsadmin', 'sasjsuser']);
691
+ return [2 /*return*/];
692
+ }
693
+ });
694
+ }); });
695
+ it('answers the administration records the fixture seeds', function () { return __awaiter(void 0, void 0, void 0, function () {
696
+ var dispatch, groups, _a, permissions, _b, clients, _c, mine, _d, _e, _f;
697
+ return __generator(this, function (_g) {
698
+ switch (_g.label) {
699
+ case 0:
700
+ dispatch = seeded().dispatch;
701
+ return [4 /*yield*/, dispatch('/SASjsApi/group')];
702
+ case 1:
703
+ groups = _g.sent();
704
+ _a = expect;
705
+ return [4 /*yield*/, groups.json()];
706
+ case 2:
707
+ _a.apply(void 0, [(_g.sent()).map(function (group) { return group.name; }).sort()]).toEqual(['Admins', 'DCGroup']);
708
+ return [4 /*yield*/, dispatch('/SASjsApi/permission')];
709
+ case 3:
710
+ permissions = _g.sent();
711
+ _b = expect;
712
+ return [4 /*yield*/, permissions.json()];
713
+ case 4:
714
+ _b.apply(void 0, [(_g.sent()).length]).toBe(3);
715
+ return [4 /*yield*/, dispatch('/SASjsApi/client')];
716
+ case 5:
717
+ clients = _g.sent();
718
+ _c = expect;
719
+ return [4 /*yield*/, clients.json()];
720
+ case 6:
721
+ _c.apply(void 0, [(_g.sent()).map(function (client) { return client.clientId; }).sort()]).toEqual(['sasjs', 'web_app']);
722
+ return [4 /*yield*/, dispatch('/SASjsApi/info/myAuthorizedRoutes')];
723
+ case 7:
724
+ mine = _g.sent();
725
+ _d = expect;
726
+ _f = (_e = Object).keys;
727
+ return [4 /*yield*/, mine.json()];
728
+ case 8:
729
+ _d.apply(void 0, [_f.apply(_e, [_g.sent()]).length]).toBeGreaterThan(0);
730
+ return [2 /*return*/];
731
+ }
732
+ });
733
+ }); });
734
+ it('answers the Admin reports from the worker activity log', function () { return __awaiter(void 0, void 0, void 0, function () {
735
+ var dispatch, empty, _a, afterFirst, _b, listed, lines, infoLines, diagnostics, report, logs, _c, tail, tailBody, wrongName, missing;
736
+ return __generator(this, function (_d) {
737
+ switch (_d.label) {
738
+ case 0:
739
+ dispatch = seeded().dispatch;
740
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/diagnostics')];
741
+ case 1:
742
+ empty = _d.sent();
743
+ _a = expect;
744
+ return [4 /*yield*/, empty.json()];
745
+ case 2:
746
+ _a.apply(void 0, [(_d.sent()).logs]).toEqual({
747
+ folder: 'worker memory',
748
+ fileCount: 0,
749
+ totalBytes: 0,
750
+ newestFile: null,
751
+ newestModified: null
752
+ });
753
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
754
+ case 3:
755
+ afterFirst = _d.sent();
756
+ _b = expect;
757
+ return [4 /*yield*/, afterFirst.json()];
758
+ case 4:
759
+ _b.apply(void 0, [(_d.sent()).files.length]).toBe(1);
760
+ // the file's mtime is the last write, not the moment a report was read:
761
+ // /info was the request immediately before this listing
762
+ return [4 /*yield*/, dispatch('/SASjsApi/info')];
763
+ case 5:
764
+ // the file's mtime is the last write, not the moment a report was read:
765
+ // /info was the request immediately before this listing
766
+ _d.sent();
767
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
768
+ case 6: return [4 /*yield*/, (_d.sent()).json()];
769
+ case 7:
770
+ listed = (_d.sent())
771
+ .files[0];
772
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/app.log?lines=20')];
773
+ case 8: return [4 /*yield*/, (_d.sent()).json()];
774
+ case 9:
775
+ lines = (_d.sent()).content.split('\n');
776
+ infoLines = lines.filter(function (line) {
777
+ return line.includes('GET /SASjsApi/info 200');
778
+ });
779
+ expect(infoLines.length).toBe(1);
780
+ expect(infoLines[0].startsWith(listed.modified)).toBe(true);
781
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/diagnostics')];
782
+ case 10:
783
+ diagnostics = _d.sent();
784
+ expect(diagnostics.status).toBe(200);
785
+ return [4 /*yield*/, diagnostics.json()];
786
+ case 11:
787
+ report = _d.sent();
788
+ expect(report.server.mode).toBe('browser');
789
+ expect(report.filesystem).toBeNull();
790
+ expect(report.logs.fileCount).toBe(1);
791
+ expect(typeof report.memory.heapTotalBytes).toBe('number');
792
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs')];
793
+ case 12:
794
+ logs = _d.sent();
795
+ _c = expect;
796
+ return [4 /*yield*/, logs.json()];
797
+ case 13:
798
+ _c.apply(void 0, [(_d.sent()).files.map(function (file) { return file.name; })]).toEqual([
799
+ 'app.log'
800
+ ]);
801
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/app.log?lines=5')];
802
+ case 14:
803
+ tail = _d.sent();
804
+ return [4 /*yield*/, tail.json()];
805
+ case 15:
806
+ tailBody = _d.sent();
807
+ expect(tail.status).toBe(200);
808
+ expect(tailBody.content).toContain('GET /SASjsApi/info 200');
809
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/secret.txt')];
810
+ case 16:
811
+ wrongName = _d.sent();
812
+ expect(wrongName.status).toBe(400);
813
+ return [4 /*yield*/, dispatch('/SASjsApi/admin/logs/other.log')];
814
+ case 17:
815
+ missing = _d.sent();
816
+ expect(missing.status).toBe(404);
817
+ return [2 /*return*/];
818
+ }
819
+ });
820
+ }); });
821
+ it('imports the fixture directory, and unpublishes the site itself', function () { return __awaiter(void 0, void 0, void 0, function () {
822
+ var dispatch, synced, _a, unpublished, _b, other;
823
+ return __generator(this, function (_c) {
824
+ switch (_c.label) {
825
+ case 0:
826
+ dispatch = seeded().dispatch;
827
+ return [4 /*yield*/, dispatch('/SASjsApi/authConfig/synchroniseWithLDAP', { method: 'POST' })];
828
+ case 1:
829
+ synced = _c.sent();
830
+ expect(synced.status).toBe(200);
831
+ _a = expect;
832
+ return [4 /*yield*/, synced.json()];
833
+ case 2:
834
+ _a.apply(void 0, [_c.sent()]).toEqual({ userCount: 0, groupCount: 0 });
835
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy', {
836
+ method: 'DELETE',
837
+ headers: { 'content-type': 'application/json' },
838
+ body: JSON.stringify({
839
+ streamServiceName: 'Test',
840
+ appLoc: appLoc
841
+ })
842
+ })];
843
+ case 3:
844
+ unpublished = _c.sent();
845
+ expect(unpublished.status).toBe(200);
846
+ _b = expect;
847
+ return [4 /*yield*/, unpublished.json()];
848
+ case 4:
849
+ _b.apply(void 0, [_c.sent()]).toEqual({ status: 'success' });
850
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy', {
851
+ method: 'DELETE',
852
+ headers: { 'content-type': 'application/json' },
853
+ body: JSON.stringify({
854
+ streamServiceName: 'Other',
855
+ appLoc: '/Public/app/other'
856
+ })
857
+ })];
858
+ case 5:
859
+ other = _c.sent();
860
+ expect(other.status).toBe(404);
861
+ return [2 /*return*/];
862
+ }
863
+ });
864
+ }); });
865
+ it('lints and formats a buffer with the bundled linter', function () { return __awaiter(void 0, void 0, void 0, function () {
866
+ var dispatch, linted, diagnostics, formatted, _a;
867
+ return __generator(this, function (_b) {
868
+ switch (_b.label) {
869
+ case 0:
870
+ dispatch = seeded().dispatch;
871
+ return [4 /*yield*/, dispatch('/SASjsApi/code/lint', runRequest({ code: 'data a;\nrun;\n' }))];
872
+ case 1:
873
+ linted = _b.sent();
874
+ expect(linted.status).toBe(200);
875
+ return [4 /*yield*/, linted.json()];
876
+ case 2:
877
+ diagnostics = _b.sent();
878
+ expect(Array.isArray(diagnostics)).toBe(true);
879
+ expect(diagnostics.length).toBeGreaterThan(0);
880
+ expect(diagnostics[0].message).toContain('Doxygen');
881
+ return [4 /*yield*/, dispatch('/SASjsApi/code/format', runRequest({ code: 'data a;set b;run;' }))];
882
+ case 3:
883
+ formatted = _b.sent();
884
+ expect(formatted.status).toBe(200);
885
+ _a = expect;
886
+ return [4 /*yield*/, formatted.text()];
887
+ case 4:
888
+ _a.apply(void 0, [_b.sent()]).toContain('@brief');
889
+ return [2 /*return*/];
890
+ }
891
+ });
892
+ }); });
893
+ it('serves the build page at the home route, and falls back when it fails', function () { return __awaiter(void 0, void 0, void 0, function () {
894
+ var worker, response, _a, runtime, original, fallback, body;
895
+ return __generator(this, function (_b) {
896
+ switch (_b.label) {
897
+ case 0:
898
+ worker = seeded();
899
+ return [4 /*yield*/, worker.dispatch('/')];
900
+ case 1:
901
+ response = _b.sent();
902
+ expect(response.status).toBe(200);
903
+ expect(response.headers.get('content-type')).toContain('text/html');
904
+ _a = expect;
905
+ return [4 /*yield*/, response.text()];
906
+ case 2:
907
+ _a.apply(void 0, [_b.sent()]).toContain('<body>home</body>');
908
+ runtime = worker.self.SASjsMockRuntime;
909
+ original = runtime.ensureReady;
910
+ runtime.ensureReady = function () { return __awaiter(void 0, void 0, void 0, function () {
911
+ return __generator(this, function (_a) {
912
+ throw new Error('home route exploded');
913
+ });
914
+ }); };
915
+ _b.label = 3;
916
+ case 3:
917
+ _b.trys.push([3, , 6, 7]);
918
+ return [4 /*yield*/, worker.dispatch('/')];
919
+ case 4:
920
+ fallback = _b.sent();
921
+ expect(fallback.status).toBe(200);
922
+ return [4 /*yield*/, fallback.text()];
923
+ case 5:
924
+ body = _b.sent();
925
+ expect(body).toContain('static page');
926
+ expect(body).not.toContain('home route exploded');
927
+ return [3 /*break*/, 7];
928
+ case 6:
929
+ runtime.ensureReady = original;
930
+ return [7 /*endfinally*/];
931
+ case 7: return [2 /*return*/];
932
+ }
933
+ });
934
+ }); });
935
+ it('matches a path whatever its casing, as the API does', function () { return __awaiter(void 0, void 0, void 0, function () {
936
+ var dispatch, response, _a;
937
+ return __generator(this, function (_b) {
938
+ switch (_b.label) {
939
+ case 0:
940
+ dispatch = seeded().dispatch;
941
+ return [4 /*yield*/, dispatch('/sasjsapi/INFO')];
942
+ case 1:
943
+ response = _b.sent();
944
+ expect(response.status).toBe(200);
945
+ _a = expect;
946
+ return [4 /*yield*/, response.json()];
947
+ case 2:
948
+ _a.apply(void 0, [(_b.sent()).mode]).toBe('desktop');
949
+ return [2 /*return*/];
950
+ }
951
+ });
952
+ }); });
953
+ it('starts a session, then reads its state, log and output', function () { return __awaiter(void 0, void 0, void 0, function () {
954
+ var dispatch, program, triggered, sessionId, state, _a, output, _b, log, missing;
955
+ return __generator(this, function (_c) {
956
+ switch (_c.label) {
957
+ case 0:
958
+ dispatch = seeded().dispatch;
959
+ program = encodeURIComponent("".concat(appLoc, "/services/public/getdata"));
960
+ return [4 /*yield*/, dispatch('/SASjsApi/stp/trigger?_program=' + program, runRequest({ params: {} }))];
961
+ case 1:
962
+ triggered = _c.sent();
963
+ expect(triggered.status).toBe(200);
964
+ return [4 /*yield*/, triggered.json()];
965
+ case 2:
966
+ sessionId = (_c.sent()).sessionId;
967
+ expect(sessionId).toBeTruthy();
968
+ return [4 /*yield*/, dispatch("/SASjsApi/session/".concat(sessionId, "/state"))];
969
+ case 3:
970
+ state = _c.sent();
971
+ _a = expect;
972
+ return [4 /*yield*/, state.json()];
973
+ case 4:
974
+ _a.apply(void 0, [_c.sent()]).toEqual({ state: 'completed' });
975
+ return [4 /*yield*/, dispatch("/SASjsApi/session/".concat(sessionId, "/output"))];
976
+ case 5:
977
+ output = _c.sent();
978
+ expect(output.status).toBe(200);
979
+ _b = expect;
980
+ return [4 /*yield*/, output.text()];
981
+ case 6:
982
+ _b.apply(void 0, [_c.sent()]).toBe('seeded');
983
+ return [4 /*yield*/, dispatch("/SASjsApi/session/".concat(sessionId, "/log"))];
984
+ case 7:
985
+ log = _c.sent();
986
+ expect(log.status).toBe(200);
987
+ return [4 /*yield*/, dispatch('/SASjsApi/session/not-a-session/state')];
988
+ case 8:
989
+ missing = _c.sent();
990
+ expect(missing.status).toBe(404);
991
+ return [2 /*return*/];
992
+ }
993
+ });
994
+ }); });
995
+ 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 () {
996
+ var dispatch, empty, _a, form, notZip, _b;
997
+ return __generator(this, function (_c) {
998
+ switch (_c.label) {
999
+ case 0:
1000
+ dispatch = seeded().dispatch;
1001
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
1002
+ method: 'POST',
1003
+ body: new FormData()
1004
+ })];
1005
+ case 1:
1006
+ empty = _c.sent();
1007
+ expect(empty.status).toBe(400);
1008
+ _a = expect;
1009
+ return [4 /*yield*/, empty.text()];
1010
+ case 2:
1011
+ _a.apply(void 0, [_c.sent()]).toBe('"file" is not present.');
1012
+ form = new FormData();
1013
+ form.set('file', new Blob([new Uint8Array([1, 2, 3])]), 'not-a-zip.zip');
1014
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
1015
+ method: 'POST',
1016
+ body: form
1017
+ })];
1018
+ case 3:
1019
+ notZip = _c.sent();
1020
+ expect(notZip.status).toBe(400);
1021
+ _b = expect;
1022
+ return [4 /*yield*/, notZip.text()];
1023
+ case 4:
1024
+ _b.apply(void 0, [_c.sent()]).toBe('The archive is not a zip file.');
1025
+ return [2 /*return*/];
1026
+ }
1027
+ });
1028
+ }); });
1029
+ it('takes a token from the query when a link cannot carry a header', function () { return __awaiter(void 0, void 0, void 0, function () {
1030
+ var worker, authorized, code, exchanged, pair, users, _a;
1031
+ return __generator(this, function (_b) {
1032
+ switch (_b.label) {
1033
+ case 0:
1034
+ worker = seeded();
1035
+ return [4 /*yield*/, worker.dispatch('/SASLogon/authorize', runRequest({ clientId: 'web_app' }))];
1036
+ case 1:
1037
+ authorized = _b.sent();
1038
+ return [4 /*yield*/, authorized.json()];
1039
+ case 2:
1040
+ code = (_b.sent()).code;
1041
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/auth/token', runRequest({ clientId: 'web_app', code: code }))];
1042
+ case 3:
1043
+ exchanged = _b.sent();
1044
+ return [4 /*yield*/, exchanged.json()];
1045
+ case 4:
1046
+ pair = _b.sent();
1047
+ return [4 /*yield*/, worker.dispatch("/SASjsApi/user?access_token=".concat(encodeURIComponent(pair.accessToken)))];
1048
+ case 5:
1049
+ users = _b.sent();
1050
+ expect(users.status).toBe(200);
1051
+ _a = expect;
1052
+ return [4 /*yield*/, users.json()];
1053
+ case 6:
1054
+ _a.apply(void 0, [(_b.sent()).length]).toBe(2);
1055
+ return [2 /*return*/];
1056
+ }
1057
+ });
1058
+ }); });
1059
+ 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 () {
1060
+ var worker, unnamed, _a, form, _b, _c, _d, _e, _f, upload;
1061
+ var _g;
1062
+ return __generator(this, function (_h) {
1063
+ switch (_h.label) {
1064
+ case 0:
1065
+ worker = seeded();
1066
+ worker.runtime.load((_g = {},
1067
+ _g[drivePath('services/public/echo.js')] = '_webout = _WEBIN_NAME1 + "|" + _WEBIN_FILEREF1;',
1068
+ _g));
1069
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
1070
+ encodeURIComponent("".concat(appLoc, "/services/public/echo")), runRequest({ params: {}, tables: [{ csv: 'A,B\r\n1,2' }] }))];
1071
+ case 1:
1072
+ unnamed = _h.sent();
1073
+ _a = expect;
1074
+ return [4 /*yield*/, unnamed.text()];
1075
+ case 2:
1076
+ _a.apply(void 0, [_h.sent()]).toBe('table1|A,B\r\n1,2');
1077
+ form = new FormData();
1078
+ form.set('appLoc', appLoc);
1079
+ _c = (_b = form).set;
1080
+ _d = ['archive'];
1081
+ _e = Blob.bind;
1082
+ _f = Uint8Array.bind;
1083
+ return [4 /*yield*/, zipOf({ 'x.txt': 'x' })];
1084
+ case 3:
1085
+ _c.apply(_b, _d.concat([new (_e.apply(Blob, [void 0, [new (_f.apply(Uint8Array, [void 0, _h.sent()]))()]]))(),
1086
+ 'deploy.zip']));
1087
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/deploy/upload', {
1088
+ method: 'POST',
1089
+ body: form
1090
+ })];
1091
+ case 4:
1092
+ upload = _h.sent();
1093
+ expect(upload.status).toBe(200);
1094
+ return [2 /*return*/];
1095
+ }
1096
+ });
1097
+ }); });
1098
+ it('expands an uploaded archive into the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
1099
+ var dispatch, archive, form, response, tree, listing, _a, _b, file, _c;
1100
+ return __generator(this, function (_d) {
1101
+ switch (_d.label) {
1102
+ case 0:
1103
+ dispatch = seeded().dispatch;
1104
+ return [4 /*yield*/, zipOf({
1105
+ 'services/public/run.js': '_webout = "deployed";',
1106
+ 'docs/readme.txt': 'hello'
1107
+ })];
1108
+ case 1:
1109
+ archive = _d.sent();
1110
+ form = new FormData();
1111
+ form.set('appLoc', appLoc);
1112
+ form.set('file', new Blob([new Uint8Array(archive)]), 'deploy.zip');
1113
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
1114
+ method: 'POST',
1115
+ body: form
1116
+ })];
1117
+ case 2:
1118
+ response = _d.sent();
1119
+ expect(response.status).toBe(200);
1120
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/filetree?_folderPath=' + encodeURIComponent(appLoc))];
1121
+ case 3:
1122
+ tree = _d.sent();
1123
+ _b = (_a = JSON).stringify;
1124
+ return [4 /*yield*/, tree.json()];
1125
+ case 4:
1126
+ listing = _b.apply(_a, [_d.sent()]);
1127
+ expect(listing).toContain('run.js');
1128
+ expect(listing).toContain('readme.txt');
1129
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
1130
+ encodeURIComponent("".concat(appLoc, "/docs/readme.txt")))];
1131
+ case 5:
1132
+ file = _d.sent();
1133
+ _c = expect;
1134
+ return [4 /*yield*/, file.text()];
1135
+ case 6:
1136
+ _c.apply(void 0, [_d.sent()]).toBe('hello');
1137
+ return [2 /*return*/];
1138
+ }
1139
+ });
1140
+ }); });
1141
+ });
579
1142
  it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
580
1143
  var dispatch, response, _a;
581
1144
  return __generator(this, function (_b) {
@@ -686,12 +1249,12 @@ describe('service worker host', function () {
686
1249
  _a = expect;
687
1250
  return [4 /*yield*/, response.json()];
688
1251
  case 2:
689
- _a.apply(void 0, [_b.sent()]).toEqual({ message: '_program is required' });
1252
+ _a.apply(void 0, [_b.sent()]).toEqual({ message: '"_program" is required' });
690
1253
  return [2 /*return*/];
691
1254
  }
692
1255
  });
693
1256
  }); });
694
- it('answers 500 with the failure shape when the service throws', function () { return __awaiter(void 0, void 0, void 0, function () {
1257
+ it('answers the documented failure envelope when the service throws', function () { return __awaiter(void 0, void 0, void 0, function () {
695
1258
  var _a, runtime, dispatch, response, body;
696
1259
  var _b;
697
1260
  return __generator(this, function (_c) {
@@ -705,11 +1268,17 @@ describe('service worker host', function () {
705
1268
  encodeURIComponent("".concat(appLoc, "/services/public/broken")))];
706
1269
  case 1:
707
1270
  response = _c.sent();
708
- return [4 /*yield*/, response.json()];
1271
+ return [4 /*yield*/, response.json()
1272
+ // the package's decision, and the server's: a run that failed is a 400
1273
+ // carrying the failure envelope, not a 500 that blames the transport
1274
+ ];
709
1275
  case 2:
710
1276
  body = _c.sent();
711
- expect(response.status).toBe(500);
1277
+ // the package's decision, and the server's: a run that failed is a 400
1278
+ // carrying the failure envelope, not a 500 that blames the transport
1279
+ expect(response.status).toBe(400);
712
1280
  expect(body.status).toBe('failure');
1281
+ expect(body.message).toBe('Job execution failed.');
713
1282
  expect(body.error).toContain('boom');
714
1283
  return [2 /*return*/];
715
1284
  }
@@ -721,13 +1290,58 @@ describe('service worker host', function () {
721
1290
  switch (_b.label) {
722
1291
  case 0:
723
1292
  dispatch = seeded().dispatch;
724
- return [4 /*yield*/, dispatch('/SASjsApi/code/execute', runRequest({ code: '_webout = "inline";', params: {} }))];
1293
+ return [4 /*yield*/, dispatch('/SASjsApi/code/execute', runRequest({ code: '_webout = "inline";', runTime: 'js', params: {} }))
1294
+ // a code route's documented body is the webout framed with the log, so the
1295
+ // run's output is the first section rather than the whole body
1296
+ ];
725
1297
  case 1:
726
1298
  response = _b.sent();
1299
+ // a code route's documented body is the webout framed with the log, so the
1300
+ // run's output is the first section rather than the whole body
1301
+ _a = expect;
1302
+ return [4 /*yield*/, response.text()];
1303
+ case 2:
1304
+ // a code route's documented body is the webout framed with the log, so the
1305
+ // run's output is the first section rather than the whole body
1306
+ _a.apply(void 0, [(_b.sent()).split(LOGS_SEPARATOR)[0].trim()]).toBe('inline');
1307
+ return [2 /*return*/];
1308
+ }
1309
+ });
1310
+ }); });
1311
+ it('reads the table envelope the page bridge posts for an XHR', function () { return __awaiter(void 0, void 0, void 0, function () {
1312
+ var worker, response, _a;
1313
+ var _b;
1314
+ return __generator(this, function (_c) {
1315
+ switch (_c.label) {
1316
+ case 0:
1317
+ worker = seeded();
1318
+ worker.runtime.load((_b = {},
1319
+ _b[drivePath('services/public/echo.js')] = '_webout = _WEBIN_NAME1 + "|" + _WEBIN_FILEREF1;',
1320
+ _b));
1321
+ return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
1322
+ encodeURIComponent("".concat(appLoc, "/services/public/echo")), runRequest({
1323
+ params: {},
1324
+ tables: [
1325
+ {
1326
+ name: 'SASControlTable',
1327
+ filename: 'SASControlTable.csv',
1328
+ csv: 'A,B\r\n1,2'
1329
+ }
1330
+ ]
1331
+ }))
1332
+ // the bridge hands the worker `{params, tables}`; a stored program sees the
1333
+ // tables as its _WEBIN variables, not as one `tables` session variable
1334
+ ];
1335
+ case 1:
1336
+ response = _c.sent();
1337
+ // the bridge hands the worker `{params, tables}`; a stored program sees the
1338
+ // tables as its _WEBIN variables, not as one `tables` session variable
727
1339
  _a = expect;
728
1340
  return [4 /*yield*/, response.text()];
729
1341
  case 2:
730
- _a.apply(void 0, [_b.sent()]).toBe('inline');
1342
+ // the bridge hands the worker `{params, tables}`; a stored program sees the
1343
+ // tables as its _WEBIN variables, not as one `tables` session variable
1344
+ _a.apply(void 0, [_c.sent()]).toBe('SASControlTable|A,B\r\n1,2');
731
1345
  return [2 /*return*/];
732
1346
  }
733
1347
  });
@@ -1505,42 +2119,55 @@ describe('service worker host', function () {
1505
2119
  }); };
1506
2120
  var _loop_1 = function (testCase) {
1507
2121
  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) {
2122
+ var worker, _a, requestPath, init, response, body, want, _b, _c, _d;
2123
+ return __generator(this, function (_e) {
2124
+ switch (_e.label) {
1511
2125
  case 0:
1512
2126
  worker = loadWorker();
1513
2127
  return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
1514
2128
  case 1:
1515
- _f.sent();
2129
+ _e.sent();
1516
2130
  worker.refuseWrites(Boolean(testCase.unwritable));
1517
2131
  _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
1518
2132
  return [4 /*yield*/, worker.dispatch(requestPath, init)];
1519
2133
  case 2:
1520
- response = _f.sent();
2134
+ response = _e.sent();
1521
2135
  expect(response.status).toBe(testCase.expect.status);
1522
2136
  if (!(testCase.expect.error !== undefined)) return [3 /*break*/, 4];
1523
- _b = expect;
1524
- return [4 /*yield*/, response.text()];
2137
+ return [4 /*yield*/, response.text()
2138
+ // the routes that answer an envelope - the two deploy routes - send
2139
+ // `{status, message}`; the rest send the message itself as text, which
2140
+ // is what the server's `res.status(code).send(err.message)` does
2141
+ ];
1525
2142
  case 3:
1526
- _b.apply(void 0, [_f.sent()]).toBe(testCase.expect.error);
1527
- _f.label = 4;
2143
+ body = _e.sent();
2144
+ // the routes that answer an envelope - the two deploy routes - send
2145
+ // `{status, message}`; the rest send the message itself as text, which
2146
+ // is what the server's `res.status(code).send(err.message)` does
2147
+ if ((response.headers.get('content-type') || '').includes('application/json'))
2148
+ expect(JSON.parse(body)).toEqual({
2149
+ status: 'failure',
2150
+ message: testCase.expect.error
2151
+ });
2152
+ else
2153
+ expect(body).toBe(testCase.expect.error);
2154
+ return [3 /*break*/, 8];
1528
2155
  case 4:
1529
2156
  want = testCase.expect.contents;
1530
2157
  if (!(typeof want === 'string')) return [3 /*break*/, 6];
1531
- _c = expect;
2158
+ _b = expect;
1532
2159
  return [4 /*yield*/, response.text()];
1533
2160
  case 5:
1534
- _c.apply(void 0, [_f.sent()]).toBe(want);
2161
+ _b.apply(void 0, [_e.sent()]).toBe(want);
1535
2162
  return [3 /*break*/, 8];
1536
2163
  case 6:
1537
2164
  if (!(want !== undefined)) return [3 /*break*/, 8];
1538
- _d = expect;
1539
- _e = Uint8Array.bind;
2165
+ _c = expect;
2166
+ _d = Uint8Array.bind;
1540
2167
  return [4 /*yield*/, response.arrayBuffer()];
1541
2168
  case 7:
1542
- _d.apply(void 0, [new (_e.apply(Uint8Array, [void 0, _f.sent()]))()]).toEqual(want);
1543
- _f.label = 8;
2169
+ _c.apply(void 0, [new (_d.apply(Uint8Array, [void 0, _e.sent()]))()]).toEqual(want);
2170
+ _e.label = 8;
1544
2171
  case 8: return [2 /*return*/];
1545
2172
  }
1546
2173
  });