@warp-drive/holodeck 0.1.0-alpha.85 → 0.1.0-alpha.88
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/README.md +4 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -23
- package/dist/index.js.map +1 -1
- package/dist/mock.d.ts +21 -4
- package/dist/mock.d.ts.map +1 -1
- package/dist/mock.js +99 -71
- package/dist/mock.js.map +1 -1
- package/package.json +11 -11
- package/server/ensure-cert.js +90 -36
- package/server/index.js +8 -25
- package/server/node.js +15 -16
- package/server/tsconfig.json +2 -1
- package/server/bun-worker.js +0 -3
- package/server/bun.js +0 -406
- package/server/compat-shim.js +0 -95
- package/server/node-compat-start.js +0 -12
package/README.md
CHANGED
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
/>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-

|
|
12
|
+

|
|
13
13
|

|
|
14
|
-
[](https://discord.gg/zT3asNS
|
|
15
|
-
)
|
|
16
|
-
[](https://discord.gg/PHBbnWJx5S
|
|
17
|
-
)
|
|
14
|
+
[](https://discord.gg/zT3asNS)
|
|
15
|
+
[](https://discord.gg/PHBbnWJx5S)
|
|
18
16
|
|
|
19
17
|
<p align="center">
|
|
20
18
|
<br>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ScaffoldGenerator } from "./mock.js";
|
|
1
|
+
import { LazyScaffold, ScaffoldGenerator } from "./mock.js";
|
|
2
2
|
import { Handler, NextFn } from "@warp-drive/core/request";
|
|
3
3
|
import { RequestContext, StructuredDataDocument } from "@warp-drive/core/types/request";
|
|
4
4
|
import { Store } from "@warp-drive/legacy/store";
|
|
@@ -50,6 +50,6 @@ export declare function installAdapterFor(owner: object, store: Store): void;
|
|
|
50
50
|
*
|
|
51
51
|
* @public
|
|
52
52
|
*/
|
|
53
|
-
export declare function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean): Promise<void>;
|
|
53
|
+
export declare function mock(owner: object, generate: ScaffoldGenerator | LazyScaffold, isRecording?: boolean): Promise<void>;
|
|
54
54
|
//#endregion
|
|
55
55
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;wBA6IgB,YAAY;EAAU;;;;;wBAQtB,UAAU,iBAAiB;;;;wBAoG3B,eAAe;;;;wBAOf;;;;;;;;;;;;qBAeH,6BAA6B;EACxC;EACA,YAAY;EAGZ,QAAc,GAAG,SAAS,gBAAgB,MAAM,OAAO,KAAK,QAAQ,uBAAuB;;;;;;;;;wBAwI7E,kBAAkB,eAAe,OAAO;;;;;;wBAuClC,KACpB,eACA,UAAU,oBAAoB,cAC9B,wBACC"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,10 @@ import { getGlobalConfig, macroCondition } from "@embroider/macros";
|
|
|
6
6
|
* @mergeModuleWith <project>
|
|
7
7
|
*/
|
|
8
8
|
const TEST_IDS = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* The shape `setTestId` stores per test context, named so the report below can
|
|
11
|
+
* take one as an argument.
|
|
12
|
+
*/
|
|
9
13
|
let HOST = "/";
|
|
10
14
|
/**
|
|
11
15
|
* @public
|
|
@@ -45,7 +49,49 @@ function setTestId(context, str) {
|
|
|
45
49
|
TRACE: {}
|
|
46
50
|
}
|
|
47
51
|
});
|
|
48
|
-
else
|
|
52
|
+
else {
|
|
53
|
+
const test = TEST_IDS.get(context);
|
|
54
|
+
TEST_IDS.delete(context);
|
|
55
|
+
if (test) reportUnrequestedMocks(test);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A mock is declared relative to the mock server (`users/1`) while the request
|
|
60
|
+
* carries the absolute url the code under test built
|
|
61
|
+
* (`https://localhost:7358/users/1`). The mock server reconciles the two by
|
|
62
|
+
* keying a fixture on the request's path, so compare them the same way.
|
|
63
|
+
*/
|
|
64
|
+
function normalizeUrlKey(url) {
|
|
65
|
+
const withoutOrigin = url.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, "");
|
|
66
|
+
return withoutOrigin.startsWith("/") ? withoutOrigin.slice(1) : withoutOrigin;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A mock the test never requested proves nothing: the test passes whether or
|
|
70
|
+
* not the code under test would have made that request. Nothing compared the
|
|
71
|
+
* two counters holodeck already keeps, so report the difference here, from the
|
|
72
|
+
* `afterEach` every suite runs, which fails the test that leaked.
|
|
73
|
+
*
|
|
74
|
+
* The original assertions are not masked by this: a framework reports an
|
|
75
|
+
* `afterEach` throw alongside the results the test body already recorded
|
|
76
|
+
* rather than in place of them.
|
|
77
|
+
*/
|
|
78
|
+
function reportUnrequestedMocks(test) {
|
|
79
|
+
const unrequested = [];
|
|
80
|
+
for (const method of Object.keys(test.mock)) {
|
|
81
|
+
const mocked = test.mock[method];
|
|
82
|
+
const requested = test.request[method] ?? {};
|
|
83
|
+
const requestCounts = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const url of Object.keys(requested)) {
|
|
85
|
+
const key = normalizeUrlKey(url);
|
|
86
|
+
requestCounts.set(key, (requestCounts.get(key) ?? 0) + requested[url]);
|
|
87
|
+
}
|
|
88
|
+
for (const url of Object.keys(mocked)) {
|
|
89
|
+
const mockCount = mocked[url];
|
|
90
|
+
const requestCount = requestCounts.get(normalizeUrlKey(url)) ?? 0;
|
|
91
|
+
if (mockCount > requestCount) unrequested.push(`\t${method} ${url} (mocked ${mockCount}, requested ${requestCount})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (unrequested.length) throw new Error(`Holodeck: this test declared mocks it never requested.\n\n${unrequested.join("\n")}\n\nA mock that is never requested proves nothing. Remove it, or make the request it describes.`);
|
|
49
95
|
}
|
|
50
96
|
const shouldRecord = macroCondition(getGlobalConfig().WarpDrive.env.SHOULD_RECORD) ? true : false;
|
|
51
97
|
let IS_RECORDING = null;
|
|
@@ -83,11 +129,34 @@ var MockServerHandler = class {
|
|
|
83
129
|
context.setStream(future.getStream());
|
|
84
130
|
return await future;
|
|
85
131
|
} catch (e) {
|
|
86
|
-
if (e instanceof Error && !(e instanceof DOMException))
|
|
132
|
+
if (e instanceof Error && !(e instanceof DOMException)) {
|
|
133
|
+
const explanation = getHolodeckExplanation(e);
|
|
134
|
+
if (explanation) e.message = `${e.message}\n\n${explanation}`;
|
|
135
|
+
e.message = e.message.split(queryForTest).join("");
|
|
136
|
+
}
|
|
87
137
|
throw e;
|
|
88
138
|
}
|
|
89
139
|
}
|
|
90
140
|
};
|
|
141
|
+
const HOLODECK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
142
|
+
"MOCK_NOT_FOUND",
|
|
143
|
+
"MISSING_X_TEST_ID_HEADER",
|
|
144
|
+
"MISSING_X_TEST_REQUEST_NUMBER_HEADER"
|
|
145
|
+
]);
|
|
146
|
+
/**
|
|
147
|
+
* The mock server explains itself in the response body, which the thrown
|
|
148
|
+
* error only carries as data. Lift that explanation into the message so it
|
|
149
|
+
* reaches a terminal and a CI log.
|
|
150
|
+
*/
|
|
151
|
+
function getHolodeckExplanation(e) {
|
|
152
|
+
const { content } = e;
|
|
153
|
+
if (!content || typeof content !== "object") return null;
|
|
154
|
+
const { errors } = content;
|
|
155
|
+
if (!Array.isArray(errors)) return null;
|
|
156
|
+
return errors.filter((error) => {
|
|
157
|
+
return !!error && typeof error === "object" && HOLODECK_ERROR_CODES.has(error.code);
|
|
158
|
+
}).map((error) => error.detail).filter((detail) => typeof detail === "string").join("\n\n") || null;
|
|
159
|
+
}
|
|
91
160
|
function setupHolodeckFetch(owner, request) {
|
|
92
161
|
const test = TEST_IDS.get(owner);
|
|
93
162
|
if (!test) throw new Error(`MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`);
|
|
@@ -152,27 +221,49 @@ function installAdapterFor(owner, store) {
|
|
|
152
221
|
* @public
|
|
153
222
|
*/
|
|
154
223
|
async function mock(owner, generate, isRecording) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
224
|
+
const test = TEST_IDS.get(owner);
|
|
225
|
+
if (!test) throw new Error(`Cannot call "mock" before configuring a testId. Use setTestId to set the testId for each test`);
|
|
226
|
+
let method;
|
|
227
|
+
let mockUrl;
|
|
228
|
+
let buildScaffold;
|
|
229
|
+
if (typeof generate === "function") {
|
|
230
|
+
const scaffold = generate();
|
|
231
|
+
({method, url: mockUrl} = scaffold);
|
|
232
|
+
buildScaffold = () => scaffold;
|
|
233
|
+
} else {
|
|
234
|
+
({method, url: mockUrl} = generate);
|
|
235
|
+
buildScaffold = generate.scaffold;
|
|
236
|
+
}
|
|
237
|
+
if (!mockUrl || !method) throw new Error(`MockError: Cannot mock a request without providing a URL and Method`);
|
|
238
|
+
const mockMethod = method.toUpperCase() ?? "GET";
|
|
239
|
+
if (!test.mock[mockMethod]) {
|
|
240
|
+
console.log(`⚠️ Using custom HTTP method ${mockMethod} for response to request ${mockUrl}`);
|
|
241
|
+
test.mock[mockMethod] = {};
|
|
242
|
+
}
|
|
243
|
+
if (!(mockUrl in test.mock[mockMethod])) test.mock[mockMethod][mockUrl] = 0;
|
|
244
|
+
const testMockNum = test.mock[mockMethod][mockUrl]++;
|
|
245
|
+
if (!getIsRecording() && !isRecording) return;
|
|
246
|
+
const requestToMock = buildScaffold();
|
|
247
|
+
const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;
|
|
248
|
+
const response = await fetch(url, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
body: JSON.stringify(requestToMock),
|
|
251
|
+
mode: "cors",
|
|
252
|
+
credentials: "omit",
|
|
253
|
+
referrerPolicy: ""
|
|
254
|
+
});
|
|
255
|
+
if (!response.ok) throw new Error(`MockError: Holodeck failed to record ${mockMethod} ${mockUrl} (${response.status} ${response.statusText}). ${await getRecordFailureDetail(response)}`);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* A failed recording is otherwise invisible until the next replay run fails
|
|
259
|
+
* with a missing fixture, so report what the server said at the point of
|
|
260
|
+
* failure.
|
|
261
|
+
*/
|
|
262
|
+
async function getRecordFailureDetail(response) {
|
|
263
|
+
try {
|
|
264
|
+
return (await response.json()).errors?.[0]?.detail ?? "The mock server gave no explanation.";
|
|
265
|
+
} catch {
|
|
266
|
+
return "The mock server gave no explanation.";
|
|
176
267
|
}
|
|
177
268
|
}
|
|
178
269
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["getGlobalConfig","macroCondition","TEST_IDS","WeakMap","HOST","setConfig","host","endsWith","setTestId","context","str","has","Error","set","id","mock","GET","PUT","PATCH","DELETE","POST","QUERY","OPTIONS","HEAD","CONNECT","TRACE","request","delete","shouldRecord","WarpDrive","env","SHOULD_RECORD","IS_RECORDING","setIsRecording","value","Boolean","getIsRecording","MockServerHandler","constructor","owner","next","queryForTest","setupHolodeckFetch","Object","assign","future","setStream","getStream","e","DOMException","message","replace","test","get","url","firstChar","includes","method","toUpperCase","console","log","mode","credentials","referrerPolicy","headers","Headers","upgradeAdapter","adapter","upgradeStore","store","adapterFor","installAdapterFor","fn","holodeckAdapterFor","modelName","_allowMissing","call","hasOverriddenFetch","useFetch","originalFetch","_fetchRequest","bind","options","String","generate","isRecording","requestToMock","mockUrl","mockMethod","testMockNum","fetch","body","JSON","stringify"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module\n * @mergeModuleWith <project>\n */\nimport { SHOULD_RECORD } from '@warp-drive/core/build-config/env';\nimport type { Handler, NextFn } from '@warp-drive/core/request';\nimport type { HTTPMethod, RequestContext, RequestInfo, StructuredDataDocument } from '@warp-drive/core/types/request';\nimport type { MinimumAdapterInterface } from '@warp-drive/legacy/compat';\nimport type { Store } from '@warp-drive/legacy/store';\n\nimport type { ScaffoldGenerator } from './mock';\n\nconst TEST_IDS = new WeakMap<\n object,\n {\n id: string;\n /**\n * keeps track of the count of calls to record a mock\n */\n mock: {\n /**\n * For each GET, we keep track of the count\n * for a specific URL\n */\n GET: Record<string, number>;\n /**\n * For each PUT, we keep track of the count\n * for a specific URL\n */\n PUT: Record<string, number>;\n /**\n * For each PATCH, we keep track of the count\n * for a specific URL\n */\n PATCH: Record<string, number>;\n /**\n * For each DELETE, we keep track of the count\n * for a specific URL\n */\n DELETE: Record<string, number>;\n /**\n * For each POST, we keep track of the count\n * for a specific URL\n */\n POST: Record<string, number>;\n /**\n * For each OPTIONS, we keep track of the count\n * for a specific URL\n */\n OPTIONS: Record<string, number>;\n /**\n * For each QUERY, we keep track of the count\n * for a specific URL\n */\n QUERY: Record<string, number>;\n /**\n * for each HEAD, we keep track of the count\n * for a specific URL\n */\n HEAD: Record<string, number>;\n /**\n * for each CONNECT, we keep track of the count\n * for a specific URL\n */\n CONNECT: Record<string, number>;\n /**\n * for each TRACE, we keep track of the count\n * for a specific URL\n */\n TRACE: Record<string, number>;\n };\n /**\n * keeps track of the count of calls to make a request\n */\n request: {\n /**\n * For each GET, we keep track of the count\n * for a specific URL\n */\n GET: Record<string, number>;\n /**\n * For each PUT, we keep track of the count\n * for a specific URL\n */\n PUT: Record<string, number>;\n /**\n * For each PATCH, we keep track of the count\n * for a specific URL\n */\n PATCH: Record<string, number>;\n /**\n * For each DELETE, we keep track of the count\n * for a specific URL\n */\n DELETE: Record<string, number>;\n /**\n * For each POST, we keep track of the count\n * for a specific URL\n */\n POST: Record<string, number>;\n /**\n * For each OPTIONS, we keep track of the count\n * for a specific URL\n */\n OPTIONS: Record<string, number>;\n /**\n * For each QUERY, we keep track of the count\n * for a specific URL\n */\n QUERY: Record<string, number>;\n /**\n * for each HEAD, we keep track of the count\n * for a specific URL\n */\n HEAD: Record<string, number>;\n /**\n * for each CONNECT, we keep track of the count\n * for a specific URL\n */\n CONNECT: Record<string, number>;\n /**\n * for each TRACE, we keep track of the count\n * for a specific URL\n */\n TRACE: Record<string, number>;\n };\n }\n>();\n\nlet HOST = '/';\n\n/**\n * @public\n */\n\nexport function setConfig({ host }: { host: string }): void {\n HOST = host.endsWith('/') ? host : `${host}/`;\n}\n\n/**\n * @public\n */\n\nexport function setTestId(context: object, str: string | null): void {\n if (str && TEST_IDS.has(context)) {\n throw new Error(`MockServerHandler is already configured with a testId.`);\n }\n if (str) {\n TEST_IDS.set(context, {\n id: str,\n mock: {\n GET: {},\n PUT: {},\n PATCH: {},\n DELETE: {},\n POST: {},\n QUERY: {},\n OPTIONS: {},\n HEAD: {},\n CONNECT: {},\n TRACE: {},\n },\n request: {\n GET: {},\n PUT: {},\n PATCH: {},\n DELETE: {},\n POST: {},\n QUERY: {},\n OPTIONS: {},\n HEAD: {},\n CONNECT: {},\n TRACE: {},\n },\n });\n } else {\n TEST_IDS.delete(context);\n }\n}\n\nconst shouldRecord = SHOULD_RECORD ? true : false;\nlet IS_RECORDING: boolean | null = null;\n\n/**\n * @public\n */\nexport function setIsRecording(value: boolean): void {\n IS_RECORDING = value === null ? value : Boolean(value);\n}\n\n/**\n * @public\n */\nexport function getIsRecording(): boolean {\n return IS_RECORDING === null ? shouldRecord : IS_RECORDING;\n}\n\n/**\n * A request handler that intercepts requests and routes them through\n * the Holodeck mock server.\n *\n * This handler modifies the request URL to include test identifiers\n * and manages request counts for accurate mocking.\n *\n * Requires that the test context be configured with a testId using `setTestId`.\n *\n * @param owner - the test context object used to retrieve the test ID.\n */\nexport class MockServerHandler implements Handler {\n declare owner: object;\n constructor(owner: object) {\n this.owner = owner;\n }\n async request<T>(context: RequestContext, next: NextFn<T>): Promise<StructuredDataDocument<T>> {\n const { request, queryForTest } = setupHolodeckFetch(this.owner, Object.assign({}, context.request));\n\n try {\n const future = next(request);\n context.setStream(future.getStream());\n return await future;\n } catch (e) {\n if (e instanceof Error && !(e instanceof DOMException)) {\n e.message = e.message.replace(queryForTest, '');\n }\n throw e;\n }\n }\n}\n\nfunction setupHolodeckFetch(owner: object, request: RequestInfo): { request: RequestInfo; queryForTest: string } {\n const test = TEST_IDS.get(owner);\n if (!test) {\n throw new Error(`MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`);\n }\n\n const url = request.url!;\n const firstChar = url.includes('?') ? '&' : '?';\n const method = (request.method?.toUpperCase() ?? 'GET') as HTTPMethod;\n\n // enable custom methods\n if (!test.request[method]) {\n // oxlint-disable-next-line no-console\n console.log(`⚠️ Using custom HTTP method ${method} for response to request ${url}`);\n\n test.request[method] = {};\n }\n if (!(url in test.request[method])) {\n test.request[method][url] = 0;\n }\n\n const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${test.request[method][url]++}`;\n request.url = url + queryForTest;\n request.method = method;\n\n request.mode = 'cors';\n request.credentials = 'omit';\n request.referrerPolicy = '';\n\n // since holodeck currently runs on a separate port\n // and we don't want to trigger cors pre-flight\n // we convert PUT to POST to keep the request in the\n // \"simple\" cors category.\n // if (request.method === 'PUT') {\n // request.method = 'POST';\n // }\n\n const headers = new Headers(request.headers);\n if (headers.has('Content-Type')) {\n // under the rules of simple-cors, content-type can only be\n // one of three things, none of which are what folks typically\n // set this to. Since holodeck always expects body to be JSON\n // this \"just works\".\n headers.set('Content-Type', 'text/plain');\n request.headers = headers;\n }\n\n return { request, queryForTest };\n}\n\ninterface HasAdapterForFn {\n adapterFor(this: Store, modelName: string): MinimumAdapterInterface;\n adapterFor(this: Store, modelName: string, _allowMissing?: true): MinimumAdapterInterface | undefined;\n}\n\n/*\n _fetchRequest(options: FetchRequestInit): Promise<Response> {\n const fetchFunction = fetch();\n\n return fetchFunction(options.url, options);\n }\n*/\ninterface PrivateAdapter {\n _fetchRequest(options: RequestInfo): Promise<Response>;\n hasOverriddenFetch: boolean;\n useFetch: boolean;\n}\n\nfunction upgradeAdapter(adapter: unknown): asserts adapter is PrivateAdapter {}\nfunction upgradeStore(store: Store): asserts store is Store & { adapterFor: HasAdapterForFn['adapterFor'] } {\n if (typeof store.adapterFor !== 'function') {\n throw new Error('Store is not compatible with Holodeck. Missing adapterFor method.');\n }\n}\n\n/**\n * Creates an adapterFor function that wraps the provided adapterFor function\n * to override the adapter's _fetchRequest method to route requests through\n * the Holodeck mock server.\n *\n * @param owner - The test context object used to retrieve the test ID.\n */\nexport function installAdapterFor(owner: object, store: Store): void {\n upgradeStore(store);\n const fn = store.adapterFor;\n function holodeckAdapterFor(\n this: Store,\n modelName: string,\n _allowMissing?: true\n ): MinimumAdapterInterface | undefined {\n const adapter = fn.call(this, modelName, _allowMissing);\n\n if (adapter) {\n upgradeAdapter(adapter);\n\n if (!adapter.hasOverriddenFetch) {\n adapter.hasOverriddenFetch = true;\n adapter.useFetch = true;\n const originalFetch = adapter._fetchRequest?.bind(adapter);\n\n adapter._fetchRequest = function (options: RequestInfo) {\n if (!originalFetch) {\n throw new Error(`Adapter ${String(modelName)} does not implement _fetchRequest`);\n }\n const { request } = setupHolodeckFetch(owner, options);\n\n return originalFetch(request);\n };\n }\n }\n\n return adapter;\n }\n store.adapterFor = holodeckAdapterFor as HasAdapterForFn['adapterFor'];\n}\n\n/**\n * Mock a request by sending the scaffold to the mock server.\n *\n * @public\n */\nexport async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean): Promise<void> {\n if (getIsRecording() || isRecording) {\n const test = TEST_IDS.get(owner);\n if (!test) {\n throw new Error(`Cannot call \"mock\" before configuring a testId. Use setTestId to set the testId for each test`);\n }\n const requestToMock = generate();\n const { url: mockUrl, method } = requestToMock;\n if (!mockUrl || !method) {\n throw new Error(`MockError: Cannot mock a request without providing a URL and Method`);\n }\n const mockMethod = (method?.toUpperCase() ?? 'GET') as HTTPMethod;\n\n // enable custom methods\n if (!test.mock[mockMethod]) {\n // oxlint-disable-next-line no-console\n console.log(`⚠️ Using custom HTTP method ${mockMethod} for response to request ${mockUrl}`);\n test.mock[mockMethod] = {};\n }\n if (!(mockUrl in test.mock[mockMethod])) {\n test.mock[mockMethod][mockUrl] = 0;\n }\n const testMockNum = test.mock[mockMethod][mockUrl]++;\n const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;\n await fetch(url, {\n method: 'POST',\n body: JSON.stringify(requestToMock),\n mode: 'cors',\n credentials: 'omit',\n referrerPolicy: '',\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["getGlobalConfig","macroCondition","TEST_IDS","WeakMap","HOST","setConfig","host","endsWith","setTestId","context","str","has","Error","set","id","mock","GET","PUT","PATCH","DELETE","POST","QUERY","OPTIONS","HEAD","CONNECT","TRACE","request","test","get","delete","reportUnrequestedMocks","normalizeUrlKey","url","withoutOrigin","replace","startsWith","slice","unrequested","method","Object","keys","mocked","requested","requestCounts","Map","key","mockCount","requestCount","push","length","join","shouldRecord","WarpDrive","env","SHOULD_RECORD","IS_RECORDING","setIsRecording","value","Boolean","getIsRecording","MockServerHandler","constructor","owner","next","queryForTest","setupHolodeckFetch","assign","future","setStream","getStream","e","DOMException","explanation","getHolodeckExplanation","message","split","HOLODECK_ERROR_CODES","Set","content","errors","Array","isArray","filter","error","code","map","detail","firstChar","includes","toUpperCase","console","log","mode","credentials","referrerPolicy","headers","Headers","upgradeAdapter","adapter","upgradeStore","store","adapterFor","installAdapterFor","fn","holodeckAdapterFor","modelName","_allowMissing","call","hasOverriddenFetch","useFetch","originalFetch","_fetchRequest","bind","options","String","generate","isRecording","mockUrl","buildScaffold","scaffold","mockMethod","testMockNum","requestToMock","response","fetch","body","JSON","stringify","ok","status","statusText","getRecordFailureDetail","json"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module\n * @mergeModuleWith <project>\n */\nimport { SHOULD_RECORD } from '@warp-drive/core/build-config/env';\nimport type { Handler, NextFn } from '@warp-drive/core/request';\nimport type { HTTPMethod, RequestContext, RequestInfo, StructuredDataDocument } from '@warp-drive/core/types/request';\nimport type { MinimumAdapterInterface } from '@warp-drive/legacy/compat';\nimport type { Store } from '@warp-drive/legacy/store';\n\nimport type { LazyScaffold, Scaffold, ScaffoldGenerator } from './mock';\n\nconst TEST_IDS = new WeakMap<\n object,\n {\n id: string;\n /**\n * keeps track of the count of calls to record a mock\n */\n mock: {\n /**\n * For each GET, we keep track of the count\n * for a specific URL\n */\n GET: Record<string, number>;\n /**\n * For each PUT, we keep track of the count\n * for a specific URL\n */\n PUT: Record<string, number>;\n /**\n * For each PATCH, we keep track of the count\n * for a specific URL\n */\n PATCH: Record<string, number>;\n /**\n * For each DELETE, we keep track of the count\n * for a specific URL\n */\n DELETE: Record<string, number>;\n /**\n * For each POST, we keep track of the count\n * for a specific URL\n */\n POST: Record<string, number>;\n /**\n * For each OPTIONS, we keep track of the count\n * for a specific URL\n */\n OPTIONS: Record<string, number>;\n /**\n * For each QUERY, we keep track of the count\n * for a specific URL\n */\n QUERY: Record<string, number>;\n /**\n * for each HEAD, we keep track of the count\n * for a specific URL\n */\n HEAD: Record<string, number>;\n /**\n * for each CONNECT, we keep track of the count\n * for a specific URL\n */\n CONNECT: Record<string, number>;\n /**\n * for each TRACE, we keep track of the count\n * for a specific URL\n */\n TRACE: Record<string, number>;\n };\n /**\n * keeps track of the count of calls to make a request\n */\n request: {\n /**\n * For each GET, we keep track of the count\n * for a specific URL\n */\n GET: Record<string, number>;\n /**\n * For each PUT, we keep track of the count\n * for a specific URL\n */\n PUT: Record<string, number>;\n /**\n * For each PATCH, we keep track of the count\n * for a specific URL\n */\n PATCH: Record<string, number>;\n /**\n * For each DELETE, we keep track of the count\n * for a specific URL\n */\n DELETE: Record<string, number>;\n /**\n * For each POST, we keep track of the count\n * for a specific URL\n */\n POST: Record<string, number>;\n /**\n * For each OPTIONS, we keep track of the count\n * for a specific URL\n */\n OPTIONS: Record<string, number>;\n /**\n * For each QUERY, we keep track of the count\n * for a specific URL\n */\n QUERY: Record<string, number>;\n /**\n * for each HEAD, we keep track of the count\n * for a specific URL\n */\n HEAD: Record<string, number>;\n /**\n * for each CONNECT, we keep track of the count\n * for a specific URL\n */\n CONNECT: Record<string, number>;\n /**\n * for each TRACE, we keep track of the count\n * for a specific URL\n */\n TRACE: Record<string, number>;\n };\n }\n>();\n\n/**\n * The shape `setTestId` stores per test context, named so the report below can\n * take one as an argument.\n */\ntype TestEntry = NonNullable<ReturnType<(typeof TEST_IDS)['get']>>;\n\nlet HOST = '/';\n\n/**\n * @public\n */\n\nexport function setConfig({ host }: { host: string }): void {\n HOST = host.endsWith('/') ? host : `${host}/`;\n}\n\n/**\n * @public\n */\n\nexport function setTestId(context: object, str: string | null): void {\n if (str && TEST_IDS.has(context)) {\n throw new Error(`MockServerHandler is already configured with a testId.`);\n }\n if (str) {\n TEST_IDS.set(context, {\n id: str,\n mock: {\n GET: {},\n PUT: {},\n PATCH: {},\n DELETE: {},\n POST: {},\n QUERY: {},\n OPTIONS: {},\n HEAD: {},\n CONNECT: {},\n TRACE: {},\n },\n request: {\n GET: {},\n PUT: {},\n PATCH: {},\n DELETE: {},\n POST: {},\n QUERY: {},\n OPTIONS: {},\n HEAD: {},\n CONNECT: {},\n TRACE: {},\n },\n });\n } else {\n const test = TEST_IDS.get(context);\n TEST_IDS.delete(context);\n\n if (test) {\n reportUnrequestedMocks(test);\n }\n }\n}\n\n/**\n * A mock is declared relative to the mock server (`users/1`) while the request\n * carries the absolute url the code under test built\n * (`https://localhost:7358/users/1`). The mock server reconciles the two by\n * keying a fixture on the request's path, so compare them the same way.\n */\nfunction normalizeUrlKey(url: string): string {\n const withoutOrigin = url.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^/]+/i, '');\n return withoutOrigin.startsWith('/') ? withoutOrigin.slice(1) : withoutOrigin;\n}\n\n/**\n * A mock the test never requested proves nothing: the test passes whether or\n * not the code under test would have made that request. Nothing compared the\n * two counters holodeck already keeps, so report the difference here, from the\n * `afterEach` every suite runs, which fails the test that leaked.\n *\n * The original assertions are not masked by this: a framework reports an\n * `afterEach` throw alongside the results the test body already recorded\n * rather than in place of them.\n */\nfunction reportUnrequestedMocks(test: TestEntry): void {\n const unrequested: string[] = [];\n\n for (const method of Object.keys(test.mock) as HTTPMethod[]) {\n const mocked = test.mock[method];\n const requested = test.request[method] ?? {};\n const requestCounts = new Map<string, number>();\n\n for (const url of Object.keys(requested)) {\n const key = normalizeUrlKey(url);\n requestCounts.set(key, (requestCounts.get(key) ?? 0) + requested[url]);\n }\n\n for (const url of Object.keys(mocked)) {\n const mockCount = mocked[url];\n const requestCount = requestCounts.get(normalizeUrlKey(url)) ?? 0;\n\n if (mockCount > requestCount) {\n unrequested.push(`\\t${method} ${url} (mocked ${mockCount}, requested ${requestCount})`);\n }\n }\n }\n\n if (unrequested.length) {\n throw new Error(\n `Holodeck: this test declared mocks it never requested.\\n\\n${unrequested.join('\\n')}\\n\\n` +\n `A mock that is never requested proves nothing. Remove it, or make the request it describes.`\n );\n }\n}\n\nconst shouldRecord = SHOULD_RECORD ? true : false;\nlet IS_RECORDING: boolean | null = null;\n\n/**\n * @public\n */\nexport function setIsRecording(value: boolean): void {\n IS_RECORDING = value === null ? value : Boolean(value);\n}\n\n/**\n * @public\n */\nexport function getIsRecording(): boolean {\n return IS_RECORDING === null ? shouldRecord : IS_RECORDING;\n}\n\n/**\n * A request handler that intercepts requests and routes them through\n * the Holodeck mock server.\n *\n * This handler modifies the request URL to include test identifiers\n * and manages request counts for accurate mocking.\n *\n * Requires that the test context be configured with a testId using `setTestId`.\n *\n * @param owner - the test context object used to retrieve the test ID.\n */\nexport class MockServerHandler implements Handler {\n declare owner: object;\n constructor(owner: object) {\n this.owner = owner;\n }\n async request<T>(context: RequestContext, next: NextFn<T>): Promise<StructuredDataDocument<T>> {\n const { request, queryForTest } = setupHolodeckFetch(this.owner, Object.assign({}, context.request));\n\n try {\n const future = next(request);\n context.setStream(future.getStream());\n return await future;\n } catch (e) {\n if (e instanceof Error && !(e instanceof DOMException)) {\n const explanation = getHolodeckExplanation(e);\n if (explanation) {\n e.message = `${e.message}\\n\\n${explanation}`;\n }\n e.message = e.message.split(queryForTest).join('');\n }\n throw e;\n }\n }\n}\n\nconst HOLODECK_ERROR_CODES = new Set([\n 'MOCK_NOT_FOUND',\n 'MISSING_X_TEST_ID_HEADER',\n 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',\n]);\n\n/**\n * The mock server explains itself in the response body, which the thrown\n * error only carries as data. Lift that explanation into the message so it\n * reaches a terminal and a CI log.\n */\nfunction getHolodeckExplanation(e: Error): string | null {\n const { content } = e as Error & { content?: unknown };\n if (!content || typeof content !== 'object') {\n return null;\n }\n const { errors } = content as { errors?: unknown };\n if (!Array.isArray(errors)) {\n return null;\n }\n\n return (\n errors\n .filter((error): error is { code: string; detail?: string } => {\n return (\n !!error && typeof error === 'object' && HOLODECK_ERROR_CODES.has((error as { code?: unknown }).code as string)\n );\n })\n .map((error) => error.detail)\n .filter((detail): detail is string => typeof detail === 'string')\n .join('\\n\\n') || null\n );\n}\n\nfunction setupHolodeckFetch(owner: object, request: RequestInfo): { request: RequestInfo; queryForTest: string } {\n const test = TEST_IDS.get(owner);\n if (!test) {\n throw new Error(`MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`);\n }\n\n const url = request.url!;\n const firstChar = url.includes('?') ? '&' : '?';\n const method = (request.method?.toUpperCase() ?? 'GET') as HTTPMethod;\n\n // enable custom methods\n if (!test.request[method]) {\n // oxlint-disable-next-line no-console\n console.log(`⚠️ Using custom HTTP method ${method} for response to request ${url}`);\n\n test.request[method] = {};\n }\n if (!(url in test.request[method])) {\n test.request[method][url] = 0;\n }\n\n const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${test.request[method][url]++}`;\n request.url = url + queryForTest;\n request.method = method;\n\n request.mode = 'cors';\n request.credentials = 'omit';\n request.referrerPolicy = '';\n\n // since holodeck currently runs on a separate port\n // and we don't want to trigger cors pre-flight\n // we convert PUT to POST to keep the request in the\n // \"simple\" cors category.\n // if (request.method === 'PUT') {\n // request.method = 'POST';\n // }\n\n const headers = new Headers(request.headers);\n if (headers.has('Content-Type')) {\n // under the rules of simple-cors, content-type can only be\n // one of three things, none of which are what folks typically\n // set this to. Since holodeck always expects body to be JSON\n // this \"just works\".\n headers.set('Content-Type', 'text/plain');\n request.headers = headers;\n }\n\n return { request, queryForTest };\n}\n\ninterface HasAdapterForFn {\n adapterFor(this: Store, modelName: string): MinimumAdapterInterface;\n adapterFor(this: Store, modelName: string, _allowMissing?: true): MinimumAdapterInterface | undefined;\n}\n\n/*\n _fetchRequest(options: FetchRequestInit): Promise<Response> {\n const fetchFunction = fetch();\n\n return fetchFunction(options.url, options);\n }\n*/\ninterface PrivateAdapter {\n _fetchRequest(options: RequestInfo): Promise<Response>;\n hasOverriddenFetch: boolean;\n useFetch: boolean;\n}\n\nfunction upgradeAdapter(adapter: unknown): asserts adapter is PrivateAdapter {}\nfunction upgradeStore(store: Store): asserts store is Store & { adapterFor: HasAdapterForFn['adapterFor'] } {\n if (typeof store.adapterFor !== 'function') {\n throw new Error('Store is not compatible with Holodeck. Missing adapterFor method.');\n }\n}\n\n/**\n * Creates an adapterFor function that wraps the provided adapterFor function\n * to override the adapter's _fetchRequest method to route requests through\n * the Holodeck mock server.\n *\n * @param owner - The test context object used to retrieve the test ID.\n */\nexport function installAdapterFor(owner: object, store: Store): void {\n upgradeStore(store);\n const fn = store.adapterFor;\n function holodeckAdapterFor(\n this: Store,\n modelName: string,\n _allowMissing?: true\n ): MinimumAdapterInterface | undefined {\n const adapter = fn.call(this, modelName, _allowMissing);\n\n if (adapter) {\n upgradeAdapter(adapter);\n\n if (!adapter.hasOverriddenFetch) {\n adapter.hasOverriddenFetch = true;\n adapter.useFetch = true;\n const originalFetch = adapter._fetchRequest?.bind(adapter);\n\n adapter._fetchRequest = function (options: RequestInfo) {\n if (!originalFetch) {\n throw new Error(`Adapter ${String(modelName)} does not implement _fetchRequest`);\n }\n const { request } = setupHolodeckFetch(owner, options);\n\n return originalFetch(request);\n };\n }\n }\n\n return adapter;\n }\n store.adapterFor = holodeckAdapterFor as HasAdapterForFn['adapterFor'];\n}\n\n/**\n * Mock a request by sending the scaffold to the mock server.\n *\n * @public\n */\nexport async function mock(\n owner: object,\n generate: ScaffoldGenerator | LazyScaffold,\n isRecording?: boolean\n): Promise<void> {\n const test = TEST_IDS.get(owner);\n if (!test) {\n throw new Error(`Cannot call \"mock\" before configuring a testId. Use setTestId to set the testId for each test`);\n }\n\n // A LazyScaffold carries its identity, so its body is built only when\n // recording. A bare generator is the only source of its own identity, so it\n // has to run either way; the helpers in ./mock never pass one.\n let method: string;\n let mockUrl: string;\n let buildScaffold: () => Scaffold;\n if (typeof generate === 'function') {\n const scaffold = generate();\n ({ method, url: mockUrl } = scaffold);\n buildScaffold = () => scaffold;\n } else {\n ({ method, url: mockUrl } = generate);\n buildScaffold = generate.scaffold;\n }\n if (!mockUrl || !method) {\n throw new Error(`MockError: Cannot mock a request without providing a URL and Method`);\n }\n const mockMethod = (method.toUpperCase() ?? 'GET') as HTTPMethod;\n\n // enable custom methods\n if (!test.mock[mockMethod]) {\n // oxlint-disable-next-line no-console\n console.log(`⚠️ Using custom HTTP method ${mockMethod} for response to request ${mockUrl}`);\n test.mock[mockMethod] = {};\n }\n if (!(mockUrl in test.mock[mockMethod])) {\n test.mock[mockMethod][mockUrl] = 0;\n }\n // counted whether or not we are recording, so that a mock the test never\n // requests is reported in replay runs too, not only while recording.\n const testMockNum = test.mock[mockMethod][mockUrl]++;\n\n // `isRecording` is the per-request RECORD override, which records even when\n // the suite as a whole is replaying.\n if (!getIsRecording() && !isRecording) {\n return;\n }\n\n const requestToMock = buildScaffold();\n const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;\n const response = await fetch(url, {\n method: 'POST',\n body: JSON.stringify(requestToMock),\n mode: 'cors',\n credentials: 'omit',\n referrerPolicy: '',\n });\n\n if (!response.ok) {\n throw new Error(\n `MockError: Holodeck failed to record ${mockMethod} ${mockUrl} (${response.status} ${response.statusText}). ${await getRecordFailureDetail(response)}`\n );\n }\n}\n\n/**\n * A failed recording is otherwise invisible until the next replay run fails\n * with a missing fixture, so report what the server said at the point of\n * failure.\n */\nasync function getRecordFailureDetail(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { errors?: { detail?: string }[] };\n const detail = body.errors?.[0]?.detail;\n return detail ?? 'The mock server gave no explanation.';\n } catch {\n return 'The mock server gave no explanation.';\n }\n}\n"],"mappings":"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"}
|
package/dist/mock.d.ts
CHANGED
|
@@ -15,6 +15,19 @@ export interface Scaffold {
|
|
|
15
15
|
* @public
|
|
16
16
|
*/
|
|
17
17
|
export type ScaffoldGenerator = () => Scaffold;
|
|
18
|
+
/**
|
|
19
|
+
* A mock whose method and url are known up front, with the rest of the
|
|
20
|
+
* scaffold built only when holodeck is recording. This is what the mock
|
|
21
|
+
* helpers pass, so that in replay mode a test's response generators never
|
|
22
|
+
* run.
|
|
23
|
+
*
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export interface LazyScaffold {
|
|
27
|
+
method: string;
|
|
28
|
+
url: string;
|
|
29
|
+
scaffold: () => Scaffold;
|
|
30
|
+
}
|
|
18
31
|
/**
|
|
19
32
|
* @public
|
|
20
33
|
*/
|
|
@@ -29,11 +42,13 @@ export type ResponseGenerator = () => Record<string, unknown>;
|
|
|
29
42
|
* - status: the status code to return (default: 200)
|
|
30
43
|
* - headers: the headers to return (default: {})
|
|
31
44
|
* - body: the body to match against for the request (default: null)
|
|
32
|
-
* - RECORD:
|
|
45
|
+
* - RECORD: record this request even when the suite is replaying, such as under CI (default: false).
|
|
46
|
+
* A local override for re-recording one request. Do not commit it; a committed RECORD means
|
|
47
|
+
* that request is never replayed against its fixture.
|
|
33
48
|
*
|
|
34
49
|
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
35
50
|
* @param response a function which generates the response to return
|
|
36
|
-
* @param options status, headers for the response, body to match against for the request, and whether to
|
|
51
|
+
* @param options status, headers for the response, body to match against for the request, and whether to force recording
|
|
37
52
|
* @return
|
|
38
53
|
*/
|
|
39
54
|
export declare function GET(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
@@ -74,11 +89,13 @@ export declare function DELETE(owner: object, url: string, response: ResponseGen
|
|
|
74
89
|
* - status: the status code to return (default: 200)
|
|
75
90
|
* - headers: the headers to return (default: {})
|
|
76
91
|
* - body: the body to match against for the request (default: null)
|
|
77
|
-
* - RECORD:
|
|
92
|
+
* - RECORD: record this request even when the suite is replaying, such as under CI (default: false).
|
|
93
|
+
* A local override for re-recording one request. Do not commit it; a committed RECORD means
|
|
94
|
+
* that request is never replayed against its fixture.
|
|
78
95
|
*
|
|
79
96
|
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
80
97
|
* @param response a function which generates the response to return
|
|
81
|
-
* @param options status, headers for the response, body to match against for the request, and whether to
|
|
98
|
+
* @param options status, headers for the response, body to match against for the request, and whether to force recording
|
|
82
99
|
* @return
|
|
83
100
|
*/
|
|
84
101
|
export declare function HEAD(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
package/dist/mock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock.d.ts","names":[],"sources":["../src/mock.ts"],"mappings":";;;;iBAKiB;EACf;EACA;EACA,SAAS;EACT,MAAM;EACN;EACA;EACA,UAAU;;;;;YAMA,0BAA0B
|
|
1
|
+
{"version":3,"file":"mock.d.ts","names":[],"sources":["../src/mock.ts"],"mappings":";;;;iBAKiB;EACf;EACA;EACA,SAAS;EACT,MAAM;EACN;EACA;EACA,UAAU;;;;;YAMA,0BAA0B;;;;;;;;;iBAUrB;EACf;EACA;EACA,gBAAgB;;;;;YAMN,0BAA0B;;;;;;;;;;;;;;;;;;;;wBAqBtB,IACd,eACA,aACA,UAAU,mBACV,UAAU,QAAQ,KAAK;EAA8C;IACpE;;;;wBAuFa,KACd,eACA,aACA,UAAU,mBACV,UAAU,QAAQ,KAAK;EAA8C;IACpE;;;;wBA4Ba,IACd,eACA,aACA,UAAU,mBACV,UAAU,QAAQ,KAAK;EAA8C;IACpE;;;;;wBA4Ba,MACd,eACA,aACA,UAAU,mBACV,UAAU,QAAQ,KAAK;EAA8C;IACpE;;;;wBA2Ba,OACd,eACA,aACA,UAAU,mBACV,UAAU,QAAQ,KAAK;EAA8C;IACpE;;;;;;;;;;;;;;;;;;;;wBA4Ca,KACd,eACA,aACA,UAAU,mBAIV,UAAU,QAAQ,KAAK;EAA8C;IACpE"}
|
package/dist/mock.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mock } from "./index.js";
|
|
2
2
|
|
|
3
3
|
//#region src/mock.ts
|
|
4
4
|
/**
|
|
@@ -11,23 +11,29 @@ import { getIsRecording, mock } from "./index.js";
|
|
|
11
11
|
* - status: the status code to return (default: 200)
|
|
12
12
|
* - headers: the headers to return (default: {})
|
|
13
13
|
* - body: the body to match against for the request (default: null)
|
|
14
|
-
* - RECORD:
|
|
14
|
+
* - RECORD: record this request even when the suite is replaying, such as under CI (default: false).
|
|
15
|
+
* A local override for re-recording one request. Do not commit it; a committed RECORD means
|
|
16
|
+
* that request is never replayed against its fixture.
|
|
15
17
|
*
|
|
16
18
|
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
17
19
|
* @param response a function which generates the response to return
|
|
18
|
-
* @param options status, headers for the response, body to match against for the request, and whether to
|
|
20
|
+
* @param options status, headers for the response, body to match against for the request, and whether to force recording
|
|
19
21
|
* @return
|
|
20
22
|
*/
|
|
21
23
|
function GET(owner, url, response, options) {
|
|
22
|
-
return mock(owner,
|
|
23
|
-
status: options?.status ?? 200,
|
|
24
|
-
statusText: options?.statusText ?? "OK",
|
|
25
|
-
headers: options?.headers ?? {},
|
|
26
|
-
body: options?.body ?? null,
|
|
24
|
+
return mock(owner, {
|
|
27
25
|
method: "GET",
|
|
28
26
|
url,
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
scaffold: () => ({
|
|
28
|
+
status: options?.status ?? 200,
|
|
29
|
+
statusText: options?.statusText ?? "OK",
|
|
30
|
+
headers: options?.headers ?? {},
|
|
31
|
+
body: options?.body ?? null,
|
|
32
|
+
method: "GET",
|
|
33
|
+
url,
|
|
34
|
+
response: response()
|
|
35
|
+
})
|
|
36
|
+
}, options?.RECORD);
|
|
31
37
|
}
|
|
32
38
|
const STATUS_TEXT_FOR = /* @__PURE__ */ new Map([
|
|
33
39
|
[200, "OK"],
|
|
@@ -96,74 +102,90 @@ const STATUS_TEXT_FOR = /* @__PURE__ */ new Map([
|
|
|
96
102
|
* Mock a POST request
|
|
97
103
|
*/
|
|
98
104
|
function POST(owner, url, response, options) {
|
|
99
|
-
return mock(owner,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
return mock(owner, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
url,
|
|
108
|
+
scaffold: () => {
|
|
109
|
+
const body = response();
|
|
110
|
+
const status = options?.status ?? (body ? 201 : 204);
|
|
111
|
+
return {
|
|
112
|
+
status,
|
|
113
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? "",
|
|
114
|
+
headers: options?.headers ?? {},
|
|
115
|
+
body: options?.body ?? null,
|
|
116
|
+
method: "POST",
|
|
117
|
+
url,
|
|
118
|
+
response: body
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}, options?.RECORD);
|
|
112
122
|
}
|
|
113
123
|
/**
|
|
114
124
|
* mock a PUT request
|
|
115
125
|
*/
|
|
116
126
|
function PUT(owner, url, response, options) {
|
|
117
|
-
return mock(owner,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
return mock(owner, {
|
|
128
|
+
method: "PUT",
|
|
129
|
+
url,
|
|
130
|
+
scaffold: () => {
|
|
131
|
+
const body = response();
|
|
132
|
+
const status = options?.status ?? (body ? 200 : 204);
|
|
133
|
+
return {
|
|
134
|
+
status,
|
|
135
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? "",
|
|
136
|
+
headers: options?.headers ?? {},
|
|
137
|
+
body: options?.body ?? null,
|
|
138
|
+
method: "PUT",
|
|
139
|
+
url,
|
|
140
|
+
response: body
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}, options?.RECORD);
|
|
130
144
|
}
|
|
131
145
|
/**
|
|
132
146
|
* mock a PATCH request
|
|
133
147
|
*
|
|
134
148
|
*/
|
|
135
149
|
function PATCH(owner, url, response, options) {
|
|
136
|
-
return mock(owner,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
return mock(owner, {
|
|
151
|
+
method: "PATCH",
|
|
152
|
+
url,
|
|
153
|
+
scaffold: () => {
|
|
154
|
+
const body = response();
|
|
155
|
+
const status = options?.status ?? (body ? 200 : 204);
|
|
156
|
+
return {
|
|
157
|
+
status,
|
|
158
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? "",
|
|
159
|
+
headers: options?.headers ?? {},
|
|
160
|
+
body: options?.body ?? null,
|
|
161
|
+
method: "PATCH",
|
|
162
|
+
url,
|
|
163
|
+
response: body
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}, options?.RECORD);
|
|
149
167
|
}
|
|
150
168
|
/**
|
|
151
169
|
* mock a DELETE request
|
|
152
170
|
*/
|
|
153
171
|
function DELETE(owner, url, response, options) {
|
|
154
|
-
return mock(owner,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
return mock(owner, {
|
|
173
|
+
method: "DELETE",
|
|
174
|
+
url,
|
|
175
|
+
scaffold: () => {
|
|
176
|
+
const body = response();
|
|
177
|
+
const status = options?.status ?? (body ? 200 : 204);
|
|
178
|
+
return {
|
|
179
|
+
status,
|
|
180
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? "",
|
|
181
|
+
headers: options?.headers ?? {},
|
|
182
|
+
body: options?.body ?? null,
|
|
183
|
+
method: "DELETE",
|
|
184
|
+
url,
|
|
185
|
+
response: body
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}, options?.RECORD);
|
|
167
189
|
}
|
|
168
190
|
/**
|
|
169
191
|
* Sets up Mocking for a HEAD request on the mock server
|
|
@@ -175,23 +197,29 @@ function DELETE(owner, url, response, options) {
|
|
|
175
197
|
* - status: the status code to return (default: 200)
|
|
176
198
|
* - headers: the headers to return (default: {})
|
|
177
199
|
* - body: the body to match against for the request (default: null)
|
|
178
|
-
* - RECORD:
|
|
200
|
+
* - RECORD: record this request even when the suite is replaying, such as under CI (default: false).
|
|
201
|
+
* A local override for re-recording one request. Do not commit it; a committed RECORD means
|
|
202
|
+
* that request is never replayed against its fixture.
|
|
179
203
|
*
|
|
180
204
|
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
181
205
|
* @param response a function which generates the response to return
|
|
182
|
-
* @param options status, headers for the response, body to match against for the request, and whether to
|
|
206
|
+
* @param options status, headers for the response, body to match against for the request, and whether to force recording
|
|
183
207
|
* @return
|
|
184
208
|
*/
|
|
185
209
|
function HEAD(owner, url, response, options) {
|
|
186
|
-
return mock(owner,
|
|
187
|
-
status: options?.status ?? 200,
|
|
188
|
-
statusText: options?.statusText ?? "OK",
|
|
189
|
-
headers: options?.headers ?? {},
|
|
190
|
-
body: options?.body ?? null,
|
|
210
|
+
return mock(owner, {
|
|
191
211
|
method: "HEAD",
|
|
192
212
|
url,
|
|
193
|
-
|
|
194
|
-
|
|
213
|
+
scaffold: () => ({
|
|
214
|
+
status: options?.status ?? 200,
|
|
215
|
+
statusText: options?.statusText ?? "OK",
|
|
216
|
+
headers: options?.headers ?? {},
|
|
217
|
+
body: options?.body ?? null,
|
|
218
|
+
method: "HEAD",
|
|
219
|
+
url,
|
|
220
|
+
response: response()
|
|
221
|
+
})
|
|
222
|
+
}, options?.RECORD);
|
|
195
223
|
}
|
|
196
224
|
|
|
197
225
|
//#endregion
|