@sasjs/cli 6.27.0 → 6.27.2
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.
|
@@ -457,7 +457,48 @@ const transcriptFor = async (programPath) => {
|
|
|
457
457
|
return undefined
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
/**
|
|
461
|
+
* The program a Studio code run came from, from the line the interface puts in
|
|
462
|
+
* front of the buffer.
|
|
463
|
+
*
|
|
464
|
+
* Run in Studio posts the buffer to `/SASjsApi/code/execute` - not the file's
|
|
465
|
+
* path - and the interface prepends `%let _program = <path>;` (its
|
|
466
|
+
* `programPathInjection`) so a program can still see where it lives. That line is
|
|
467
|
+
* the only thing in such a request that names the file, and it is what lets the
|
|
468
|
+
* recording answer a Press Run on the program it recorded.
|
|
469
|
+
*
|
|
470
|
+
* The line is written whatever was sent - the whole buffer or a selection of it -
|
|
471
|
+
* so the recording answers the program its path names, and a visitor who edits
|
|
472
|
+
* the code before pressing Run still sees the session recorded for that file. A
|
|
473
|
+
* browser cannot run SAS at all, so the alternative is a refusal for every SAS
|
|
474
|
+
* run; the demo's program says what it is, and its comment says where the answer
|
|
475
|
+
* comes from.
|
|
476
|
+
*/
|
|
477
|
+
const recordedProgramIn = (code, runTime) => {
|
|
478
|
+
if (String(runTime || '').toLowerCase() !== 'sas') return undefined
|
|
479
|
+
|
|
480
|
+
// anchored: the interface writes the line FIRST (`%let _program = ${path};\n`
|
|
481
|
+
// + the buffer), so a `%let _program` further down is the program's own code -
|
|
482
|
+
// a stored program that sets it deliberately, or a datalines row that looks
|
|
483
|
+
// like it - and is not what this host answers. Without the anchor, a caller's
|
|
484
|
+
// own SAS program could name the recorded file and be handed its session.
|
|
485
|
+
const match = /^\s*%let\s+_program\s*=\s*([^;]+);/i.exec(String(code || ''))
|
|
486
|
+
|
|
487
|
+
return match ? match[1].trim() : undefined
|
|
488
|
+
}
|
|
489
|
+
|
|
460
490
|
// ---------------------------------------------------------------- the runner
|
|
491
|
+
/**
|
|
492
|
+
* What a SAS run this host cannot answer is told.
|
|
493
|
+
*
|
|
494
|
+
* `sas` is one of the host's run times, so the message must not claim the run
|
|
495
|
+
* time is missing: what is missing is the binary, because this is a browser. It
|
|
496
|
+
* names the one thing that does answer SAS here, so a reader whose program is
|
|
497
|
+
* refused knows why the demo's own program is not.
|
|
498
|
+
*/
|
|
499
|
+
const SAS_UNAVAILABLE =
|
|
500
|
+
'SAS is not available on this host: a browser has no SAS binary, and this demo answers only the program it ships a recorded session of.'
|
|
501
|
+
|
|
461
502
|
/**
|
|
462
503
|
* The work a run does, over the browser's drive.
|
|
463
504
|
*
|
|
@@ -477,17 +518,19 @@ class BrowserRuntime extends API.MemorySessionHost {
|
|
|
477
518
|
const program = request.program || {}
|
|
478
519
|
const params = Object.assign({}, request.vars || {})
|
|
479
520
|
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
521
|
+
// SAS never reaches the runner. `sas` is one of this host's run times - the
|
|
522
|
+
// recording answers the program it names - but the answer for anything else
|
|
523
|
+
// is the refusal a client already understands, not a JavaScript SyntaxError
|
|
524
|
+
// quoting the SAS source, which is what handing it to the runner would
|
|
525
|
+
// produce. The file is resolved because the request may name the program
|
|
526
|
+
// without its extension.
|
|
527
|
+
const resolved =
|
|
528
|
+
program.code === undefined ? R.resolveProgram(program.path) : undefined
|
|
529
|
+
const isSas =
|
|
530
|
+
String(request.runTime || '').toLowerCase() === 'sas' ||
|
|
531
|
+
Boolean(resolved && /\.sas$/i.test(resolved))
|
|
487
532
|
|
|
488
|
-
|
|
489
|
-
throw runFailure('"runTime" must be one of [js]')
|
|
490
|
-
}
|
|
533
|
+
if (isSas) throw runFailure(SAS_UNAVAILABLE)
|
|
491
534
|
|
|
492
535
|
const payload = { params, tables: tablesOf(request.files) }
|
|
493
536
|
if (program.code !== undefined) payload.code = program.code
|
|
@@ -580,6 +623,23 @@ const withoutBase = (pathname) =>
|
|
|
580
623
|
? pathname.slice(BASE.length) || '/'
|
|
581
624
|
: pathname
|
|
582
625
|
|
|
626
|
+
// ------------------------------------------------------------- what it can run
|
|
627
|
+
/**
|
|
628
|
+
* The run times this host answers, in the order a client should try them.
|
|
629
|
+
*
|
|
630
|
+
* `js` is executed here, in the page's own JavaScript engine. `sas` is answered
|
|
631
|
+
* from the recorded session (`host/sas-transcript.json`) for the one program the
|
|
632
|
+
* recording names; any other SAS program is refused by `SAS_UNAVAILABLE`, which
|
|
633
|
+
* names the missing binary rather than a missing run time - a browser has none.
|
|
634
|
+
*
|
|
635
|
+
* Both the `/SASjsApi/info` answer and the runtime service read this list, so the
|
|
636
|
+
* run times the host states and the ones its service accepts cannot drift apart.
|
|
637
|
+
* `sas` is stated because that is what offers Run on a `.sas` file at all; a
|
|
638
|
+
* program the recording does not name is then refused with its reason, not by the
|
|
639
|
+
* run time being absent from a list the host publishes.
|
|
640
|
+
*/
|
|
641
|
+
const RUN_TIMES = ['js', 'sas']
|
|
642
|
+
|
|
583
643
|
// ------------------------------------------------------------- host reports
|
|
584
644
|
// The Admin routes ask a host what it is doing. A browser has no process, but it
|
|
585
645
|
// does have a page, a heap and this worker's own activity, so the reports are
|
|
@@ -626,7 +686,7 @@ const adminReports = {
|
|
|
626
686
|
cpuCount: navigator.hardwareConcurrency || 1,
|
|
627
687
|
cpuModel: 'unknown',
|
|
628
688
|
loadAverage: [],
|
|
629
|
-
runTimes:
|
|
689
|
+
runTimes: RUN_TIMES,
|
|
630
690
|
sasjsRoot: '/'
|
|
631
691
|
},
|
|
632
692
|
memory: {
|
|
@@ -772,16 +832,21 @@ const homeService = API.createHomeService(async () => {
|
|
|
772
832
|
|
|
773
833
|
const SERVICES = {
|
|
774
834
|
drive: API.createDriveService(DRIVE_STORE),
|
|
775
|
-
|
|
835
|
+
// The package's browser fixture states one run time; this host serves two, and
|
|
836
|
+
// `info` is what the Studio reads to decide whether a file may be run at all,
|
|
837
|
+
// so the answer is the host's own list rather than the fixture's.
|
|
838
|
+
info: API.createInfoService(
|
|
839
|
+
Object.assign({}, API.BROWSER_INFO, { runTimes: RUN_TIMES })
|
|
840
|
+
),
|
|
776
841
|
identity: IDENTITY,
|
|
777
842
|
records: RECORDS,
|
|
778
843
|
runtime: API.createRuntimeService(IDENTITY_STORE, RUNTIME, {
|
|
779
|
-
// `js` is executed here; `sas` is offered
|
|
780
|
-
//
|
|
781
|
-
// program with no recording is refused in the
|
|
782
|
-
// not be handed to the JavaScript runtime as
|
|
783
|
-
//
|
|
784
|
-
runTimes:
|
|
844
|
+
// `js` is executed here; `sas` is offered because the recording answers the
|
|
845
|
+
// program it names, and the Studio offers Run on a `.sas` file only for a run
|
|
846
|
+
// time the host states. A `.sas` program with no recording is refused in the
|
|
847
|
+
// runner, deliberately - it must not be handed to the JavaScript runtime as
|
|
848
|
+
// source, which is what the guard in BrowserRuntime.run prevents.
|
|
849
|
+
runTimes: RUN_TIMES
|
|
785
850
|
}),
|
|
786
851
|
session: API.createSessionService(IDENTITY_STORE, RUNTIME),
|
|
787
852
|
authConfig: API.createAuthConfigService({}),
|
|
@@ -1146,15 +1211,17 @@ const runRoute = async (routeId, payload, subject) => {
|
|
|
1146
1211
|
runRequest.program = query._program || body._program
|
|
1147
1212
|
}
|
|
1148
1213
|
|
|
1149
|
-
// A recorded SAS session is answered here, ahead of the run
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1214
|
+
// A recorded SAS session is answered here, ahead of the run. `sas` IS one of
|
|
1215
|
+
// this host's run times - it is what the Studio reads to offer Run on a .sas
|
|
1216
|
+
// file - and the recording is how it is answered, so the check comes before
|
|
1217
|
+
// the package would compile the source as JavaScript. A trigger is excluded:
|
|
1218
|
+
// it has to answer with the session record the route builds, which a client
|
|
1219
|
+
// parses as JSON, so a recording is not an answer there.
|
|
1220
|
+
if (!trigger) {
|
|
1221
|
+
const recorded = code
|
|
1222
|
+
? recordedProgramIn(body.code, body.runTime)
|
|
1223
|
+
: runRequest.program
|
|
1224
|
+
const recording = recorded ? await transcriptFor(recorded) : undefined
|
|
1158
1225
|
|
|
1159
1226
|
if (recording)
|
|
1160
1227
|
return artifactResponse(
|
|
@@ -1168,7 +1235,10 @@ const runRoute = async (routeId, payload, subject) => {
|
|
|
1168
1235
|
// the same rule runtime-core applies to a service that declares none
|
|
1169
1236
|
headers: SEEDED_HEADER_LINE
|
|
1170
1237
|
},
|
|
1171
|
-
|
|
1238
|
+
// a code run carries no `_debug` and still answers with its log: the
|
|
1239
|
+
// package compiles a code run at its own debug level, and the Studio
|
|
1240
|
+
// splits the three parts on the separator it writes
|
|
1241
|
+
code ? true : vars._debug !== undefined
|
|
1172
1242
|
)
|
|
1173
1243
|
)
|
|
1174
1244
|
}
|
|
@@ -1357,12 +1427,14 @@ const routeRequest = async (request, url) => {
|
|
|
1357
1427
|
|
|
1358
1428
|
if (!refusal) broadcast({ type: 'sasjs-host-error', path, message })
|
|
1359
1429
|
// A refusal the API decided on carries its status and, where the route
|
|
1360
|
-
// answers an envelope, the body to send. The
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1430
|
+
// answers an envelope, the body to send. The deploy routes state one in the
|
|
1431
|
+
// document - a refusal there is `{status: 'failure', message}` - while the
|
|
1432
|
+
// other drive routes send the message itself as plain text, which is what the
|
|
1433
|
+
// server's `res.status(code).send(err.message)` does.
|
|
1364
1434
|
if (API.isApiError(e)) {
|
|
1365
1435
|
if (e.body !== undefined) return json(e.body, e.status)
|
|
1436
|
+
if (/\/SASjsApi\/drive\/deploy(\/|$)/.test(apiPath))
|
|
1437
|
+
return json({ status: 'failure', message: e.message }, e.status)
|
|
1366
1438
|
if (apiPath.includes('/SASjsApi/drive/')) return text(e.message, e.status)
|
|
1367
1439
|
return json({ message: e.message }, e.status)
|
|
1368
1440
|
}
|
|
@@ -90,6 +90,7 @@ var path_1 = __importDefault(require("path"));
|
|
|
90
90
|
var api_1 = require("@sasjs/api");
|
|
91
91
|
var os_1 = __importDefault(require("os"));
|
|
92
92
|
var yazl_1 = __importDefault(require("yazl"));
|
|
93
|
+
var utils_1 = require("@sasjs/utils");
|
|
93
94
|
var mockSite_1 = require("../mockSite");
|
|
94
95
|
/**
|
|
95
96
|
* The other half of the parity claim: the service worker is what turns an
|
|
@@ -122,8 +123,9 @@ var lintBundle = function () {
|
|
|
122
123
|
}
|
|
123
124
|
return lintBundlePath;
|
|
124
125
|
};
|
|
125
|
-
var loadWorker = function (base) {
|
|
126
|
+
var loadWorker = function (base, options) {
|
|
126
127
|
if (base === void 0) { base = ''; }
|
|
128
|
+
if (options === void 0) { options = {}; }
|
|
127
129
|
jest.resetModules();
|
|
128
130
|
var handlers = {};
|
|
129
131
|
var refusingWrites = false;
|
|
@@ -205,6 +207,22 @@ var loadWorker = function (base) {
|
|
|
205
207
|
fixture = global.__sasTranscript;
|
|
206
208
|
return [2 /*return*/, fixture ? jsonResponse(fixture) : new Response('', { status: 404 })];
|
|
207
209
|
}
|
|
210
|
+
// the webout() block the build writes beside the worker, from the same
|
|
211
|
+
// @sasjs/utils generator sasjs/server uses. Absent unless a test asks for
|
|
212
|
+
// it: a site built before the block existed keeps the `_webout`-only path.
|
|
213
|
+
if (options.prelude && target.includes('webout-prelude'))
|
|
214
|
+
return [2 /*return*/, jsonResponse({
|
|
215
|
+
sentinels: mockSite_1.WEBOUT_SENTINELS,
|
|
216
|
+
template: (0, utils_1.getWeboutPrelude)({
|
|
217
|
+
weboutPath: mockSite_1.WEBOUT_SENTINELS.weboutPath,
|
|
218
|
+
headersPath: mockSite_1.WEBOUT_SENTINELS.headersPath,
|
|
219
|
+
program: mockSite_1.WEBOUT_SENTINELS.program,
|
|
220
|
+
debug: mockSite_1.WEBOUT_SENTINELS.debug,
|
|
221
|
+
username: mockSite_1.WEBOUT_SENTINELS.username,
|
|
222
|
+
userId: mockSite_1.WEBOUT_SENTINELS.userId,
|
|
223
|
+
apiServerUrl: mockSite_1.WEBOUT_SENTINELS.apiServerUrl
|
|
224
|
+
})
|
|
225
|
+
})];
|
|
208
226
|
return [2 /*return*/, target.includes('vfs-seed')
|
|
209
227
|
? jsonResponse({
|
|
210
228
|
files: (_a = {},
|
|
@@ -260,7 +278,8 @@ var jsonResponse = function (body) { return ({
|
|
|
260
278
|
return [2 /*return*/, body];
|
|
261
279
|
}); }); }
|
|
262
280
|
}); };
|
|
263
|
-
/** A zip archive of text
|
|
281
|
+
/** A zip archive of members - text, or bytes for a base64 member - written the way
|
|
282
|
+
* a deploy upload ships one.
|
|
264
283
|
* The first member is stored rather than deflated, so both methods the reader
|
|
265
284
|
* accepts are exercised by the one case. */
|
|
266
285
|
var zipOf = function (files) {
|
|
@@ -392,6 +411,164 @@ describe('service worker host', function () {
|
|
|
392
411
|
}
|
|
393
412
|
});
|
|
394
413
|
}); });
|
|
414
|
+
it('runs a JS program written the way sasjs/server runs one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
415
|
+
var worker, response, body, _a, _b;
|
|
416
|
+
var _c;
|
|
417
|
+
return __generator(this, function (_d) {
|
|
418
|
+
switch (_d.label) {
|
|
419
|
+
case 0:
|
|
420
|
+
worker = loadWorker('', { prelude: true });
|
|
421
|
+
worker.runtime.load((_c = {},
|
|
422
|
+
_c[drivePath('services/public/run.js')] = [
|
|
423
|
+
"const rows = [{ name: 'Alice', age: 14 }, { name: 'Bob', age: 15 }]",
|
|
424
|
+
'console.log(`Read ${rows.length} rows`)',
|
|
425
|
+
"webout('OPEN')",
|
|
426
|
+
"webout('OBJ', 'adults', rows.filter((row) => row.age > 13))",
|
|
427
|
+
"webout('CLOSE')"
|
|
428
|
+
].join('\n'),
|
|
429
|
+
_c));
|
|
430
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/stp/execute?_program=' +
|
|
431
|
+
encodeURIComponent("".concat(appLoc, "/services/public/run")))];
|
|
432
|
+
case 1:
|
|
433
|
+
response = _d.sent();
|
|
434
|
+
expect(response.status).toBe(200);
|
|
435
|
+
_b = (_a = JSON).parse;
|
|
436
|
+
return [4 /*yield*/, response.text()];
|
|
437
|
+
case 2:
|
|
438
|
+
body = _b.apply(_a, [_d.sent()]);
|
|
439
|
+
expect(body.adults).toEqual([
|
|
440
|
+
{ name: 'Alice', age: 14 },
|
|
441
|
+
{ name: 'Bob', age: 15 }
|
|
442
|
+
]);
|
|
443
|
+
return [2 /*return*/];
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}); });
|
|
447
|
+
it('answers a Run in Studio on the recorded program, as a code run', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
448
|
+
var worker, response, parts;
|
|
449
|
+
var _a, _b;
|
|
450
|
+
return __generator(this, function (_c) {
|
|
451
|
+
switch (_c.label) {
|
|
452
|
+
case 0:
|
|
453
|
+
// Press Run in Studio posts the buffer to /code/execute with the file's own
|
|
454
|
+
// path prepended as `%let _program` - the path is not a query parameter, as
|
|
455
|
+
// it is for stp/execute. The recording is what answers it: without this the
|
|
456
|
+
// demo's .sas program has a Run button (the host states `sas`) and no way to
|
|
457
|
+
// answer it, because a browser cannot compile SAS.
|
|
458
|
+
;
|
|
459
|
+
global.__sasTranscript = (_a = {},
|
|
460
|
+
_a["".concat(appLoc, "/services/public/program.sas")] = {
|
|
461
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
462
|
+
log: 'NOTE: recorded',
|
|
463
|
+
print: 'The SAS System'
|
|
464
|
+
},
|
|
465
|
+
_a);
|
|
466
|
+
worker = loadWorker();
|
|
467
|
+
worker.runtime.load((_b = {},
|
|
468
|
+
_b[drivePath('services/public/program.sas')] = 'data work.class; run;',
|
|
469
|
+
_b));
|
|
470
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({
|
|
471
|
+
code: "%let _program = ".concat(appLoc, "/services/public/program.sas;\ndata work.class; run;"),
|
|
472
|
+
runTime: 'sas'
|
|
473
|
+
}))];
|
|
474
|
+
case 1:
|
|
475
|
+
response = _c.sent();
|
|
476
|
+
expect(response.status).toBe(200);
|
|
477
|
+
return [4 /*yield*/, response.text()];
|
|
478
|
+
case 2:
|
|
479
|
+
parts = (_c.sent()).split(LOGS_SEPARATOR);
|
|
480
|
+
expect(parts[0].trim()).toBe('{"SYSSITE":"00000000"}');
|
|
481
|
+
expect(parts[1]).toContain('NOTE: recorded');
|
|
482
|
+
expect(parts[2]).toContain('The SAS System');
|
|
483
|
+
delete global.__sasTranscript;
|
|
484
|
+
return [2 /*return*/];
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}); });
|
|
488
|
+
it('answers only the line the interface writes, not one further down', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
489
|
+
var worker, response, _a;
|
|
490
|
+
var _b, _c;
|
|
491
|
+
return __generator(this, function (_d) {
|
|
492
|
+
switch (_d.label) {
|
|
493
|
+
case 0:
|
|
494
|
+
// The interface puts `%let _program` first; a line further down belongs to
|
|
495
|
+
// the program. Answered, it would hand a caller's own SAS program the session
|
|
496
|
+
// recorded for another file - the "inventing a session" the refusal exists to
|
|
497
|
+
// avoid, with a different file's log.
|
|
498
|
+
;
|
|
499
|
+
global.__sasTranscript = (_b = {},
|
|
500
|
+
_b["".concat(appLoc, "/services/public/program.sas")] = {
|
|
501
|
+
webout: '{"SYSSITE":"00000000"}',
|
|
502
|
+
log: 'NOTE: recorded',
|
|
503
|
+
print: 'The SAS System'
|
|
504
|
+
},
|
|
505
|
+
_b);
|
|
506
|
+
worker = loadWorker();
|
|
507
|
+
worker.runtime.load((_c = {},
|
|
508
|
+
_c[drivePath('services/public/mine.sas')] = 'data work.class; run;',
|
|
509
|
+
_c));
|
|
510
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({
|
|
511
|
+
code: "data _null_;\n%let _program = ".concat(appLoc, "/services/public/program.sas;\nrun;"),
|
|
512
|
+
runTime: 'sas'
|
|
513
|
+
}))];
|
|
514
|
+
case 1:
|
|
515
|
+
response = _d.sent();
|
|
516
|
+
expect(response.status).toBe(400);
|
|
517
|
+
_a = expect;
|
|
518
|
+
return [4 /*yield*/, response.text()];
|
|
519
|
+
case 2:
|
|
520
|
+
_a.apply(void 0, [_d.sent()]).toContain('SAS is not available');
|
|
521
|
+
delete global.__sasTranscript;
|
|
522
|
+
return [2 /*return*/];
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}); });
|
|
526
|
+
it('refuses SAS code that names no recorded program', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
527
|
+
var worker, _a, _b, code, response, body, e_1_1;
|
|
528
|
+
var e_1, _c;
|
|
529
|
+
return __generator(this, function (_d) {
|
|
530
|
+
switch (_d.label) {
|
|
531
|
+
case 0:
|
|
532
|
+
worker = loadWorker();
|
|
533
|
+
_d.label = 1;
|
|
534
|
+
case 1:
|
|
535
|
+
_d.trys.push([1, 7, 8, 9]);
|
|
536
|
+
_a = __values([
|
|
537
|
+
'data work.class; run;',
|
|
538
|
+
"%let _program = ".concat(appLoc, "/services/public/other.sas;\ndata work.class; run;")
|
|
539
|
+
]), _b = _a.next();
|
|
540
|
+
_d.label = 2;
|
|
541
|
+
case 2:
|
|
542
|
+
if (!!_b.done) return [3 /*break*/, 6];
|
|
543
|
+
code = _b.value;
|
|
544
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/code/execute', runRequest({ code: code, runTime: 'sas' }))];
|
|
545
|
+
case 3:
|
|
546
|
+
response = _d.sent();
|
|
547
|
+
expect(response.status).toBe(400);
|
|
548
|
+
return [4 /*yield*/, response.text()];
|
|
549
|
+
case 4:
|
|
550
|
+
body = _d.sent();
|
|
551
|
+
expect(body).toContain('SAS is not available');
|
|
552
|
+
expect(body).not.toContain('SyntaxError');
|
|
553
|
+
_d.label = 5;
|
|
554
|
+
case 5:
|
|
555
|
+
_b = _a.next();
|
|
556
|
+
return [3 /*break*/, 2];
|
|
557
|
+
case 6: return [3 /*break*/, 9];
|
|
558
|
+
case 7:
|
|
559
|
+
e_1_1 = _d.sent();
|
|
560
|
+
e_1 = { error: e_1_1 };
|
|
561
|
+
return [3 /*break*/, 9];
|
|
562
|
+
case 8:
|
|
563
|
+
try {
|
|
564
|
+
if (_b && !_b.done && (_c = _a.return)) _c.call(_a);
|
|
565
|
+
}
|
|
566
|
+
finally { if (e_1) throw e_1.error; }
|
|
567
|
+
return [7 /*endfinally*/];
|
|
568
|
+
case 9: return [2 /*return*/];
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}); });
|
|
395
572
|
it('refuses a .sas program no recording names, as it always did', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
396
573
|
var worker, response, body;
|
|
397
574
|
var _a;
|
|
@@ -410,7 +587,8 @@ describe('service worker host', function () {
|
|
|
410
587
|
return [4 /*yield*/, response.text()];
|
|
411
588
|
case 2:
|
|
412
589
|
body = _b.sent();
|
|
413
|
-
expect(body).toContain('
|
|
590
|
+
expect(body).toContain('SAS is not available');
|
|
591
|
+
expect(body).toContain('recorded session');
|
|
414
592
|
expect(body).not.toContain('SyntaxError');
|
|
415
593
|
return [2 /*return*/];
|
|
416
594
|
}
|
|
@@ -1266,10 +1444,17 @@ describe('service worker host', function () {
|
|
|
1266
1444
|
case 1:
|
|
1267
1445
|
empty = _c.sent();
|
|
1268
1446
|
expect(empty.status).toBe(400);
|
|
1447
|
+
// the deploy routes answer a refusal as the envelope the document states for
|
|
1448
|
+
// them, which is what the corpus pins for the same route
|
|
1269
1449
|
_a = expect;
|
|
1270
|
-
return [4 /*yield*/, empty.
|
|
1450
|
+
return [4 /*yield*/, empty.json()];
|
|
1271
1451
|
case 2:
|
|
1272
|
-
|
|
1452
|
+
// the deploy routes answer a refusal as the envelope the document states for
|
|
1453
|
+
// them, which is what the corpus pins for the same route
|
|
1454
|
+
_a.apply(void 0, [_c.sent()]).toEqual({
|
|
1455
|
+
status: 'failure',
|
|
1456
|
+
message: '"file" is not present.'
|
|
1457
|
+
});
|
|
1273
1458
|
form = new FormData();
|
|
1274
1459
|
form.set('file', new Blob([new Uint8Array([1, 2, 3])]), 'not-a-zip.zip');
|
|
1275
1460
|
return [4 /*yield*/, dispatch('/SASjsApi/drive/deploy/upload', {
|
|
@@ -1280,9 +1465,12 @@ describe('service worker host', function () {
|
|
|
1280
1465
|
notZip = _c.sent();
|
|
1281
1466
|
expect(notZip.status).toBe(400);
|
|
1282
1467
|
_b = expect;
|
|
1283
|
-
return [4 /*yield*/, notZip.
|
|
1468
|
+
return [4 /*yield*/, notZip.json()];
|
|
1284
1469
|
case 4:
|
|
1285
|
-
_b.apply(void 0, [_c.sent()]).
|
|
1470
|
+
_b.apply(void 0, [_c.sent()]).toEqual({
|
|
1471
|
+
status: 'failure',
|
|
1472
|
+
message: 'The archive is not a zip file.'
|
|
1473
|
+
});
|
|
1286
1474
|
return [2 /*return*/];
|
|
1287
1475
|
}
|
|
1288
1476
|
});
|
|
@@ -1536,8 +1724,8 @@ describe('service worker host', function () {
|
|
|
1536
1724
|
});
|
|
1537
1725
|
}); });
|
|
1538
1726
|
it('publishes, lists, serves and removes an app through the package surface', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1539
|
-
var dispatch, _a, _b, path_2, listing, _c,
|
|
1540
|
-
var
|
|
1727
|
+
var dispatch, _a, _b, path_2, listing, _c, e_2_1, _d, _e, path_3, landing, e_3_1, root, archive, form, published, app, _f, _g, path_4, listing, _h, e_4_1, _j, _k, path_5, page, _l, e_5_1, folder, _m, refused, missing, removed, _o, after, _p, gone;
|
|
1728
|
+
var e_2, _q, e_3, _r, e_4, _s, e_5, _t;
|
|
1541
1729
|
return __generator(this, function (_u) {
|
|
1542
1730
|
switch (_u.label) {
|
|
1543
1731
|
case 0:
|
|
@@ -1564,14 +1752,14 @@ describe('service worker host', function () {
|
|
|
1564
1752
|
return [3 /*break*/, 2];
|
|
1565
1753
|
case 6: return [3 /*break*/, 9];
|
|
1566
1754
|
case 7:
|
|
1567
|
-
|
|
1568
|
-
|
|
1755
|
+
e_2_1 = _u.sent();
|
|
1756
|
+
e_2 = { error: e_2_1 };
|
|
1569
1757
|
return [3 /*break*/, 9];
|
|
1570
1758
|
case 8:
|
|
1571
1759
|
try {
|
|
1572
1760
|
if (_b && !_b.done && (_q = _a.return)) _q.call(_a);
|
|
1573
1761
|
}
|
|
1574
|
-
finally { if (
|
|
1762
|
+
finally { if (e_2) throw e_2.error; }
|
|
1575
1763
|
return [7 /*endfinally*/];
|
|
1576
1764
|
case 9:
|
|
1577
1765
|
_u.trys.push([9, 14, 15, 16]);
|
|
@@ -1591,14 +1779,14 @@ describe('service worker host', function () {
|
|
|
1591
1779
|
return [3 /*break*/, 10];
|
|
1592
1780
|
case 13: return [3 /*break*/, 16];
|
|
1593
1781
|
case 14:
|
|
1594
|
-
|
|
1595
|
-
|
|
1782
|
+
e_3_1 = _u.sent();
|
|
1783
|
+
e_3 = { error: e_3_1 };
|
|
1596
1784
|
return [3 /*break*/, 16];
|
|
1597
1785
|
case 15:
|
|
1598
1786
|
try {
|
|
1599
1787
|
if (_e && !_e.done && (_r = _d.return)) _r.call(_d);
|
|
1600
1788
|
}
|
|
1601
|
-
finally { if (
|
|
1789
|
+
finally { if (e_3) throw e_3.error; }
|
|
1602
1790
|
return [7 /*endfinally*/];
|
|
1603
1791
|
case 16: return [4 /*yield*/, dispatch('/AppStream')];
|
|
1604
1792
|
case 17:
|
|
@@ -1651,14 +1839,14 @@ describe('service worker host', function () {
|
|
|
1651
1839
|
return [3 /*break*/, 22];
|
|
1652
1840
|
case 26: return [3 /*break*/, 29];
|
|
1653
1841
|
case 27:
|
|
1654
|
-
|
|
1655
|
-
|
|
1842
|
+
e_4_1 = _u.sent();
|
|
1843
|
+
e_4 = { error: e_4_1 };
|
|
1656
1844
|
return [3 /*break*/, 29];
|
|
1657
1845
|
case 28:
|
|
1658
1846
|
try {
|
|
1659
1847
|
if (_g && !_g.done && (_s = _f.return)) _s.call(_f);
|
|
1660
1848
|
}
|
|
1661
|
-
finally { if (
|
|
1849
|
+
finally { if (e_4) throw e_4.error; }
|
|
1662
1850
|
return [7 /*endfinally*/];
|
|
1663
1851
|
case 29:
|
|
1664
1852
|
_u.trys.push([29, 35, 36, 37]);
|
|
@@ -1685,14 +1873,14 @@ describe('service worker host', function () {
|
|
|
1685
1873
|
return [3 /*break*/, 30];
|
|
1686
1874
|
case 34: return [3 /*break*/, 37];
|
|
1687
1875
|
case 35:
|
|
1688
|
-
|
|
1689
|
-
|
|
1876
|
+
e_5_1 = _u.sent();
|
|
1877
|
+
e_5 = { error: e_5_1 };
|
|
1690
1878
|
return [3 /*break*/, 37];
|
|
1691
1879
|
case 36:
|
|
1692
1880
|
try {
|
|
1693
1881
|
if (_k && !_k.done && (_t = _j.return)) _t.call(_j);
|
|
1694
1882
|
}
|
|
1695
|
-
finally { if (
|
|
1883
|
+
finally { if (e_5) throw e_5.error; }
|
|
1696
1884
|
return [7 /*endfinally*/];
|
|
1697
1885
|
case 37: return [4 /*yield*/, dispatch("/app/".concat(app.id))];
|
|
1698
1886
|
case 38:
|
|
@@ -1995,30 +2183,33 @@ describe('service worker host', function () {
|
|
|
1995
2183
|
case 0:
|
|
1996
2184
|
dispatch = seeded().dispatch;
|
|
1997
2185
|
return [4 /*yield*/, dispatch('/SASjsApi/info')
|
|
1998
|
-
// the documented body, field for field
|
|
1999
|
-
//
|
|
2000
|
-
//
|
|
2001
|
-
//
|
|
2186
|
+
// the documented body, field for field: a property the document does not
|
|
2187
|
+
// state, or a login form told there are accounts when there are none, is
|
|
2188
|
+
// this host describing an API the server does not serve. `runTimes` is the
|
|
2189
|
+
// host's own, not the package's browser fixture: `sas` is what makes the
|
|
2190
|
+
// Studio offer Run on a .sas file, and the recording is how it is answered.
|
|
2002
2191
|
];
|
|
2003
2192
|
case 1:
|
|
2004
2193
|
info = _f.sent();
|
|
2005
|
-
// the documented body, field for field
|
|
2006
|
-
//
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2194
|
+
// the documented body, field for field: a property the document does not
|
|
2195
|
+
// state, or a login form told there are accounts when there are none, is
|
|
2196
|
+
// this host describing an API the server does not serve. `runTimes` is the
|
|
2197
|
+
// host's own, not the package's browser fixture: `sas` is what makes the
|
|
2198
|
+
// Studio offer Run on a .sas file, and the recording is how it is answered.
|
|
2009
2199
|
_a = expect;
|
|
2010
2200
|
return [4 /*yield*/, info.json()];
|
|
2011
2201
|
case 2:
|
|
2012
|
-
// the documented body, field for field
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2015
|
-
//
|
|
2202
|
+
// the documented body, field for field: a property the document does not
|
|
2203
|
+
// state, or a login form told there are accounts when there are none, is
|
|
2204
|
+
// this host describing an API the server does not serve. `runTimes` is the
|
|
2205
|
+
// host's own, not the package's browser fixture: `sas` is what makes the
|
|
2206
|
+
// Studio offer Run on a .sas file, and the recording is how it is answered.
|
|
2016
2207
|
_a.apply(void 0, [_f.sent()]).toEqual({
|
|
2017
2208
|
mode: 'desktop',
|
|
2018
2209
|
cors: 'disable',
|
|
2019
2210
|
httpPort: null,
|
|
2020
2211
|
httpsPort: null,
|
|
2021
|
-
runTimes: ['js'],
|
|
2212
|
+
runTimes: ['js', 'sas'],
|
|
2022
2213
|
authProviders: [],
|
|
2023
2214
|
localLoginEnabled: false,
|
|
2024
2215
|
version: 'dev'
|
|
@@ -2631,7 +2822,7 @@ describe('service worker host', function () {
|
|
|
2631
2822
|
}); });
|
|
2632
2823
|
});
|
|
2633
2824
|
describe('conformance corpus', function () {
|
|
2634
|
-
var
|
|
2825
|
+
var e_6, _a, e_7, _b;
|
|
2635
2826
|
/**
|
|
2636
2827
|
* The corpus is what "the same API" means: the cases @sasjs/api runs against
|
|
2637
2828
|
* its own services, and against a live sasjs/server, run here against this
|
|
@@ -2640,7 +2831,7 @@ describe('service worker host', function () {
|
|
|
2640
2831
|
* used to have - and it is a test rather than a review.
|
|
2641
2832
|
*/
|
|
2642
2833
|
var httpFor = function (testCase) {
|
|
2643
|
-
var
|
|
2834
|
+
var e_8, _a;
|
|
2644
2835
|
var _b;
|
|
2645
2836
|
var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
|
|
2646
2837
|
var query = new URLSearchParams();
|
|
@@ -2651,12 +2842,12 @@ describe('service worker host', function () {
|
|
|
2651
2842
|
query.set(key, String(value));
|
|
2652
2843
|
}
|
|
2653
2844
|
}
|
|
2654
|
-
catch (
|
|
2845
|
+
catch (e_8_1) { e_8 = { error: e_8_1 }; }
|
|
2655
2846
|
finally {
|
|
2656
2847
|
try {
|
|
2657
2848
|
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
2658
2849
|
}
|
|
2659
|
-
finally { if (
|
|
2850
|
+
finally { if (e_8) throw e_8.error; }
|
|
2660
2851
|
}
|
|
2661
2852
|
var suffix = query.toString() ? "?".concat(query) : '';
|
|
2662
2853
|
var init = { method: method };
|
|
@@ -2687,8 +2878,8 @@ describe('service worker host', function () {
|
|
|
2687
2878
|
* does.
|
|
2688
2879
|
*/
|
|
2689
2880
|
var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
2690
|
-
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents,
|
|
2691
|
-
var
|
|
2881
|
+
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_9_1;
|
|
2882
|
+
var e_10, _g, e_9, _h;
|
|
2692
2883
|
return __generator(this, function (_j) {
|
|
2693
2884
|
switch (_j.label) {
|
|
2694
2885
|
case 0:
|
|
@@ -2700,12 +2891,12 @@ describe('service worker host', function () {
|
|
|
2700
2891
|
text["/sasjs/drive/files".concat(file)] = contents;
|
|
2701
2892
|
}
|
|
2702
2893
|
}
|
|
2703
|
-
catch (
|
|
2894
|
+
catch (e_10_1) { e_10 = { error: e_10_1 }; }
|
|
2704
2895
|
finally {
|
|
2705
2896
|
try {
|
|
2706
2897
|
if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
|
|
2707
2898
|
}
|
|
2708
|
-
finally { if (
|
|
2899
|
+
finally { if (e_10) throw e_10.error; }
|
|
2709
2900
|
}
|
|
2710
2901
|
return [4 /*yield*/, runtime.load(text)];
|
|
2711
2902
|
case 1:
|
|
@@ -2728,87 +2919,393 @@ describe('service worker host', function () {
|
|
|
2728
2919
|
return [3 /*break*/, 3];
|
|
2729
2920
|
case 6: return [3 /*break*/, 9];
|
|
2730
2921
|
case 7:
|
|
2731
|
-
|
|
2732
|
-
|
|
2922
|
+
e_9_1 = _j.sent();
|
|
2923
|
+
e_9 = { error: e_9_1 };
|
|
2733
2924
|
return [3 /*break*/, 9];
|
|
2734
2925
|
case 8:
|
|
2735
2926
|
try {
|
|
2736
2927
|
if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
|
|
2737
2928
|
}
|
|
2738
|
-
finally { if (
|
|
2929
|
+
finally { if (e_9) throw e_9.error; }
|
|
2739
2930
|
return [7 /*endfinally*/];
|
|
2740
2931
|
case 9: return [2 /*return*/];
|
|
2741
2932
|
}
|
|
2742
2933
|
});
|
|
2743
2934
|
}); };
|
|
2935
|
+
/**
|
|
2936
|
+
* The expectation kinds this harness asserts.
|
|
2937
|
+
*
|
|
2938
|
+
* A case that states one it does not implement fails rather than passing
|
|
2939
|
+
* unproven - `keys`, `shape` and `encoding` are asserted by the package's own
|
|
2940
|
+
* corpus run and not here, and a case that starts using them has to be told
|
|
2941
|
+
* so by this list rather than by silence.
|
|
2942
|
+
*/
|
|
2943
|
+
var ASSERTED = ['status', 'error', 'partial', 'body', 'contents'];
|
|
2944
|
+
var assertExpectations = function (response, testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
2945
|
+
var unasserted, bytes, _a, _b, text, _c, isJson, body;
|
|
2946
|
+
return __generator(this, function (_d) {
|
|
2947
|
+
switch (_d.label) {
|
|
2948
|
+
case 0:
|
|
2949
|
+
unasserted = Object.keys(testCase.expect).filter(function (kind) { return !ASSERTED.includes(kind); });
|
|
2950
|
+
if (unasserted.length)
|
|
2951
|
+
throw new Error("".concat(testCase.id, ": this harness does not assert ").concat(unasserted.join(', ')));
|
|
2952
|
+
expect(response.status).toBe(testCase.expect.status);
|
|
2953
|
+
if (!(testCase.expect.contents instanceof Uint8Array)) return [3 /*break*/, 2];
|
|
2954
|
+
_b = Uint8Array.bind;
|
|
2955
|
+
return [4 /*yield*/, response.arrayBuffer()];
|
|
2956
|
+
case 1:
|
|
2957
|
+
_a = new (_b.apply(Uint8Array, [void 0, _d.sent()]))();
|
|
2958
|
+
return [3 /*break*/, 3];
|
|
2959
|
+
case 2:
|
|
2960
|
+
_a = undefined;
|
|
2961
|
+
_d.label = 3;
|
|
2962
|
+
case 3:
|
|
2963
|
+
bytes = _a;
|
|
2964
|
+
if (!(bytes === undefined)) return [3 /*break*/, 5];
|
|
2965
|
+
return [4 /*yield*/, response.text()];
|
|
2966
|
+
case 4:
|
|
2967
|
+
_c = _d.sent();
|
|
2968
|
+
return [3 /*break*/, 6];
|
|
2969
|
+
case 5:
|
|
2970
|
+
_c = undefined;
|
|
2971
|
+
_d.label = 6;
|
|
2972
|
+
case 6:
|
|
2973
|
+
text = _c;
|
|
2974
|
+
isJson = (response.headers.get('content-type') || '').includes('application/json');
|
|
2975
|
+
if (testCase.expect.error !== undefined) {
|
|
2976
|
+
// the routes that answer an envelope - the two deploy routes - send
|
|
2977
|
+
// `{status, message}`; the rest send the message itself as text, which is
|
|
2978
|
+
// what the server's `res.status(code).send(err.message)` does
|
|
2979
|
+
if (isJson) {
|
|
2980
|
+
body = JSON.parse(text);
|
|
2981
|
+
expect(body.message).toBe(testCase.expect.error);
|
|
2982
|
+
// a refusal that states the envelope it expects is compared on the whole
|
|
2983
|
+
// of it, not only on the message
|
|
2984
|
+
if (testCase.expect.body !== undefined)
|
|
2985
|
+
expect(body).toEqual(testCase.expect.body);
|
|
2986
|
+
}
|
|
2987
|
+
else {
|
|
2988
|
+
// a case cannot state an envelope and be answered with text: one of the
|
|
2989
|
+
// two is wrong, and silence would hide which
|
|
2990
|
+
if (testCase.expect.body !== undefined)
|
|
2991
|
+
throw new Error("".concat(testCase.id, ": the answer is text, so its body cannot be asserted"));
|
|
2992
|
+
expect(text).toBe(testCase.expect.error);
|
|
2993
|
+
}
|
|
2994
|
+
return [2 /*return*/];
|
|
2995
|
+
}
|
|
2996
|
+
if (testCase.expect.contents !== undefined) {
|
|
2997
|
+
if (bytes !== undefined)
|
|
2998
|
+
expect(bytes).toEqual(testCase.expect.contents);
|
|
2999
|
+
else
|
|
3000
|
+
expect(text).toEqual(testCase.expect.contents);
|
|
3001
|
+
return [2 /*return*/];
|
|
3002
|
+
}
|
|
3003
|
+
if (testCase.expect.body !== undefined)
|
|
3004
|
+
expect(JSON.parse(text)).toEqual(testCase.expect.body);
|
|
3005
|
+
if (testCase.expect.partial !== undefined)
|
|
3006
|
+
expect(JSON.parse(text)).toMatchObject(testCase.expect.partial);
|
|
3007
|
+
return [2 /*return*/];
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
}); };
|
|
2744
3011
|
var _loop_1 = function (testCase) {
|
|
2745
3012
|
it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2746
|
-
var worker, _a, requestPath, init, response
|
|
2747
|
-
return __generator(this, function (
|
|
2748
|
-
switch (
|
|
3013
|
+
var worker, _a, requestPath, init, response;
|
|
3014
|
+
return __generator(this, function (_b) {
|
|
3015
|
+
switch (_b.label) {
|
|
2749
3016
|
case 0:
|
|
2750
3017
|
worker = loadWorker();
|
|
2751
3018
|
return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
|
|
2752
3019
|
case 1:
|
|
2753
|
-
|
|
3020
|
+
_b.sent();
|
|
2754
3021
|
worker.refuseWrites(Boolean(testCase.unwritable));
|
|
2755
3022
|
_a = httpFor(testCase), requestPath = _a.path, init = _a.init;
|
|
2756
3023
|
return [4 /*yield*/, worker.dispatch(requestPath, init)];
|
|
2757
3024
|
case 2:
|
|
2758
|
-
response =
|
|
2759
|
-
|
|
2760
|
-
if (!(testCase.expect.error !== undefined)) return [3 /*break*/, 4];
|
|
2761
|
-
return [4 /*yield*/, response.text()
|
|
2762
|
-
// the routes that answer an envelope - the two deploy routes - send
|
|
2763
|
-
// `{status, message}`; the rest send the message itself as text, which
|
|
2764
|
-
// is what the server's `res.status(code).send(err.message)` does
|
|
2765
|
-
];
|
|
3025
|
+
response = _b.sent();
|
|
3026
|
+
return [4 /*yield*/, assertExpectations(response, testCase)];
|
|
2766
3027
|
case 3:
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
3028
|
+
_b.sent();
|
|
3029
|
+
return [2 /*return*/];
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
}); });
|
|
3033
|
+
};
|
|
3034
|
+
try {
|
|
3035
|
+
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()) {
|
|
3036
|
+
var testCase = DRIVE_CORPUS_1_1.value;
|
|
3037
|
+
_loop_1(testCase);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
catch (e_6_1) { e_6 = { error: e_6_1 }; }
|
|
3041
|
+
finally {
|
|
3042
|
+
try {
|
|
3043
|
+
if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
|
|
3044
|
+
}
|
|
3045
|
+
finally { if (e_6) throw e_6.error; }
|
|
3046
|
+
}
|
|
3047
|
+
/** The app a removal case names, as the corpus states it. */
|
|
3048
|
+
var CORPUS_APP = {
|
|
3049
|
+
appLoc: '/Public/myApp',
|
|
3050
|
+
streamWebFolder: 'web',
|
|
3051
|
+
publishedAt: '2026-01-01T00:00:00.000Z'
|
|
3052
|
+
};
|
|
3053
|
+
/**
|
|
3054
|
+
* A second app that no removal case names.
|
|
3055
|
+
*
|
|
3056
|
+
* Without it, a removal that ignored the id and deleted whatever was
|
|
3057
|
+
* published would pass both removal cases - the unknown-id one sees an empty
|
|
3058
|
+
* index either way. With it, the assertion after the answer is that the app
|
|
3059
|
+
* the id did *not* name is still listed.
|
|
3060
|
+
*/
|
|
3061
|
+
var BYSTANDER = {
|
|
3062
|
+
id: '0b9a1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d',
|
|
3063
|
+
name: 'bystander',
|
|
3064
|
+
appLoc: '/Public/bystander',
|
|
3065
|
+
streamWebFolder: 'web',
|
|
3066
|
+
publishedAt: '2026-01-01T00:00:00.000Z'
|
|
3067
|
+
};
|
|
3068
|
+
/**
|
|
3069
|
+
* The index a removal case starts from - built from the id the case asks to
|
|
3070
|
+
* remove, not from the answer it expects, so a host that removed some other
|
|
3071
|
+
* app would answer a different id and fail.
|
|
3072
|
+
*/
|
|
3073
|
+
var seedIndex = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
3074
|
+
var id, named, entries;
|
|
3075
|
+
var _a, _b, _c;
|
|
3076
|
+
return __generator(this, function (_d) {
|
|
3077
|
+
switch (_d.label) {
|
|
3078
|
+
case 0:
|
|
3079
|
+
if (testCase.route !== 'DELETE /SASjsApi/drive/deploy')
|
|
3080
|
+
return [2 /*return*/];
|
|
3081
|
+
id = (_a = testCase.request.body) === null || _a === void 0 ? void 0 : _a.id;
|
|
3082
|
+
named = (_b = testCase.expect.partial) === null || _b === void 0 ? void 0 : _b.app;
|
|
3083
|
+
entries = [BYSTANDER];
|
|
3084
|
+
// the app the case expects to remove is published only when the case names
|
|
3085
|
+
// the id it removes: the unknown-id case asks for one that is not there, and
|
|
3086
|
+
// publishing it would make that case pass by accident. It goes in *after* the
|
|
3087
|
+
// bystander, so a removal that takes the first entry rather than the named
|
|
3088
|
+
// one removes the bystander and both assertions below fail.
|
|
3089
|
+
if (id !== undefined && (named === null || named === void 0 ? void 0 : named.id) === id)
|
|
3090
|
+
entries.push(__assign({ id: id, name: (_c = named.name) !== null && _c !== void 0 ? _c : 'myApp' }, CORPUS_APP));
|
|
3091
|
+
return [4 /*yield*/, runtime.load({
|
|
3092
|
+
'/sasjs/drive/files/.sasjs/apps.json': JSON.stringify(entries)
|
|
3093
|
+
})];
|
|
3094
|
+
case 1:
|
|
3095
|
+
_d.sent();
|
|
3096
|
+
return [2 /*return*/];
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
}); };
|
|
3100
|
+
/**
|
|
3101
|
+
* The archive a client uploads, built from the tree the case states.
|
|
3102
|
+
*
|
|
3103
|
+
* The corpus is transport-agnostic: an upload case gives the tree the archive
|
|
3104
|
+
* has to expand to, and the folder in the query where the document puts it.
|
|
3105
|
+
* This host takes an archive, so the harness makes one - which is the only way
|
|
3106
|
+
* the mock's own expansion, and the tree it produces, gets exercised by the
|
|
3107
|
+
* cases that describe it.
|
|
3108
|
+
*/
|
|
3109
|
+
var archiveOf = function (tree) { return __awaiter(void 0, void 0, void 0, function () {
|
|
3110
|
+
var files, walk;
|
|
3111
|
+
return __generator(this, function (_a) {
|
|
3112
|
+
files = {};
|
|
3113
|
+
walk = function (members, prefix) {
|
|
3114
|
+
var e_11, _a;
|
|
3115
|
+
var _b;
|
|
3116
|
+
try {
|
|
3117
|
+
for (var _c = __values(members || []), _d = _c.next(); !_d.done; _d = _c.next()) {
|
|
3118
|
+
var member = _d.value;
|
|
3119
|
+
var at = prefix ? "".concat(prefix, "/").concat(member.name) : member.name;
|
|
3120
|
+
if (member.type === 'folder')
|
|
3121
|
+
walk(member.members, at);
|
|
2776
3122
|
else
|
|
2777
|
-
|
|
2778
|
-
|
|
3123
|
+
files[at] =
|
|
3124
|
+
member.encoding === 'base64'
|
|
3125
|
+
? new Uint8Array(Buffer.from(String(member.code), 'base64'))
|
|
3126
|
+
: String((_b = member.code) !== null && _b !== void 0 ? _b : '');
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
catch (e_11_1) { e_11 = { error: e_11_1 }; }
|
|
3130
|
+
finally {
|
|
3131
|
+
try {
|
|
3132
|
+
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
|
|
3133
|
+
}
|
|
3134
|
+
finally { if (e_11) throw e_11.error; }
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
walk(tree === null || tree === void 0 ? void 0 : tree.members, '');
|
|
3138
|
+
return [2 /*return*/, zipOf(files)];
|
|
3139
|
+
});
|
|
3140
|
+
}); };
|
|
3141
|
+
/**
|
|
3142
|
+
* The request a deploy case describes, as HTTP.
|
|
3143
|
+
*
|
|
3144
|
+
* The JSON and removal routes carry their fields as they are stated. The upload
|
|
3145
|
+
* route's body is an archive, so the tree becomes one and the fields the case
|
|
3146
|
+
* states beside it travel as form fields - a host's multipart body, with the
|
|
3147
|
+
* folder also in the query where the document puts it.
|
|
3148
|
+
*/
|
|
3149
|
+
var deployRequest = function (testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
3150
|
+
var _a, requestPath, init, body, archive, form, _b, _c, _d, field, value;
|
|
3151
|
+
var e_12, _e;
|
|
3152
|
+
return __generator(this, function (_f) {
|
|
3153
|
+
switch (_f.label) {
|
|
3154
|
+
case 0:
|
|
3155
|
+
_a = httpFor(testCase), requestPath = _a.path, init = _a.init;
|
|
3156
|
+
if (!testCase.route.endsWith('/deploy/upload'))
|
|
3157
|
+
return [2 /*return*/, { requestPath: requestPath, init: init }];
|
|
3158
|
+
body = (testCase.request.body || {});
|
|
3159
|
+
return [4 /*yield*/, archiveOf(body.fileTree)];
|
|
3160
|
+
case 1:
|
|
3161
|
+
archive = _f.sent();
|
|
3162
|
+
form = new FormData();
|
|
3163
|
+
try {
|
|
3164
|
+
for (_b = __values(Object.entries(body)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
3165
|
+
_d = __read(_c.value, 2), field = _d[0], value = _d[1];
|
|
3166
|
+
if (field !== 'fileTree' && value !== undefined)
|
|
3167
|
+
form.set(field, String(value));
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
catch (e_12_1) { e_12 = { error: e_12_1 }; }
|
|
3171
|
+
finally {
|
|
3172
|
+
try {
|
|
3173
|
+
if (_c && !_c.done && (_e = _b.return)) _e.call(_b);
|
|
3174
|
+
}
|
|
3175
|
+
finally { if (e_12) throw e_12.error; }
|
|
3176
|
+
}
|
|
3177
|
+
form.set('file', new Blob([new Uint8Array(archive)]), 'deploy.zip');
|
|
3178
|
+
return [2 /*return*/, { requestPath: requestPath, init: { method: 'POST', body: form } }];
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
}); };
|
|
3182
|
+
var _loop_2 = function (testCase) {
|
|
3183
|
+
it(testCase.id, function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
3184
|
+
var worker, _a, requestPath, init, response, listing, ids, _b, _c, path_6, file, e_13_1, _d, _e, path_7, file, e_14_1;
|
|
3185
|
+
var e_13, _f, e_14, _g;
|
|
3186
|
+
var _h, _j, _k;
|
|
3187
|
+
return __generator(this, function (_l) {
|
|
3188
|
+
switch (_l.label) {
|
|
3189
|
+
case 0:
|
|
3190
|
+
worker = loadWorker();
|
|
3191
|
+
return [4 /*yield*/, seedDrive(worker.runtime, testCase)];
|
|
3192
|
+
case 1:
|
|
3193
|
+
_l.sent();
|
|
3194
|
+
return [4 /*yield*/, seedIndex(worker.runtime, testCase)];
|
|
3195
|
+
case 2:
|
|
3196
|
+
_l.sent();
|
|
3197
|
+
worker.refuseWrites(Boolean(testCase.unwritable));
|
|
3198
|
+
return [4 /*yield*/, deployRequest(testCase)];
|
|
3199
|
+
case 3:
|
|
3200
|
+
_a = _l.sent(), requestPath = _a.requestPath, init = _a.init;
|
|
3201
|
+
return [4 /*yield*/, worker.dispatch(requestPath, init)];
|
|
2779
3202
|
case 4:
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
3203
|
+
response = _l.sent();
|
|
3204
|
+
return [4 /*yield*/, assertExpectations(response, testCase)
|
|
3205
|
+
// a removal takes the app the id names, and leaves the rest published
|
|
3206
|
+
];
|
|
2784
3207
|
case 5:
|
|
2785
|
-
|
|
2786
|
-
return [3 /*break*/, 8];
|
|
3208
|
+
_l.sent();
|
|
3209
|
+
if (!(testCase.route === 'DELETE /SASjsApi/drive/deploy')) return [3 /*break*/, 8];
|
|
3210
|
+
return [4 /*yield*/, worker.dispatch('/app/apps.json')];
|
|
2787
3211
|
case 6:
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
_d = Uint8Array.bind;
|
|
2791
|
-
return [4 /*yield*/, response.arrayBuffer()];
|
|
3212
|
+
listing = _l.sent();
|
|
3213
|
+
return [4 /*yield*/, listing.json()];
|
|
2792
3214
|
case 7:
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
3215
|
+
ids = (_l.sent()).apps.map(function (app) { return app.id; });
|
|
3216
|
+
expect(ids).toContain(BYSTANDER.id);
|
|
3217
|
+
if (response.status === 200)
|
|
3218
|
+
expect(ids).not.toContain((_h = testCase.request.body) === null || _h === void 0 ? void 0 : _h.id);
|
|
3219
|
+
_l.label = 8;
|
|
3220
|
+
case 8:
|
|
3221
|
+
_l.trys.push([8, 13, 14, 15]);
|
|
3222
|
+
_b = __values(((_j = testCase.expectDrive) === null || _j === void 0 ? void 0 : _j.present) || []), _c = _b.next();
|
|
3223
|
+
_l.label = 9;
|
|
3224
|
+
case 9:
|
|
3225
|
+
if (!!_c.done) return [3 /*break*/, 12];
|
|
3226
|
+
path_6 = _c.value;
|
|
3227
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent(path_6))];
|
|
3228
|
+
case 10:
|
|
3229
|
+
file = _l.sent();
|
|
3230
|
+
expect({ path: path_6, status: file.status }).toEqual({ path: path_6, status: 200 });
|
|
3231
|
+
_l.label = 11;
|
|
3232
|
+
case 11:
|
|
3233
|
+
_c = _b.next();
|
|
3234
|
+
return [3 /*break*/, 9];
|
|
3235
|
+
case 12: return [3 /*break*/, 15];
|
|
3236
|
+
case 13:
|
|
3237
|
+
e_13_1 = _l.sent();
|
|
3238
|
+
e_13 = { error: e_13_1 };
|
|
3239
|
+
return [3 /*break*/, 15];
|
|
3240
|
+
case 14:
|
|
3241
|
+
try {
|
|
3242
|
+
if (_c && !_c.done && (_f = _b.return)) _f.call(_b);
|
|
3243
|
+
}
|
|
3244
|
+
finally { if (e_13) throw e_13.error; }
|
|
3245
|
+
return [7 /*endfinally*/];
|
|
3246
|
+
case 15:
|
|
3247
|
+
_l.trys.push([15, 20, 21, 22]);
|
|
3248
|
+
_d = __values(((_k = testCase.expectDrive) === null || _k === void 0 ? void 0 : _k.missing) || []), _e = _d.next();
|
|
3249
|
+
_l.label = 16;
|
|
3250
|
+
case 16:
|
|
3251
|
+
if (!!_e.done) return [3 /*break*/, 19];
|
|
3252
|
+
path_7 = _e.value;
|
|
3253
|
+
return [4 /*yield*/, worker.dispatch('/SASjsApi/drive/file?_filePath=' + encodeURIComponent(path_7))];
|
|
3254
|
+
case 17:
|
|
3255
|
+
file = _l.sent();
|
|
3256
|
+
expect({ path: path_7, status: file.status }).toEqual({ path: path_7, status: 404 });
|
|
3257
|
+
_l.label = 18;
|
|
3258
|
+
case 18:
|
|
3259
|
+
_e = _d.next();
|
|
3260
|
+
return [3 /*break*/, 16];
|
|
3261
|
+
case 19: return [3 /*break*/, 22];
|
|
3262
|
+
case 20:
|
|
3263
|
+
e_14_1 = _l.sent();
|
|
3264
|
+
e_14 = { error: e_14_1 };
|
|
3265
|
+
return [3 /*break*/, 22];
|
|
3266
|
+
case 21:
|
|
3267
|
+
try {
|
|
3268
|
+
if (_e && !_e.done && (_g = _d.return)) _g.call(_d);
|
|
3269
|
+
}
|
|
3270
|
+
finally { if (e_14) throw e_14.error; }
|
|
3271
|
+
return [7 /*endfinally*/];
|
|
3272
|
+
case 22: return [2 /*return*/];
|
|
2796
3273
|
}
|
|
2797
3274
|
});
|
|
2798
3275
|
}); });
|
|
2799
3276
|
};
|
|
2800
3277
|
try {
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
3278
|
+
/**
|
|
3279
|
+
* The deploy routes: the JSON form and the removal, plus the upload route
|
|
3280
|
+
* through an archive this harness builds from the tree the case states.
|
|
3281
|
+
*
|
|
3282
|
+
* The corpus describes an upload by what the archive has to expand to, so a
|
|
3283
|
+
* host that takes bytes can run the same case as one that is handed an
|
|
3284
|
+
* expansion - `deployRequest` makes the archive, and what it asserts is this
|
|
3285
|
+
* worker's own unzipping and tree-building rather than the corpus's
|
|
3286
|
+
* assumption. The one case that cannot travel this way is the expansion that is
|
|
3287
|
+
* not a tree, which the corpus marks `transport: 'expansion'`.
|
|
3288
|
+
*
|
|
3289
|
+
* The removal cases need an app to remove, so the index is seeded with the one
|
|
3290
|
+
* the case names and a bystander that must survive it. The state a case leaves
|
|
3291
|
+
* is checked too - `expectDrive` - because both deploy answers are `200` and
|
|
3292
|
+
* replacing a folder only differs from keeping it in what the drive holds
|
|
3293
|
+
* afterwards.
|
|
3294
|
+
*/
|
|
3295
|
+
for (var _c = __values(api_1.DEPLOY_CORPUS.filter(
|
|
3296
|
+
// a case a host that takes an archive cannot produce, which the corpus marks
|
|
3297
|
+
// on the case itself rather than leaving each host to name it
|
|
3298
|
+
function (entry) { return entry.transport !== 'expansion'; })), _d = _c.next(); !_d.done; _d = _c.next()) {
|
|
3299
|
+
var testCase = _d.value;
|
|
3300
|
+
_loop_2(testCase);
|
|
2804
3301
|
}
|
|
2805
3302
|
}
|
|
2806
|
-
catch (
|
|
3303
|
+
catch (e_7_1) { e_7 = { error: e_7_1 }; }
|
|
2807
3304
|
finally {
|
|
2808
3305
|
try {
|
|
2809
|
-
if (
|
|
3306
|
+
if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
|
|
2810
3307
|
}
|
|
2811
|
-
finally { if (
|
|
3308
|
+
finally { if (e_7) throw e_7.error; }
|
|
2812
3309
|
}
|
|
2813
3310
|
});
|
|
2814
3311
|
/**
|
|
@@ -2820,7 +3317,7 @@ describe('service worker host', function () {
|
|
|
2820
3317
|
* are the package's.
|
|
2821
3318
|
*/
|
|
2822
3319
|
describe('identity and record conformance', function () {
|
|
2823
|
-
var
|
|
3320
|
+
var e_15, _a;
|
|
2824
3321
|
/**
|
|
2825
3322
|
* Boots the worker from a fixture, the way the page does: the case's own
|
|
2826
3323
|
* identity, and `subject: null` for no implicit caller, so a case that
|
|
@@ -2853,7 +3350,7 @@ describe('service worker host', function () {
|
|
|
2853
3350
|
};
|
|
2854
3351
|
/** The request a case describes, as HTTP: path parameters filled, token sent. */
|
|
2855
3352
|
var httpForCase = function (testCase) {
|
|
2856
|
-
var
|
|
3353
|
+
var e_16, _a;
|
|
2857
3354
|
var _b = __read(testCase.route.split(' '), 2), method = _b[0], routePath = _b[1];
|
|
2858
3355
|
var filled = routePath.replace(/\{(\w+)\}/g, function (_match, name) {
|
|
2859
3356
|
return encodeURIComponent(String((testCase.request.params || {})[name]));
|
|
@@ -2866,12 +3363,12 @@ describe('service worker host', function () {
|
|
|
2866
3363
|
query.set(key, String(value));
|
|
2867
3364
|
}
|
|
2868
3365
|
}
|
|
2869
|
-
catch (
|
|
3366
|
+
catch (e_16_1) { e_16 = { error: e_16_1 }; }
|
|
2870
3367
|
finally {
|
|
2871
3368
|
try {
|
|
2872
3369
|
if (_d && !_d.done && (_a = _c.return)) _a.call(_c);
|
|
2873
3370
|
}
|
|
2874
|
-
finally { if (
|
|
3371
|
+
finally { if (e_16) throw e_16.error; }
|
|
2875
3372
|
}
|
|
2876
3373
|
var suffix = query.toString() ? "?".concat(query) : '';
|
|
2877
3374
|
var headers = {};
|
|
@@ -2965,7 +3462,7 @@ describe('service worker host', function () {
|
|
|
2965
3462
|
}
|
|
2966
3463
|
});
|
|
2967
3464
|
}); };
|
|
2968
|
-
var
|
|
3465
|
+
var _loop_3 = function (testCase) {
|
|
2969
3466
|
it("".concat(testCase.id, ": ").concat(testCase.description), function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
2970
3467
|
var worker, _a, requestPath, init, _b, _c;
|
|
2971
3468
|
return __generator(this, function (_d) {
|
|
@@ -2990,15 +3487,15 @@ describe('service worker host', function () {
|
|
|
2990
3487
|
try {
|
|
2991
3488
|
for (var _b = __values(__spreadArray(__spreadArray([], __read(api_1.IDENTITY_CORPUS), false), __read(api_1.RECORD_CORPUS), false)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
|
2992
3489
|
var testCase = _c.value;
|
|
2993
|
-
|
|
3490
|
+
_loop_3(testCase);
|
|
2994
3491
|
}
|
|
2995
3492
|
}
|
|
2996
|
-
catch (
|
|
3493
|
+
catch (e_15_1) { e_15 = { error: e_15_1 }; }
|
|
2997
3494
|
finally {
|
|
2998
3495
|
try {
|
|
2999
3496
|
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
|
3000
3497
|
}
|
|
3001
|
-
finally { if (
|
|
3498
|
+
finally { if (e_15) throw e_15.error; }
|
|
3002
3499
|
}
|
|
3003
3500
|
it('answers as the fixture administrator when no subject was chosen', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
3004
3501
|
var worker, response, rules;
|
package/package.json
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@sasjs/adapter": "6.0.2",
|
|
74
|
-
"@sasjs/api": "0.
|
|
74
|
+
"@sasjs/api": "0.28.0",
|
|
75
75
|
"@sasjs/core": "7.1.12",
|
|
76
76
|
"@sasjs/lint": "5.1.1",
|
|
77
77
|
"@sasjs/utils": "4.11.0",
|
|
@@ -125,5 +125,5 @@
|
|
|
125
125
|
"ws": "8.21.0",
|
|
126
126
|
"nanoid": "3.3.18"
|
|
127
127
|
},
|
|
128
|
-
"version": "6.27.
|
|
128
|
+
"version": "6.27.2"
|
|
129
129
|
}
|