@sasjs/cli 6.25.0 → 6.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/commands/build/internal/mockSite/assets/persist.js +165 -26
- package/build/commands/build/internal/mockSite/assets/sw.js +419 -63
- package/build/commands/build/internal/mockSite/mockSite.d.ts +5 -0
- package/build/commands/build/internal/mockSite/mockSite.js +49 -6
- package/build/commands/build/internal/mockSite/spec/mockSite.spec.js +150 -0
- package/build/commands/build/internal/mockSite/spec/persist.spec.js +335 -15
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +522 -18
- package/package.json +5 -5
|
@@ -86,6 +86,7 @@ var fs_1 = __importDefault(require("fs"));
|
|
|
86
86
|
var os_1 = __importDefault(require("os"));
|
|
87
87
|
var path_1 = __importDefault(require("path"));
|
|
88
88
|
var esbuild_1 = require("esbuild");
|
|
89
|
+
var api_1 = require("@sasjs/api");
|
|
89
90
|
var utils_1 = require("@sasjs/utils");
|
|
90
91
|
/**
|
|
91
92
|
* The webout() block is the one @sasjs/utils generates for the server, emitted
|
|
@@ -346,6 +347,36 @@ exports.DRIVE_FORMAT = 1;
|
|
|
346
347
|
var megabytes = function (bytes) {
|
|
347
348
|
return "".concat(Math.round((bytes / 1024 / 1024) * 10) / 10, " MB");
|
|
348
349
|
};
|
|
350
|
+
/**
|
|
351
|
+
* The identity the site ships, and which file it came from.
|
|
352
|
+
*
|
|
353
|
+
* `mock-identity.json` beside the build wins, exactly as `mock-snapshot.json`
|
|
354
|
+
* replaces the drive seed: one file, one load path, and which one wins is decided
|
|
355
|
+
* here rather than at run time. Without it the package's demo fixture ships, so a
|
|
356
|
+
* mock works with no configuration at all.
|
|
357
|
+
*
|
|
358
|
+
* The fixture is validated at build time - a malformed one stops the build while
|
|
359
|
+
* the person who wrote it is watching, instead of producing a site that boots to
|
|
360
|
+
* a refusal nobody can connect to a file.
|
|
361
|
+
*/
|
|
362
|
+
var readIdentityFixture = function (buildFolder) {
|
|
363
|
+
var file = path_1.default.join(buildFolder, 'mock-identity.json');
|
|
364
|
+
if (!fs_1.default.existsSync(file))
|
|
365
|
+
return { fixture: api_1.DEFAULT_IDENTITY, source: 'default' };
|
|
366
|
+
var parsed;
|
|
367
|
+
try {
|
|
368
|
+
parsed = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
throw new Error("".concat(file, " is not valid JSON: ").concat((e === null || e === void 0 ? void 0 : e.message) || e));
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
return { fixture: (0, api_1.parseIdentity)(parsed), source: 'mock-identity.json' };
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
throw new Error("".concat(file, " cannot be used: ").concat((e === null || e === void 0 ? void 0 : e.message) || e));
|
|
378
|
+
}
|
|
379
|
+
};
|
|
349
380
|
var readDriveFile = function (absolute) {
|
|
350
381
|
var buffer = fs_1.default.readFileSync(absolute);
|
|
351
382
|
var text = buffer.toString('utf8');
|
|
@@ -728,7 +759,7 @@ var patchIndexHtml = function (html, version, basePath, outputFolder) {
|
|
|
728
759
|
*/
|
|
729
760
|
function buildMockSite(target) {
|
|
730
761
|
return __awaiter(this, void 0, void 0, function () {
|
|
731
|
-
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, transcriptSource, hasTranscript, indexPath, _d, patched, warnings;
|
|
762
|
+
var projectDir, appLocRaw, appLoc, buildFolder, basePath, webSourceRaw, webFolder, outputFolder, streamConfig, excluded, _a, seed, skipped, jsPrograms, sasPrograms, driveEntries, identity, webFileCount, hostFolder, _b, _c, asset, hostVersion, buildStamp, swPath, snapshotSource, hasSnapshot, transcriptSource, hasTranscript, indexPath, _d, patched, warnings;
|
|
732
763
|
var e_8, _e;
|
|
733
764
|
var _f, _g, _h, _j, _k, _l;
|
|
734
765
|
return __generator(this, function (_m) {
|
|
@@ -812,6 +843,7 @@ function buildMockSite(target) {
|
|
|
812
843
|
})
|
|
813
844
|
.join(', ')).concat(skipped.length > 3 ? ', ...' : ''));
|
|
814
845
|
}
|
|
846
|
+
identity = readIdentityFixture(buildFolder);
|
|
815
847
|
// Only now is the previous site expendable.
|
|
816
848
|
fs_1.default.rmSync(outputFolder, { recursive: true, force: true });
|
|
817
849
|
fs_1.default.mkdirSync(outputFolder, { recursive: true });
|
|
@@ -868,6 +900,11 @@ function buildMockSite(target) {
|
|
|
868
900
|
apiServerUrl: exports.WEBOUT_SENTINELS.apiServerUrl
|
|
869
901
|
})
|
|
870
902
|
}));
|
|
903
|
+
// 2a-ii. the identity the site signs in with: the fixture the build chose, in
|
|
904
|
+
// the one place the runtime reads it. `window.SASjsMock.as()` and a reset both
|
|
905
|
+
// come back to this file, and the page stores the store's snapshot beside the
|
|
906
|
+
// drive so an account created through the routes survives a reload.
|
|
907
|
+
fs_1.default.writeFileSync(path_1.default.join(hostFolder, 'identity.json'), JSON.stringify(identity.fixture, null, 2));
|
|
871
908
|
hostVersion = (0, crypto_1.createHash)('md5')
|
|
872
909
|
.update([
|
|
873
910
|
'runtime-core.js',
|
|
@@ -876,16 +913,20 @@ function buildMockSite(target) {
|
|
|
876
913
|
'boot.js',
|
|
877
914
|
'api-browser.js',
|
|
878
915
|
'lint-browser.js',
|
|
879
|
-
'webout-prelude.json'
|
|
916
|
+
'webout-prelude.json',
|
|
917
|
+
'identity.json'
|
|
880
918
|
]
|
|
881
919
|
.map(function (asset) { return fs_1.default.readFileSync(path_1.default.join(hostFolder, asset)); })
|
|
882
920
|
.reduce(function (all, buf) { return Buffer.concat([all, buf]); }, Buffer.alloc(0)))
|
|
883
921
|
.digest('hex')
|
|
884
922
|
.slice(0, 10);
|
|
885
923
|
buildStamp = (0, crypto_1.createHash)('md5')
|
|
886
|
-
.update(JSON.stringify(
|
|
887
|
-
.
|
|
888
|
-
|
|
924
|
+
.update(JSON.stringify({
|
|
925
|
+
files: Object.keys(seed)
|
|
926
|
+
.sort()
|
|
927
|
+
.map(function (key) { return [key, seed[key]]; }),
|
|
928
|
+
identity: identity.fixture
|
|
929
|
+
}))
|
|
889
930
|
.digest('hex')
|
|
890
931
|
.slice(0, 10);
|
|
891
932
|
swPath = path_1.default.join(outputFolder, 'sw.js');
|
|
@@ -913,6 +954,7 @@ function buildMockSite(target) {
|
|
|
913
954
|
programFolders: PROGRAM_FOLDERS,
|
|
914
955
|
snapshot: hasSnapshot,
|
|
915
956
|
sasTranscript: hasTranscript,
|
|
957
|
+
identity: identity.source,
|
|
916
958
|
buildStamp: buildStamp,
|
|
917
959
|
builtAt: new Date().toISOString()
|
|
918
960
|
}, null, 2));
|
|
@@ -927,7 +969,8 @@ function buildMockSite(target) {
|
|
|
927
969
|
jsPrograms: jsPrograms,
|
|
928
970
|
webFileCount: webFileCount,
|
|
929
971
|
snapshot: hasSnapshot,
|
|
930
|
-
sasTranscript: hasTranscript
|
|
972
|
+
sasTranscript: hasTranscript,
|
|
973
|
+
identitySource: identity.source
|
|
931
974
|
}];
|
|
932
975
|
}
|
|
933
976
|
});
|
|
@@ -1107,4 +1107,154 @@ describe('browser-hosted build', function () {
|
|
|
1107
1107
|
}
|
|
1108
1108
|
});
|
|
1109
1109
|
}); });
|
|
1110
|
+
/**
|
|
1111
|
+
* The fixture the site signs in with: one file, and which fixture wins is
|
|
1112
|
+
* decided at build time. A project replaces it the same way it replaces the
|
|
1113
|
+
* drive seed, so a site ships the identity its author chose rather than one
|
|
1114
|
+
* the runtime happens to hold.
|
|
1115
|
+
*/
|
|
1116
|
+
it('ships the packaged identity fixture when the project has none', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1117
|
+
var result, shipped;
|
|
1118
|
+
return __generator(this, function (_a) {
|
|
1119
|
+
switch (_a.label) {
|
|
1120
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1121
|
+
case 1:
|
|
1122
|
+
result = _a.sent();
|
|
1123
|
+
expect(result.identitySource).toBe('default');
|
|
1124
|
+
expect(readConfig().identity).toBe('default');
|
|
1125
|
+
shipped = JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'identity.json'), 'utf8'));
|
|
1126
|
+
expect(shipped.format).toBe(1);
|
|
1127
|
+
expect(shipped.users.map(function (user) { return user.username; })).toEqual([
|
|
1128
|
+
'sasjsadmin',
|
|
1129
|
+
'sasjsuser'
|
|
1130
|
+
]);
|
|
1131
|
+
// the same fixture the package ships, so a mock signs in with no config
|
|
1132
|
+
expect(shipped.clients.map(function (client) { return client.clientId; })).toEqual([
|
|
1133
|
+
'web_app',
|
|
1134
|
+
'sasjs'
|
|
1135
|
+
]);
|
|
1136
|
+
return [2 /*return*/];
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
}); });
|
|
1140
|
+
it("ships the project's mock-identity.json instead of the packaged fixture", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1141
|
+
var fixture, result;
|
|
1142
|
+
return __generator(this, function (_a) {
|
|
1143
|
+
switch (_a.label) {
|
|
1144
|
+
case 0:
|
|
1145
|
+
fixture = {
|
|
1146
|
+
format: 1,
|
|
1147
|
+
users: [
|
|
1148
|
+
{
|
|
1149
|
+
uid: 'user-auditor',
|
|
1150
|
+
username: 'auditor',
|
|
1151
|
+
displayName: 'Auditor',
|
|
1152
|
+
password: 'Orion123',
|
|
1153
|
+
isAdmin: true,
|
|
1154
|
+
isActive: true,
|
|
1155
|
+
needsToUpdatePassword: false
|
|
1156
|
+
}
|
|
1157
|
+
],
|
|
1158
|
+
groups: [],
|
|
1159
|
+
permissions: [],
|
|
1160
|
+
clients: [
|
|
1161
|
+
{
|
|
1162
|
+
clientId: 'audit_app',
|
|
1163
|
+
clientSecret: 'audit_app',
|
|
1164
|
+
accessTokenExpiration: 3600,
|
|
1165
|
+
refreshTokenExpiration: 86400
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
};
|
|
1169
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-identity.json'), JSON.stringify(fixture));
|
|
1170
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1171
|
+
case 1:
|
|
1172
|
+
result = _a.sent();
|
|
1173
|
+
expect(result.identitySource).toBe('mock-identity.json');
|
|
1174
|
+
expect(readConfig().identity).toBe('mock-identity.json');
|
|
1175
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'identity.json'), 'utf8'))).toEqual(fixture);
|
|
1176
|
+
return [2 /*return*/];
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
}); });
|
|
1180
|
+
it('stamps the build with the fixture, so an identity-only change re-seeds it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1181
|
+
var before, fixture;
|
|
1182
|
+
return __generator(this, function (_a) {
|
|
1183
|
+
switch (_a.label) {
|
|
1184
|
+
case 0:
|
|
1185
|
+
// the stamp decides whether a visitor keeps the drive and identity row they
|
|
1186
|
+
// already have; `host/identity.json` is only read when there is no row, so a
|
|
1187
|
+
// project that edits only mock-identity.json has to move it
|
|
1188
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1189
|
+
case 1:
|
|
1190
|
+
// the stamp decides whether a visitor keeps the drive and identity row they
|
|
1191
|
+
// already have; `host/identity.json` is only read when there is no row, so a
|
|
1192
|
+
// project that edits only mock-identity.json has to move it
|
|
1193
|
+
_a.sent();
|
|
1194
|
+
before = readConfig().buildStamp;
|
|
1195
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1196
|
+
case 2:
|
|
1197
|
+
_a.sent();
|
|
1198
|
+
expect(readConfig().buildStamp).toBe(before);
|
|
1199
|
+
fixture = {
|
|
1200
|
+
format: 1,
|
|
1201
|
+
users: [
|
|
1202
|
+
{
|
|
1203
|
+
uid: 'user-auditor',
|
|
1204
|
+
username: 'auditor',
|
|
1205
|
+
displayName: 'Auditor',
|
|
1206
|
+
password: 'Orion123',
|
|
1207
|
+
isAdmin: true,
|
|
1208
|
+
isActive: true,
|
|
1209
|
+
needsToUpdatePassword: false
|
|
1210
|
+
}
|
|
1211
|
+
],
|
|
1212
|
+
groups: [],
|
|
1213
|
+
permissions: [],
|
|
1214
|
+
clients: [
|
|
1215
|
+
{
|
|
1216
|
+
clientId: 'audit_app',
|
|
1217
|
+
clientSecret: 'audit_app',
|
|
1218
|
+
accessTokenExpiration: 3600,
|
|
1219
|
+
refreshTokenExpiration: 86400
|
|
1220
|
+
}
|
|
1221
|
+
]
|
|
1222
|
+
};
|
|
1223
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-identity.json'), JSON.stringify(fixture));
|
|
1224
|
+
return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1225
|
+
case 3:
|
|
1226
|
+
_a.sent();
|
|
1227
|
+
expect(readConfig().buildStamp).not.toBe(before);
|
|
1228
|
+
return [2 /*return*/];
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
}); });
|
|
1232
|
+
it('refuses a mock-identity.json it cannot read, leaving the previous site in place', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1233
|
+
var before, wrongFormat;
|
|
1234
|
+
return __generator(this, function (_a) {
|
|
1235
|
+
switch (_a.label) {
|
|
1236
|
+
case 0: return [4 /*yield*/, (0, mockSite_1.buildMockSite)(target())];
|
|
1237
|
+
case 1:
|
|
1238
|
+
_a.sent();
|
|
1239
|
+
before = fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'identity.json'), 'utf8');
|
|
1240
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-identity.json'), '{ not json');
|
|
1241
|
+
return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target())).rejects.toThrow(/mock-identity\.json is not valid JSON/)];
|
|
1242
|
+
case 2:
|
|
1243
|
+
_a.sent();
|
|
1244
|
+
expect(fs_1.default.readFileSync(path_1.default.join(out(), 'host', 'identity.json'), 'utf8')).toBe(before);
|
|
1245
|
+
wrongFormat = {
|
|
1246
|
+
format: 0,
|
|
1247
|
+
users: [],
|
|
1248
|
+
groups: [],
|
|
1249
|
+
permissions: [],
|
|
1250
|
+
clients: []
|
|
1251
|
+
};
|
|
1252
|
+
write(path_1.default.join(root, 'sasjsbuild', 'mock-identity.json'), JSON.stringify(wrongFormat));
|
|
1253
|
+
return [4 /*yield*/, expect((0, mockSite_1.buildMockSite)(target())).rejects.toThrow(/mock-identity\.json cannot be used: The identity fixture states format 0/)];
|
|
1254
|
+
case 3:
|
|
1255
|
+
_a.sent();
|
|
1256
|
+
return [2 /*return*/];
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
}); });
|
|
1110
1260
|
});
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
2
13
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
14
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
15
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -94,6 +105,7 @@ var createIndexedDB = function () {
|
|
|
94
105
|
data().delete(key);
|
|
95
106
|
},
|
|
96
107
|
clear: function () { return data().clear(); },
|
|
108
|
+
get: function (key) { return request(function () { return data().get(key); }); },
|
|
97
109
|
getAllKeys: function () { return request(function () { return __spreadArray([], __read(data().keys()), false); }); },
|
|
98
110
|
getAll: function () { return request(function () { return __spreadArray([], __read(data().values()), false); }); }
|
|
99
111
|
};
|
|
@@ -192,16 +204,17 @@ var loadPersist = function (options) {
|
|
|
192
204
|
})];
|
|
193
205
|
});
|
|
194
206
|
}); });
|
|
195
|
-
var idb =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
var idb = (function () {
|
|
208
|
+
var fake = createIndexedDB();
|
|
209
|
+
Object.entries(options.seeded || {}).forEach(function (_a) {
|
|
210
|
+
var _b = __read(_a, 2), key = _b[0], value = _b[1];
|
|
211
|
+
return fake.rows.set(key, value);
|
|
212
|
+
});
|
|
213
|
+
// the identity row lives beside the drive, under its own key
|
|
214
|
+
if (options.seededIdentity)
|
|
215
|
+
fake.rows.set('current', options.seededIdentity);
|
|
216
|
+
return fake;
|
|
217
|
+
})();
|
|
205
218
|
global.indexedDB = idb;
|
|
206
219
|
require('../assets/persist.js');
|
|
207
220
|
var reply = function (data) { return __spreadArray([], __read(listeners), false).forEach(function (fn) { return fn({ data: data }); }); };
|
|
@@ -512,7 +525,10 @@ describe('page-side store', function () {
|
|
|
512
525
|
_b.sent();
|
|
513
526
|
clearInterval(answering);
|
|
514
527
|
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
515
|
-
type: 'sasjs-seed'
|
|
528
|
+
type: 'sasjs-seed',
|
|
529
|
+
files: {},
|
|
530
|
+
identity: undefined,
|
|
531
|
+
subject: undefined
|
|
516
532
|
});
|
|
517
533
|
return [2 /*return*/];
|
|
518
534
|
}
|
|
@@ -542,6 +558,7 @@ describe('page-side store', function () {
|
|
|
542
558
|
controller: controller,
|
|
543
559
|
ready: Promise.resolve({}),
|
|
544
560
|
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
561
|
+
seededIdentity: { fixture: FIXTURE, subject: 'sasjsuser' },
|
|
545
562
|
buildStamp: 'build-b',
|
|
546
563
|
storedStamp: 'build-a'
|
|
547
564
|
}), window = _a.window, listeners = _a.listeners, stored = _a.stored, rows = _a.rows;
|
|
@@ -550,9 +567,14 @@ describe('page-side store', function () {
|
|
|
550
567
|
case 1:
|
|
551
568
|
_b.sent();
|
|
552
569
|
stop();
|
|
553
|
-
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
570
|
+
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
571
|
+
type: 'sasjs-seed',
|
|
572
|
+
files: {},
|
|
573
|
+
identity: undefined,
|
|
574
|
+
subject: undefined
|
|
575
|
+
});
|
|
554
576
|
expect(stored.get('sasjs-mock-build')).toBe('build-b');
|
|
555
|
-
// the old drive
|
|
577
|
+
// the old drive and the old fixture are gone, not merged with the new build
|
|
556
578
|
expect(rows.size).toBe(0);
|
|
557
579
|
return [2 /*return*/];
|
|
558
580
|
}
|
|
@@ -575,7 +597,12 @@ describe('page-side store', function () {
|
|
|
575
597
|
case 1:
|
|
576
598
|
_b.sent();
|
|
577
599
|
stop();
|
|
578
|
-
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
600
|
+
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
601
|
+
type: 'sasjs-seed',
|
|
602
|
+
files: {},
|
|
603
|
+
identity: undefined,
|
|
604
|
+
subject: undefined
|
|
605
|
+
});
|
|
579
606
|
return [2 /*return*/];
|
|
580
607
|
}
|
|
581
608
|
});
|
|
@@ -671,7 +698,300 @@ describe('page-side store', function () {
|
|
|
671
698
|
expect(String(url)).toBe('https://host.test/demo/host/mock-config.json');
|
|
672
699
|
expect(init).toEqual({ cache: 'no-store' });
|
|
673
700
|
// and the stamp it read is acted on
|
|
674
|
-
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
701
|
+
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
702
|
+
type: 'sasjs-seed',
|
|
703
|
+
files: {},
|
|
704
|
+
identity: undefined,
|
|
705
|
+
subject: undefined
|
|
706
|
+
});
|
|
707
|
+
return [2 /*return*/];
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}); });
|
|
711
|
+
/**
|
|
712
|
+
* The fixture a stored identity carries. The page never reads inside it - the
|
|
713
|
+
* worker parses it - so this only has to be the object that was stored.
|
|
714
|
+
*/
|
|
715
|
+
/**
|
|
716
|
+
* Waits for an asynchronous write rather than guessing how long it takes: the
|
|
717
|
+
* page persists through IndexedDB, whose requests settle on their own
|
|
718
|
+
* schedule, and a fixed sleep that is comfortable on one runner is too short
|
|
719
|
+
* on another.
|
|
720
|
+
*/
|
|
721
|
+
var until = function (ready_1) {
|
|
722
|
+
var args_1 = [];
|
|
723
|
+
for (var _i = 1; _i < arguments.length; _i++) {
|
|
724
|
+
args_1[_i - 1] = arguments[_i];
|
|
725
|
+
}
|
|
726
|
+
return __awaiter(void 0, __spreadArray([ready_1], __read(args_1), false), void 0, function (ready, timeout) {
|
|
727
|
+
var started;
|
|
728
|
+
if (timeout === void 0) { timeout = 2000; }
|
|
729
|
+
return __generator(this, function (_a) {
|
|
730
|
+
switch (_a.label) {
|
|
731
|
+
case 0:
|
|
732
|
+
started = Date.now();
|
|
733
|
+
_a.label = 1;
|
|
734
|
+
case 1:
|
|
735
|
+
if (!!ready()) return [3 /*break*/, 3];
|
|
736
|
+
if (Date.now() - started > timeout)
|
|
737
|
+
throw new Error('timed out waiting for the write');
|
|
738
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
|
|
739
|
+
case 2:
|
|
740
|
+
_a.sent();
|
|
741
|
+
return [3 /*break*/, 1];
|
|
742
|
+
case 3: return [2 /*return*/];
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
};
|
|
747
|
+
var FIXTURE = {
|
|
748
|
+
format: 1,
|
|
749
|
+
users: [{ username: 'sasjsadmin' }, { username: 'sasjsuser' }],
|
|
750
|
+
groups: [],
|
|
751
|
+
permissions: [],
|
|
752
|
+
clients: [{ clientId: 'web_app' }]
|
|
753
|
+
};
|
|
754
|
+
/**
|
|
755
|
+
* Answers every question the page has asked, once each.
|
|
756
|
+
*
|
|
757
|
+
* The page asks the worker more than one thing at boot (a seed or a load, and
|
|
758
|
+
* whatever a caller asked for next), and the order is not fixed: both go
|
|
759
|
+
* through `waitForController`. Answering "the last call" therefore answers the
|
|
760
|
+
* wrong one as often as the right one.
|
|
761
|
+
*/
|
|
762
|
+
var answerAll = function (controller, listeners, payload) {
|
|
763
|
+
var answered = new Set();
|
|
764
|
+
var answering = setInterval(function () {
|
|
765
|
+
controller.postMessage.mock.calls.forEach(function (_a, index) {
|
|
766
|
+
var _b = __read(_a, 1), message = _b[0];
|
|
767
|
+
if (answered.has(index))
|
|
768
|
+
return;
|
|
769
|
+
answered.add(index);
|
|
770
|
+
__spreadArray([], __read(listeners), false).forEach(function (fn) {
|
|
771
|
+
return fn({
|
|
772
|
+
data: __assign({ type: message.type + '-ack' }, payload(index, message.type))
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}, 5);
|
|
777
|
+
return function () { return clearInterval(answering); };
|
|
778
|
+
};
|
|
779
|
+
it('hands a stored fixture and subject to the worker at boot', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
780
|
+
var controller, _a, window, listeners, stop;
|
|
781
|
+
return __generator(this, function (_b) {
|
|
782
|
+
switch (_b.label) {
|
|
783
|
+
case 0:
|
|
784
|
+
controller = { postMessage: jest.fn() };
|
|
785
|
+
_a = loadPersist({
|
|
786
|
+
controller: controller,
|
|
787
|
+
ready: Promise.resolve({}),
|
|
788
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
789
|
+
seededIdentity: { fixture: FIXTURE, subject: 'sasjsuser' }
|
|
790
|
+
}), window = _a.window, listeners = _a.listeners;
|
|
791
|
+
stop = answerBoot(controller, listeners);
|
|
792
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
793
|
+
case 1:
|
|
794
|
+
_b.sent();
|
|
795
|
+
stop();
|
|
796
|
+
expect(controller.postMessage).toHaveBeenCalledWith(expect.objectContaining({
|
|
797
|
+
type: 'sasjs-load',
|
|
798
|
+
identity: FIXTURE,
|
|
799
|
+
subject: 'sasjsuser'
|
|
800
|
+
}));
|
|
801
|
+
return [2 /*return*/];
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
}); });
|
|
805
|
+
it('stores the fixture the worker reports when a route changed it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
806
|
+
var controller, _a, reply, rows;
|
|
807
|
+
return __generator(this, function (_b) {
|
|
808
|
+
switch (_b.label) {
|
|
809
|
+
case 0:
|
|
810
|
+
controller = { postMessage: jest.fn() };
|
|
811
|
+
_a = loadPersist({ controller: controller }), reply = _a.reply, rows = _a.rows;
|
|
812
|
+
reply({ type: 'sasjs-persist', files: {}, identity: FIXTURE });
|
|
813
|
+
return [4 /*yield*/, until(function () { return rows.has('current'); })];
|
|
814
|
+
case 1:
|
|
815
|
+
_b.sent();
|
|
816
|
+
expect(rows.get('current')).toEqual({
|
|
817
|
+
fixture: FIXTURE,
|
|
818
|
+
subject: undefined
|
|
819
|
+
});
|
|
820
|
+
return [2 /*return*/];
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}); });
|
|
824
|
+
it('drops a stored fixture this build cannot read, and boots from its own', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
825
|
+
var controller, _a, window, listeners, rows, stop, boot;
|
|
826
|
+
return __generator(this, function (_b) {
|
|
827
|
+
switch (_b.label) {
|
|
828
|
+
case 0:
|
|
829
|
+
controller = { postMessage: jest.fn() };
|
|
830
|
+
_a = loadPersist({
|
|
831
|
+
controller: controller,
|
|
832
|
+
ready: Promise.resolve({}),
|
|
833
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
834
|
+
seededIdentity: { fixture: __assign(__assign({}, FIXTURE), { format: 0 }), subject: null }
|
|
835
|
+
}), window = _a.window, listeners = _a.listeners, rows = _a.rows;
|
|
836
|
+
stop = answerAll(controller, listeners, function (index) {
|
|
837
|
+
return index === 0
|
|
838
|
+
? {
|
|
839
|
+
error: 'The identity fixture states format 0.',
|
|
840
|
+
errorKind: 'identity'
|
|
841
|
+
}
|
|
842
|
+
: { booted: true };
|
|
843
|
+
});
|
|
844
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
845
|
+
case 1:
|
|
846
|
+
boot = _b.sent();
|
|
847
|
+
stop();
|
|
848
|
+
expect(boot).toEqual({ type: 'sasjs-load-ack', booted: true });
|
|
849
|
+
// the retry carries the drive, and none of the fixture it could not read
|
|
850
|
+
expect(controller.postMessage.mock.calls[1][0]).toEqual({
|
|
851
|
+
type: 'sasjs-load',
|
|
852
|
+
files: { '/sasjs/drive/stored.txt': 'stored' }
|
|
853
|
+
});
|
|
854
|
+
expect(rows.has('current')).toBe(false);
|
|
855
|
+
return [2 /*return*/];
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}); });
|
|
859
|
+
it('keeps the stored identity when the boot failed for another reason', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
860
|
+
var controller, _a, window, listeners, rows, stop, boot;
|
|
861
|
+
return __generator(this, function (_b) {
|
|
862
|
+
switch (_b.label) {
|
|
863
|
+
case 0:
|
|
864
|
+
controller = { postMessage: jest.fn() };
|
|
865
|
+
_a = loadPersist({
|
|
866
|
+
controller: controller,
|
|
867
|
+
ready: Promise.resolve({}),
|
|
868
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
869
|
+
seededIdentity: { fixture: FIXTURE, subject: 'sasjsuser' }
|
|
870
|
+
}), window = _a.window, listeners = _a.listeners, rows = _a.rows;
|
|
871
|
+
stop = answerAll(controller, listeners, function () { return ({
|
|
872
|
+
error: 'the drive could not be read',
|
|
873
|
+
errorKind: 'host'
|
|
874
|
+
}); });
|
|
875
|
+
return [4 /*yield*/, window.__sasjsMockReady];
|
|
876
|
+
case 1:
|
|
877
|
+
boot = _b.sent();
|
|
878
|
+
stop();
|
|
879
|
+
expect(boot.error).toBe('the drive could not be read');
|
|
880
|
+
expect(rows.get('current')).toEqual({
|
|
881
|
+
fixture: FIXTURE,
|
|
882
|
+
subject: 'sasjsuser'
|
|
883
|
+
});
|
|
884
|
+
// and it did not re-boot without the fixture
|
|
885
|
+
expect(controller.postMessage).toHaveBeenCalledTimes(1);
|
|
886
|
+
return [2 /*return*/];
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}); });
|
|
890
|
+
it('as() stores the subject, returns the pair and restarts the app', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
891
|
+
var controller, _a, window, listeners, rows, stop, pair;
|
|
892
|
+
return __generator(this, function (_b) {
|
|
893
|
+
switch (_b.label) {
|
|
894
|
+
case 0:
|
|
895
|
+
controller = { postMessage: jest.fn() };
|
|
896
|
+
_a = loadPersist({ controller: controller }), window = _a.window, listeners = _a.listeners, rows = _a.rows;
|
|
897
|
+
window.location = { reload: jest.fn() };
|
|
898
|
+
stop = answerAll(controller, listeners, function (_index, type) {
|
|
899
|
+
return type === 'sasjs-as'
|
|
900
|
+
? {
|
|
901
|
+
username: 'sasjsuser',
|
|
902
|
+
accessToken: 'access-for-user2',
|
|
903
|
+
refreshToken: 'refresh-for-user2',
|
|
904
|
+
identity: FIXTURE
|
|
905
|
+
}
|
|
906
|
+
: {};
|
|
907
|
+
});
|
|
908
|
+
return [4 /*yield*/, window.SASjsMock.as('sasjsuser')];
|
|
909
|
+
case 1:
|
|
910
|
+
pair = _b.sent();
|
|
911
|
+
stop();
|
|
912
|
+
expect(controller.postMessage).toHaveBeenCalledWith({
|
|
913
|
+
type: 'sasjs-as',
|
|
914
|
+
username: 'sasjsuser'
|
|
915
|
+
});
|
|
916
|
+
expect(pair).toEqual({
|
|
917
|
+
username: 'sasjsuser',
|
|
918
|
+
accessToken: 'access-for-user2',
|
|
919
|
+
refreshToken: 'refresh-for-user2'
|
|
920
|
+
});
|
|
921
|
+
// the subject outlives the restart, beside the fixture it names
|
|
922
|
+
return [4 /*yield*/, until(function () { return window.location.reload.mock.calls.length > 0; })];
|
|
923
|
+
case 2:
|
|
924
|
+
// the subject outlives the restart, beside the fixture it names
|
|
925
|
+
_b.sent();
|
|
926
|
+
return [4 /*yield*/, until(function () { return rows.has('current'); })];
|
|
927
|
+
case 3:
|
|
928
|
+
_b.sent();
|
|
929
|
+
expect(rows.get('current')).toEqual({
|
|
930
|
+
fixture: FIXTURE,
|
|
931
|
+
subject: 'sasjsuser'
|
|
932
|
+
});
|
|
933
|
+
expect(window.location.reload).toHaveBeenCalled();
|
|
934
|
+
return [2 /*return*/];
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}); });
|
|
938
|
+
it('surfaces a refusal from the worker rather than storing a subject', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
939
|
+
var controller, _a, window, listeners, rows, stop;
|
|
940
|
+
return __generator(this, function (_b) {
|
|
941
|
+
switch (_b.label) {
|
|
942
|
+
case 0:
|
|
943
|
+
controller = { postMessage: jest.fn() };
|
|
944
|
+
_a = loadPersist({ controller: controller }), window = _a.window, listeners = _a.listeners, rows = _a.rows;
|
|
945
|
+
stop = answerAll(controller, listeners, function (_index, type) {
|
|
946
|
+
return type === 'sasjs-as' ? { error: 'no account named "nobody"' } : {};
|
|
947
|
+
});
|
|
948
|
+
return [4 /*yield*/, expect(window.SASjsMock.as('nobody')).rejects.toThrow('no account named "nobody"')];
|
|
949
|
+
case 1:
|
|
950
|
+
_b.sent();
|
|
951
|
+
stop();
|
|
952
|
+
expect(rows.has('current')).toBe(false);
|
|
953
|
+
return [2 /*return*/];
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}); });
|
|
957
|
+
it('reset reports a refusal from the worker rather than claiming success', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
958
|
+
var controller, _a, window, listeners, stop;
|
|
959
|
+
return __generator(this, function (_b) {
|
|
960
|
+
switch (_b.label) {
|
|
961
|
+
case 0:
|
|
962
|
+
controller = { postMessage: jest.fn() };
|
|
963
|
+
_a = loadPersist({ controller: controller }), window = _a.window, listeners = _a.listeners;
|
|
964
|
+
stop = answerAll(controller, listeners, function (_index, type) {
|
|
965
|
+
return type === 'sasjs-reset'
|
|
966
|
+
? { error: 'host/identity.json cannot be used: it is not readable' }
|
|
967
|
+
: {};
|
|
968
|
+
});
|
|
969
|
+
return [4 /*yield*/, expect(window.SASjsMock.reset()).rejects.toThrow('host/identity.json cannot be used')];
|
|
970
|
+
case 1:
|
|
971
|
+
_b.sent();
|
|
972
|
+
stop();
|
|
973
|
+
return [2 /*return*/];
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}); });
|
|
977
|
+
it('reset clears the identity beside the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
978
|
+
var controller, _a, window, listeners, rows, stop;
|
|
979
|
+
return __generator(this, function (_b) {
|
|
980
|
+
switch (_b.label) {
|
|
981
|
+
case 0:
|
|
982
|
+
controller = { postMessage: jest.fn() };
|
|
983
|
+
_a = loadPersist({
|
|
984
|
+
controller: controller,
|
|
985
|
+
seeded: { '/sasjs/drive/stored.txt': 'stored' },
|
|
986
|
+
seededIdentity: { fixture: FIXTURE, subject: 'sasjsuser' }
|
|
987
|
+
}), window = _a.window, listeners = _a.listeners, rows = _a.rows;
|
|
988
|
+
stop = answerAll(controller, listeners, function () { return ({}); });
|
|
989
|
+
return [4 /*yield*/, window.SASjsMock.reset()];
|
|
990
|
+
case 1:
|
|
991
|
+
_b.sent();
|
|
992
|
+
stop();
|
|
993
|
+
expect(rows.has('current')).toBe(false);
|
|
994
|
+
expect(rows.has('/sasjs/drive/stored.txt')).toBe(false);
|
|
675
995
|
return [2 /*return*/];
|
|
676
996
|
}
|
|
677
997
|
});
|