@sasjs/cli 6.11.0 → 6.12.1

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,12 @@ 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 __importDefault = (this && this.__importDefault) || function (mod) {
39
+ return (mod && mod.__esModule) ? mod : { "default": mod };
40
+ };
38
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ var fs_1 = __importDefault(require("fs"));
43
+ var path_1 = __importDefault(require("path"));
39
44
  var utils_1 = require("@sasjs/utils");
40
45
  var mockSite_1 = require("../mockSite");
41
46
  /**
@@ -249,6 +254,83 @@ describe('browser runtime', function () {
249
254
  expect(result.log).toContain('boom');
250
255
  expect(result.webout).toBe('');
251
256
  });
257
+ /**
258
+ * The shims a service reaches for are node's, refusals included: a mock that
259
+ * answered a copy from a missing file with an empty file, or accepted
260
+ * `createHash('sha256')`, would let a service pass here and fail on the server.
261
+ */
262
+ it("refuses what the fs, path and crypto shims refuse, with node's words", function () {
263
+ var runtime = loadRuntime();
264
+ var failure = function (code) { return runtime.run({ code: code }).error; };
265
+ expect(failure("fs.copyFileSync('/missing.csv', '/x.csv')")).toMatch(/ENOENT: no such file, copy/);
266
+ expect(failure("fs.statSync('/missing.csv')")).toMatch(/ENOENT: no such file or directory, stat/);
267
+ expect(failure("fs.readFileSync('/missing.csv')")).toMatch(/ENOENT: no such file or directory, open/);
268
+ expect(failure("fs.writeFileSync('/x.csv', 'x', { flag: 'r' })")).toMatch(/Unsupported flag 'r' in the browser host/);
269
+ expect(failure("require('crypto').createHash('sha256')")).toMatch(/Unsupported hash in browser host: sha256/);
270
+ expect(failure("require('net')")).toMatch(/Cannot find module 'net' in the browser runtime host/);
271
+ });
272
+ it('answers the path and Buffer helpers a service uses', function () {
273
+ var runtime = loadRuntime();
274
+ var result = runtime.run({
275
+ code: [
276
+ "const path = require('path')",
277
+ '_webout = JSON.stringify({',
278
+ " ext: path.extname('/data/a.csv'),",
279
+ " none: path.extname('csv'),",
280
+ " base: path.basename('/data/a.csv'),",
281
+ " dir: path.dirname('/data/a.csv')",
282
+ '})'
283
+ ].join('\n')
284
+ });
285
+ expect(JSON.parse(result.webout)).toEqual({
286
+ ext: '.csv',
287
+ none: '',
288
+ base: 'a.csv',
289
+ dir: '/data'
290
+ });
291
+ });
292
+ it('hashes with the md5 the worker imports, as node does', function () {
293
+ var runtime = loadRuntime();
294
+ // md5.js declares its hash in the global scope of the worker that imports
295
+ // it; a jest module would hide it, so it is evaluated the way the worker
296
+ // does - which is also what makes this the real md5 rather than a stub
297
+ var source = fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'assets', 'md5.js'), 'utf8');
298
+ global.MD5 = new Function("".concat(source, "\nreturn MD5"))();
299
+ var result = runtime.run({
300
+ code: "_webout = require('crypto').createHash('md5').update('abc').digest('hex')"
301
+ });
302
+ // md5('abc'), the digest node gives for the same call
303
+ expect(result.webout).toBe('900150983cd24fb0d6963f7d28e17f72');
304
+ });
305
+ it('reports a write that fails through the callback node hands it', function () {
306
+ var runtime = loadRuntime();
307
+ var result = runtime.run({
308
+ code: [
309
+ 'let reported = null',
310
+ "fs.writeFile('/x.csv', 'x', { flag: 'r' }, (err) => { reported = err && err.message })",
311
+ '_webout = String(reported)'
312
+ ].join('\n')
313
+ });
314
+ expect(result.webout).toMatch(/Unsupported flag 'r' in the browser host/);
315
+ });
316
+ it('refuses to boot a worker that has neither a seed nor a snapshot', function () { return __awaiter(void 0, void 0, void 0, function () {
317
+ var runtime, info;
318
+ return __generator(this, function (_a) {
319
+ switch (_a.label) {
320
+ case 0:
321
+ runtime = loadRuntime();
322
+ global.fetch = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
323
+ return [2 /*return*/, ({ ok: false })];
324
+ }); }); });
325
+ return [4 /*yield*/, runtime.seed()];
326
+ case 1:
327
+ info = _a.sent();
328
+ expect(info.booted).toBe(false);
329
+ expect(info.error).toMatch(/No drive to load/);
330
+ return [2 /*return*/];
331
+ }
332
+ });
333
+ }); });
252
334
  it('collects service console output into the log', function () {
253
335
  var runtime = loadRuntime();
254
336
  var result = runtime.run({
@@ -847,24 +929,37 @@ describe('browser runtime', function () {
847
929
  });
848
930
  expect(result.webout).toBe('AQID');
849
931
  });
850
- it('lists folders and builds the drive tree', function () { return __awaiter(void 0, void 0, void 0, function () {
851
- var runtime, listing, tree;
852
- var _a;
853
- return __generator(this, function (_b) {
854
- switch (_b.label) {
932
+ it('hands the API a store over the drive it seeded', function () { return __awaiter(void 0, void 0, void 0, function () {
933
+ var runtime, listing, _a, _b;
934
+ var _c;
935
+ return __generator(this, function (_d) {
936
+ switch (_d.label) {
855
937
  case 0:
856
938
  runtime = loadRuntime();
857
- return [4 /*yield*/, seedDrive(runtime, (_a = {},
858
- _a[drivePath('services/public/getdata.js')] = '_webout = "x";',
859
- _a[drivePath('data/a.txt')] = 'a',
860
- _a))];
939
+ return [4 /*yield*/, seedDrive(runtime, (_c = {},
940
+ _c[drivePath('services/public/getdata.js')] = '_webout = "x";',
941
+ _c[drivePath('data/a.txt')] = 'a',
942
+ _c))
943
+ // drive-relative paths, the form every @sasjs/api service speaks: the app
944
+ // loc is part of the path, the host's own root is not
945
+ ];
861
946
  case 1:
862
- _b.sent();
863
- listing = runtime.list("".concat(appLoc, "/services/public"));
947
+ _d.sent();
948
+ return [4 /*yield*/, runtime.driveStore.list(appLoc + '/services/public')];
949
+ case 2:
950
+ listing = _d.sent();
864
951
  expect(listing.files).toEqual(['getdata.js']);
865
- tree = runtime.tree();
866
- expect(JSON.stringify(tree)).toContain('getdata.js');
867
- expect(runtime.read("".concat(appLoc, "/data/a.txt")).contents).toBe('a');
952
+ _a = expect;
953
+ return [4 /*yield*/, runtime.driveStore.read(appLoc + '/data/a.txt')];
954
+ case 3:
955
+ _a.apply(void 0, [_d.sent()]).toBe('a');
956
+ _b = expect;
957
+ return [4 /*yield*/, runtime.driveStore.stat(appLoc + '/data')];
958
+ case 4:
959
+ _b.apply(void 0, [_d.sent()]).toEqual({
960
+ directory: true,
961
+ size: 0
962
+ });
868
963
  return [2 /*return*/];
869
964
  }
870
965
  });
@@ -949,9 +1044,11 @@ describe('browser runtime', function () {
949
1044
  var runtime = loadRuntime();
950
1045
  expect(function () { return runtime.run({ program: 'services/public/missing' }); }).toThrow(/Program not found in drive/);
951
1046
  });
952
- describe('drive writes', function () {
953
- // the write API speaks drive-relative paths, and the seed holds absolute
954
- // ones, so both sides go through the same root here
1047
+ describe('drive store', function () {
1048
+ // the store the API is handed: drive-relative paths in, the drive's own
1049
+ // absolute keys behind. What a status or a message should be is the
1050
+ // package's business (spec/sw.spec.ts runs its corpus through this worker),
1051
+ // so these cases are the storage contract only.
955
1052
  var root = '/sasjs/drive/files';
956
1053
  var seededRuntime = function () { return __awaiter(void 0, void 0, void 0, function () {
957
1054
  var runtime;
@@ -975,220 +1072,344 @@ describe('browser runtime', function () {
975
1072
  }
976
1073
  });
977
1074
  }); };
978
- var status = function (fn) {
979
- try {
980
- fn();
981
- }
982
- catch (e) {
983
- return e.status;
984
- }
985
- return 200;
986
- };
987
- it('creates a file, and the page is told to persist it', function () { return __awaiter(void 0, void 0, void 0, function () {
988
- var runtime;
989
- var _a;
990
- return __generator(this, function (_b) {
991
- switch (_b.label) {
1075
+ it('reads text as text and hands back the entry it holds', function () { return __awaiter(void 0, void 0, void 0, function () {
1076
+ var runtime, store, _a, _b, _c, _d;
1077
+ return __generator(this, function (_e) {
1078
+ switch (_e.label) {
992
1079
  case 0: return [4 /*yield*/, seededRuntime()];
993
1080
  case 1:
994
- runtime = _b.sent();
995
- expect(runtime.save('/data/new/deep/up.csv', 'x,y\n')).toEqual({
996
- status: 'success'
997
- });
998
- // the write is what the page receives after the request: a worker is
999
- // terminated between requests, so without this the upload is gone
1000
- expect(runtime.takeDirty()).toEqual((_a = {},
1001
- _a["".concat(root, "/data/new/deep/up.csv")] = 'x,y\n',
1002
- _a));
1003
- expect(runtime.read('/data/new/deep/up.csv')).toEqual({
1004
- contents: 'x,y\n',
1005
- binary: false
1081
+ runtime = _e.sent();
1082
+ store = runtime.driveStore;
1083
+ _a = expect;
1084
+ return [4 /*yield*/, store.exists('/data/keep.csv')];
1085
+ case 2:
1086
+ _a.apply(void 0, [_e.sent()]).toBe(true);
1087
+ _b = expect;
1088
+ return [4 /*yield*/, store.exists('/data/missing.csv')];
1089
+ case 3:
1090
+ _b.apply(void 0, [_e.sent()]).toBe(false);
1091
+ _c = expect;
1092
+ return [4 /*yield*/, store.read('/data/keep.csv')];
1093
+ case 4:
1094
+ _c.apply(void 0, [_e.sent()]).toBe('a,b\n1,2\n');
1095
+ _d = expect;
1096
+ return [4 /*yield*/, store.stat('/data/keep.csv')];
1097
+ case 5:
1098
+ _d.apply(void 0, [_e.sent()]).toEqual({
1099
+ directory: false,
1100
+ size: 8
1006
1101
  });
1007
1102
  return [2 /*return*/];
1008
1103
  }
1009
1104
  });
1010
1105
  }); });
1011
- it('stores an upload as bytes, the way the server stores one', function () { return __awaiter(void 0, void 0, void 0, function () {
1012
- var runtime;
1013
- return __generator(this, function (_a) {
1014
- switch (_a.label) {
1106
+ it('keeps an entry written as bytes as bytes, and measures it in bytes', function () { return __awaiter(void 0, void 0, void 0, function () {
1107
+ var runtime, store, _a, _b;
1108
+ return __generator(this, function (_c) {
1109
+ switch (_c.label) {
1015
1110
  case 0: return [4 /*yield*/, seededRuntime()];
1016
1111
  case 1:
1017
- runtime = _a.sent();
1018
- runtime.save('/data/pack.zip', Uint8Array.from([80, 75, 3, 4]));
1019
- expect(runtime.read('/data/pack.zip')).toEqual({
1020
- contents: 'UEsDBA==',
1021
- binary: true
1112
+ runtime = _c.sent();
1113
+ store = runtime.driveStore;
1114
+ return [4 /*yield*/, store.write('/data/pack.zip', Uint8Array.from([80, 75, 3, 4]))];
1115
+ case 2:
1116
+ _c.sent();
1117
+ _a = expect;
1118
+ return [4 /*yield*/, store.read('/data/pack.zip')];
1119
+ case 3:
1120
+ _a.apply(void 0, [_c.sent()]).toEqual(Uint8Array.from([80, 75, 3, 4]));
1121
+ _b = expect;
1122
+ return [4 /*yield*/, store.stat('/data/pack.zip')];
1123
+ case 4:
1124
+ _b.apply(void 0, [_c.sent()]).toEqual({
1125
+ directory: false,
1126
+ size: 4
1022
1127
  });
1023
- // and a service reading it gets a Buffer, as it does on the server
1024
- expect(runtime.run({
1025
- code: "_webout = typeof require('fs').readFileSync('".concat(root, "/data/pack.zip');")
1026
- }).webout).toBe('object');
1027
- expect(runtime.run({
1028
- code: "_webout = require('fs').readFileSync('".concat(root, "/data/pack.zip', 'base64');")
1029
- }).webout).toBe('UEsDBA==');
1030
1128
  return [2 /*return*/];
1031
1129
  }
1032
1130
  });
1033
1131
  }); });
1034
- it('refuses to create a file over one that exists', function () { return __awaiter(void 0, void 0, void 0, function () {
1132
+ it('reports a write to the page, so the entry is persisted', function () { return __awaiter(void 0, void 0, void 0, function () {
1035
1133
  var runtime;
1036
1134
  return __generator(this, function (_a) {
1037
1135
  switch (_a.label) {
1038
1136
  case 0: return [4 /*yield*/, seededRuntime()];
1039
1137
  case 1:
1040
1138
  runtime = _a.sent();
1041
- expect(status(function () { return runtime.save('/data/keep.csv', 'x'); })).toBe(409);
1139
+ return [4 /*yield*/, runtime.driveStore.write('/data/new/deep/up.csv', 'x,y')];
1140
+ case 2:
1141
+ _a.sent();
1142
+ expect(Object.keys(runtime.takeDirty())).toEqual([
1143
+ "".concat(root, "/data/new/deep/up.csv")
1144
+ ]);
1145
+ expect(runtime.dumpAll()["".concat(root, "/data/new/deep/up.csv")]).toBe('x,y');
1042
1146
  return [2 /*return*/];
1043
1147
  }
1044
1148
  });
1045
1149
  }); });
1046
- it('replaces a file, and refuses one that is not there', function () { return __awaiter(void 0, void 0, void 0, function () {
1047
- var runtime;
1048
- return __generator(this, function (_a) {
1049
- switch (_a.label) {
1150
+ it('reports a deletion, so the page drops the entry', function () { return __awaiter(void 0, void 0, void 0, function () {
1151
+ var runtime, _a;
1152
+ var _b;
1153
+ return __generator(this, function (_c) {
1154
+ switch (_c.label) {
1050
1155
  case 0: return [4 /*yield*/, seededRuntime()];
1051
1156
  case 1:
1052
- runtime = _a.sent();
1053
- expect(runtime.update('/data/keep.csv', 'replaced')).toEqual({
1054
- status: 'success'
1055
- });
1056
- expect(runtime.read('/data/keep.csv').contents).toBe('replaced');
1057
- expect(status(function () { return runtime.update('/data/missing.csv', 'x'); })).toBe(404);
1157
+ runtime = _c.sent();
1158
+ return [4 /*yield*/, runtime.driveStore.remove('/data/keep.csv')];
1159
+ case 2:
1160
+ _c.sent();
1161
+ expect(runtime.takeDirty()).toEqual((_b = {}, _b["".concat(root, "/data/keep.csv")] = null, _b));
1162
+ _a = expect;
1163
+ return [4 /*yield*/, runtime.driveStore.exists('/data/keep.csv')];
1164
+ case 3:
1165
+ _a.apply(void 0, [_c.sent()]).toBe(false);
1058
1166
  return [2 /*return*/];
1059
1167
  }
1060
1168
  });
1061
1169
  }); });
1062
- it('deletes a file, and reports a deletion to the page', function () { return __awaiter(void 0, void 0, void 0, function () {
1063
- var runtime;
1064
- var _a;
1065
- return __generator(this, function (_b) {
1066
- switch (_b.label) {
1170
+ it('creates a folder and lists the immediate children of a folder', function () { return __awaiter(void 0, void 0, void 0, function () {
1171
+ var runtime, store, _a, _b;
1172
+ return __generator(this, function (_c) {
1173
+ switch (_c.label) {
1067
1174
  case 0: return [4 /*yield*/, seededRuntime()];
1068
1175
  case 1:
1069
- runtime = _b.sent();
1070
- expect(runtime.remove('/data/keep.csv')).toEqual({ status: 'success' });
1071
- expect(runtime.takeDirty()).toEqual((_a = {},
1072
- _a["".concat(root, "/data/keep.csv")] = null,
1073
- _a));
1074
- expect(status(function () { return runtime.read('/data/keep.csv'); })).toBe(404);
1075
- expect(status(function () { return runtime.remove('/data/keep.csv'); })).toBe(404);
1176
+ runtime = _c.sent();
1177
+ store = runtime.driveStore;
1178
+ return [4 /*yield*/, store.mkdirp('/data/reports/nested')];
1179
+ case 2:
1180
+ _c.sent();
1181
+ return [4 /*yield*/, store.write('/data/reports/a.csv', 'a')];
1182
+ case 3:
1183
+ _c.sent();
1184
+ _a = expect;
1185
+ return [4 /*yield*/, store.list('/data')];
1186
+ case 4:
1187
+ _a.apply(void 0, [_c.sent()]).toEqual({
1188
+ files: ['keep.csv'],
1189
+ folders: ['reports']
1190
+ });
1191
+ _b = expect;
1192
+ return [4 /*yield*/, store.list('/data/reports')];
1193
+ case 5:
1194
+ _b.apply(void 0, [_c.sent()]).toEqual({
1195
+ files: ['a.csv'],
1196
+ folders: ['nested']
1197
+ });
1076
1198
  return [2 /*return*/];
1077
1199
  }
1078
1200
  });
1079
1201
  }); });
1080
- it('creates a folder, and refuses one that already exists', function () { return __awaiter(void 0, void 0, void 0, function () {
1081
- var runtime;
1082
- return __generator(this, function (_a) {
1083
- switch (_a.label) {
1202
+ it('removes a folder with everything under it, and reports each entry', function () { return __awaiter(void 0, void 0, void 0, function () {
1203
+ var runtime, store, _a, _b;
1204
+ var _c;
1205
+ return __generator(this, function (_d) {
1206
+ switch (_d.label) {
1084
1207
  case 0: return [4 /*yield*/, seededRuntime()];
1085
1208
  case 1:
1086
- runtime = _a.sent();
1087
- expect(runtime.mkdir('/data/reports')).toEqual({ status: 'success' });
1088
- expect(runtime.list('/data').folders).toContain('reports');
1089
- expect(status(function () { return runtime.mkdir('/data/reports'); })).toBe(409);
1209
+ runtime = _d.sent();
1210
+ store = runtime.driveStore;
1211
+ return [4 /*yield*/, store.mkdirp('/data/reports')];
1212
+ case 2:
1213
+ _d.sent();
1214
+ return [4 /*yield*/, store.write('/data/reports/a.csv', 'a')];
1215
+ case 3:
1216
+ _d.sent();
1217
+ return [4 /*yield*/, store.write('/data/reports/nested/b.csv', 'b')];
1218
+ case 4:
1219
+ _d.sent();
1220
+ runtime.takeDirty();
1221
+ return [4 /*yield*/, store.remove('/data/reports')];
1222
+ case 5:
1223
+ _d.sent();
1224
+ _a = expect;
1225
+ return [4 /*yield*/, store.exists('/data/reports')];
1226
+ case 6:
1227
+ _a.apply(void 0, [_d.sent()]).toBe(false);
1228
+ _b = expect;
1229
+ return [4 /*yield*/, store.list('/data')];
1230
+ case 7:
1231
+ _b.apply(void 0, [_d.sent()]).toEqual({
1232
+ files: ['keep.csv'],
1233
+ folders: []
1234
+ });
1235
+ // every file the removal took goes to the page, or IndexedDB keeps it
1236
+ expect(runtime.takeDirty()).toEqual((_c = {},
1237
+ _c["".concat(root, "/data/reports/a.csv")] = null,
1238
+ _c["".concat(root, "/data/reports/nested/b.csv")] = null,
1239
+ _c));
1090
1240
  return [2 /*return*/];
1091
1241
  }
1092
1242
  });
1093
1243
  }); });
1094
- it('deletes a folder with everything under it', function () { return __awaiter(void 0, void 0, void 0, function () {
1095
- var runtime, cleared;
1096
- return __generator(this, function (_a) {
1097
- switch (_a.label) {
1244
+ it('moves a file, and marks both ends', function () { return __awaiter(void 0, void 0, void 0, function () {
1245
+ var runtime, _a, _b;
1246
+ var _c;
1247
+ return __generator(this, function (_d) {
1248
+ switch (_d.label) {
1098
1249
  case 0: return [4 /*yield*/, seededRuntime()];
1099
1250
  case 1:
1100
- runtime = _a.sent();
1101
- runtime.mkdir('/data/reports');
1102
- runtime.save('/data/reports/a.csv', 'a');
1103
- runtime.save('/data/reports/nested/b.csv', 'b');
1104
- expect(runtime.rmdir('/data/reports')).toEqual({ status: 'success' });
1105
- expect(runtime.list('/data').folders).not.toContain('reports');
1106
- expect(status(function () { return runtime.read('/data/reports/a.csv'); })).toBe(404);
1107
- expect(status(function () { return runtime.read('/data/reports/nested/b.csv'); })).toBe(404);
1108
- cleared = runtime.takeDirty();
1109
- expect(cleared["".concat(root, "/data/reports/a.csv")]).toBeNull();
1110
- expect(cleared["".concat(root, "/data/reports/nested/b.csv")]).toBeNull();
1251
+ runtime = _d.sent();
1252
+ return [4 /*yield*/, runtime.driveStore.move('/data/keep.csv', '/data/kept.csv')];
1253
+ case 2:
1254
+ _d.sent();
1255
+ _a = expect;
1256
+ return [4 /*yield*/, runtime.driveStore.read('/data/kept.csv')];
1257
+ case 3:
1258
+ _a.apply(void 0, [_d.sent()]).toBe('a,b\n1,2\n');
1259
+ _b = expect;
1260
+ return [4 /*yield*/, runtime.driveStore.exists('/data/keep.csv')];
1261
+ case 4:
1262
+ _b.apply(void 0, [_d.sent()]).toBe(false);
1263
+ expect(runtime.takeDirty()).toEqual((_c = {},
1264
+ _c["".concat(root, "/data/kept.csv")] = 'a,b\n1,2\n',
1265
+ _c["".concat(root, "/data/keep.csv")] = null,
1266
+ _c));
1111
1267
  return [2 /*return*/];
1112
1268
  }
1113
1269
  });
1114
1270
  }); });
1115
- it('renames a file, and moves a folder with its contents', function () { return __awaiter(void 0, void 0, void 0, function () {
1116
- var runtime;
1117
- return __generator(this, function (_a) {
1118
- switch (_a.label) {
1271
+ it('moves a folder with its contents, bytes and all', function () { return __awaiter(void 0, void 0, void 0, function () {
1272
+ var runtime, store, _a, _b, _c;
1273
+ return __generator(this, function (_d) {
1274
+ switch (_d.label) {
1119
1275
  case 0: return [4 /*yield*/, seededRuntime()];
1120
1276
  case 1:
1121
- runtime = _a.sent();
1122
- runtime.mkdir('/data/reports');
1123
- runtime.save('/data/reports/a.csv', 'a');
1124
- expect(runtime.rename('/data/keep.csv', '/data/kept.csv')).toEqual({
1125
- status: 'success'
1277
+ runtime = _d.sent();
1278
+ store = runtime.driveStore;
1279
+ return [4 /*yield*/, store.mkdirp('/data/reports')];
1280
+ case 2:
1281
+ _d.sent();
1282
+ return [4 /*yield*/, store.write('/data/reports/a.csv', 'a')];
1283
+ case 3:
1284
+ _d.sent();
1285
+ return [4 /*yield*/, store.write('/data/reports/nested/pack.zip', Uint8Array.from([1, 2, 3]))];
1286
+ case 4:
1287
+ _d.sent();
1288
+ return [4 /*yield*/, store.move('/data/reports', '/archive')];
1289
+ case 5:
1290
+ _d.sent();
1291
+ _a = expect;
1292
+ return [4 /*yield*/, store.list('/archive')];
1293
+ case 6:
1294
+ _a.apply(void 0, [_d.sent()]).toEqual({
1295
+ files: ['a.csv'],
1296
+ folders: ['nested']
1126
1297
  });
1127
- expect(runtime.read('/data/kept.csv').contents).toBe('a,b\n1,2\n');
1128
- expect(status(function () { return runtime.read('/data/keep.csv'); })).toBe(404);
1129
- expect(runtime.rename('/data/reports', '/archive')).toEqual({
1130
- status: 'success'
1131
- });
1132
- expect(runtime.read('/archive/a.csv').contents).toBe('a');
1133
- expect(status(function () { return runtime.read('/data/reports/a.csv'); })).toBe(404);
1134
- expect(runtime.list('/archive').files).toEqual(['a.csv']);
1298
+ _b = expect;
1299
+ return [4 /*yield*/, store.read('/archive/nested/pack.zip')];
1300
+ case 7:
1301
+ _b.apply(void 0, [_d.sent()]).toEqual(Uint8Array.from([1, 2, 3]));
1302
+ _c = expect;
1303
+ return [4 /*yield*/, store.exists('/data/reports')];
1304
+ case 8:
1305
+ _c.apply(void 0, [_d.sent()]).toBe(false);
1135
1306
  return [2 /*return*/];
1136
1307
  }
1137
1308
  });
1138
1309
  }); });
1139
- it('refuses a rename onto something that exists', function () { return __awaiter(void 0, void 0, void 0, function () {
1140
- var runtime;
1310
+ it('refuses a write under a path that is a file, rather than writing through it', function () { return __awaiter(void 0, void 0, void 0, function () {
1311
+ var runtime, _a;
1312
+ return __generator(this, function (_b) {
1313
+ switch (_b.label) {
1314
+ case 0: return [4 /*yield*/, seededRuntime()
1315
+ // mkdirp would otherwise turn the file into a folder to hold the child
1316
+ ];
1317
+ case 1:
1318
+ runtime = _b.sent();
1319
+ // mkdirp would otherwise turn the file into a folder to hold the child
1320
+ return [4 /*yield*/, expect(runtime.driveStore.write('/data/keep.csv/child.csv', 'b')).rejects.toThrow(/an ancestor of the path is a file/)];
1321
+ case 2:
1322
+ // mkdirp would otherwise turn the file into a folder to hold the child
1323
+ _b.sent();
1324
+ _a = expect;
1325
+ return [4 /*yield*/, runtime.driveStore.read('/data/keep.csv')];
1326
+ case 3:
1327
+ _a.apply(void 0, [_b.sent()]).toBe('a,b\n1,2\n');
1328
+ return [2 /*return*/];
1329
+ }
1330
+ });
1331
+ }); });
1332
+ it("refuses a path it does not hold, in the drive's own words", function () { return __awaiter(void 0, void 0, void 0, function () {
1333
+ var runtime, store;
1141
1334
  return __generator(this, function (_a) {
1142
1335
  switch (_a.label) {
1143
1336
  case 0: return [4 /*yield*/, seededRuntime()];
1144
1337
  case 1:
1145
1338
  runtime = _a.sent();
1146
- runtime.save('/data/other.csv', 'o');
1147
- expect(status(function () { return runtime.rename('/data/keep.csv', '/data/other.csv'); })).toBe(409);
1148
- expect(status(function () { return runtime.rename('/data/nope.csv', '/data/x.csv'); })).toBe(404);
1339
+ store = runtime.driveStore;
1340
+ // the API checks existence before it reads, so these are the store's own
1341
+ // refusals rather than answers a client normally sees
1342
+ return [4 /*yield*/, expect(store.read('/data/missing.csv')).rejects.toThrow("File doesn't exist.")];
1343
+ case 2:
1344
+ // the API checks existence before it reads, so these are the store's own
1345
+ // refusals rather than answers a client normally sees
1346
+ _a.sent();
1347
+ return [4 /*yield*/, expect(store.stat('/data/missing.csv')).rejects.toThrow(/ENOENT/)];
1348
+ case 3:
1349
+ _a.sent();
1350
+ return [4 /*yield*/, expect(store.move('/data/missing.csv', '/data/x.csv')).rejects.toThrow(/no such folder/)];
1351
+ case 4:
1352
+ _a.sent();
1149
1353
  return [2 /*return*/];
1150
1354
  }
1151
1355
  });
1152
1356
  }); });
1153
- it('refuses any path that lands outside the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
1154
- var runtime;
1155
- return __generator(this, function (_a) {
1156
- switch (_a.label) {
1157
- case 0: return [4 /*yield*/, seededRuntime()
1158
- // a leading slash is drive-relative, and '..' is resolved before the
1159
- // containment test - the rule the server applies in resolveWithinDrive
1160
- ];
1357
+ it('refuses a path that climbs out of the drive, whatever the caller', function () { return __awaiter(void 0, void 0, void 0, function () {
1358
+ var runtime, store, _a;
1359
+ return __generator(this, function (_b) {
1360
+ switch (_b.label) {
1361
+ case 0: return [4 /*yield*/, seededRuntime()];
1161
1362
  case 1:
1162
- runtime = _a.sent();
1163
- // a leading slash is drive-relative, and '..' is resolved before the
1164
- // containment test - the rule the server applies in resolveWithinDrive
1165
- expect(status(function () { return runtime.save('/../escape.csv', 'x'); })).toBe(400);
1166
- expect(status(function () { return runtime.save('/data/../../escape.csv', 'x'); })).toBe(400);
1167
- expect(status(function () { return runtime.remove('/../../etc/passwd'); })).toBe(400);
1168
- expect(status(function () { return runtime.rename('/../x', '/data/y'); })).toBe(400);
1169
- expect(status(function () { return runtime.rename('/data/keep.csv', '/../y'); })).toBe(400);
1170
- // a file whose NAME starts with dots is not a traversal
1171
- expect(runtime.save('/data/..weird.csv', 'x')).toEqual({
1172
- status: 'success'
1173
- });
1363
+ runtime = _b.sent();
1364
+ store = runtime.driveStore;
1365
+ // the API refuses these first, with the wording a client sees; this is the
1366
+ // storage boundary refusing on its own, because the store is handed out and
1367
+ // a caller that skips the package must not be able to leave the drive
1368
+ return [4 /*yield*/, expect(store.read('/../escape.csv')).rejects.toThrow(/Can't reach a path outside the drive/)];
1369
+ case 2:
1370
+ // the API refuses these first, with the wording a client sees; this is the
1371
+ // storage boundary refusing on its own, because the store is handed out and
1372
+ // a caller that skips the package must not be able to leave the drive
1373
+ _b.sent();
1374
+ return [4 /*yield*/, expect(store.write('/data/../../escape.csv', 'x')).rejects.toThrow(/Can't reach a path outside the drive/)];
1375
+ case 3:
1376
+ _b.sent();
1377
+ return [4 /*yield*/, expect(store.list('/..')).rejects.toThrow(/Can't reach a path outside the drive/)];
1378
+ case 4:
1379
+ _b.sent();
1380
+ _a = expect;
1381
+ return [4 /*yield*/, store.exists('/data/keep.csv')];
1382
+ case 5:
1383
+ _a.apply(void 0, [_b.sent()]).toBe(true);
1174
1384
  return [2 /*return*/];
1175
1385
  }
1176
1386
  });
1177
1387
  }); });
1178
- it('keeps the bytes of an entry it moves', function () { return __awaiter(void 0, void 0, void 0, function () {
1179
- var runtime;
1180
- return __generator(this, function (_a) {
1181
- switch (_a.label) {
1388
+ it('counts the root as a folder, and its children as the listing', function () { return __awaiter(void 0, void 0, void 0, function () {
1389
+ var runtime, _a, _b;
1390
+ return __generator(this, function (_c) {
1391
+ switch (_c.label) {
1182
1392
  case 0: return [4 /*yield*/, seededRuntime()];
1183
1393
  case 1:
1184
- runtime = _a.sent();
1185
- runtime.save('/data/pack.zip', Uint8Array.from([1, 2, 3]));
1186
- runtime.rename('/data/pack.zip', '/data/moved.zip');
1187
- expect(runtime.read('/data/moved.zip')).toEqual({
1188
- contents: 'AQID',
1189
- binary: true
1394
+ runtime = _c.sent();
1395
+ _a = expect;
1396
+ return [4 /*yield*/, runtime.driveStore.stat('')];
1397
+ case 2:
1398
+ _a.apply(void 0, [_c.sent()]).toEqual({
1399
+ directory: true,
1400
+ size: 0
1401
+ });
1402
+ // the seed's own folder sits under the root too, alongside the app loc the
1403
+ // host creates at boot, so the listing is the drive as it stands
1404
+ _b = expect;
1405
+ return [4 /*yield*/, runtime.driveStore.list('')];
1406
+ case 3:
1407
+ // the seed's own folder sits under the root too, alongside the app loc the
1408
+ // host creates at boot, so the listing is the drive as it stands
1409
+ _b.apply(void 0, [_c.sent()]).toEqual({
1410
+ files: [],
1411
+ folders: ['Public', 'data']
1190
1412
  });
1191
- expect(runtime.stats().files).toBe(2);
1192
1413
  return [2 /*return*/];
1193
1414
  }
1194
1415
  });