@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.
@@ -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 mock seeds the file empty (the server
179
- // seeds text/html), so the block's record is the only one here.
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('leaves the headers alone for a service that only assigns _webout', function () { return __awaiter(void 0, void 0, void 0, function () {
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
- // Unchanged behaviour: a raw assignment declares no type here, and the
193
- // worker's binary fallback depends on this file staying empty.
194
- expect(result.headers).toEqual('');
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
  });