@blueprint-ts/core 4.1.0-beta.4 → 4.1.0-beta.6
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/dist/persistenceDrivers/MemoryPersistenceDriver.d.ts +11 -0
- package/dist/persistenceDrivers/MemoryPersistenceDriver.js +22 -0
- package/dist/persistenceDrivers/index.d.ts +2 -1
- package/dist/persistenceDrivers/index.js +2 -1
- package/dist/requests/BaseRequest.d.ts +2 -0
- package/dist/requests/BaseRequest.js +7 -2
- package/dist/requests/contracts/BaseRequestContract.d.ts +2 -0
- package/dist/requests/drivers/mock/MockRequestAssertionError.d.ts +3 -0
- package/dist/requests/drivers/mock/MockRequestAssertionError.js +6 -0
- package/dist/requests/drivers/mock/MockRequestDriver.d.ts +160 -0
- package/dist/requests/drivers/mock/MockRequestDriver.js +588 -0
- package/dist/requests/drivers/mock/MockRequestTestHelpers.d.ts +20 -0
- package/dist/requests/drivers/mock/MockRequestTestHelpers.js +70 -0
- package/dist/requests/drivers/mock/MockResponseHandler.d.ts +22 -0
- package/dist/requests/drivers/mock/MockResponseHandler.js +59 -0
- package/dist/requests/index.d.ts +5 -2
- package/dist/requests/index.js +4 -1
- package/dist/vue/forms/BaseForm.d.ts +4 -0
- package/dist/vue/forms/BaseForm.js +40 -43
- package/dist/vue/forms/index.d.ts +5 -2
- package/dist/vue/forms/index.js +3 -1
- package/dist/vue/forms/persistence/StrictPersistenceRestorePolicy.d.ts +4 -0
- package/dist/vue/forms/persistence/StrictPersistenceRestorePolicy.js +42 -0
- package/dist/vue/forms/persistence/index.d.ts +3 -0
- package/dist/vue/forms/persistence/index.js +2 -0
- package/dist/vue/forms/persistence/types.d.ts +23 -0
- package/dist/vue/forms/persistence/types.js +1 -0
- package/dist/vue/forms/persistence/utils.d.ts +2 -0
- package/dist/vue/forms/persistence/utils.js +77 -0
- package/package.json +1 -1
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { ResponseException } from '../../exceptions/ResponseException';
|
|
11
|
+
import { isEqual } from 'lodash-es';
|
|
12
|
+
import { MockRequestAssertionError } from './MockRequestAssertionError';
|
|
13
|
+
import { MockResponseHandler } from './MockResponseHandler';
|
|
14
|
+
function isPredicate(value) {
|
|
15
|
+
return typeof value === 'function';
|
|
16
|
+
}
|
|
17
|
+
function parseUrl(url) {
|
|
18
|
+
if (url instanceof URL) {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
return new URL(url, 'https://mock-request.invalid');
|
|
22
|
+
}
|
|
23
|
+
function buildUrlWithoutQuery(url) {
|
|
24
|
+
const clone = new URL(url.toString());
|
|
25
|
+
clone.search = '';
|
|
26
|
+
return clone.toString();
|
|
27
|
+
}
|
|
28
|
+
function parseQuery(url) {
|
|
29
|
+
const query = {};
|
|
30
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
31
|
+
const current = query[key];
|
|
32
|
+
if (current === undefined) {
|
|
33
|
+
query[key] = value;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(current)) {
|
|
37
|
+
current.push(value);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
query[key] = [current, value];
|
|
41
|
+
}
|
|
42
|
+
return query;
|
|
43
|
+
}
|
|
44
|
+
function stringify(value) {
|
|
45
|
+
return value === undefined ? 'undefined' : JSON.stringify(value, null, 2);
|
|
46
|
+
}
|
|
47
|
+
function getPredicateDescription(predicate) {
|
|
48
|
+
var _a;
|
|
49
|
+
return (_a = predicate.description) !== null && _a !== void 0 ? _a : '[custom matcher]';
|
|
50
|
+
}
|
|
51
|
+
function getTextBody(body) {
|
|
52
|
+
if (!body || body.kind !== 'text') {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return body.value;
|
|
56
|
+
}
|
|
57
|
+
function getJsonBody(body) {
|
|
58
|
+
const textBody = getTextBody(body);
|
|
59
|
+
if (textBody === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(textBody);
|
|
64
|
+
}
|
|
65
|
+
catch (_a) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function collectJsonDiffPaths(expected, actual, path = '', maxPaths = 10) {
|
|
70
|
+
const mismatches = [];
|
|
71
|
+
const visit = (left, right, currentPath) => {
|
|
72
|
+
if (mismatches.length >= maxPaths) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (JSON.stringify(left) === JSON.stringify(right)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
79
|
+
if (left.length !== right.length) {
|
|
80
|
+
mismatches.push(currentPath || '(root)');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
left.forEach((item, index) => {
|
|
84
|
+
visit(item, right[index], currentPath ? `${currentPath}.${index}` : String(index));
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (isRecord(left) && isRecord(right)) {
|
|
89
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
visit(left[key], right[key], currentPath ? `${currentPath}.${key}` : key);
|
|
92
|
+
if (mismatches.length >= maxPaths) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
mismatches.push(currentPath || '(root)');
|
|
99
|
+
};
|
|
100
|
+
visit(expected, actual, path);
|
|
101
|
+
return mismatches;
|
|
102
|
+
}
|
|
103
|
+
function isRecord(value) {
|
|
104
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
105
|
+
}
|
|
106
|
+
export class MockRequestExpectationBuilder {
|
|
107
|
+
constructor(driver, criteria) {
|
|
108
|
+
this.driver = driver;
|
|
109
|
+
this.criteria = Object.assign({}, criteria);
|
|
110
|
+
}
|
|
111
|
+
withHeaders(headers) {
|
|
112
|
+
this.criteria.headers = headers;
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
withQuery(query) {
|
|
116
|
+
this.criteria.query = query;
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
withBody(body) {
|
|
120
|
+
this.criteria.body = body;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
respond(response) {
|
|
124
|
+
this.driver.expect(Object.assign(Object.assign({}, this.criteria), { response }));
|
|
125
|
+
return this.driver;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export class MockRequestDriver {
|
|
129
|
+
constructor(config, expectations = [], options = {}) {
|
|
130
|
+
var _a;
|
|
131
|
+
this.config = config;
|
|
132
|
+
this.expectations = [];
|
|
133
|
+
this.history = [];
|
|
134
|
+
this.expectations.push(...expectations);
|
|
135
|
+
this.matchMode = (_a = options.matchMode) !== null && _a !== void 0 ? _a : 'ordered';
|
|
136
|
+
}
|
|
137
|
+
setMatchMode(mode) {
|
|
138
|
+
this.matchMode = mode;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
ordered() {
|
|
142
|
+
return this.setMatchMode('ordered');
|
|
143
|
+
}
|
|
144
|
+
unordered() {
|
|
145
|
+
return this.setMatchMode('unordered');
|
|
146
|
+
}
|
|
147
|
+
expect(expectation) {
|
|
148
|
+
this.expectations.push(expectation);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
expectAny(criteria) {
|
|
152
|
+
return new MockRequestExpectationBuilder(this, criteria);
|
|
153
|
+
}
|
|
154
|
+
reset() {
|
|
155
|
+
this.expectations = [];
|
|
156
|
+
this.history = [];
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
getHistory() {
|
|
160
|
+
return [...this.history];
|
|
161
|
+
}
|
|
162
|
+
assertNoPendingExpectations() {
|
|
163
|
+
this.assertExpectationsMet();
|
|
164
|
+
}
|
|
165
|
+
assertExpectationsMet() {
|
|
166
|
+
if (this.expectations.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const nextExpectation = this.expectations[0];
|
|
170
|
+
if (!nextExpectation) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
throw new MockRequestAssertionError([
|
|
174
|
+
`Expected ${this.expectations.length} more mocked request(s).`,
|
|
175
|
+
`Match mode: ${this.matchMode}`,
|
|
176
|
+
`Next expected request: ${this.describeExpectation(nextExpectation)}`
|
|
177
|
+
].join('\n'));
|
|
178
|
+
}
|
|
179
|
+
send(url, method, headers, body, requestConfig) {
|
|
180
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
181
|
+
var _a;
|
|
182
|
+
if ((_a = requestConfig === null || requestConfig === void 0 ? void 0 : requestConfig.abortSignal) === null || _a === void 0 ? void 0 : _a.aborted) {
|
|
183
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
184
|
+
}
|
|
185
|
+
const actualRequest = yield this.normalizeActualRequest(url, method, headers, body);
|
|
186
|
+
const { index, expectation } = yield this.selectExpectation(actualRequest);
|
|
187
|
+
this.expectations.splice(index, 1);
|
|
188
|
+
this.history.push(this.toHistoryEntry(actualRequest));
|
|
189
|
+
const response = new MockResponseHandler(expectation.response);
|
|
190
|
+
if (!response.getRawResponse().ok) {
|
|
191
|
+
throw new ResponseException(response);
|
|
192
|
+
}
|
|
193
|
+
return response;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
selectExpectation(actualRequest) {
|
|
197
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
if (this.expectations.length === 0) {
|
|
199
|
+
throw new MockRequestAssertionError(this.buildUnexpectedRequestMessage(actualRequest));
|
|
200
|
+
}
|
|
201
|
+
if (this.matchMode === 'ordered') {
|
|
202
|
+
const expectation = this.expectations[0];
|
|
203
|
+
if (!expectation) {
|
|
204
|
+
throw new MockRequestAssertionError(this.buildUnexpectedRequestMessage(actualRequest));
|
|
205
|
+
}
|
|
206
|
+
const failure = yield this.matchExpectation(expectation, actualRequest);
|
|
207
|
+
if (failure) {
|
|
208
|
+
throw new MockRequestAssertionError(this.buildMismatchMessage(failure, actualRequest));
|
|
209
|
+
}
|
|
210
|
+
return { index: 0, expectation };
|
|
211
|
+
}
|
|
212
|
+
const failures = [];
|
|
213
|
+
for (const [index, expectation] of this.expectations.entries()) {
|
|
214
|
+
const failure = yield this.matchExpectation(expectation, actualRequest);
|
|
215
|
+
if (!failure) {
|
|
216
|
+
return { index, expectation };
|
|
217
|
+
}
|
|
218
|
+
failures.push(failure);
|
|
219
|
+
}
|
|
220
|
+
throw new MockRequestAssertionError(this.buildUnorderedMismatchMessage(actualRequest, failures));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
matchExpectation(expectation, actualRequest) {
|
|
224
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
225
|
+
const methodFailure = this.matchMethod(expectation.method, actualRequest, expectation);
|
|
226
|
+
if (methodFailure) {
|
|
227
|
+
return methodFailure;
|
|
228
|
+
}
|
|
229
|
+
const urlFailure = this.matchUrl(expectation, actualRequest);
|
|
230
|
+
if (urlFailure) {
|
|
231
|
+
return urlFailure;
|
|
232
|
+
}
|
|
233
|
+
const headersFailure = this.matchHeaders(expectation.headers, actualRequest, expectation);
|
|
234
|
+
if (headersFailure) {
|
|
235
|
+
return headersFailure;
|
|
236
|
+
}
|
|
237
|
+
const queryFailure = this.matchQuery(expectation.query, actualRequest, expectation);
|
|
238
|
+
if (queryFailure) {
|
|
239
|
+
return queryFailure;
|
|
240
|
+
}
|
|
241
|
+
return yield this.matchBody(expectation.body, actualRequest, expectation);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
matchMethod(method, actualRequest, expectation) {
|
|
245
|
+
if (isPredicate(method)) {
|
|
246
|
+
if (method(actualRequest.method)) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
field: 'method',
|
|
251
|
+
message: 'Method matcher returned false.',
|
|
252
|
+
expectation,
|
|
253
|
+
expectedValue: getPredicateDescription(method),
|
|
254
|
+
actualValue: actualRequest.method
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (method === actualRequest.method) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
field: 'method',
|
|
262
|
+
message: 'HTTP method did not match.',
|
|
263
|
+
expectation,
|
|
264
|
+
expectedValue: method,
|
|
265
|
+
actualValue: actualRequest.method
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
matchUrl(expectation, actualRequest) {
|
|
269
|
+
if (isPredicate(expectation.url)) {
|
|
270
|
+
if (expectation.url(actualRequest.parsedUrl)) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
field: 'url',
|
|
275
|
+
message: 'URL matcher returned false.',
|
|
276
|
+
expectation,
|
|
277
|
+
expectedValue: getPredicateDescription(expectation.url),
|
|
278
|
+
actualValue: actualRequest.url
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const expectedUrl = parseUrl(expectation.url);
|
|
282
|
+
const actualComparableUrl = expectation.query !== undefined && expectedUrl.search.length === 0 ? actualRequest.urlWithoutQuery : actualRequest.url;
|
|
283
|
+
const expectedComparableUrl = expectation.query !== undefined && expectedUrl.search.length === 0 ? buildUrlWithoutQuery(expectedUrl) : this.normalizeUrl(expectation.url);
|
|
284
|
+
if (expectedComparableUrl === actualComparableUrl) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
field: 'url',
|
|
289
|
+
message: 'URL did not match.',
|
|
290
|
+
expectation,
|
|
291
|
+
expectedValue: expectedComparableUrl,
|
|
292
|
+
actualValue: actualComparableUrl
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
matchHeaders(headers, actualRequest, expectation) {
|
|
296
|
+
if (headers === undefined) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
if (isPredicate(headers)) {
|
|
300
|
+
if (headers(actualRequest.headers)) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
field: 'headers',
|
|
305
|
+
message: 'Header matcher returned false.',
|
|
306
|
+
expectation,
|
|
307
|
+
expectedValue: getPredicateDescription(headers),
|
|
308
|
+
actualValue: actualRequest.headers
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const expectedHeaders = this.resolveHeaders(headers);
|
|
312
|
+
if (isEqual(expectedHeaders, actualRequest.headers)) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
field: 'headers',
|
|
317
|
+
message: 'Headers did not match exactly.',
|
|
318
|
+
expectation,
|
|
319
|
+
expectedValue: expectedHeaders,
|
|
320
|
+
actualValue: actualRequest.headers
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
matchQuery(query, actualRequest, expectation) {
|
|
324
|
+
if (query === undefined) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
if (isPredicate(query)) {
|
|
328
|
+
if (query(actualRequest.query)) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
field: 'query',
|
|
333
|
+
message: 'Query matcher returned false.',
|
|
334
|
+
expectation,
|
|
335
|
+
expectedValue: getPredicateDescription(query),
|
|
336
|
+
actualValue: actualRequest.query
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (isEqual(query, actualRequest.query)) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
field: 'query',
|
|
344
|
+
message: 'Query parameters did not match exactly.',
|
|
345
|
+
expectation,
|
|
346
|
+
expectedValue: query,
|
|
347
|
+
actualValue: actualRequest.query
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
matchBody(body, actualRequest, expectation) {
|
|
351
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
352
|
+
if (body === undefined) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
if (isPredicate(body)) {
|
|
356
|
+
const context = this.createBodyMatchContext(actualRequest);
|
|
357
|
+
if (body(context)) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
field: 'body',
|
|
362
|
+
message: 'Body matcher returned false.',
|
|
363
|
+
expectation,
|
|
364
|
+
expectedValue: getPredicateDescription(body),
|
|
365
|
+
actualValue: this.describeBodyForMessage(actualRequest.body)
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const expectedBody = yield this.normalizeBody(body);
|
|
369
|
+
if (JSON.stringify(expectedBody) === JSON.stringify(actualRequest.body)) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
const expectedJson = getJsonBody(expectedBody);
|
|
373
|
+
const actualJson = getJsonBody(actualRequest.body);
|
|
374
|
+
return Object.assign({ field: 'body', message: expectedJson !== undefined && actualJson !== undefined ? 'JSON body did not match.' : 'Body did not match exactly.', expectation, expectedValue: expectedJson !== null && expectedJson !== void 0 ? expectedJson : expectedBody, actualValue: actualJson !== null && actualJson !== void 0 ? actualJson : actualRequest.body }, (expectedJson !== undefined && actualJson !== undefined ? { diffPaths: collectJsonDiffPaths(expectedJson, actualJson) } : {}));
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
createBodyMatchContext(actualRequest) {
|
|
378
|
+
return Object.assign(Object.assign({ method: actualRequest.method, url: actualRequest.url, headers: actualRequest.headers }, (actualRequest.body !== undefined ? { body: actualRequest.body } : {})), { getJson: () => getJsonBody(actualRequest.body), getText: () => getTextBody(actualRequest.body), getQuery: () => actualRequest.query });
|
|
379
|
+
}
|
|
380
|
+
normalizeActualRequest(url, method, headers, body) {
|
|
381
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
382
|
+
var _a;
|
|
383
|
+
const resolvedUrl = parseUrl(url);
|
|
384
|
+
const normalizedBody = yield this.normalizeBody(body === null || body === void 0 ? void 0 : body.getContent());
|
|
385
|
+
return Object.assign({ method, url: this.normalizeUrl(url), urlWithoutQuery: buildUrlWithoutQuery(resolvedUrl), parsedUrl: resolvedUrl, headers: this.resolveHeaders(Object.assign(Object.assign(Object.assign({}, (_a = this.config) === null || _a === void 0 ? void 0 : _a.headers), headers), body === null || body === void 0 ? void 0 : body.getHeaders())), query: parseQuery(resolvedUrl) }, (normalizedBody !== undefined ? { body: normalizedBody } : {}));
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
toHistoryEntry(request) {
|
|
389
|
+
return Object.assign({ method: request.method, url: request.url, headers: request.headers }, (request.body !== undefined ? { body: request.body } : {}));
|
|
390
|
+
}
|
|
391
|
+
buildUnexpectedRequestMessage(actualRequest) {
|
|
392
|
+
return ['Unexpected request received with no expectations left.', '', 'Actual request:', this.formatRequestSnapshot(actualRequest)].join('\n');
|
|
393
|
+
}
|
|
394
|
+
buildUnorderedMismatchMessage(actualRequest, failures) {
|
|
395
|
+
const pending = this.expectations.map((expectation, index) => `${index + 1}. ${this.describeExpectation(expectation)}`).join('\n');
|
|
396
|
+
const firstFailure = failures[0];
|
|
397
|
+
return [
|
|
398
|
+
'Mock request did not match any pending expectation.',
|
|
399
|
+
firstFailure ? `Closest failure: ${firstFailure.message}` : 'No expectation matched.',
|
|
400
|
+
'',
|
|
401
|
+
'Actual request:',
|
|
402
|
+
this.formatRequestSnapshot(actualRequest),
|
|
403
|
+
'',
|
|
404
|
+
'Pending expectations:',
|
|
405
|
+
pending
|
|
406
|
+
].join('\n');
|
|
407
|
+
}
|
|
408
|
+
buildMismatchMessage(failure, actualRequest) {
|
|
409
|
+
const lines = [
|
|
410
|
+
`Mock request ${failure.field} mismatch.`,
|
|
411
|
+
failure.message,
|
|
412
|
+
'',
|
|
413
|
+
'Expected request:',
|
|
414
|
+
this.formatExpectationSnapshot(failure.expectation),
|
|
415
|
+
'',
|
|
416
|
+
'Actual request:',
|
|
417
|
+
this.formatRequestSnapshot(actualRequest)
|
|
418
|
+
];
|
|
419
|
+
if (failure.expectedValue !== undefined || failure.actualValue !== undefined) {
|
|
420
|
+
lines.push('', `Expected ${failure.field}: ${stringify(failure.expectedValue)}`, `Actual ${failure.field}: ${stringify(failure.actualValue)}`);
|
|
421
|
+
}
|
|
422
|
+
if (failure.diffPaths && failure.diffPaths.length > 0) {
|
|
423
|
+
lines.push('', `Differing JSON paths: ${failure.diffPaths.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
return lines.join('\n');
|
|
426
|
+
}
|
|
427
|
+
formatExpectationSnapshot(expectation) {
|
|
428
|
+
const lines = [`method: ${this.describeMatcher(expectation.method)}`, `url: ${this.describeUrlMatcher(expectation)}`];
|
|
429
|
+
if (expectation.headers !== undefined) {
|
|
430
|
+
lines.push(`headers: ${this.describeMatcher(expectation.headers)}`);
|
|
431
|
+
}
|
|
432
|
+
if (expectation.query !== undefined) {
|
|
433
|
+
lines.push(`query: ${this.describeMatcher(expectation.query)}`);
|
|
434
|
+
}
|
|
435
|
+
if (expectation.body !== undefined) {
|
|
436
|
+
lines.push(`body: ${this.describeBodyMatcher(expectation.body)}`);
|
|
437
|
+
}
|
|
438
|
+
return lines.join('\n');
|
|
439
|
+
}
|
|
440
|
+
formatRequestSnapshot(request) {
|
|
441
|
+
const lines = [
|
|
442
|
+
`method: ${request.method}`,
|
|
443
|
+
`url: ${request.url}`,
|
|
444
|
+
`headers: ${stringify(request.headers)}`,
|
|
445
|
+
`query: ${stringify(request.query)}`,
|
|
446
|
+
`body: ${stringify(this.describeBodyForMessage(request.body))}`
|
|
447
|
+
];
|
|
448
|
+
return lines.join('\n');
|
|
449
|
+
}
|
|
450
|
+
describeExpectation(expectation) {
|
|
451
|
+
return `${this.describeMatcher(expectation.method)} ${this.describeUrlMatcher(expectation)}`;
|
|
452
|
+
}
|
|
453
|
+
describeMatcher(matcher) {
|
|
454
|
+
if (isPredicate(matcher)) {
|
|
455
|
+
return getPredicateDescription(matcher);
|
|
456
|
+
}
|
|
457
|
+
if (typeof matcher === 'object') {
|
|
458
|
+
return stringify(matcher);
|
|
459
|
+
}
|
|
460
|
+
return String(matcher);
|
|
461
|
+
}
|
|
462
|
+
describeUrlMatcher(expectation) {
|
|
463
|
+
if (isPredicate(expectation.url)) {
|
|
464
|
+
return getPredicateDescription(expectation.url);
|
|
465
|
+
}
|
|
466
|
+
const url = parseUrl(expectation.url);
|
|
467
|
+
if (expectation.query !== undefined && url.search.length === 0) {
|
|
468
|
+
return buildUrlWithoutQuery(url);
|
|
469
|
+
}
|
|
470
|
+
return this.normalizeUrl(expectation.url);
|
|
471
|
+
}
|
|
472
|
+
describeBodyMatcher(body) {
|
|
473
|
+
if (isPredicate(body)) {
|
|
474
|
+
return getPredicateDescription(body);
|
|
475
|
+
}
|
|
476
|
+
return stringify(body);
|
|
477
|
+
}
|
|
478
|
+
describeBodyForMessage(body) {
|
|
479
|
+
const json = getJsonBody(body);
|
|
480
|
+
return json !== null && json !== void 0 ? json : body;
|
|
481
|
+
}
|
|
482
|
+
normalizeUrl(url) {
|
|
483
|
+
return parseUrl(url).toString();
|
|
484
|
+
}
|
|
485
|
+
resolveHeaders(headers) {
|
|
486
|
+
const resolved = {};
|
|
487
|
+
for (const key in headers) {
|
|
488
|
+
const value = headers[key];
|
|
489
|
+
if (value === undefined) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
resolved[key] = typeof value === 'function' ? value() : value;
|
|
493
|
+
}
|
|
494
|
+
return resolved;
|
|
495
|
+
}
|
|
496
|
+
normalizeBody(body) {
|
|
497
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
498
|
+
if (body === undefined) {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
if (body === null) {
|
|
502
|
+
return { kind: 'null' };
|
|
503
|
+
}
|
|
504
|
+
if (typeof body === 'string') {
|
|
505
|
+
return { kind: 'text', value: body };
|
|
506
|
+
}
|
|
507
|
+
if (body instanceof FormData) {
|
|
508
|
+
return yield this.normalizeFormData(body);
|
|
509
|
+
}
|
|
510
|
+
if (body instanceof Blob) {
|
|
511
|
+
const mimeType = body.type || undefined;
|
|
512
|
+
return Object.assign(Object.assign({ kind: 'binary' }, (mimeType !== undefined ? { mimeType } : {})), { bytes: Array.from(new Uint8Array(yield this.readBlob(body))) });
|
|
513
|
+
}
|
|
514
|
+
if (this.isBufferSource(body)) {
|
|
515
|
+
return {
|
|
516
|
+
kind: 'binary',
|
|
517
|
+
bytes: Array.from(new Uint8Array(this.toArrayBuffer(body)))
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
kind: 'text',
|
|
522
|
+
value: JSON.stringify(body)
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
normalizeFormData(body) {
|
|
527
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
528
|
+
const entries = [];
|
|
529
|
+
for (const [key, value] of body.entries()) {
|
|
530
|
+
if (typeof value === 'string') {
|
|
531
|
+
entries.push({ key, value: { kind: 'text', value } });
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
entries.push({
|
|
535
|
+
key,
|
|
536
|
+
value: Object.assign(Object.assign({ kind: 'file' }, this.getOptionalBlobDetails(value)), { bytes: Array.from(new Uint8Array(yield this.readBlob(value))) })
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return { kind: 'form-data', entries };
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
getBlobName(value) {
|
|
543
|
+
if (typeof File !== 'undefined' && value instanceof File) {
|
|
544
|
+
return value.name;
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
getOptionalBlobDetails(value) {
|
|
549
|
+
const name = this.getBlobName(value);
|
|
550
|
+
const mimeType = value.type || undefined;
|
|
551
|
+
return Object.assign(Object.assign({}, (name !== undefined ? { name } : {})), (mimeType !== undefined ? { mimeType } : {}));
|
|
552
|
+
}
|
|
553
|
+
readBlob(value) {
|
|
554
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
555
|
+
const blobWithReader = value;
|
|
556
|
+
if (typeof blobWithReader.arrayBuffer === 'function') {
|
|
557
|
+
return yield blobWithReader.arrayBuffer();
|
|
558
|
+
}
|
|
559
|
+
if (typeof FileReader !== 'undefined') {
|
|
560
|
+
return yield new Promise((resolve, reject) => {
|
|
561
|
+
const reader = new FileReader();
|
|
562
|
+
reader.onload = () => resolve(reader.result);
|
|
563
|
+
reader.onerror = () => { var _a; return reject((_a = reader.error) !== null && _a !== void 0 ? _a : new Error('Failed to read Blob contents.')); };
|
|
564
|
+
reader.readAsArrayBuffer(value);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return yield new Response(value).arrayBuffer();
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
isBufferSource(value) {
|
|
571
|
+
return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
|
|
572
|
+
}
|
|
573
|
+
toArrayBuffer(value) {
|
|
574
|
+
if (value instanceof ArrayBuffer) {
|
|
575
|
+
return value.slice(0);
|
|
576
|
+
}
|
|
577
|
+
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
export function getMockRequestJsonBody(request) {
|
|
581
|
+
return getJsonBody(request.body);
|
|
582
|
+
}
|
|
583
|
+
export function getMockRequestTextBody(request) {
|
|
584
|
+
return getTextBody(request.body);
|
|
585
|
+
}
|
|
586
|
+
export function getMockRequestQuery(request) {
|
|
587
|
+
return parseQuery(parseUrl(request.url));
|
|
588
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DriverConfigContract } from '../../contracts/DriverConfigContract';
|
|
2
|
+
import { type ResolvedHeadersContract } from '../../contracts/HeadersContract';
|
|
3
|
+
import { type MockNormalizedRequestBody, type MockRequestBodyMatchContext, type MockRequestDriverOptions, MockRequestDriver, type MockRequestExpectation, type MockRequestHistoryEntry, type MockRequestPredicate, type MockRequestQuery, getMockRequestJsonBody, getMockRequestQuery, getMockRequestTextBody } from './MockRequestDriver';
|
|
4
|
+
import { type MockResponseDefinition } from './MockResponseHandler';
|
|
5
|
+
export interface InstallMockRequestDriverOptions extends MockRequestDriverOptions {
|
|
6
|
+
config?: DriverConfigContract;
|
|
7
|
+
expectations?: MockRequestExpectation[];
|
|
8
|
+
}
|
|
9
|
+
export declare function matchHeaders(expectedSubset: ResolvedHeadersContract): MockRequestPredicate<ResolvedHeadersContract>;
|
|
10
|
+
export declare function matchQuery(expectedSubset: MockRequestQuery): MockRequestPredicate<MockRequestQuery>;
|
|
11
|
+
export declare function expectJsonBody(expected: unknown, options?: {
|
|
12
|
+
partial?: boolean;
|
|
13
|
+
}): MockRequestPredicate<MockRequestBodyMatchContext>;
|
|
14
|
+
export declare function jsonResponse<ResponseBody extends object | string | Blob | BufferSource | null | undefined>(status: number, body: ResponseBody, headers?: ResolvedHeadersContract): MockResponseDefinition;
|
|
15
|
+
export declare function validationError(errors: Record<string, string[]>, message?: string): MockResponseDefinition;
|
|
16
|
+
export declare function emptyResponse(status?: number, headers?: ResolvedHeadersContract): MockResponseDefinition;
|
|
17
|
+
export declare function installMockRequestDriver(options?: InstallMockRequestDriverOptions): MockRequestDriver;
|
|
18
|
+
export declare function resetMockRequestDriver(): MockRequestDriver;
|
|
19
|
+
export { getMockRequestJsonBody, getMockRequestTextBody, getMockRequestQuery };
|
|
20
|
+
export type { MockNormalizedRequestBody, MockRequestHistoryEntry };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isEqual } from 'lodash-es';
|
|
2
|
+
import { BaseRequest } from '../../BaseRequest';
|
|
3
|
+
import { MockRequestDriver, getMockRequestJsonBody, getMockRequestQuery, getMockRequestTextBody } from './MockRequestDriver';
|
|
4
|
+
function createPredicate(description, predicate) {
|
|
5
|
+
const matcher = ((value) => predicate(value));
|
|
6
|
+
matcher.description = description;
|
|
7
|
+
return matcher;
|
|
8
|
+
}
|
|
9
|
+
function matchesSubset(actual, expected) {
|
|
10
|
+
if (Array.isArray(expected)) {
|
|
11
|
+
if (!Array.isArray(actual) || actual.length < expected.length) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return expected.every((item, index) => matchesSubset(actual[index], item));
|
|
15
|
+
}
|
|
16
|
+
if (isRecord(expected)) {
|
|
17
|
+
if (!isRecord(actual)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return Object.keys(expected).every((key) => matchesSubset(actual[key], expected[key]));
|
|
21
|
+
}
|
|
22
|
+
return isEqual(actual, expected);
|
|
23
|
+
}
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
export function matchHeaders(expectedSubset) {
|
|
28
|
+
return createPredicate(`headers subset ${JSON.stringify(expectedSubset)}`, (actual) => Object.entries(expectedSubset).every(([key, value]) => actual[key] === value));
|
|
29
|
+
}
|
|
30
|
+
export function matchQuery(expectedSubset) {
|
|
31
|
+
return createPredicate(`query subset ${JSON.stringify(expectedSubset)}`, (actual) => Object.entries(expectedSubset).every(([key, value]) => isEqual(actual[key], value)));
|
|
32
|
+
}
|
|
33
|
+
export function expectJsonBody(expected, options = {}) {
|
|
34
|
+
return createPredicate(options.partial ? `JSON body partial ${JSON.stringify(expected)}` : `JSON body ${JSON.stringify(expected)}`, (context) => {
|
|
35
|
+
const actual = context.getJson();
|
|
36
|
+
if (actual === undefined) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return options.partial ? matchesSubset(actual, expected) : isEqual(actual, expected);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function jsonResponse(status, body, headers) {
|
|
43
|
+
return Object.assign(Object.assign({ status }, (headers !== undefined ? { headers } : {})), { body });
|
|
44
|
+
}
|
|
45
|
+
export function validationError(errors, message = 'The given data was invalid.') {
|
|
46
|
+
return jsonResponse(422, {
|
|
47
|
+
message,
|
|
48
|
+
errors
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function emptyResponse(status = 204, headers) {
|
|
52
|
+
return Object.assign({ status }, (headers !== undefined ? { headers } : {}));
|
|
53
|
+
}
|
|
54
|
+
let installedMockRequestDriver;
|
|
55
|
+
export function installMockRequestDriver(options = {}) {
|
|
56
|
+
var _a;
|
|
57
|
+
const driver = new MockRequestDriver(options.config, (_a = options.expectations) !== null && _a !== void 0 ? _a : [], options.matchMode !== undefined ? { matchMode: options.matchMode } : {});
|
|
58
|
+
BaseRequest.setRequestDriver(driver);
|
|
59
|
+
installedMockRequestDriver = driver;
|
|
60
|
+
return driver;
|
|
61
|
+
}
|
|
62
|
+
export function resetMockRequestDriver() {
|
|
63
|
+
if (!installedMockRequestDriver) {
|
|
64
|
+
return installMockRequestDriver();
|
|
65
|
+
}
|
|
66
|
+
installedMockRequestDriver.reset();
|
|
67
|
+
BaseRequest.setRequestDriver(installedMockRequestDriver);
|
|
68
|
+
return installedMockRequestDriver;
|
|
69
|
+
}
|
|
70
|
+
export { getMockRequestJsonBody, getMockRequestTextBody, getMockRequestQuery };
|