@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.
@@ -167,7 +167,8 @@ describe('browser-hosted build', function () {
167
167
  'md5.js',
168
168
  'runtime-core.js',
169
169
  'persist.js',
170
- 'intercept.js'
170
+ 'intercept.js',
171
+ 'api-browser.js'
171
172
  ]), _b = _a.next(); !_b.done; _b = _a.next()) {
172
173
  asset = _b.value;
173
174
  expect(fs_1.default.existsSync(path_1.default.join(out(), 'host', asset))).toBe(true);
@@ -189,6 +190,124 @@ describe('browser-hosted build', function () {
189
190
  }
190
191
  });
191
192
  }); });
193
+ it('ships the @sasjs/api build the CLI resolves, and versions both imports', function () { return __awaiter(void 0, void 0, void 0, function () {
194
+ var shipped, installed, sw;
195
+ return __generator(this, function (_a) {
196
+ switch (_a.label) {
197
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())
198
+ // the same file the installed package carries: a vendored copy is exactly
199
+ // how the worker and the server drift apart
200
+ ];
201
+ case 1:
202
+ _a.sent();
203
+ shipped = fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'api-browser.js'));
204
+ installed = fs_1.default.readFileSync(path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js'));
205
+ expect(shipped.equals(installed)).toBe(true);
206
+ sw = fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8');
207
+ // both imports carry the host version: a worker caches what it imports, so
208
+ // a rebuild that changes either file has to change the URL
209
+ expect(sw).toMatch(/importScripts\('\/host\/runtime-core\.js\?v=[0-9a-f]{10}'\)/);
210
+ expect(sw).toMatch(/importScripts\('\/host\/api-browser\.js\?v=[0-9a-f]{10}'\)/);
211
+ expect(sw).not.toContain("importScripts('/host/runtime-core.js')");
212
+ return [2 /*return*/];
213
+ }
214
+ });
215
+ }); });
216
+ it('names the package when its browser build is missing, and keeps the previous site', function () { return __awaiter(void 0, void 0, void 0, function () {
217
+ var browserBuild, exists;
218
+ return __generator(this, function (_a) {
219
+ switch (_a.label) {
220
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
221
+ case 1:
222
+ _a.sent();
223
+ write(path_1.default.join(out(), 'stale.txt'), 'previous');
224
+ browserBuild = path_1.default.join(path_1.default.dirname(require.resolve('@sasjs/api')), '..', '..', 'build', 'browser.js');
225
+ exists = jest.spyOn(fs_1.default, 'existsSync');
226
+ exists.mockImplementation(function (checked) { return String(checked) !== browserBuild; });
227
+ _a.label = 2;
228
+ case 2:
229
+ _a.trys.push([2, , 4, 5]);
230
+ return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target())).rejects.toThrow(/@sasjs\/api browser build is missing/)];
231
+ case 3:
232
+ _a.sent();
233
+ return [3 /*break*/, 5];
234
+ case 4:
235
+ exists.mockRestore();
236
+ return [7 /*endfinally*/];
237
+ case 5:
238
+ // and the refusal lands before anything is written, so the site that was
239
+ // there is still there
240
+ expect(fs_1.default.existsSync(path_1.default.join(out(), 'stale.txt'))).toBe(true);
241
+ expect(fs_1.default.readFileSync(path_1.default.join(out(), 'stale.txt'), 'utf8')).toBe('previous');
242
+ return [2 /*return*/];
243
+ }
244
+ });
245
+ }); });
246
+ /**
247
+ * The worker's scripts share one global scope, so a name declared at the top
248
+ * level of two of them is a SyntaxError in the browser - and invisible to this
249
+ * suite, where each file is a module of its own. `importScripts` is the
250
+ * browser's rule: evaluate the files the build wrote, in one context, once.
251
+ */
252
+ it('evaluates the shipped scripts in one global scope, the way a worker does', function () { return __awaiter(void 0, void 0, void 0, function () {
253
+ var vm, sandbox, context, imported;
254
+ return __generator(this, function (_a) {
255
+ switch (_a.label) {
256
+ case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
257
+ case 1:
258
+ _a.sent();
259
+ vm = require('vm');
260
+ sandbox = {
261
+ console: console,
262
+ Request: Request,
263
+ Response: Response,
264
+ Headers: Headers,
265
+ URL: URL,
266
+ FormData: FormData,
267
+ Blob: Blob,
268
+ TextEncoder: TextEncoder,
269
+ TextDecoder: TextDecoder,
270
+ Uint8Array: Uint8Array,
271
+ ArrayBuffer: ArrayBuffer,
272
+ atob: atob,
273
+ btoa: btoa,
274
+ setTimeout: setTimeout,
275
+ clearTimeout: clearTimeout,
276
+ location: { origin: 'https://mock.example.com' },
277
+ clients: { claim: function () { }, matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
278
+ return [2 /*return*/, []];
279
+ }); }); } },
280
+ skipWaiting: function () { },
281
+ addEventListener: function () { },
282
+ fetch: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
283
+ return [2 /*return*/, new Response('{}', { headers: { 'content-type': 'application/json' } })];
284
+ }); }); }
285
+ };
286
+ sandbox.self = sandbox;
287
+ context = vm.createContext(sandbox);
288
+ imported = new Set();
289
+ sandbox.importScripts = function (url) {
290
+ if (imported.has(url))
291
+ return;
292
+ imported.add(url);
293
+ var file = path_1.default.join(out(), url.replace(/^\//, '').split('?')[0]);
294
+ vm.runInContext(fs_1.default.readFileSync(file, 'utf8'), context, {
295
+ filename: file
296
+ });
297
+ };
298
+ expect(function () {
299
+ return vm.runInContext(fs_1.default.readFileSync(path_1.default.join(out(), 'sw.js'), 'utf8'), context, {
300
+ filename: 'sw.js'
301
+ });
302
+ }).not.toThrow();
303
+ // both scripts are loaded, and the drive the API is handed came from the
304
+ // runtime rather than a second implementation
305
+ expect(Object.keys(sandbox.SASjsMockRuntime)).toContain('driveStore');
306
+ expect(typeof sandbox.SASjsApi.createDriveService).toBe('function');
307
+ return [2 /*return*/];
308
+ }
309
+ });
310
+ }); });
192
311
  it('ships the webout() block with sentinels the runtime fills in', function () { return __awaiter(void 0, void 0, void 0, function () {
193
312
  var prelude, _a, _b, sentinel;
194
313
  var e_2, _c;
@@ -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,4 +1044,375 @@ 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
  });
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.
1052
+ var root = '/sasjs/drive/files';
1053
+ var seededRuntime = function () { return __awaiter(void 0, void 0, void 0, function () {
1054
+ var runtime;
1055
+ var _a;
1056
+ return __generator(this, function (_b) {
1057
+ switch (_b.label) {
1058
+ case 0:
1059
+ runtime = loadRuntime();
1060
+ return [4 /*yield*/, seedDrive(runtime, (_a = {},
1061
+ _a["".concat(root, "/data/keep.csv")] = 'a,b\n1,2\n',
1062
+ _a))
1063
+ // boot marks the whole drive dirty, so the page persists a complete
1064
+ // snapshot; clearing it here leaves each test with only its own writes
1065
+ ];
1066
+ case 1:
1067
+ _b.sent();
1068
+ // boot marks the whole drive dirty, so the page persists a complete
1069
+ // snapshot; clearing it here leaves each test with only its own writes
1070
+ runtime.takeDirty();
1071
+ return [2 /*return*/, runtime];
1072
+ }
1073
+ });
1074
+ }); };
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) {
1079
+ case 0: return [4 /*yield*/, seededRuntime()];
1080
+ case 1:
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
1101
+ });
1102
+ return [2 /*return*/];
1103
+ }
1104
+ });
1105
+ }); });
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) {
1110
+ case 0: return [4 /*yield*/, seededRuntime()];
1111
+ case 1:
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
1127
+ });
1128
+ return [2 /*return*/];
1129
+ }
1130
+ });
1131
+ }); });
1132
+ it('reports a write to the page, so the entry is persisted', function () { return __awaiter(void 0, void 0, void 0, function () {
1133
+ var runtime;
1134
+ return __generator(this, function (_a) {
1135
+ switch (_a.label) {
1136
+ case 0: return [4 /*yield*/, seededRuntime()];
1137
+ case 1:
1138
+ runtime = _a.sent();
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');
1146
+ return [2 /*return*/];
1147
+ }
1148
+ });
1149
+ }); });
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) {
1155
+ case 0: return [4 /*yield*/, seededRuntime()];
1156
+ case 1:
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);
1166
+ return [2 /*return*/];
1167
+ }
1168
+ });
1169
+ }); });
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) {
1174
+ case 0: return [4 /*yield*/, seededRuntime()];
1175
+ case 1:
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
+ });
1198
+ return [2 /*return*/];
1199
+ }
1200
+ });
1201
+ }); });
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) {
1207
+ case 0: return [4 /*yield*/, seededRuntime()];
1208
+ case 1:
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));
1240
+ return [2 /*return*/];
1241
+ }
1242
+ });
1243
+ }); });
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) {
1249
+ case 0: return [4 /*yield*/, seededRuntime()];
1250
+ case 1:
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));
1267
+ return [2 /*return*/];
1268
+ }
1269
+ });
1270
+ }); });
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) {
1275
+ case 0: return [4 /*yield*/, seededRuntime()];
1276
+ case 1:
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']
1297
+ });
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);
1306
+ return [2 /*return*/];
1307
+ }
1308
+ });
1309
+ }); });
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;
1334
+ return __generator(this, function (_a) {
1335
+ switch (_a.label) {
1336
+ case 0: return [4 /*yield*/, seededRuntime()];
1337
+ case 1:
1338
+ runtime = _a.sent();
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();
1353
+ return [2 /*return*/];
1354
+ }
1355
+ });
1356
+ }); });
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()];
1362
+ case 1:
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);
1384
+ return [2 /*return*/];
1385
+ }
1386
+ });
1387
+ }); });
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) {
1392
+ case 0: return [4 /*yield*/, seededRuntime()];
1393
+ case 1:
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']
1412
+ });
1413
+ return [2 /*return*/];
1414
+ }
1415
+ });
1416
+ }); });
1417
+ });
952
1418
  });