@sasjs/cli 6.19.3 → 6.20.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.
- package/build/commands/build/internal/mockSite/assets/intercept.js +4 -1
- package/build/commands/build/internal/mockSite/assets/sw.js +77 -24
- package/build/commands/build/internal/mockSite/spec/intercept.spec.js +23 -2
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +275 -70
- package/package.json +1 -1
|
@@ -125,8 +125,11 @@
|
|
|
125
125
|
xhr.dispatchEvent(new ProgressEvent('loadend'))
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// `/api` is the short spelling of the API and `/SASjsApi` the canonical one:
|
|
129
|
+
// the build bridges both, so a caller that uses either reaches the mock.
|
|
128
130
|
const isSasjs = (url) =>
|
|
129
|
-
typeof url === 'string' &&
|
|
131
|
+
typeof url === 'string' &&
|
|
132
|
+
(url.indexOf('/SASjsApi/') >= 0 || url.indexOf('/api/') >= 0)
|
|
130
133
|
|
|
131
134
|
const proto = XMLHttpRequest.prototype
|
|
132
135
|
const nativeOpen = proto.open
|
|
@@ -327,6 +327,15 @@ const RUNTIME = new BrowserRuntime()
|
|
|
327
327
|
*/
|
|
328
328
|
const BASE = (self.location.pathname || '').replace(/\/sw\.js.*$/, '')
|
|
329
329
|
|
|
330
|
+
/**
|
|
331
|
+
* The short spelling of the App Stream landing page.
|
|
332
|
+
*
|
|
333
|
+
* It is the default: the interface links to `/app`, and sasjs/server serves it
|
|
334
|
+
* there while keeping `/AppStream` as a redirect. The worker mirrors that, so a
|
|
335
|
+
* link the interface builds reaches the same page on the static demo.
|
|
336
|
+
*/
|
|
337
|
+
const SHORT_APP_STREAM_PATH = '/app'
|
|
338
|
+
|
|
330
339
|
const withoutBase = (pathname) =>
|
|
331
340
|
BASE && pathname.startsWith(BASE)
|
|
332
341
|
? pathname.slice(BASE.length) || '/'
|
|
@@ -575,6 +584,19 @@ const payloadTooLarge = (limit) => {
|
|
|
575
584
|
*/
|
|
576
585
|
const readJsonBody = async (request) => {
|
|
577
586
|
const body = await readJson(request)
|
|
587
|
+
|
|
588
|
+
// The bridge forwards a JSON body as `json` - a request the page made with
|
|
589
|
+
// `JSON.stringify` has no fields for the envelope it wraps everything in, so
|
|
590
|
+
// the payload is the whole point of the message. Reading only `params` here
|
|
591
|
+
// dropped it: `code/execute` and `code/lint` arrived with no code.
|
|
592
|
+
if (
|
|
593
|
+
body &&
|
|
594
|
+
typeof body === 'object' &&
|
|
595
|
+
body.json &&
|
|
596
|
+
typeof body.json === 'object'
|
|
597
|
+
)
|
|
598
|
+
return { body: body.json, files: [] }
|
|
599
|
+
|
|
578
600
|
if (
|
|
579
601
|
body &&
|
|
580
602
|
typeof body === 'object' &&
|
|
@@ -876,6 +898,11 @@ const contentsResponse = (answer, path) => {
|
|
|
876
898
|
|
|
877
899
|
const routeRequest = async (request, url) => {
|
|
878
900
|
const path = withoutBase(url.pathname).replace(/\/+$/, '') || '/'
|
|
901
|
+
// `/api` is the short spelling of the API, and `/SASjsApi` the canonical one:
|
|
902
|
+
// both are answered, so the alias is stripped once here and everything below -
|
|
903
|
+
// the route lookup, the drive routes' plain-text refusals, the log - works
|
|
904
|
+
// from one path.
|
|
905
|
+
const apiPath = path.replace(/^\/api(?=\/|$)/i, '/SASjsApi')
|
|
879
906
|
const query = {}
|
|
880
907
|
for (const [k, v] of url.searchParams.entries()) query[k] = v
|
|
881
908
|
|
|
@@ -888,27 +915,31 @@ const routeRequest = async (request, url) => {
|
|
|
888
915
|
// every visit. Unanswered, those requests reach the static host and the page
|
|
889
916
|
// parses this site's index.html as JSON.
|
|
890
917
|
//
|
|
891
|
-
//
|
|
892
|
-
//
|
|
893
|
-
//
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
918
|
+
// `/app` is the spelling the interface links to and the server serves; the
|
|
919
|
+
// canonical `/AppStream` still answers, because a client built before the
|
|
920
|
+
// short paths existed asks for it. The answers are the same either way, so
|
|
921
|
+
// `/AppStream` redirects to `/app` - exactly as the server redirects it -
|
|
922
|
+
// and the landing page, the listing and the missing-name refusal are decided
|
|
923
|
+
// on both.
|
|
924
|
+
//
|
|
925
|
+
// The listing is empty rather than read from `registry()`, which records this
|
|
926
|
+
// deployment's own folder so the delete route has something to remove: the
|
|
927
|
+
// stream really does serve nothing at `/app/<name>/` here, and a listing that
|
|
928
|
+
// named it would offer a tile that 404s.
|
|
929
|
+
// `path` has its trailing slashes stripped, and the landing answers differ by
|
|
930
|
+
// exactly that, so they are decided on the request's own pathname - minus the
|
|
931
|
+
// folder the site is served from, which the patterns do not name.
|
|
901
932
|
const stream = withoutBase(url.pathname)
|
|
902
933
|
|
|
903
934
|
if (/^\/AppStream$/i.test(stream))
|
|
904
935
|
return new Response(null, {
|
|
905
936
|
status: 301,
|
|
906
|
-
headers: { location: `${BASE}
|
|
937
|
+
headers: { location: `${BASE}${SHORT_APP_STREAM_PATH}` }
|
|
907
938
|
})
|
|
908
939
|
|
|
909
940
|
if (
|
|
910
|
-
/^\/AppStream
|
|
911
|
-
/^\/AppStream\/index\.html$/i.test(stream)
|
|
941
|
+
/^\/(app|AppStream)\/?$/i.test(stream) ||
|
|
942
|
+
/^\/(app|AppStream)\/index\.html$/i.test(stream)
|
|
912
943
|
)
|
|
913
944
|
return new Response(
|
|
914
945
|
[
|
|
@@ -921,9 +952,10 @@ const routeRequest = async (request, url) => {
|
|
|
921
952
|
{ status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } }
|
|
922
953
|
)
|
|
923
954
|
|
|
924
|
-
if (/^\/AppStream\/apps\.json$/i.test(stream))
|
|
955
|
+
if (/^\/(app|AppStream)\/apps\.json$/i.test(stream))
|
|
956
|
+
return json({ apps: [] })
|
|
925
957
|
|
|
926
|
-
if (/^\/AppStream\//i.test(stream)) {
|
|
958
|
+
if (/^\/(app|AppStream)\//i.test(stream)) {
|
|
927
959
|
return json(
|
|
928
960
|
{
|
|
929
961
|
message:
|
|
@@ -935,8 +967,9 @@ const routeRequest = async (request, url) => {
|
|
|
935
967
|
|
|
936
968
|
// The package's own matcher, so a route is recognised however its path is
|
|
937
969
|
// cased and its parameters are read the way the document states them.
|
|
938
|
-
const matched = API.findRoute(request.method,
|
|
939
|
-
if (!matched)
|
|
970
|
+
const matched = API.findRoute(request.method, apiPath)
|
|
971
|
+
if (!matched)
|
|
972
|
+
return json({ message: 'Not part of the API: ' + apiPath }, 404)
|
|
940
973
|
|
|
941
974
|
const routeId = matched.route.id
|
|
942
975
|
const subject = await subjectOf(request, query)
|
|
@@ -988,8 +1021,21 @@ const routeRequest = async (request, url) => {
|
|
|
988
1021
|
return json(answer.body, answer.status)
|
|
989
1022
|
} catch (e) {
|
|
990
1023
|
const message = (e && e.stack) || String(e)
|
|
991
|
-
|
|
992
|
-
|
|
1024
|
+
const status = (e && e.status) || 500
|
|
1025
|
+
const refusal = status >= 400 && status < 500
|
|
1026
|
+
|
|
1027
|
+
// A 4xx is an ANSWER: the API decided, the caller was told, and a client
|
|
1028
|
+
// that expected it (Studio creates its default program and accepts the 409)
|
|
1029
|
+
// has already handled it. Broadcasting it put four console errors on a page
|
|
1030
|
+
// whose only fault was opening a file that was already there. A 5xx is the
|
|
1031
|
+
// host failing, and that is worth reporting to the page.
|
|
1032
|
+
logEntry(
|
|
1033
|
+
`${request.method} ${path} ${refusal ? 'refused' : 'ERROR'} ${
|
|
1034
|
+
(e && e.message) || message
|
|
1035
|
+
}`
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
if (!refusal) broadcast({ type: 'sasjs-host-error', path, message })
|
|
993
1039
|
// A refusal the API decided on carries its status and, where the route
|
|
994
1040
|
// answers an envelope, the body to send. The drive's routes send the message
|
|
995
1041
|
// itself as plain text - the server answers them with `res.status(code)
|
|
@@ -997,7 +1043,7 @@ const routeRequest = async (request, url) => {
|
|
|
997
1043
|
// envelope the other routes already answer with.
|
|
998
1044
|
if (API.isApiError(e)) {
|
|
999
1045
|
if (e.body !== undefined) return json(e.body, e.status)
|
|
1000
|
-
if (
|
|
1046
|
+
if (apiPath.includes('/SASjsApi/drive/')) return text(e.message, e.status)
|
|
1001
1047
|
return json({ message: e.message }, e.status)
|
|
1002
1048
|
}
|
|
1003
1049
|
return json(
|
|
@@ -1053,10 +1099,17 @@ self.addEventListener('fetch', (event) => {
|
|
|
1053
1099
|
// The host's own paths are answered here, including one the table does not
|
|
1054
1100
|
// have - falling through to the static host would put the SPA's index.html
|
|
1055
1101
|
// behind a typo in a caller's URL, or behind a listing an interface parses as
|
|
1056
|
-
// JSON. `/
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
|
|
1102
|
+
// JSON. `/app` is one of those: the package serves app deployments, but the
|
|
1103
|
+
// listing is the server's web route, and an interface that lists apps asks for
|
|
1104
|
+
// it on every visit.
|
|
1105
|
+
//
|
|
1106
|
+
// `/api` and `/api/` on their own are the API's documentation page - a file
|
|
1107
|
+
// the build copies into the site - so the worker stays out of the way there
|
|
1108
|
+
// and the static host answers it. Every other `/api/...` is an API call on the
|
|
1109
|
+
// short spelling, which routeRequest canonicalises to `/SASjsApi`.
|
|
1110
|
+
if (/^\/api\/?$/i.test(path)) return
|
|
1111
|
+
|
|
1112
|
+
if (!/^\/(SASjsApi|SASLogon|AppStream|api|app)(\/|$)/i.test(path)) return
|
|
1060
1113
|
event.respondWith(handle(event.request, url))
|
|
1061
1114
|
})
|
|
1062
1115
|
|
|
@@ -173,13 +173,34 @@ describe('page-side bridge', function () {
|
|
|
173
173
|
fetchImpl = jest.fn();
|
|
174
174
|
XHR = loadBridge(fetchImpl).XHR;
|
|
175
175
|
xhr = new XHR();
|
|
176
|
-
xhr.open('GET', '/
|
|
176
|
+
xhr.open('GET', '/health');
|
|
177
177
|
xhr.send();
|
|
178
178
|
expect(xhr.sentWith).toBeUndefined();
|
|
179
179
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
180
180
|
return [2 /*return*/];
|
|
181
181
|
});
|
|
182
182
|
}); });
|
|
183
|
+
it('bridges the short API spelling as well as the canonical one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
184
|
+
var fetchImpl, XHR, xhr, _a, url;
|
|
185
|
+
return __generator(this, function (_b) {
|
|
186
|
+
switch (_b.label) {
|
|
187
|
+
case 0:
|
|
188
|
+
fetchImpl = jest.fn(function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
189
|
+
return [2 /*return*/, jsonResponse('{"status":"ok"}')];
|
|
190
|
+
}); }); });
|
|
191
|
+
XHR = loadBridge(fetchImpl).XHR;
|
|
192
|
+
xhr = new XHR();
|
|
193
|
+
return [4 /*yield*/, run(xhr, 'GET', '/api/info')];
|
|
194
|
+
case 1:
|
|
195
|
+
_b.sent();
|
|
196
|
+
_a = __read(fetchImpl.mock.calls[0], 1), url = _a[0];
|
|
197
|
+
expect(url).toBe('/api/info');
|
|
198
|
+
expect(xhr.status).toBe(200);
|
|
199
|
+
expect(xhr.responseText).toBe('{"status":"ok"}');
|
|
200
|
+
return [2 /*return*/];
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}); });
|
|
183
204
|
it('converts a multipart body to JSON and replays the response onto the XHR', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
184
205
|
var fetchImpl, XHR, form, xhr, _a, init;
|
|
185
206
|
return __generator(this, function (_b) {
|
|
@@ -286,7 +307,7 @@ describe('page-side bridge', function () {
|
|
|
286
307
|
expect(second.XHR.prototype).not.toBe(first.XHR.prototype);
|
|
287
308
|
expect(Object.getPrototypeOf(second.XHR.prototype)).toBe(FakeXHR.prototype);
|
|
288
309
|
var xhr = new second.XHR();
|
|
289
|
-
xhr.open('GET', '/
|
|
310
|
+
xhr.open('GET', '/health');
|
|
290
311
|
xhr.send();
|
|
291
312
|
expect(xhr.nativeSendCount).toBe(1);
|
|
292
313
|
});
|
|
@@ -35,6 +35,17 @@ 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 __values = (this && this.__values) || function(o) {
|
|
39
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
40
|
+
if (m) return m.call(o);
|
|
41
|
+
if (o && typeof o.length === "number") return {
|
|
42
|
+
next: function () {
|
|
43
|
+
if (o && i >= o.length) o = void 0;
|
|
44
|
+
return { value: o && o[i++], done: !o };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
48
|
+
};
|
|
38
49
|
var __read = (this && this.__read) || function (o, n) {
|
|
39
50
|
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
40
51
|
if (!m) return o;
|
|
@@ -51,17 +62,6 @@ var __read = (this && this.__read) || function (o, n) {
|
|
|
51
62
|
}
|
|
52
63
|
return ar;
|
|
53
64
|
};
|
|
54
|
-
var __values = (this && this.__values) || function(o) {
|
|
55
|
-
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
56
|
-
if (m) return m.call(o);
|
|
57
|
-
if (o && typeof o.length === "number") return {
|
|
58
|
-
next: function () {
|
|
59
|
-
if (o && i >= o.length) o = void 0;
|
|
60
|
-
return { value: o && o[i++], done: !o };
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
64
|
-
};
|
|
65
65
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
66
66
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
67
67
|
};
|
|
@@ -115,9 +115,14 @@ var loadWorker = function (base) {
|
|
|
115
115
|
handlers[type].push(fn);
|
|
116
116
|
},
|
|
117
117
|
skipWaiting: function () { },
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
// a test can put a client here to observe what the host broadcasts; with
|
|
119
|
+
// none, a message reaches nobody and nothing can be asserted about it
|
|
120
|
+
clients: {
|
|
121
|
+
claim: function () { },
|
|
122
|
+
matchAll: function () { return __awaiter(void 0, void 0, void 0, function () { var _a; return __generator(this, function (_b) {
|
|
123
|
+
return [2 /*return*/, (_a = global.__mockHostClients) !== null && _a !== void 0 ? _a : []];
|
|
124
|
+
}); }); }
|
|
125
|
+
}
|
|
121
126
|
};
|
|
122
127
|
global.self = self;
|
|
123
128
|
global.importScripts = function (path) {
|
|
@@ -893,51 +898,59 @@ describe('service worker host', function () {
|
|
|
893
898
|
});
|
|
894
899
|
}); });
|
|
895
900
|
it('answers every route under the folder the site is published in', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
896
|
-
var worker, info, _a, page, folder, stream, landing, apps, _b;
|
|
897
|
-
var
|
|
898
|
-
return __generator(this, function (
|
|
899
|
-
switch (
|
|
901
|
+
var worker, info, _a, page, folder, stream, landing, apps, _b, aliased, _c;
|
|
902
|
+
var _d;
|
|
903
|
+
return __generator(this, function (_e) {
|
|
904
|
+
switch (_e.label) {
|
|
900
905
|
case 0:
|
|
901
906
|
worker = loadWorker('/demo');
|
|
902
|
-
worker.runtime.load((
|
|
903
|
-
|
|
904
|
-
|
|
907
|
+
worker.runtime.load((_d = {},
|
|
908
|
+
_d[drivePath('services/public/getdata.js')] = '_webout = "seeded";',
|
|
909
|
+
_d));
|
|
905
910
|
return [4 /*yield*/, worker.dispatch('/demo/SASjsApi/info')];
|
|
906
911
|
case 1:
|
|
907
|
-
info =
|
|
912
|
+
info = _e.sent();
|
|
908
913
|
expect(info.status).toBe(200);
|
|
909
914
|
_a = expect;
|
|
910
915
|
return [4 /*yield*/, info.json()];
|
|
911
916
|
case 2:
|
|
912
|
-
_a.apply(void 0, [(
|
|
917
|
+
_a.apply(void 0, [(_e.sent()).mode]).toBe('desktop');
|
|
913
918
|
return [4 /*yield*/, worker.dispatch('/demo/')];
|
|
914
919
|
case 3:
|
|
915
|
-
page =
|
|
920
|
+
page = _e.sent();
|
|
916
921
|
expect(page.status).toBe(200);
|
|
917
922
|
expect(page.headers.get('content-type')).toContain('text/html');
|
|
918
923
|
return [4 /*yield*/, worker.dispatch('/demo')];
|
|
919
924
|
case 4:
|
|
920
|
-
folder =
|
|
925
|
+
folder = _e.sent();
|
|
921
926
|
expect(folder.status).toBe(200);
|
|
922
927
|
expect(folder.headers.get('content-type')).toContain('text/html');
|
|
923
928
|
return [4 /*yield*/, worker.dispatch('/demo/AppStream')];
|
|
924
929
|
case 5:
|
|
925
|
-
stream =
|
|
930
|
+
stream = _e.sent();
|
|
926
931
|
expect(stream.status).toBe(301);
|
|
927
|
-
expect(stream.headers.get('location')).toBe('/demo/
|
|
928
|
-
return [4 /*yield*/, worker.dispatch('/demo/
|
|
932
|
+
expect(stream.headers.get('location')).toBe('/demo/app');
|
|
933
|
+
return [4 /*yield*/, worker.dispatch('/demo/app')];
|
|
929
934
|
case 6:
|
|
930
|
-
landing =
|
|
935
|
+
landing = _e.sent();
|
|
931
936
|
expect(landing.status).toBe(200);
|
|
932
937
|
expect(landing.headers.get('content-type')).toContain('text/html');
|
|
933
|
-
return [4 /*yield*/, worker.dispatch('/demo/
|
|
938
|
+
return [4 /*yield*/, worker.dispatch('/demo/app/apps.json')];
|
|
934
939
|
case 7:
|
|
935
|
-
apps =
|
|
940
|
+
apps = _e.sent();
|
|
936
941
|
expect(apps.status).toBe(200);
|
|
937
942
|
_b = expect;
|
|
938
943
|
return [4 /*yield*/, apps.json()];
|
|
939
944
|
case 8:
|
|
940
|
-
_b.apply(void 0, [
|
|
945
|
+
_b.apply(void 0, [_e.sent()]).toEqual({ apps: [] });
|
|
946
|
+
return [4 /*yield*/, worker.dispatch('/demo/api/info')];
|
|
947
|
+
case 9:
|
|
948
|
+
aliased = _e.sent();
|
|
949
|
+
expect(aliased.status).toBe(200);
|
|
950
|
+
_c = expect;
|
|
951
|
+
return [4 /*yield*/, aliased.json()];
|
|
952
|
+
case 10:
|
|
953
|
+
_c.apply(void 0, [(_e.sent()).mode]).toBe('desktop');
|
|
941
954
|
// a path outside the folder is not the site's to answer
|
|
942
955
|
expect(worker.dispatch('/other/SASjsApi/info')).toBeUndefined();
|
|
943
956
|
return [2 /*return*/];
|
|
@@ -1233,6 +1246,83 @@ describe('service worker host', function () {
|
|
|
1233
1246
|
}
|
|
1234
1247
|
});
|
|
1235
1248
|
}); });
|
|
1249
|
+
/**
|
|
1250
|
+
* A refusal is the API's answer, and the page that asked gets it. Reporting it
|
|
1251
|
+
* as a host error put console errors on screens that had handled the refusal
|
|
1252
|
+
* themselves - Studio's 409 being the one that started this.
|
|
1253
|
+
*/
|
|
1254
|
+
it('reports a host failure to the page, and stays quiet about a refusal', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1255
|
+
var worker, messages, write, hostErrors, failed, _a, _b;
|
|
1256
|
+
return __generator(this, function (_c) {
|
|
1257
|
+
switch (_c.label) {
|
|
1258
|
+
case 0:
|
|
1259
|
+
worker = seeded();
|
|
1260
|
+
messages = [];
|
|
1261
|
+
global.__mockHostClients = [
|
|
1262
|
+
{ postMessage: function (message) { return messages.push(message); } }
|
|
1263
|
+
];
|
|
1264
|
+
write = function () {
|
|
1265
|
+
var form = new FormData();
|
|
1266
|
+
form.set('file', new Blob(['x']), 'new.txt');
|
|
1267
|
+
return worker.dispatch('/SASjsApi/drive/file?_filePath=' +
|
|
1268
|
+
encodeURIComponent('/Public/new.txt'), { method: 'POST', body: form });
|
|
1269
|
+
};
|
|
1270
|
+
hostErrors = function () {
|
|
1271
|
+
return messages.filter(function (message) { return message.type === 'sasjs-host-error'; }).length;
|
|
1272
|
+
};
|
|
1273
|
+
// a drive that cannot be written: a failure of the host, not an answer
|
|
1274
|
+
worker.refuseWrites(true);
|
|
1275
|
+
return [4 /*yield*/, write()];
|
|
1276
|
+
case 1:
|
|
1277
|
+
failed = _c.sent();
|
|
1278
|
+
expect(failed.status).toBeGreaterThanOrEqual(500);
|
|
1279
|
+
expect(hostErrors()).toBe(1);
|
|
1280
|
+
// a create over a file that is already there: the API answered, and the
|
|
1281
|
+
// caller handles it
|
|
1282
|
+
messages.length = 0;
|
|
1283
|
+
worker.refuseWrites(false);
|
|
1284
|
+
_a = expect;
|
|
1285
|
+
return [4 /*yield*/, write()];
|
|
1286
|
+
case 2:
|
|
1287
|
+
_a.apply(void 0, [(_c.sent()).status]).toBe(200);
|
|
1288
|
+
_b = expect;
|
|
1289
|
+
return [4 /*yield*/, write()];
|
|
1290
|
+
case 3:
|
|
1291
|
+
_b.apply(void 0, [(_c.sent()).status]).toBe(409);
|
|
1292
|
+
expect(hostErrors()).toBe(0);
|
|
1293
|
+
delete global.__mockHostClients;
|
|
1294
|
+
return [2 /*return*/];
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
}); });
|
|
1298
|
+
it('keeps the payload of a JSON body the page bridge forwarded', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1299
|
+
var dispatch, body, response, findings;
|
|
1300
|
+
return __generator(this, function (_a) {
|
|
1301
|
+
switch (_a.label) {
|
|
1302
|
+
case 0:
|
|
1303
|
+
dispatch = seeded().dispatch;
|
|
1304
|
+
body = JSON.stringify({
|
|
1305
|
+
params: {},
|
|
1306
|
+
tables: [],
|
|
1307
|
+
json: { code: 'data _null_;\n x = 1; \nrun;\n', filePath: '/a.sas' }
|
|
1308
|
+
});
|
|
1309
|
+
return [4 /*yield*/, dispatch('/SASjsApi/code/lint', {
|
|
1310
|
+
method: 'POST',
|
|
1311
|
+
headers: { 'content-type': 'application/json' },
|
|
1312
|
+
body: body
|
|
1313
|
+
})];
|
|
1314
|
+
case 1:
|
|
1315
|
+
response = _a.sent();
|
|
1316
|
+
expect(response.status).toBe(200);
|
|
1317
|
+
return [4 /*yield*/, response.json()];
|
|
1318
|
+
case 2:
|
|
1319
|
+
findings = _a.sent();
|
|
1320
|
+
expect(Array.isArray(findings)).toBe(true);
|
|
1321
|
+
expect(JSON.stringify(findings)).toContain('trailing spaces');
|
|
1322
|
+
return [2 /*return*/];
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
}); });
|
|
1236
1326
|
it('answers 404 for a drive file the drive does not hold', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1237
1327
|
var dispatch, response, _a;
|
|
1238
1328
|
return __generator(this, function (_b) {
|
|
@@ -1251,42 +1341,157 @@ describe('service worker host', function () {
|
|
|
1251
1341
|
}
|
|
1252
1342
|
});
|
|
1253
1343
|
}); });
|
|
1254
|
-
it('answers the app stream
|
|
1255
|
-
var dispatch, listing,
|
|
1256
|
-
|
|
1257
|
-
|
|
1344
|
+
it('answers the app stream on the short path, and the canonical one as an alias', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1345
|
+
var dispatch, _a, _b, path_2, listing, _c, e_1_1, _d, _e, path_3, landing, _f, e_2_1, root, canonicalLanding, _g, _h, _j, path_4, app, _k, e_3_1;
|
|
1346
|
+
var e_1, _l, e_2, _m, e_3, _o;
|
|
1347
|
+
return __generator(this, function (_p) {
|
|
1348
|
+
switch (_p.label) {
|
|
1258
1349
|
case 0:
|
|
1259
1350
|
dispatch = seeded().dispatch;
|
|
1260
|
-
|
|
1351
|
+
_p.label = 1;
|
|
1261
1352
|
case 1:
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
return [4 /*yield*/, listing.json()];
|
|
1353
|
+
_p.trys.push([1, 7, 8, 9]);
|
|
1354
|
+
_a = __values(['/app/apps.json', '/AppStream/apps.json']), _b = _a.next();
|
|
1355
|
+
_p.label = 2;
|
|
1266
1356
|
case 2:
|
|
1267
|
-
|
|
1268
|
-
|
|
1357
|
+
if (!!_b.done) return [3 /*break*/, 6];
|
|
1358
|
+
path_2 = _b.value;
|
|
1359
|
+
return [4 /*yield*/, dispatch(path_2)];
|
|
1269
1360
|
case 3:
|
|
1270
|
-
|
|
1271
|
-
expect(
|
|
1272
|
-
expect
|
|
1273
|
-
return [4 /*yield*/,
|
|
1361
|
+
listing = _p.sent();
|
|
1362
|
+
expect(listing.status).toBe(200);
|
|
1363
|
+
_c = expect;
|
|
1364
|
+
return [4 /*yield*/, listing.json()];
|
|
1274
1365
|
case 4:
|
|
1275
|
-
|
|
1366
|
+
_c.apply(void 0, [_p.sent()]).toEqual({ apps: [] });
|
|
1367
|
+
_p.label = 5;
|
|
1368
|
+
case 5:
|
|
1369
|
+
_b = _a.next();
|
|
1370
|
+
return [3 /*break*/, 2];
|
|
1371
|
+
case 6: return [3 /*break*/, 9];
|
|
1372
|
+
case 7:
|
|
1373
|
+
e_1_1 = _p.sent();
|
|
1374
|
+
e_1 = { error: e_1_1 };
|
|
1375
|
+
return [3 /*break*/, 9];
|
|
1376
|
+
case 8:
|
|
1377
|
+
try {
|
|
1378
|
+
if (_b && !_b.done && (_l = _a.return)) _l.call(_a);
|
|
1379
|
+
}
|
|
1380
|
+
finally { if (e_1) throw e_1.error; }
|
|
1381
|
+
return [7 /*endfinally*/];
|
|
1382
|
+
case 9:
|
|
1383
|
+
_p.trys.push([9, 15, 16, 17]);
|
|
1384
|
+
_d = __values(['/app', '/app/', '/app/index.html']), _e = _d.next();
|
|
1385
|
+
_p.label = 10;
|
|
1386
|
+
case 10:
|
|
1387
|
+
if (!!_e.done) return [3 /*break*/, 14];
|
|
1388
|
+
path_3 = _e.value;
|
|
1389
|
+
return [4 /*yield*/, dispatch(path_3)];
|
|
1390
|
+
case 11:
|
|
1391
|
+
landing = _p.sent();
|
|
1276
1392
|
expect(landing.status).toBe(200);
|
|
1277
1393
|
expect(landing.headers.get('content-type')).toContain('text/html');
|
|
1278
|
-
|
|
1394
|
+
_f = expect;
|
|
1279
1395
|
return [4 /*yield*/, landing.text()];
|
|
1280
|
-
case
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
case
|
|
1284
|
-
|
|
1396
|
+
case 12:
|
|
1397
|
+
_f.apply(void 0, [_p.sent()]).toContain('serves no apps');
|
|
1398
|
+
_p.label = 13;
|
|
1399
|
+
case 13:
|
|
1400
|
+
_e = _d.next();
|
|
1401
|
+
return [3 /*break*/, 10];
|
|
1402
|
+
case 14: return [3 /*break*/, 17];
|
|
1403
|
+
case 15:
|
|
1404
|
+
e_2_1 = _p.sent();
|
|
1405
|
+
e_2 = { error: e_2_1 };
|
|
1406
|
+
return [3 /*break*/, 17];
|
|
1407
|
+
case 16:
|
|
1408
|
+
try {
|
|
1409
|
+
if (_e && !_e.done && (_m = _d.return)) _m.call(_d);
|
|
1410
|
+
}
|
|
1411
|
+
finally { if (e_2) throw e_2.error; }
|
|
1412
|
+
return [7 /*endfinally*/];
|
|
1413
|
+
case 17: return [4 /*yield*/, dispatch('/AppStream')];
|
|
1414
|
+
case 18:
|
|
1415
|
+
root = _p.sent();
|
|
1416
|
+
expect(root.status).toBe(301);
|
|
1417
|
+
expect(root.headers.get('location')).toBe('/app');
|
|
1418
|
+
return [4 /*yield*/, dispatch('/AppStream/')];
|
|
1419
|
+
case 19:
|
|
1420
|
+
canonicalLanding = _p.sent();
|
|
1421
|
+
expect(canonicalLanding.status).toBe(200);
|
|
1422
|
+
_g = expect;
|
|
1423
|
+
return [4 /*yield*/, canonicalLanding.text()];
|
|
1424
|
+
case 20:
|
|
1425
|
+
_g.apply(void 0, [_p.sent()]).toContain('serves no apps');
|
|
1426
|
+
_p.label = 21;
|
|
1427
|
+
case 21:
|
|
1428
|
+
_p.trys.push([21, 27, 28, 29]);
|
|
1429
|
+
_h = __values(['/app/Demo/', '/AppStream/Demo/']), _j = _h.next();
|
|
1430
|
+
_p.label = 22;
|
|
1431
|
+
case 22:
|
|
1432
|
+
if (!!_j.done) return [3 /*break*/, 26];
|
|
1433
|
+
path_4 = _j.value;
|
|
1434
|
+
return [4 /*yield*/, dispatch(path_4)];
|
|
1435
|
+
case 23:
|
|
1436
|
+
app = _p.sent();
|
|
1285
1437
|
expect(app.status).toBe(404);
|
|
1286
|
-
|
|
1438
|
+
_k = expect;
|
|
1287
1439
|
return [4 /*yield*/, app.json()];
|
|
1288
|
-
case
|
|
1289
|
-
|
|
1440
|
+
case 24:
|
|
1441
|
+
_k.apply(void 0, [(_p.sent()).message]).toContain('serves no apps');
|
|
1442
|
+
_p.label = 25;
|
|
1443
|
+
case 25:
|
|
1444
|
+
_j = _h.next();
|
|
1445
|
+
return [3 /*break*/, 22];
|
|
1446
|
+
case 26: return [3 /*break*/, 29];
|
|
1447
|
+
case 27:
|
|
1448
|
+
e_3_1 = _p.sent();
|
|
1449
|
+
e_3 = { error: e_3_1 };
|
|
1450
|
+
return [3 /*break*/, 29];
|
|
1451
|
+
case 28:
|
|
1452
|
+
try {
|
|
1453
|
+
if (_j && !_j.done && (_o = _h.return)) _o.call(_h);
|
|
1454
|
+
}
|
|
1455
|
+
finally { if (e_3) throw e_3.error; }
|
|
1456
|
+
return [7 /*endfinally*/];
|
|
1457
|
+
case 29: return [2 /*return*/];
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
}); });
|
|
1461
|
+
it('answers the API on /api as well as on /SASjsApi', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1462
|
+
var dispatch, canonical, short, _a, _b, _c, missing, _d;
|
|
1463
|
+
return __generator(this, function (_e) {
|
|
1464
|
+
switch (_e.label) {
|
|
1465
|
+
case 0:
|
|
1466
|
+
dispatch = seeded().dispatch;
|
|
1467
|
+
return [4 /*yield*/, dispatch('/SASjsApi/info')];
|
|
1468
|
+
case 1:
|
|
1469
|
+
canonical = _e.sent();
|
|
1470
|
+
return [4 /*yield*/, dispatch('/api/info')];
|
|
1471
|
+
case 2:
|
|
1472
|
+
short = _e.sent();
|
|
1473
|
+
expect(short.status).toBe(200);
|
|
1474
|
+
_b = expect;
|
|
1475
|
+
return [4 /*yield*/, short.json()];
|
|
1476
|
+
case 3:
|
|
1477
|
+
_c = (_a = _b.apply(void 0, [_e.sent()])).toEqual;
|
|
1478
|
+
return [4 /*yield*/, canonical.json()];
|
|
1479
|
+
case 4:
|
|
1480
|
+
_c.apply(_a, [_e.sent()]);
|
|
1481
|
+
return [4 /*yield*/, dispatch('/api/drive/file?_filePath=' + encodeURIComponent('/missing.txt'))];
|
|
1482
|
+
case 5:
|
|
1483
|
+
missing = _e.sent();
|
|
1484
|
+
expect(missing.status).toBe(404);
|
|
1485
|
+
expect(missing.headers.get('content-type')).toContain('text/plain');
|
|
1486
|
+
_d = expect;
|
|
1487
|
+
return [4 /*yield*/, missing.text()];
|
|
1488
|
+
case 6:
|
|
1489
|
+
_d.apply(void 0, [_e.sent()]).toBe("File doesn't exist.");
|
|
1490
|
+
// The documentation page is a file the build copies into the site, so the
|
|
1491
|
+
// worker leaves `/api` and `/api/` to the static host instead of answering
|
|
1492
|
+
// them as routes.
|
|
1493
|
+
expect(dispatch('/api')).toBeUndefined();
|
|
1494
|
+
expect(dispatch('/api/')).toBeUndefined();
|
|
1290
1495
|
return [2 /*return*/];
|
|
1291
1496
|
}
|
|
1292
1497
|
});
|
|
@@ -2141,7 +2346,7 @@ describe('service worker host', function () {
|
|
|
2141
2346
|
}); });
|
|
2142
2347
|
});
|
|
2143
2348
|
describe('conformance corpus', function () {
|
|
2144
|
-
var
|
|
2349
|
+
var e_4, _a;
|
|
2145
2350
|
/**
|
|
2146
2351
|
* The corpus is what "the same API" means: the cases @sasjs/api runs against
|
|
2147
2352
|
* its own services, and against a live sasjs/server, run here against this
|
|
@@ -2150,7 +2355,7 @@ describe('service worker host', function () {
|
|
|
2150
2355
|
* used to have - and it is a test rather than a review.
|
|
2151
2356
|
*/
|
|
2152
2357
|
var httpFor = function (testCase) {
|
|
2153
|
-
var
|
|
2358
|
+
var e_5, _a;
|
|
2154
2359
|
var _b;
|
|
2155
2360
|
var _c = __read(testCase.route.split(' '), 2), method = _c[0], routePath = _c[1];
|
|
2156
2361
|
var query = new URLSearchParams();
|
|
@@ -2161,12 +2366,12 @@ describe('service worker host', function () {
|
|
|
2161
2366
|
query.set(key, String(value));
|
|
2162
2367
|
}
|
|
2163
2368
|
}
|
|
2164
|
-
catch (
|
|
2369
|
+
catch (e_5_1) { e_5 = { error: e_5_1 }; }
|
|
2165
2370
|
finally {
|
|
2166
2371
|
try {
|
|
2167
2372
|
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
2168
2373
|
}
|
|
2169
|
-
finally { if (
|
|
2374
|
+
finally { if (e_5) throw e_5.error; }
|
|
2170
2375
|
}
|
|
2171
2376
|
var suffix = query.toString() ? "?".concat(query) : '';
|
|
2172
2377
|
var init = { method: method };
|
|
@@ -2197,8 +2402,8 @@ describe('service worker host', function () {
|
|
|
2197
2402
|
* does.
|
|
2198
2403
|
*/
|
|
2199
2404
|
var seedDrive = function (runtime, testCase) { return __awaiter(void 0, void 0, void 0, function () {
|
|
2200
|
-
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents,
|
|
2201
|
-
var
|
|
2405
|
+
var text, _a, _b, _c, file, contents, _d, _e, _f, file, contents, e_6_1;
|
|
2406
|
+
var e_7, _g, e_6, _h;
|
|
2202
2407
|
return __generator(this, function (_j) {
|
|
2203
2408
|
switch (_j.label) {
|
|
2204
2409
|
case 0:
|
|
@@ -2210,12 +2415,12 @@ describe('service worker host', function () {
|
|
|
2210
2415
|
text["/sasjs/drive/files".concat(file)] = contents;
|
|
2211
2416
|
}
|
|
2212
2417
|
}
|
|
2213
|
-
catch (
|
|
2418
|
+
catch (e_7_1) { e_7 = { error: e_7_1 }; }
|
|
2214
2419
|
finally {
|
|
2215
2420
|
try {
|
|
2216
2421
|
if (_b && !_b.done && (_g = _a.return)) _g.call(_a);
|
|
2217
2422
|
}
|
|
2218
|
-
finally { if (
|
|
2423
|
+
finally { if (e_7) throw e_7.error; }
|
|
2219
2424
|
}
|
|
2220
2425
|
return [4 /*yield*/, runtime.load(text)];
|
|
2221
2426
|
case 1:
|
|
@@ -2238,14 +2443,14 @@ describe('service worker host', function () {
|
|
|
2238
2443
|
return [3 /*break*/, 3];
|
|
2239
2444
|
case 6: return [3 /*break*/, 9];
|
|
2240
2445
|
case 7:
|
|
2241
|
-
|
|
2242
|
-
|
|
2446
|
+
e_6_1 = _j.sent();
|
|
2447
|
+
e_6 = { error: e_6_1 };
|
|
2243
2448
|
return [3 /*break*/, 9];
|
|
2244
2449
|
case 8:
|
|
2245
2450
|
try {
|
|
2246
2451
|
if (_e && !_e.done && (_h = _d.return)) _h.call(_d);
|
|
2247
2452
|
}
|
|
2248
|
-
finally { if (
|
|
2453
|
+
finally { if (e_6) throw e_6.error; }
|
|
2249
2454
|
return [7 /*endfinally*/];
|
|
2250
2455
|
case 9: return [2 /*return*/];
|
|
2251
2456
|
}
|
|
@@ -2313,12 +2518,12 @@ describe('service worker host', function () {
|
|
|
2313
2518
|
_loop_1(testCase);
|
|
2314
2519
|
}
|
|
2315
2520
|
}
|
|
2316
|
-
catch (
|
|
2521
|
+
catch (e_4_1) { e_4 = { error: e_4_1 }; }
|
|
2317
2522
|
finally {
|
|
2318
2523
|
try {
|
|
2319
2524
|
if (DRIVE_CORPUS_1_1 && !DRIVE_CORPUS_1_1.done && (_a = DRIVE_CORPUS_1.return)) _a.call(DRIVE_CORPUS_1);
|
|
2320
2525
|
}
|
|
2321
|
-
finally { if (
|
|
2526
|
+
finally { if (e_4) throw e_4.error; }
|
|
2322
2527
|
}
|
|
2323
2528
|
});
|
|
2324
2529
|
});
|
package/package.json
CHANGED