@sasjs/cli 6.10.0 → 6.11.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/runtime-core.js +248 -2
- package/build/commands/build/internal/mockSite/assets/sw.js +209 -14
- package/build/commands/build/internal/mockSite/spec/runtime-core.spec.js +255 -6
- package/build/commands/build/internal/mockSite/spec/sw.spec.js +576 -4
- package/package.json +1 -1
|
@@ -175,23 +175,27 @@ describe('browser runtime', function () {
|
|
|
175
175
|
expect(parsed.SYSPROCESSNAME).toEqual('Browser');
|
|
176
176
|
expect(parsed.SYSSCPL).toEqual('browser');
|
|
177
177
|
// The content type is the point: the block sets it, so the worker hands the
|
|
178
|
-
// app JSON rather than a string. The
|
|
179
|
-
//
|
|
178
|
+
// app JSON rather than a string. The session seed is text/html, so this
|
|
179
|
+
// record has to come last for the last-line-wins rule to land on JSON.
|
|
180
180
|
expect(result.headers).toContain('content-type: application/json');
|
|
181
|
+
expect(result.headers.trim().split('\n').pop()).toEqual('content-type: application/json');
|
|
181
182
|
return [2 /*return*/];
|
|
182
183
|
}
|
|
183
184
|
});
|
|
184
185
|
}); });
|
|
185
|
-
it('
|
|
186
|
+
it('seeds the headers the way a JS session does, for a service that only assigns _webout', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
186
187
|
var runtime, result;
|
|
187
188
|
return __generator(this, function (_a) {
|
|
188
189
|
runtime = loadRuntime();
|
|
189
190
|
result = runtime.run({ code: "_webout = '<h1>raw</h1>'" });
|
|
190
191
|
expect(result.error).toBeNull();
|
|
191
192
|
expect(result.webout).toEqual('<h1>raw</h1>');
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
193
|
+
// A service that declares nothing answers text/html on a server, so the
|
|
194
|
+
// mock starts from the same line rather than from an empty file. The worker
|
|
195
|
+
// treats this value as undeclared for a byte body, which is why the seed
|
|
196
|
+
// does not turn a download into text/html.
|
|
197
|
+
expect(result.headers).toEqual('content-type: text/html; charset=utf-8\n');
|
|
198
|
+
expect(runtime.HEADERS_SEED).toEqual('content-type: text/html; charset=utf-8\n');
|
|
195
199
|
return [2 /*return*/];
|
|
196
200
|
});
|
|
197
201
|
}); });
|
|
@@ -945,4 +949,249 @@ describe('browser runtime', function () {
|
|
|
945
949
|
var runtime = loadRuntime();
|
|
946
950
|
expect(function () { return runtime.run({ program: 'services/public/missing' }); }).toThrow(/Program not found in drive/);
|
|
947
951
|
});
|
|
952
|
+
describe('drive writes', function () {
|
|
953
|
+
// the write API speaks drive-relative paths, and the seed holds absolute
|
|
954
|
+
// ones, so both sides go through the same root here
|
|
955
|
+
var root = '/sasjs/drive/files';
|
|
956
|
+
var seededRuntime = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
957
|
+
var runtime;
|
|
958
|
+
var _a;
|
|
959
|
+
return __generator(this, function (_b) {
|
|
960
|
+
switch (_b.label) {
|
|
961
|
+
case 0:
|
|
962
|
+
runtime = loadRuntime();
|
|
963
|
+
return [4 /*yield*/, seedDrive(runtime, (_a = {},
|
|
964
|
+
_a["".concat(root, "/data/keep.csv")] = 'a,b\n1,2\n',
|
|
965
|
+
_a))
|
|
966
|
+
// boot marks the whole drive dirty, so the page persists a complete
|
|
967
|
+
// snapshot; clearing it here leaves each test with only its own writes
|
|
968
|
+
];
|
|
969
|
+
case 1:
|
|
970
|
+
_b.sent();
|
|
971
|
+
// boot marks the whole drive dirty, so the page persists a complete
|
|
972
|
+
// snapshot; clearing it here leaves each test with only its own writes
|
|
973
|
+
runtime.takeDirty();
|
|
974
|
+
return [2 /*return*/, runtime];
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
}); };
|
|
978
|
+
var status = function (fn) {
|
|
979
|
+
try {
|
|
980
|
+
fn();
|
|
981
|
+
}
|
|
982
|
+
catch (e) {
|
|
983
|
+
return e.status;
|
|
984
|
+
}
|
|
985
|
+
return 200;
|
|
986
|
+
};
|
|
987
|
+
it('creates a file, and the page is told to persist it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
988
|
+
var runtime;
|
|
989
|
+
var _a;
|
|
990
|
+
return __generator(this, function (_b) {
|
|
991
|
+
switch (_b.label) {
|
|
992
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
993
|
+
case 1:
|
|
994
|
+
runtime = _b.sent();
|
|
995
|
+
expect(runtime.save('/data/new/deep/up.csv', 'x,y\n')).toEqual({
|
|
996
|
+
status: 'success'
|
|
997
|
+
});
|
|
998
|
+
// the write is what the page receives after the request: a worker is
|
|
999
|
+
// terminated between requests, so without this the upload is gone
|
|
1000
|
+
expect(runtime.takeDirty()).toEqual((_a = {},
|
|
1001
|
+
_a["".concat(root, "/data/new/deep/up.csv")] = 'x,y\n',
|
|
1002
|
+
_a));
|
|
1003
|
+
expect(runtime.read('/data/new/deep/up.csv')).toEqual({
|
|
1004
|
+
contents: 'x,y\n',
|
|
1005
|
+
binary: false
|
|
1006
|
+
});
|
|
1007
|
+
return [2 /*return*/];
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}); });
|
|
1011
|
+
it('stores an upload as bytes, the way the server stores one', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1012
|
+
var runtime;
|
|
1013
|
+
return __generator(this, function (_a) {
|
|
1014
|
+
switch (_a.label) {
|
|
1015
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1016
|
+
case 1:
|
|
1017
|
+
runtime = _a.sent();
|
|
1018
|
+
runtime.save('/data/pack.zip', Uint8Array.from([80, 75, 3, 4]));
|
|
1019
|
+
expect(runtime.read('/data/pack.zip')).toEqual({
|
|
1020
|
+
contents: 'UEsDBA==',
|
|
1021
|
+
binary: true
|
|
1022
|
+
});
|
|
1023
|
+
// and a service reading it gets a Buffer, as it does on the server
|
|
1024
|
+
expect(runtime.run({
|
|
1025
|
+
code: "_webout = typeof require('fs').readFileSync('".concat(root, "/data/pack.zip');")
|
|
1026
|
+
}).webout).toBe('object');
|
|
1027
|
+
expect(runtime.run({
|
|
1028
|
+
code: "_webout = require('fs').readFileSync('".concat(root, "/data/pack.zip', 'base64');")
|
|
1029
|
+
}).webout).toBe('UEsDBA==');
|
|
1030
|
+
return [2 /*return*/];
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}); });
|
|
1034
|
+
it('refuses to create a file over one that exists', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1035
|
+
var runtime;
|
|
1036
|
+
return __generator(this, function (_a) {
|
|
1037
|
+
switch (_a.label) {
|
|
1038
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1039
|
+
case 1:
|
|
1040
|
+
runtime = _a.sent();
|
|
1041
|
+
expect(status(function () { return runtime.save('/data/keep.csv', 'x'); })).toBe(409);
|
|
1042
|
+
return [2 /*return*/];
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}); });
|
|
1046
|
+
it('replaces a file, and refuses one that is not there', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1047
|
+
var runtime;
|
|
1048
|
+
return __generator(this, function (_a) {
|
|
1049
|
+
switch (_a.label) {
|
|
1050
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1051
|
+
case 1:
|
|
1052
|
+
runtime = _a.sent();
|
|
1053
|
+
expect(runtime.update('/data/keep.csv', 'replaced')).toEqual({
|
|
1054
|
+
status: 'success'
|
|
1055
|
+
});
|
|
1056
|
+
expect(runtime.read('/data/keep.csv').contents).toBe('replaced');
|
|
1057
|
+
expect(status(function () { return runtime.update('/data/missing.csv', 'x'); })).toBe(404);
|
|
1058
|
+
return [2 /*return*/];
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
}); });
|
|
1062
|
+
it('deletes a file, and reports a deletion to the page', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1063
|
+
var runtime;
|
|
1064
|
+
var _a;
|
|
1065
|
+
return __generator(this, function (_b) {
|
|
1066
|
+
switch (_b.label) {
|
|
1067
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1068
|
+
case 1:
|
|
1069
|
+
runtime = _b.sent();
|
|
1070
|
+
expect(runtime.remove('/data/keep.csv')).toEqual({ status: 'success' });
|
|
1071
|
+
expect(runtime.takeDirty()).toEqual((_a = {},
|
|
1072
|
+
_a["".concat(root, "/data/keep.csv")] = null,
|
|
1073
|
+
_a));
|
|
1074
|
+
expect(status(function () { return runtime.read('/data/keep.csv'); })).toBe(404);
|
|
1075
|
+
expect(status(function () { return runtime.remove('/data/keep.csv'); })).toBe(404);
|
|
1076
|
+
return [2 /*return*/];
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
}); });
|
|
1080
|
+
it('creates a folder, and refuses one that already exists', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1081
|
+
var runtime;
|
|
1082
|
+
return __generator(this, function (_a) {
|
|
1083
|
+
switch (_a.label) {
|
|
1084
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1085
|
+
case 1:
|
|
1086
|
+
runtime = _a.sent();
|
|
1087
|
+
expect(runtime.mkdir('/data/reports')).toEqual({ status: 'success' });
|
|
1088
|
+
expect(runtime.list('/data').folders).toContain('reports');
|
|
1089
|
+
expect(status(function () { return runtime.mkdir('/data/reports'); })).toBe(409);
|
|
1090
|
+
return [2 /*return*/];
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}); });
|
|
1094
|
+
it('deletes a folder with everything under it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1095
|
+
var runtime, cleared;
|
|
1096
|
+
return __generator(this, function (_a) {
|
|
1097
|
+
switch (_a.label) {
|
|
1098
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1099
|
+
case 1:
|
|
1100
|
+
runtime = _a.sent();
|
|
1101
|
+
runtime.mkdir('/data/reports');
|
|
1102
|
+
runtime.save('/data/reports/a.csv', 'a');
|
|
1103
|
+
runtime.save('/data/reports/nested/b.csv', 'b');
|
|
1104
|
+
expect(runtime.rmdir('/data/reports')).toEqual({ status: 'success' });
|
|
1105
|
+
expect(runtime.list('/data').folders).not.toContain('reports');
|
|
1106
|
+
expect(status(function () { return runtime.read('/data/reports/a.csv'); })).toBe(404);
|
|
1107
|
+
expect(status(function () { return runtime.read('/data/reports/nested/b.csv'); })).toBe(404);
|
|
1108
|
+
cleared = runtime.takeDirty();
|
|
1109
|
+
expect(cleared["".concat(root, "/data/reports/a.csv")]).toBeNull();
|
|
1110
|
+
expect(cleared["".concat(root, "/data/reports/nested/b.csv")]).toBeNull();
|
|
1111
|
+
return [2 /*return*/];
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}); });
|
|
1115
|
+
it('renames a file, and moves a folder with its contents', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1116
|
+
var runtime;
|
|
1117
|
+
return __generator(this, function (_a) {
|
|
1118
|
+
switch (_a.label) {
|
|
1119
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1120
|
+
case 1:
|
|
1121
|
+
runtime = _a.sent();
|
|
1122
|
+
runtime.mkdir('/data/reports');
|
|
1123
|
+
runtime.save('/data/reports/a.csv', 'a');
|
|
1124
|
+
expect(runtime.rename('/data/keep.csv', '/data/kept.csv')).toEqual({
|
|
1125
|
+
status: 'success'
|
|
1126
|
+
});
|
|
1127
|
+
expect(runtime.read('/data/kept.csv').contents).toBe('a,b\n1,2\n');
|
|
1128
|
+
expect(status(function () { return runtime.read('/data/keep.csv'); })).toBe(404);
|
|
1129
|
+
expect(runtime.rename('/data/reports', '/archive')).toEqual({
|
|
1130
|
+
status: 'success'
|
|
1131
|
+
});
|
|
1132
|
+
expect(runtime.read('/archive/a.csv').contents).toBe('a');
|
|
1133
|
+
expect(status(function () { return runtime.read('/data/reports/a.csv'); })).toBe(404);
|
|
1134
|
+
expect(runtime.list('/archive').files).toEqual(['a.csv']);
|
|
1135
|
+
return [2 /*return*/];
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}); });
|
|
1139
|
+
it('refuses a rename onto something that exists', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1140
|
+
var runtime;
|
|
1141
|
+
return __generator(this, function (_a) {
|
|
1142
|
+
switch (_a.label) {
|
|
1143
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1144
|
+
case 1:
|
|
1145
|
+
runtime = _a.sent();
|
|
1146
|
+
runtime.save('/data/other.csv', 'o');
|
|
1147
|
+
expect(status(function () { return runtime.rename('/data/keep.csv', '/data/other.csv'); })).toBe(409);
|
|
1148
|
+
expect(status(function () { return runtime.rename('/data/nope.csv', '/data/x.csv'); })).toBe(404);
|
|
1149
|
+
return [2 /*return*/];
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
}); });
|
|
1153
|
+
it('refuses any path that lands outside the drive', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1154
|
+
var runtime;
|
|
1155
|
+
return __generator(this, function (_a) {
|
|
1156
|
+
switch (_a.label) {
|
|
1157
|
+
case 0: return [4 /*yield*/, seededRuntime()
|
|
1158
|
+
// a leading slash is drive-relative, and '..' is resolved before the
|
|
1159
|
+
// containment test - the rule the server applies in resolveWithinDrive
|
|
1160
|
+
];
|
|
1161
|
+
case 1:
|
|
1162
|
+
runtime = _a.sent();
|
|
1163
|
+
// a leading slash is drive-relative, and '..' is resolved before the
|
|
1164
|
+
// containment test - the rule the server applies in resolveWithinDrive
|
|
1165
|
+
expect(status(function () { return runtime.save('/../escape.csv', 'x'); })).toBe(400);
|
|
1166
|
+
expect(status(function () { return runtime.save('/data/../../escape.csv', 'x'); })).toBe(400);
|
|
1167
|
+
expect(status(function () { return runtime.remove('/../../etc/passwd'); })).toBe(400);
|
|
1168
|
+
expect(status(function () { return runtime.rename('/../x', '/data/y'); })).toBe(400);
|
|
1169
|
+
expect(status(function () { return runtime.rename('/data/keep.csv', '/../y'); })).toBe(400);
|
|
1170
|
+
// a file whose NAME starts with dots is not a traversal
|
|
1171
|
+
expect(runtime.save('/data/..weird.csv', 'x')).toEqual({
|
|
1172
|
+
status: 'success'
|
|
1173
|
+
});
|
|
1174
|
+
return [2 /*return*/];
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
}); });
|
|
1178
|
+
it('keeps the bytes of an entry it moves', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1179
|
+
var runtime;
|
|
1180
|
+
return __generator(this, function (_a) {
|
|
1181
|
+
switch (_a.label) {
|
|
1182
|
+
case 0: return [4 /*yield*/, seededRuntime()];
|
|
1183
|
+
case 1:
|
|
1184
|
+
runtime = _a.sent();
|
|
1185
|
+
runtime.save('/data/pack.zip', Uint8Array.from([1, 2, 3]));
|
|
1186
|
+
runtime.rename('/data/pack.zip', '/data/moved.zip');
|
|
1187
|
+
expect(runtime.read('/data/moved.zip')).toEqual({
|
|
1188
|
+
contents: 'AQID',
|
|
1189
|
+
binary: true
|
|
1190
|
+
});
|
|
1191
|
+
expect(runtime.stats().files).toBe(2);
|
|
1192
|
+
return [2 /*return*/];
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
}); });
|
|
1196
|
+
});
|
|
948
1197
|
});
|