@sasjs/cli 6.10.1 → 6.12.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,7 +35,39 @@ 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 __read = (this && this.__read) || function (o, n) {
39
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
40
+ if (!m) return o;
41
+ var i = m.call(o), r, ar = [], e;
42
+ try {
43
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
44
+ }
45
+ catch (error) { e = { error: error }; }
46
+ finally {
47
+ try {
48
+ if (r && !r.done && (m = i["return"])) m.call(i);
49
+ }
50
+ finally { if (e) throw e.error; }
51
+ }
52
+ return ar;
53
+ };
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
66
+ return (mod && mod.__esModule) ? mod : { "default": mod };
67
+ };
38
68
  Object.defineProperty(exports, "__esModule", { value: true });
69
+ var path_1 = __importDefault(require("path"));
70
+ var api_1 = require("@sasjs/api");
39
71
  /**
40
72
  * The other half of the parity claim: the service worker is what turns an
41
73
  * /SASjsApi/ request into a response, so its framing (webout body, the _debug
@@ -47,6 +79,13 @@ var LOGS_SEPARATOR = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784';
47
79
  var drivePath = function (relative) {
48
80
  return "/sasjs/drive/files".concat(appLoc, "/").concat(relative);
49
81
  };
82
+ /**
83
+ * The browser build of @sasjs/api that mockSite.ts copies into the site, from the
84
+ * installed package - the worker under test must be the worker the build ships.
85
+ */
86
+ var apiBrowserBuild = function () {
87
+ return path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
88
+ };
50
89
  var loadWorker = function () {
51
90
  jest.resetModules();
52
91
  var handlers = {};
@@ -65,6 +104,8 @@ var loadWorker = function () {
65
104
  global.importScripts = function (path) {
66
105
  if (path.includes('md5'))
67
106
  return;
107
+ if (path.includes('api-browser'))
108
+ return require(apiBrowserBuild());
68
109
  require('../assets/runtime-core.js');
69
110
  };
70
111
  require('../assets/sw.js');
@@ -514,9 +555,9 @@ describe('service worker host', function () {
514
555
  response = _b.sent();
515
556
  expect(response.status).toBe(404);
516
557
  _a = expect;
517
- return [4 /*yield*/, response.json()];
558
+ return [4 /*yield*/, response.text()];
518
559
  case 2:
519
- _a.apply(void 0, [(_b.sent()).message]).toBe('File not found');
560
+ _a.apply(void 0, [_b.sent()]).toBe("File doesn't exist.");
520
561
  return [2 /*return*/];
521
562
  }
522
563
  });
@@ -567,9 +608,9 @@ describe('service worker host', function () {
567
608
  response = _c.sent();
568
609
  expect(response.status).toBe(500);
569
610
  _a = expect;
570
- return [4 /*yield*/, response.json()];
611
+ return [4 /*yield*/, response.text()];
571
612
  case 2:
572
- _a.apply(void 0, [(_c.sent()).message]).toContain('Corrupt binary entry');
613
+ _a.apply(void 0, [_c.sent()]).toContain('Corrupt binary entry');
573
614
  return [2 /*return*/];
574
615
  }
575
616
  });
@@ -799,4 +840,669 @@ describe('service worker host', function () {
799
840
  var dispatch = seeded().dispatch;
800
841
  expect(dispatch('/index.html')).toBeUndefined();
801
842
  });
843
+ describe('drive writes', function () {
844
+ /**
845
+ * The response as a client sees it. A drive refusal is plain text (the
846
+ * server sends `res.status(code).send(err.message)`), a success is JSON.
847
+ */
848
+ var answer = function (response) { return __awaiter(void 0, void 0, void 0, function () {
849
+ var resolved, type, _a;
850
+ var _b;
851
+ return __generator(this, function (_c) {
852
+ switch (_c.label) {
853
+ case 0: return [4 /*yield*/, response];
854
+ case 1:
855
+ resolved = _c.sent();
856
+ type = resolved.headers.get('content-type') || '';
857
+ _b = {
858
+ status: resolved.status
859
+ };
860
+ if (!type.includes('application/json')) return [3 /*break*/, 3];
861
+ return [4 /*yield*/, resolved.json()];
862
+ case 2:
863
+ _a = _c.sent();
864
+ return [3 /*break*/, 5];
865
+ case 3: return [4 /*yield*/, resolved.text()];
866
+ case 4:
867
+ _a = _c.sent();
868
+ _c.label = 5;
869
+ case 5: return [2 /*return*/, (_b.body = _a,
870
+ _b)];
871
+ }
872
+ });
873
+ }); };
874
+ it('answers a drive refusal as text, the way the server sends one', function () { return __awaiter(void 0, void 0, void 0, function () {
875
+ var dispatch, response, _a;
876
+ return __generator(this, function (_b) {
877
+ switch (_b.label) {
878
+ case 0:
879
+ dispatch = seeded().dispatch;
880
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
881
+ encodeURIComponent('/data/missing.csv'))
882
+ // the server's drive routes answer `res.status(code).send(err.message)`,
883
+ // and its own suite asserts `res.text` - so a client that reads the body
884
+ // as text must get the message here too, not a JSON envelope
885
+ ];
886
+ case 1:
887
+ response = _b.sent();
888
+ // the server's drive routes answer `res.status(code).send(err.message)`,
889
+ // and its own suite asserts `res.text` - so a client that reads the body
890
+ // as text must get the message here too, not a JSON envelope
891
+ expect(response.status).toBe(404);
892
+ expect(response.headers.get('content-type')).toContain('text/plain');
893
+ _a = expect;
894
+ return [4 /*yield*/, response.text()];
895
+ case 2:
896
+ _a.apply(void 0, [_b.sent()]).toBe("File doesn't exist.");
897
+ return [2 /*return*/];
898
+ }
899
+ });
900
+ }); });
901
+ it('creates a file, which the drive then serves', function () { return __awaiter(void 0, void 0, void 0, function () {
902
+ var dispatch, created, read, _a;
903
+ return __generator(this, function (_b) {
904
+ switch (_b.label) {
905
+ case 0:
906
+ dispatch = seeded().dispatch;
907
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
908
+ encodeURIComponent('/data/users.csv'), (function () {
909
+ var form = new FormData();
910
+ form.set('file', new Blob(['id,name\n1,Ann\n']), 'users.csv');
911
+ return { method: 'POST', body: form };
912
+ })()))];
913
+ case 1:
914
+ created = _b.sent();
915
+ expect(created).toEqual({ status: 200, body: { status: 'success' } });
916
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
917
+ encodeURIComponent('/data/users.csv'))];
918
+ case 2:
919
+ read = _b.sent();
920
+ _a = expect;
921
+ return [4 /*yield*/, read.text()];
922
+ case 3:
923
+ _a.apply(void 0, [_b.sent()]).toBe('id,name\n1,Ann\n');
924
+ return [2 /*return*/];
925
+ }
926
+ });
927
+ }); });
928
+ it('creates a file at the path in the form when the query has none', function () { return __awaiter(void 0, void 0, void 0, function () {
929
+ var dispatch, form, created, read, _a;
930
+ return __generator(this, function (_b) {
931
+ switch (_b.label) {
932
+ case 0:
933
+ dispatch = seeded().dispatch;
934
+ form = new FormData();
935
+ form.set('file', new Blob(['x']), 'x.txt');
936
+ form.set('filePath', '/data/from-form.txt');
937
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file', { method: 'POST', body: form }))];
938
+ case 1:
939
+ created = _b.sent();
940
+ expect(created.body).toEqual({ status: 'success' });
941
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
942
+ encodeURIComponent('/data/from-form.txt'))];
943
+ case 2:
944
+ read = _b.sent();
945
+ _a = expect;
946
+ return [4 /*yield*/, read.text()];
947
+ case 3:
948
+ _a.apply(void 0, [_b.sent()]).toBe('x');
949
+ return [2 /*return*/];
950
+ }
951
+ });
952
+ }); });
953
+ it('answers 409 for a create over an existing file, 404 for a replace of a missing one', function () { return __awaiter(void 0, void 0, void 0, function () {
954
+ var dispatch, form, again, missing;
955
+ return __generator(this, function (_a) {
956
+ switch (_a.label) {
957
+ case 0:
958
+ dispatch = seeded().dispatch;
959
+ form = function () {
960
+ var f = new FormData();
961
+ f.set('file', new Blob(['x']), 'x.csv');
962
+ return f;
963
+ };
964
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/x.csv'), { method: 'POST', body: form() })];
965
+ case 1:
966
+ _a.sent();
967
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/x.csv'), { method: 'POST', body: form() }))];
968
+ case 2:
969
+ again = _a.sent();
970
+ expect(again.status).toBe(409);
971
+ expect(again.body).toEqual('File already exists.');
972
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
973
+ encodeURIComponent('/data/missing.csv'), { method: 'PATCH', body: form() }))];
974
+ case 3:
975
+ missing = _a.sent();
976
+ expect(missing.status).toBe(404);
977
+ expect(missing.body).toEqual("File doesn't exist.");
978
+ return [2 /*return*/];
979
+ }
980
+ });
981
+ }); });
982
+ it('replaces a file and deletes one', function () { return __awaiter(void 0, void 0, void 0, function () {
983
+ var dispatch, form, replacement, replaced, read, _a, removed, gone;
984
+ return __generator(this, function (_b) {
985
+ switch (_b.label) {
986
+ case 0:
987
+ dispatch = seeded().dispatch;
988
+ form = new FormData();
989
+ form.set('file', new Blob(['v1']), 'v.csv');
990
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'), { method: 'POST', body: form })];
991
+ case 1:
992
+ _b.sent();
993
+ replacement = new FormData();
994
+ replacement.set('file', new Blob(['v2']), 'v.csv');
995
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'), { method: 'PATCH', body: replacement }))];
996
+ case 2:
997
+ replaced = _b.sent();
998
+ expect(replaced.body).toEqual({ status: 'success' });
999
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'))];
1000
+ case 3:
1001
+ read = _b.sent();
1002
+ _a = expect;
1003
+ return [4 /*yield*/, read.text()];
1004
+ case 4:
1005
+ _a.apply(void 0, [_b.sent()]).toBe('v2');
1006
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'), { method: 'DELETE' }))];
1007
+ case 5:
1008
+ removed = _b.sent();
1009
+ expect(removed.body).toEqual({ status: 'success' });
1010
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/v.csv'))];
1011
+ case 6:
1012
+ gone = _b.sent();
1013
+ expect(gone.status).toBe(404);
1014
+ return [2 /*return*/];
1015
+ }
1016
+ });
1017
+ }); });
1018
+ it('refuses an upload with no file, no path, or a blocked extension', function () { return __awaiter(void 0, void 0, void 0, function () {
1019
+ var dispatch, empty, noFile, form, noPath, sh, blocked;
1020
+ return __generator(this, function (_a) {
1021
+ switch (_a.label) {
1022
+ case 0:
1023
+ dispatch = seeded().dispatch;
1024
+ empty = new FormData();
1025
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/x.csv'), { method: 'POST', body: empty }))];
1026
+ case 1:
1027
+ noFile = _a.sent();
1028
+ expect(noFile.status).toBe(400);
1029
+ expect(noFile.body).toEqual('"file" is not present.');
1030
+ form = new FormData();
1031
+ form.set('file', new Blob(['x']), 'x.csv');
1032
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file', { method: 'POST', body: form }))];
1033
+ case 2:
1034
+ noPath = _a.sent();
1035
+ expect(noPath.status).toBe(400);
1036
+ expect(noPath.body).toEqual('"_filePath" is required');
1037
+ sh = new FormData();
1038
+ sh.set('file', new Blob(['x']), 'run.sh');
1039
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
1040
+ encodeURIComponent('/data/run.sh'), { method: 'POST', body: sh }))];
1041
+ case 3:
1042
+ blocked = _a.sent();
1043
+ expect(blocked.status).toBe(400);
1044
+ expect(blocked.body).toEqual('Invalid file extension');
1045
+ return [2 /*return*/];
1046
+ }
1047
+ });
1048
+ }); });
1049
+ it('refuses a path outside the drive on every write route', function () { return __awaiter(void 0, void 0, void 0, function () {
1050
+ var dispatch, form, outside, rename;
1051
+ return __generator(this, function (_a) {
1052
+ switch (_a.label) {
1053
+ case 0:
1054
+ dispatch = seeded().dispatch;
1055
+ form = new FormData();
1056
+ form.set('file', new Blob(['x']), 'x.csv');
1057
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
1058
+ encodeURIComponent('/../escape.csv'), { method: 'POST', body: form }))];
1059
+ case 1:
1060
+ outside = _a.sent();
1061
+ expect(outside.status).toBe(400);
1062
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/rename', {
1063
+ method: 'POST',
1064
+ headers: { 'content-type': 'application/json' },
1065
+ body: JSON.stringify({ oldPath: '/../x', newPath: '/data/y' })
1066
+ }))];
1067
+ case 2:
1068
+ rename = _a.sent();
1069
+ expect(rename.status).toBe(400);
1070
+ return [2 /*return*/];
1071
+ }
1072
+ });
1073
+ }); });
1074
+ it('creates a folder, lists it, and deletes it with its contents', function () { return __awaiter(void 0, void 0, void 0, function () {
1075
+ var dispatch, made, form, listed, _a, removed, gone;
1076
+ return __generator(this, function (_b) {
1077
+ switch (_b.label) {
1078
+ case 0:
1079
+ dispatch = seeded().dispatch;
1080
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/folder', {
1081
+ method: 'POST',
1082
+ headers: { 'content-type': 'application/json' },
1083
+ body: JSON.stringify({ folderPath: '/data/reports' })
1084
+ }))];
1085
+ case 1:
1086
+ made = _b.sent();
1087
+ expect(made.body).toEqual({ status: 'success' });
1088
+ form = new FormData();
1089
+ form.set('file', new Blob(['a']), 'a.csv');
1090
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
1091
+ encodeURIComponent('/data/reports/a.csv'), { method: 'POST', body: form })];
1092
+ case 2:
1093
+ _b.sent();
1094
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/folder?_folderPath=' + encodeURIComponent('/data'))];
1095
+ case 3:
1096
+ listed = _b.sent();
1097
+ _a = expect;
1098
+ return [4 /*yield*/, listed.json()];
1099
+ case 4:
1100
+ _a.apply(void 0, [_b.sent()]).toEqual({
1101
+ files: [],
1102
+ folders: ['reports']
1103
+ });
1104
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/folder?_folderPath=' +
1105
+ encodeURIComponent('/data/reports'), { method: 'DELETE' }))];
1106
+ case 5:
1107
+ removed = _b.sent();
1108
+ expect(removed.body).toEqual({ status: 'success' });
1109
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
1110
+ encodeURIComponent('/data/reports/a.csv'))];
1111
+ case 6:
1112
+ gone = _b.sent();
1113
+ expect(gone.status).toBe(404);
1114
+ return [2 /*return*/];
1115
+ }
1116
+ });
1117
+ }); });
1118
+ it('refuses a folder delete with no path', function () { return __awaiter(void 0, void 0, void 0, function () {
1119
+ var dispatch, removed;
1120
+ return __generator(this, function (_a) {
1121
+ switch (_a.label) {
1122
+ case 0:
1123
+ dispatch = seeded().dispatch;
1124
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/folder', { method: 'DELETE' }))];
1125
+ case 1:
1126
+ removed = _a.sent();
1127
+ expect(removed.status).toBe(400);
1128
+ expect(removed.body).toEqual('"_folderPath" is required');
1129
+ return [2 /*return*/];
1130
+ }
1131
+ });
1132
+ }); });
1133
+ it('refuses a rename into a blocked extension, as the server does', function () { return __awaiter(void 0, void 0, void 0, function () {
1134
+ var dispatch, form, toBlocked, read;
1135
+ return __generator(this, function (_a) {
1136
+ switch (_a.label) {
1137
+ case 0:
1138
+ dispatch = seeded().dispatch;
1139
+ form = new FormData();
1140
+ form.set('file', new Blob(['r']), 'r.csv');
1141
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/old.csv'), { method: 'POST', body: form })];
1142
+ case 1:
1143
+ _a.sent();
1144
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/rename', {
1145
+ method: 'POST',
1146
+ headers: { 'content-type': 'application/json' },
1147
+ body: JSON.stringify({
1148
+ oldPath: '/data/old.csv',
1149
+ newPath: '/data/x.sh'
1150
+ })
1151
+ }))
1152
+ // The extension rule is the drive's file-path rule, and a rename is a
1153
+ // file operation: sasjs/api (since 0.2.1, sasjs/api#12) applies the rule
1154
+ // to a rename whose target is a file, so both hosts refuse the move and
1155
+ // no entry can be created that the API itself cannot read back. A folder
1156
+ // is not subject to the file-path schema, so renaming a folder to a
1157
+ // dotted name stays allowed.
1158
+ ];
1159
+ case 2:
1160
+ toBlocked = _a.sent();
1161
+ // The extension rule is the drive's file-path rule, and a rename is a
1162
+ // file operation: sasjs/api (since 0.2.1, sasjs/api#12) applies the rule
1163
+ // to a rename whose target is a file, so both hosts refuse the move and
1164
+ // no entry can be created that the API itself cannot read back. A folder
1165
+ // is not subject to the file-path schema, so renaming a folder to a
1166
+ // dotted name stays allowed.
1167
+ expect(toBlocked.status).toBe(400);
1168
+ expect(toBlocked.body).toBe('Invalid file extension');
1169
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
1170
+ encodeURIComponent('/data/old.csv')))];
1171
+ case 3:
1172
+ read = _a.sent();
1173
+ expect(read.status).toBe(200);
1174
+ expect(read.body).toBe('r');
1175
+ return [2 /*return*/];
1176
+ }
1177
+ });
1178
+ }); });
1179
+ it('refuses a file where a folder already holds the path', function () { return __awaiter(void 0, void 0, void 0, function () {
1180
+ var dispatch, form, atFolder;
1181
+ return __generator(this, function (_a) {
1182
+ switch (_a.label) {
1183
+ case 0:
1184
+ dispatch = seeded().dispatch;
1185
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/folder', {
1186
+ method: 'POST',
1187
+ headers: { 'content-type': 'application/json' },
1188
+ body: JSON.stringify({ folderPath: '/data/reports' })
1189
+ })];
1190
+ case 1:
1191
+ _a.sent();
1192
+ form = new FormData();
1193
+ form.set('file', new Blob(['r']), 'r.csv');
1194
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
1195
+ encodeURIComponent('/data/reports'), { method: 'POST', body: form }))
1196
+ // the server fails this at the filesystem (EISDIR); naming it as a conflict
1197
+ // keeps the drive from holding one path as both a file and a folder
1198
+ ];
1199
+ case 2:
1200
+ atFolder = _a.sent();
1201
+ // the server fails this at the filesystem (EISDIR); naming it as a conflict
1202
+ // keeps the drive from holding one path as both a file and a folder
1203
+ expect(atFolder.status).toBe(409);
1204
+ return [2 /*return*/];
1205
+ }
1206
+ });
1207
+ }); });
1208
+ it('refuses a file under a path that is already a file', function () { return __awaiter(void 0, void 0, void 0, function () {
1209
+ var dispatch, form, child, under, stillAFile, _a;
1210
+ return __generator(this, function (_b) {
1211
+ switch (_b.label) {
1212
+ case 0:
1213
+ dispatch = seeded().dispatch;
1214
+ form = new FormData();
1215
+ form.set('file', new Blob(['a']), 'a.csv');
1216
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/a.csv'), { method: 'POST', body: form })];
1217
+ case 1:
1218
+ _b.sent();
1219
+ child = new FormData();
1220
+ child.set('file', new Blob(['b']), 'b.csv');
1221
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
1222
+ encodeURIComponent('/data/a.csv/child.csv'), { method: 'POST', body: child }))
1223
+ // mkdirp would otherwise turn the file into a folder to hold the child.
1224
+ // The server loses this at the filesystem (ENOTDIR) and answers a 500, so
1225
+ // this answers a 500 too: the refusal is the point, the status is the
1226
+ // server's class, and the message says which path was in the way.
1227
+ ];
1228
+ case 2:
1229
+ under = _b.sent();
1230
+ // mkdirp would otherwise turn the file into a folder to hold the child.
1231
+ // The server loses this at the filesystem (ENOTDIR) and answers a 500, so
1232
+ // this answers a 500 too: the refusal is the point, the status is the
1233
+ // server's class, and the message says which path was in the way.
1234
+ expect(under.status).toBe(500);
1235
+ expect(under.body).toEqual({
1236
+ message: 'ENOTDIR: an ancestor of the path is a file: /data/a.csv'
1237
+ });
1238
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/a.csv'))];
1239
+ case 3:
1240
+ stillAFile = _b.sent();
1241
+ _a = expect;
1242
+ return [4 /*yield*/, stillAFile.text()];
1243
+ case 4:
1244
+ _a.apply(void 0, [_b.sent()]).toBe('a');
1245
+ return [2 /*return*/];
1246
+ }
1247
+ });
1248
+ }); });
1249
+ it('serves a download with the type its extension implies', function () { return __awaiter(void 0, void 0, void 0, function () {
1250
+ var dispatch, form, csv, _a, sas, program;
1251
+ return __generator(this, function (_b) {
1252
+ switch (_b.label) {
1253
+ case 0:
1254
+ dispatch = seeded().dispatch;
1255
+ form = new FormData();
1256
+ form.set('file', new Blob(['id,name\n1,Ann\n']), 'users.csv');
1257
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
1258
+ encodeURIComponent('/data/users.csv'), { method: 'POST', body: form })];
1259
+ case 1:
1260
+ _b.sent();
1261
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' +
1262
+ encodeURIComponent('/data/users.csv'))
1263
+ // an upload is stored as bytes, as multer leaves it on the server; the
1264
+ // extension is what makes it answer text/csv rather than octet-stream
1265
+ ];
1266
+ case 2:
1267
+ csv = _b.sent();
1268
+ // an upload is stored as bytes, as multer leaves it on the server; the
1269
+ // extension is what makes it answer text/csv rather than octet-stream
1270
+ expect(csv.headers.get('content-type')).toBe('text/csv');
1271
+ _a = expect;
1272
+ return [4 /*yield*/, csv.text()];
1273
+ case 3:
1274
+ _a.apply(void 0, [_b.sent()]).toBe('id,name\n1,Ann\n');
1275
+ sas = new FormData();
1276
+ sas.set('file', new Blob(['data _null_; run;']), 'p.sas');
1277
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/p.sas'), { method: 'POST', body: sas })];
1278
+ case 4:
1279
+ _b.sent();
1280
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/p.sas'))];
1281
+ case 5:
1282
+ program = _b.sent();
1283
+ expect(program.headers.get('content-type')).toBe('text/plain');
1284
+ return [2 /*return*/];
1285
+ }
1286
+ });
1287
+ }); });
1288
+ it('refuses an upload larger than the host allows, before reading it', function () { return __awaiter(void 0, void 0, void 0, function () {
1289
+ var dispatch, runtime, form, response;
1290
+ return __generator(this, function (_a) {
1291
+ switch (_a.label) {
1292
+ case 0:
1293
+ dispatch = seeded().dispatch;
1294
+ runtime = loadWorker().runtime;
1295
+ form = new FormData();
1296
+ form.set('file', new Blob([new Uint8Array(runtime.MAX_UPLOAD_BYTES + 1)]), 'big.bin');
1297
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/file?_filePath=' +
1298
+ encodeURIComponent('/data/big.bin'), { method: 'POST', body: form }))];
1299
+ case 1:
1300
+ response = _a.sent();
1301
+ expect(response.status).toBe(413);
1302
+ expect(response.body).toContain('larger than the browser host allows');
1303
+ return [2 /*return*/];
1304
+ }
1305
+ });
1306
+ }); });
1307
+ it('renames a file through the route the API documents', function () { return __awaiter(void 0, void 0, void 0, function () {
1308
+ var dispatch, form, renamed, read, _a;
1309
+ return __generator(this, function (_b) {
1310
+ switch (_b.label) {
1311
+ case 0:
1312
+ dispatch = seeded().dispatch;
1313
+ form = new FormData();
1314
+ form.set('file', new Blob(['r']), 'r.csv');
1315
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/old.csv'), { method: 'POST', body: form })];
1316
+ case 1:
1317
+ _b.sent();
1318
+ return [4 /*yield*/, answer(dispatch('/SASjsApi/drive/rename', {
1319
+ method: 'POST',
1320
+ headers: { 'content-type': 'application/json' },
1321
+ body: JSON.stringify({
1322
+ oldPath: '/data/old.csv',
1323
+ newPath: '/data/new.csv'
1324
+ })
1325
+ }))];
1326
+ case 2:
1327
+ renamed = _b.sent();
1328
+ expect(renamed.body).toEqual({ status: 'success' });
1329
+ return [4 /*yield*/, dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent('/data/new.csv'))];
1330
+ case 3:
1331
+ read = _b.sent();
1332
+ _a = expect;
1333
+ return [4 /*yield*/, read.text()];
1334
+ case 4:
1335
+ _a.apply(void 0, [_b.sent()]).toBe('r');
1336
+ return [2 /*return*/];
1337
+ }
1338
+ });
1339
+ }); });
1340
+ });
1341
+ describe('conformance corpus', function () {
1342
+ var e_1, _a;
1343
+ /**
1344
+ * The corpus is what "the same API" means: the cases @sasjs/api runs against
1345
+ * its own services, and against a live sasjs/server, run here against this
1346
+ * worker's HTTP surface. A case that answers differently is this host
1347
+ * drifting from the server - the failure mode the hand-written drive routes
1348
+ * used to have - and it is a test rather than a review.
1349
+ */
1350
+ var httpFor = function (testCase) {
1351
+ var e_2, _a;
1352
+ var _b;
1353
+ var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
1354
+ var query = new URLSearchParams();
1355
+ try {
1356
+ for (var _d = __values(Object.entries(testCase.request.query || {})), _e = _d.next(); !_e.done; _e = _d.next()) {
1357
+ var _f = __read(_e.value, 2), key = _f[0], value = _f[1];
1358
+ if (value !== undefined)
1359
+ query.set(key, String(value));
1360
+ }
1361
+ }
1362
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
1363
+ finally {
1364
+ try {
1365
+ if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
1366
+ }
1367
+ finally { if (e_2) throw e_2.error; }
1368
+ }
1369
+ var suffix = query.toString() ? "?".concat(query) : '';
1370
+ var init = { method: method };
1371
+ var file = testCase.request.file;
1372
+ if (file) {
1373
+ var form = new FormData();
1374
+ // the Blob types want a view over an ArrayBuffer; a Uint8Array from the
1375
+ // corpus is typed over ArrayBufferLike, which a Blob does not accept
1376
+ var bytes = typeof file.contents === 'string'
1377
+ ? new TextEncoder().encode(file.contents)
1378
+ : new Uint8Array(file.contents);
1379
+ form.set('file', new Blob([bytes]), file.name);
1380
+ var formPath = (_b = testCase.request.body) === null || _b === void 0 ? void 0 : _b.filePath;
1381
+ if (formPath !== undefined)
1382
+ form.set('filePath', String(formPath));
1383
+ init.body = form;
1384
+ }
1385
+ else if (testCase.request.body) {
1386
+ init.body = JSON.stringify(testCase.request.body);
1387
+ init.headers = { 'content-type': 'application/json' };
1388
+ }
1389
+ return { path: routePath + suffix, init: init };
1390
+ };
1391
+ /**
1392
+ * The case's own files, at the paths the drive stores them under. `load`
1393
+ * restores the seed snapshot, which is a map of strings - a byte entry is a
1394
+ * snapshot of an upload, so it goes in through the store the way an upload
1395
+ * does.
1396
+ */
1397
+ var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
1398
+ var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_3_1;
1399
+ var e_4, _g, e_3, _h;
1400
+ return __generator(this, function (_j) {
1401
+ switch (_j.label) {
1402
+ case 0:
1403
+ text = {};
1404
+ try {
1405
+ for (_a = __values(Object.entries(testCase.seed || {})), _b = _a.next(); !_b.done; _b = _a.next()) {
1406
+ _c = __read(_b.value, 2), file = _c[0], contents = _c[1];
1407
+ if (typeof contents === 'string')
1408
+ text["/sasjs/drive/files".concat(file)] = contents;
1409
+ }
1410
+ }
1411
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
1412
+ finally {
1413
+ try {
1414
+ if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
1415
+ }
1416
+ finally { if (e_4) throw e_4.error; }
1417
+ }
1418
+ return [4 /*yield*/, runtime.load(text)];
1419
+ case 1:
1420
+ _j.sent();
1421
+ _j.label = 2;
1422
+ case 2:
1423
+ _j.trys.push([2, 7, 8, 9]);
1424
+ _d = __values(Object.entries(testCase.seed || {})), _e = _d.next();
1425
+ _j.label = 3;
1426
+ case 3:
1427
+ if (!!_e.done) return [3 /*break*/, 6];
1428
+ _f = __read(_e.value, 2), file = _f[0], contents = _f[1];
1429
+ if (!(typeof contents !== 'string')) return [3 /*break*/, 5];
1430
+ return [4 /*yield*/, runtime.driveStore.write(file, contents)];
1431
+ case 4:
1432
+ _j.sent();
1433
+ _j.label = 5;
1434
+ case 5:
1435
+ _e = _d.next();
1436
+ return [3 /*break*/, 3];
1437
+ case 6: return [3 /*break*/, 9];
1438
+ case 7:
1439
+ e_3_1 = _j.sent();
1440
+ e_3 = { error: e_3_1 };
1441
+ return [3 /*break*/, 9];
1442
+ case 8:
1443
+ try {
1444
+ if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
1445
+ }
1446
+ finally { if (e_3) throw e_3.error; }
1447
+ return [7 /*endfinally*/];
1448
+ case 9: return [2 /*return*/];
1449
+ }
1450
+ });
1451
+ }); };
1452
+ var _loop_1 = function (testCase) {
1453
+ it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
1454
+ var worker, _a, requestPath, init, response, _b, want, _c, _d, _e;
1455
+ return __generator(this, function (_f) {
1456
+ switch (_f.label) {
1457
+ case 0:
1458
+ worker = loadWorker();
1459
+ return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
1460
+ case 1:
1461
+ _f.sent();
1462
+ _a = httpFor(testCase), requestPath = _a.path, init = _a.init;
1463
+ return [4 /*yield*/, worker.dispatch(requestPath, init)];
1464
+ case 2:
1465
+ response = _f.sent();
1466
+ expect(response.status).toBe(testCase.expect.status);
1467
+ if (!(testCase.expect.error !== undefined)) return [3 /*break*/, 4];
1468
+ _b = expect;
1469
+ return [4 /*yield*/, response.text()];
1470
+ case 3:
1471
+ _b.apply(void 0, [_f.sent()]).toBe(testCase.expect.error);
1472
+ _f.label = 4;
1473
+ case 4:
1474
+ want = testCase.expect.contents;
1475
+ if (!(typeof want === 'string')) return [3 /*break*/, 6];
1476
+ _c = expect;
1477
+ return [4 /*yield*/, response.text()];
1478
+ case 5:
1479
+ _c.apply(void 0, [_f.sent()]).toBe(want);
1480
+ return [3 /*break*/, 8];
1481
+ case 6:
1482
+ if (!(want !== undefined)) return [3 /*break*/, 8];
1483
+ _d = expect;
1484
+ _e = Uint8Array.bind;
1485
+ return [4 /*yield*/, response.arrayBuffer()];
1486
+ case 7:
1487
+ _d.apply(void 0, [new (_e.apply(Uint8Array, [void 0, _f.sent()]))()]).toEqual(want);
1488
+ _f.label = 8;
1489
+ case 8: return [2 /*return*/];
1490
+ }
1491
+ });
1492
+ }); });
1493
+ };
1494
+ try {
1495
+ for (var DRIVE_CORPUS_1 = __values(api_1.DRIVE_CORPUS), DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next(); !DRIVE_CORPUS_1_1.done; DRIVE_CORPUS_1_1 = DRIVE_CORPUS_1.next()) {
1496
+ var testCase = DRIVE_CORPUS_1_1.value;
1497
+ _loop_1(testCase);
1498
+ }
1499
+ }
1500
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
1501
+ finally {
1502
+ try {
1503
+ if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
1504
+ }
1505
+ finally { if (e_1) throw e_1.error; }
1506
+ }
1507
+ });
802
1508
  });