@warp-drive/holodeck 0.0.1 → 0.1.0-alpha.14
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 +8 -8
- package/declarations/index.d.ts +58 -0
- package/declarations/mock.d.ts +65 -0
- package/dist/index.js +158 -23
- package/dist/mock.js +89 -7
- package/package.json +32 -18
- package/server/ensure-cert.js +4 -0
- package/server/index.js +100 -33
- package/dist/index.js.map +0 -1
- package/dist/mock.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<img
|
|
3
3
|
class="project-logo"
|
|
4
|
-
src="./logos/
|
|
4
|
+
src="./logos/warp-drive-logo-dark.svg#gh-light-mode-only"
|
|
5
5
|
alt="WarpDrive"
|
|
6
|
-
width="
|
|
6
|
+
width="200px"
|
|
7
7
|
title="WarpDrive" />
|
|
8
8
|
<img
|
|
9
9
|
class="project-logo"
|
|
10
|
-
src="./logos/
|
|
10
|
+
src="./logos/warp-drive-logo-gold.svg#gh-dark-mode-only"
|
|
11
11
|
alt="WarpDrive"
|
|
12
|
-
width="
|
|
12
|
+
width="200px"
|
|
13
13
|
title="WarpDrive" />
|
|
14
14
|
</p>
|
|
15
15
|
|
|
@@ -163,10 +163,10 @@ const MockHost = `https://${window.location.hostname}:${Number(window.location.p
|
|
|
163
163
|
setConfig({ host: MockHost });
|
|
164
164
|
|
|
165
165
|
QUnit.hooks.beforeEach(function (assert) {
|
|
166
|
-
setTestId(assert.test.testId);
|
|
166
|
+
setTestId(this, assert.test.testId);
|
|
167
167
|
});
|
|
168
168
|
QUnit.hooks.afterEach(function (assert) {
|
|
169
|
-
setTestId(null);
|
|
169
|
+
setTestId(this, null);
|
|
170
170
|
});
|
|
171
171
|
```
|
|
172
172
|
|
|
@@ -249,7 +249,7 @@ await launch({
|
|
|
249
249
|
img.project-logo {
|
|
250
250
|
padding: 0 5em 1em 5em;
|
|
251
251
|
width: 100px;
|
|
252
|
-
border-bottom: 2px solid #
|
|
252
|
+
border-bottom: 2px solid #bbb;
|
|
253
253
|
margin: 0 auto;
|
|
254
254
|
display: block;
|
|
255
255
|
}
|
|
@@ -265,7 +265,7 @@ await launch({
|
|
|
265
265
|
display: inline-block;
|
|
266
266
|
padding: .2rem 0;
|
|
267
267
|
color: #000;
|
|
268
|
-
border-bottom: 3px solid #
|
|
268
|
+
border-bottom: 3px solid #bbb;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
details > details {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Handler, NextFn } from "@warp-drive/core/request";
|
|
2
|
+
import type { RequestContext, StructuredDataDocument } from "@warp-drive/core/types/request";
|
|
3
|
+
import type { MinimumAdapterInterface } from "@warp-drive/legacy/compat";
|
|
4
|
+
import type { Store } from "@warp-drive/legacy/store";
|
|
5
|
+
import type { ScaffoldGenerator } from "./mock.js";
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export declare function setConfig({ host }: {
|
|
10
|
+
host: string;
|
|
11
|
+
}): void;
|
|
12
|
+
/**
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export declare function setTestId(context: object, str: string | null): void;
|
|
16
|
+
/**
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare function setIsRecording(value: boolean): void;
|
|
20
|
+
/**
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export declare function getIsRecording(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A request handler that intercepts requests and routes them through
|
|
26
|
+
* the Holodeck mock server.
|
|
27
|
+
*
|
|
28
|
+
* This handler modifies the request URL to include test identifiers
|
|
29
|
+
* and manages request counts for accurate mocking.
|
|
30
|
+
*
|
|
31
|
+
* Requires that the test context be configured with a testId using `setTestId`.
|
|
32
|
+
*
|
|
33
|
+
* @param owner - the test context object used to retrieve the test ID.
|
|
34
|
+
*/
|
|
35
|
+
export declare class MockServerHandler implements Handler {
|
|
36
|
+
owner: object;
|
|
37
|
+
constructor(owner: object);
|
|
38
|
+
request<T>(context: RequestContext, next: NextFn<T>): Promise<StructuredDataDocument<T>>;
|
|
39
|
+
}
|
|
40
|
+
interface HasAdapterForFn {
|
|
41
|
+
adapterFor(this: Store, modelName: string): MinimumAdapterInterface;
|
|
42
|
+
adapterFor(this: Store, modelName: string, _allowMissing?: true): MinimumAdapterInterface | undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates an adapterFor function that wraps the provided adapterFor function
|
|
46
|
+
* to override the adapter's _fetchRequest method to route requests through
|
|
47
|
+
* the Holodeck mock server.
|
|
48
|
+
*
|
|
49
|
+
* @param owner - The test context object used to retrieve the test ID.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createAdapterFor(owner: object, store: HasAdapterForFn): HasAdapterForFn["adapterFor"];
|
|
52
|
+
/**
|
|
53
|
+
* Mock a request by sending the scaffold to the mock server.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
export declare function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean): Promise<void>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @public
|
|
3
|
+
*/
|
|
4
|
+
export interface Scaffold {
|
|
5
|
+
status: number;
|
|
6
|
+
statusText?: string;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
body: Record<string, string> | string | null;
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
response: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export type ScaffoldGenerator = () => Scaffold;
|
|
17
|
+
/**
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export type ResponseGenerator = () => Record<string, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Sets up Mocking for a GET request on the mock server
|
|
23
|
+
* for the supplied url.
|
|
24
|
+
*
|
|
25
|
+
* The response body is generated by the supplied response function.
|
|
26
|
+
*
|
|
27
|
+
* Available options:
|
|
28
|
+
* - status: the status code to return (default: 200)
|
|
29
|
+
* - headers: the headers to return (default: {})
|
|
30
|
+
* - body: the body to match against for the request (default: null)
|
|
31
|
+
* - RECORD: whether to record the request (default: false)
|
|
32
|
+
*
|
|
33
|
+
* @param url the url to mock, relative to the mock server host (e.g. `users/1`)
|
|
34
|
+
* @param response a function which generates the response to return
|
|
35
|
+
* @param options status, headers for the response, body to match against for the request, and whether to record the request
|
|
36
|
+
* @return
|
|
37
|
+
*/
|
|
38
|
+
export declare function GET(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
39
|
+
RECORD?: boolean;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Mock a POST request
|
|
43
|
+
*/
|
|
44
|
+
export declare function POST(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
45
|
+
RECORD?: boolean;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* mock a PUT request
|
|
49
|
+
*/
|
|
50
|
+
export declare function PUT(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
51
|
+
RECORD?: boolean;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* mock a PATCH request
|
|
55
|
+
*
|
|
56
|
+
*/
|
|
57
|
+
export declare function PATCH(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
58
|
+
RECORD?: boolean;
|
|
59
|
+
}): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* mock a DELETE request
|
|
62
|
+
*/
|
|
63
|
+
export declare function DELETE(owner: object, url: string, response: ResponseGenerator, options?: Partial<Omit<Scaffold, "response" | "url" | "method">> & {
|
|
64
|
+
RECORD?: boolean;
|
|
65
|
+
}): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
import { SHOULD_RECORD } from '@warp-drive/core/build-config/env';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module
|
|
5
|
+
* @mergeModuleWith <project>
|
|
6
|
+
*/
|
|
1
7
|
const TEST_IDS = new WeakMap();
|
|
2
8
|
let HOST = 'https://localhost:1135/';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
|
|
3
14
|
function setConfig({
|
|
4
15
|
host
|
|
5
16
|
}) {
|
|
6
17
|
HOST = host.endsWith('/') ? host : `${host}/`;
|
|
7
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
|
|
8
24
|
function setTestId(context, str) {
|
|
9
25
|
if (str && TEST_IDS.has(context)) {
|
|
10
26
|
throw new Error(`MockServerHandler is already configured with a testId.`);
|
|
@@ -12,37 +28,72 @@ function setTestId(context, str) {
|
|
|
12
28
|
if (str) {
|
|
13
29
|
TEST_IDS.set(context, {
|
|
14
30
|
id: str,
|
|
15
|
-
|
|
16
|
-
|
|
31
|
+
mock: {
|
|
32
|
+
GET: {},
|
|
33
|
+
PUT: {},
|
|
34
|
+
PATCH: {},
|
|
35
|
+
DELETE: {},
|
|
36
|
+
POST: {},
|
|
37
|
+
QUERY: {},
|
|
38
|
+
OPTIONS: {},
|
|
39
|
+
HEAD: {},
|
|
40
|
+
CONNECT: {},
|
|
41
|
+
TRACE: {}
|
|
42
|
+
},
|
|
43
|
+
request: {
|
|
44
|
+
GET: {},
|
|
45
|
+
PUT: {},
|
|
46
|
+
PATCH: {},
|
|
47
|
+
DELETE: {},
|
|
48
|
+
POST: {},
|
|
49
|
+
QUERY: {},
|
|
50
|
+
OPTIONS: {},
|
|
51
|
+
HEAD: {},
|
|
52
|
+
CONNECT: {},
|
|
53
|
+
TRACE: {}
|
|
54
|
+
}
|
|
17
55
|
});
|
|
18
56
|
} else {
|
|
19
57
|
TEST_IDS.delete(context);
|
|
20
58
|
}
|
|
21
59
|
}
|
|
22
|
-
|
|
60
|
+
const shouldRecord = SHOULD_RECORD ? true : false;
|
|
61
|
+
let IS_RECORDING = null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
23
66
|
function setIsRecording(value) {
|
|
24
|
-
IS_RECORDING = Boolean(value);
|
|
67
|
+
IS_RECORDING = value === null ? value : Boolean(value);
|
|
25
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
26
73
|
function getIsRecording() {
|
|
27
|
-
return IS_RECORDING;
|
|
74
|
+
return IS_RECORDING === null ? shouldRecord : IS_RECORDING;
|
|
28
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A request handler that intercepts requests and routes them through
|
|
79
|
+
* the Holodeck mock server.
|
|
80
|
+
*
|
|
81
|
+
* This handler modifies the request URL to include test identifiers
|
|
82
|
+
* and manages request counts for accurate mocking.
|
|
83
|
+
*
|
|
84
|
+
* Requires that the test context be configured with a testId using `setTestId`.
|
|
85
|
+
*
|
|
86
|
+
* @param owner - the test context object used to retrieve the test ID.
|
|
87
|
+
*/
|
|
29
88
|
class MockServerHandler {
|
|
30
89
|
constructor(owner) {
|
|
31
90
|
this.owner = owner;
|
|
32
91
|
}
|
|
33
92
|
async request(context, next) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
const request = Object.assign({}, context.request);
|
|
39
|
-
const isRecording = request.url.endsWith('/__record');
|
|
40
|
-
const firstChar = request.url.includes('?') ? '&' : '?';
|
|
41
|
-
const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${isRecording ? test.mock++ : test.request++}`;
|
|
42
|
-
request.url = request.url + queryForTest;
|
|
43
|
-
request.mode = 'cors';
|
|
44
|
-
request.credentials = 'omit';
|
|
45
|
-
request.referrerPolicy = '';
|
|
93
|
+
const {
|
|
94
|
+
request,
|
|
95
|
+
queryForTest
|
|
96
|
+
} = setupHolodeckFetch(this.owner, Object.assign({}, context.request));
|
|
46
97
|
try {
|
|
47
98
|
const future = next(request);
|
|
48
99
|
context.setStream(future.getStream());
|
|
@@ -55,17 +106,102 @@ class MockServerHandler {
|
|
|
55
106
|
}
|
|
56
107
|
}
|
|
57
108
|
}
|
|
58
|
-
|
|
109
|
+
function setupHolodeckFetch(owner, request) {
|
|
59
110
|
const test = TEST_IDS.get(owner);
|
|
60
111
|
if (!test) {
|
|
61
|
-
throw new Error(`
|
|
112
|
+
throw new Error(`MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`);
|
|
113
|
+
}
|
|
114
|
+
const url = request.url;
|
|
115
|
+
const firstChar = url.includes('?') ? '&' : '?';
|
|
116
|
+
const method = request.method?.toUpperCase() ?? 'GET';
|
|
117
|
+
|
|
118
|
+
// enable custom methods
|
|
119
|
+
if (!test.request[method]) {
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.log(`⚠️ Using custom HTTP method ${method} for response to request ${url}`);
|
|
122
|
+
test.request[method] = {};
|
|
62
123
|
}
|
|
63
|
-
|
|
124
|
+
if (!(url in test.request[method])) {
|
|
125
|
+
test.request[method][url] = 0;
|
|
126
|
+
}
|
|
127
|
+
const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${test.request[method][url]++}`;
|
|
128
|
+
request.url = url + queryForTest;
|
|
129
|
+
request.mode = 'cors';
|
|
130
|
+
request.credentials = 'omit';
|
|
131
|
+
request.referrerPolicy = '';
|
|
132
|
+
return {
|
|
133
|
+
request,
|
|
134
|
+
queryForTest
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates an adapterFor function that wraps the provided adapterFor function
|
|
140
|
+
* to override the adapter's _fetchRequest method to route requests through
|
|
141
|
+
* the Holodeck mock server.
|
|
142
|
+
*
|
|
143
|
+
* @param owner - The test context object used to retrieve the test ID.
|
|
144
|
+
*/
|
|
145
|
+
function createAdapterFor(owner, store) {
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
147
|
+
const adapterFor = store.adapterFor;
|
|
148
|
+
return function holodeckAdapterFor(modelName, _allowMissing) {
|
|
149
|
+
const adapter = adapterFor.call(this, modelName, _allowMissing);
|
|
150
|
+
if (adapter) {
|
|
151
|
+
if (!adapter.hasOverriddenFetch) {
|
|
152
|
+
adapter.hasOverriddenFetch = true;
|
|
153
|
+
adapter.useFetch = true;
|
|
154
|
+
const originalFetch = adapter._fetchRequest?.bind(adapter);
|
|
155
|
+
adapter._fetchRequest = function (options) {
|
|
156
|
+
if (!originalFetch) {
|
|
157
|
+
throw new Error(`Adapter ${String(modelName)} does not implement _fetchRequest`);
|
|
158
|
+
}
|
|
159
|
+
const {
|
|
160
|
+
request
|
|
161
|
+
} = setupHolodeckFetch(owner, options);
|
|
162
|
+
return originalFetch(request);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return adapter;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Mock a request by sending the scaffold to the mock server.
|
|
172
|
+
*
|
|
173
|
+
* @public
|
|
174
|
+
*/
|
|
175
|
+
async function mock(owner, generate, isRecording) {
|
|
64
176
|
if (getIsRecording() || isRecording) {
|
|
177
|
+
const test = TEST_IDS.get(owner);
|
|
178
|
+
if (!test) {
|
|
179
|
+
throw new Error(`Cannot call "mock" before configuring a testId. Use setTestId to set the testId for each test`);
|
|
180
|
+
}
|
|
181
|
+
const requestToMock = generate();
|
|
182
|
+
const {
|
|
183
|
+
url: mockUrl,
|
|
184
|
+
method
|
|
185
|
+
} = requestToMock;
|
|
186
|
+
if (!mockUrl || !method) {
|
|
187
|
+
throw new Error(`MockError: Cannot mock a request without providing a URL and Method`);
|
|
188
|
+
}
|
|
189
|
+
const mockMethod = method?.toUpperCase() ?? 'GET';
|
|
190
|
+
|
|
191
|
+
// enable custom methods
|
|
192
|
+
if (!test.mock[mockMethod]) {
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.log(`⚠️ Using custom HTTP method ${mockMethod} for response to request ${mockUrl}`);
|
|
195
|
+
test.mock[mockMethod] = {};
|
|
196
|
+
}
|
|
197
|
+
if (!(mockUrl in test.mock[mockMethod])) {
|
|
198
|
+
test.mock[mockMethod][mockUrl] = 0;
|
|
199
|
+
}
|
|
200
|
+
const testMockNum = test.mock[mockMethod][mockUrl]++;
|
|
65
201
|
const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;
|
|
66
202
|
await fetch(url, {
|
|
67
203
|
method: 'POST',
|
|
68
|
-
body: JSON.stringify(
|
|
204
|
+
body: JSON.stringify(requestToMock),
|
|
69
205
|
mode: 'cors',
|
|
70
206
|
credentials: 'omit',
|
|
71
207
|
referrerPolicy: ''
|
|
@@ -73,5 +209,4 @@ async function mock(owner, generate, isRecording) {
|
|
|
73
209
|
}
|
|
74
210
|
}
|
|
75
211
|
|
|
76
|
-
export { MockServerHandler, getIsRecording, mock, setConfig, setIsRecording, setTestId };
|
|
77
|
-
//# sourceMappingURL=index.js.map
|
|
212
|
+
export { MockServerHandler, createAdapterFor, getIsRecording, mock, setConfig, setIsRecording, setTestId };
|
package/dist/mock.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { mock, getIsRecording } from './index.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* Sets up Mocking for a GET request on the mock server
|
|
5
17
|
* for the supplied url.
|
|
@@ -28,11 +40,81 @@ function GET(owner, url, response, options) {
|
|
|
28
40
|
response: response()
|
|
29
41
|
}), getIsRecording() || (options?.RECORD ?? false));
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
const STATUS_TEXT_FOR = new Map([[200, 'OK'], [201, 'Created'], [202, 'Accepted'], [203, 'Non-Authoritative Information'], [204, 'No Content'], [205, 'Reset Content'], [206, 'Partial Content'], [207, 'Multi-Status'], [208, 'Already Reported'], [226, 'IM Used'], [300, 'Multiple Choices'], [301, 'Moved Permanently'], [302, 'Found'], [303, 'See Other'], [304, 'Not Modified'], [307, 'Temporary Redirect'], [308, 'Permanent Redirect'], [400, 'Bad Request'], [401, 'Unauthorized'], [402, 'Payment Required'], [403, 'Forbidden'], [404, 'Not Found'], [405, 'Method Not Allowed'], [406, 'Not Acceptable'], [407, 'Proxy Authentication Required'], [408, 'Request Timeout'], [409, 'Conflict'], [410, 'Gone'], [411, 'Length Required'], [412, 'Precondition Failed'], [413, 'Payload Too Large'], [414, 'URI Too Long'], [415, 'Unsupported Media Type'], [416, 'Range Not Satisfiable'], [417, 'Expectation Failed'], [419, 'Page Expired'], [420, 'Enhance Your Calm'], [421, 'Misdirected Request'], [422, 'Unprocessable Entity'], [423, 'Locked'], [424, 'Failed Dependency'], [425, 'Too Early'], [426, 'Upgrade Required'], [428, 'Precondition Required'], [429, 'Too Many Requests'], [430, 'Request Header Fields Too Large'], [431, 'Request Header Fields Too Large'], [450, 'Blocked By Windows Parental Controls'], [451, 'Unavailable For Legal Reasons'], [500, 'Internal Server Error'], [501, 'Not Implemented'], [502, 'Bad Gateway'], [503, 'Service Unavailable'], [504, 'Gateway Timeout'], [505, 'HTTP Version Not Supported'], [506, 'Variant Also Negotiates'], [507, 'Insufficient Storage'], [508, 'Loop Detected'], [509, 'Bandwidth Limit Exceeded'], [510, 'Not Extended'], [511, 'Network Authentication Required']]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mock a POST request
|
|
47
|
+
*/
|
|
48
|
+
function POST(owner, url, response, options) {
|
|
49
|
+
return mock(owner, () => {
|
|
50
|
+
const body = response();
|
|
51
|
+
const status = options?.status ?? (body ? 201 : 204);
|
|
52
|
+
return {
|
|
53
|
+
status: status,
|
|
54
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? '',
|
|
55
|
+
headers: options?.headers ?? {},
|
|
56
|
+
body: options?.body ?? null,
|
|
57
|
+
method: 'POST',
|
|
58
|
+
url,
|
|
59
|
+
response: body
|
|
60
|
+
};
|
|
61
|
+
}, getIsRecording() || (options?.RECORD ?? false));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* mock a PUT request
|
|
66
|
+
*/
|
|
67
|
+
function PUT(owner, url, response, options) {
|
|
68
|
+
return mock(owner, () => {
|
|
69
|
+
const body = response();
|
|
70
|
+
const status = options?.status ?? (body ? 200 : 204);
|
|
71
|
+
return {
|
|
72
|
+
status: status,
|
|
73
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? '',
|
|
74
|
+
headers: options?.headers ?? {},
|
|
75
|
+
body: options?.body ?? null,
|
|
76
|
+
method: 'PUT',
|
|
77
|
+
url,
|
|
78
|
+
response: body
|
|
79
|
+
};
|
|
80
|
+
}, getIsRecording() || (options?.RECORD ?? false));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* mock a PATCH request
|
|
84
|
+
*
|
|
85
|
+
*/
|
|
86
|
+
function PATCH(owner, url, response, options) {
|
|
87
|
+
return mock(owner, () => {
|
|
88
|
+
const body = response();
|
|
89
|
+
const status = options?.status ?? (body ? 200 : 204);
|
|
90
|
+
return {
|
|
91
|
+
status: status,
|
|
92
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? '',
|
|
93
|
+
headers: options?.headers ?? {},
|
|
94
|
+
body: options?.body ?? null,
|
|
95
|
+
method: 'PATCH',
|
|
96
|
+
url,
|
|
97
|
+
response: body
|
|
98
|
+
};
|
|
99
|
+
}, getIsRecording() || (options?.RECORD ?? false));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* mock a DELETE request
|
|
103
|
+
*/
|
|
104
|
+
function DELETE(owner, url, response, options) {
|
|
105
|
+
return mock(owner, () => {
|
|
106
|
+
const body = response();
|
|
107
|
+
const status = options?.status ?? (body ? 200 : 204);
|
|
108
|
+
return {
|
|
109
|
+
status: status,
|
|
110
|
+
statusText: options?.statusText ?? STATUS_TEXT_FOR.get(status) ?? '',
|
|
111
|
+
headers: options?.headers ?? {},
|
|
112
|
+
body: options?.body ?? null,
|
|
113
|
+
method: 'DELETE',
|
|
114
|
+
url,
|
|
115
|
+
response: body
|
|
116
|
+
};
|
|
117
|
+
}, getIsRecording() || (options?.RECORD ?? false));
|
|
118
|
+
}
|
|
36
119
|
|
|
37
|
-
export { DELETE, GET, PATCH, POST, PUT
|
|
38
|
-
//# sourceMappingURL=mock.js.map
|
|
120
|
+
export { DELETE, GET, PATCH, POST, PUT };
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warp-drive/holodeck",
|
|
3
3
|
"description": "⚡️ Simple, Fast HTTP Mocking for Tests",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.1.0-alpha.14",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chris Thoburn <runspired@users.noreply.github.com>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+ssh://git@github.com:
|
|
9
|
+
"url": "git+ssh://git@github.com:warp-drive-data/warp-drive.git",
|
|
10
10
|
"directory": "packages/holodeck"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://github.com/
|
|
13
|
-
"bugs": "https://github.com/
|
|
12
|
+
"homepage": "https://github.com/warp-drive-data/warp-drive",
|
|
13
|
+
"bugs": "https://github.com/warp-drive-data/warp-drive/issues",
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">= 18.20.8"
|
|
16
16
|
},
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"extends": "../../package.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"chalk": "^5.
|
|
25
|
-
"hono": "^4.
|
|
26
|
-
"@hono/node-server": "^1.
|
|
24
|
+
"chalk": "^5.5.0",
|
|
25
|
+
"hono": "^4.9.4",
|
|
26
|
+
"@hono/node-server": "^1.19.0"
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"files": [
|
|
@@ -32,25 +32,36 @@
|
|
|
32
32
|
"README.md",
|
|
33
33
|
"LICENSE.md",
|
|
34
34
|
"server",
|
|
35
|
+
"declarations",
|
|
35
36
|
"logos"
|
|
36
37
|
],
|
|
37
38
|
"bin": {
|
|
38
39
|
"ensure-cert": "./server/ensure-cert.js"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@warp-drive/
|
|
42
|
+
"@warp-drive/utilities": "5.8.0-alpha.14",
|
|
43
|
+
"@warp-drive/legacy": "5.8.0-alpha.14",
|
|
44
|
+
"@warp-drive/core": "5.8.0-alpha.14"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@warp-drive/utilities": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@warp-drive/legacy": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
43
53
|
},
|
|
44
54
|
"devDependencies": {
|
|
45
|
-
"@babel/core": "^7.
|
|
46
|
-
"@babel/plugin-transform-typescript": "^7.
|
|
47
|
-
"@babel/preset-env": "^7.
|
|
48
|
-
"@babel/preset-typescript": "^7.27.
|
|
49
|
-
"@babel/runtime": "^7.
|
|
50
|
-
"@
|
|
51
|
-
"@warp-drive/
|
|
52
|
-
"@warp-drive/
|
|
53
|
-
"
|
|
55
|
+
"@babel/core": "^7.28.3",
|
|
56
|
+
"@babel/plugin-transform-typescript": "^7.28.0",
|
|
57
|
+
"@babel/preset-env": "^7.28.3",
|
|
58
|
+
"@babel/preset-typescript": "^7.27.1",
|
|
59
|
+
"@babel/runtime": "^7.28.3",
|
|
60
|
+
"@warp-drive/utilities": "5.8.0-alpha.14",
|
|
61
|
+
"@warp-drive/legacy": "5.8.0-alpha.14",
|
|
62
|
+
"@warp-drive/core": "5.8.0-alpha.14",
|
|
63
|
+
"@warp-drive/internal-config": "5.8.0-alpha.14",
|
|
64
|
+
"vite": "^7.1.3"
|
|
54
65
|
},
|
|
55
66
|
"exports": {
|
|
56
67
|
".": {
|
|
@@ -58,14 +69,17 @@
|
|
|
58
69
|
"bun": "./server/index.js",
|
|
59
70
|
"deno": "./server/index.js",
|
|
60
71
|
"browser": {
|
|
72
|
+
"types": "./declarations/index.d.ts",
|
|
61
73
|
"default": "./dist/index.js"
|
|
62
74
|
},
|
|
63
75
|
"import": {
|
|
76
|
+
"types": "./declarations/index.d.ts",
|
|
64
77
|
"default": "./dist/index.js"
|
|
65
78
|
},
|
|
66
79
|
"default": "./server/index.js"
|
|
67
80
|
},
|
|
68
81
|
"./mock": {
|
|
82
|
+
"types": "./declarations/mock.d.ts",
|
|
69
83
|
"default": "./dist/mock.js"
|
|
70
84
|
}
|
|
71
85
|
},
|
package/server/ensure-cert.js
CHANGED
|
@@ -11,6 +11,10 @@ function getShellConfigFilePath() {
|
|
|
11
11
|
return path.join(homedir(), '.zshrc');
|
|
12
12
|
case '/bin/bash':
|
|
13
13
|
return path.join(homedir(), '.bashrc');
|
|
14
|
+
case '/opt/homebrew/bin/fish':
|
|
15
|
+
case '/usr/local/bin/fish':
|
|
16
|
+
case '/bin/fish':
|
|
17
|
+
return path.join(homedir(), '.config', 'fish', 'config.fish');
|
|
14
18
|
default:
|
|
15
19
|
throw Error(
|
|
16
20
|
`Unable to determine configuration file for shell: ${shell}. Manual SSL Cert Setup Required for Holodeck.`
|
package/server/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import zlib from 'node:zlib';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
13
|
import path from 'path';
|
|
14
|
+
import { threadId, parentPort } from 'node:worker_threads';
|
|
14
15
|
|
|
15
16
|
const isBun = typeof Bun !== 'undefined';
|
|
16
17
|
const DEBUG =
|
|
@@ -106,11 +107,21 @@ function generateFilepath(options) {
|
|
|
106
107
|
const { body } = options;
|
|
107
108
|
const bodyHash = body ? crypto.createHash('md5').update(JSON.stringify(body)).digest('hex') : null;
|
|
108
109
|
const cacheDir = generateFileDir(options);
|
|
109
|
-
return `${cacheDir}/${bodyHash ?
|
|
110
|
+
return `${cacheDir}/${bodyHash ? bodyHash : 'res'}`;
|
|
110
111
|
}
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
Generate a human scannable file name for the test assets to be stored in,
|
|
115
|
+
the `.mock-cache` directory should be checked-in to the codebase.
|
|
116
|
+
*/
|
|
111
117
|
function generateFileDir(options) {
|
|
112
118
|
const { projectRoot, testId, url, method, testRequestNumber } = options;
|
|
113
|
-
|
|
119
|
+
const normalizedUrl = url.startsWith('/') ? url.slice(1) : url;
|
|
120
|
+
// make path look nice but not be a sub-directory
|
|
121
|
+
// using alternative `/`-like characters would be nice but results in odd encoding
|
|
122
|
+
// on disk path
|
|
123
|
+
const pathUrl = normalizedUrl.replaceAll('/', '_');
|
|
124
|
+
return `${projectRoot}/.mock-cache/${testId}/${method}::${pathUrl}::${testRequestNumber}`;
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
async function replayRequest(context, cacheKey) {
|
|
@@ -131,34 +142,54 @@ async function replayRequest(context, cacheKey) {
|
|
|
131
142
|
status: '400',
|
|
132
143
|
code: 'MOCK_NOT_FOUND',
|
|
133
144
|
title: 'Mock not found',
|
|
134
|
-
detail: `No meta was found for ${context.req.method} ${context.req.url}. You may need to record a mock for this request.`,
|
|
145
|
+
detail: `No meta was found for ${context.req.method} ${context.req.url}. The expected cacheKey was ${cacheKey}. You may need to record a mock for this request.`,
|
|
135
146
|
},
|
|
136
147
|
],
|
|
137
148
|
})
|
|
138
149
|
);
|
|
139
150
|
}
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
?
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
try {
|
|
153
|
+
const bodyPath = `${cacheKey}.body.br`;
|
|
154
|
+
const bodyInit =
|
|
155
|
+
metaJson.status !== 204 && metaJson.status < 500
|
|
156
|
+
? isBun
|
|
157
|
+
? Bun.file(bodyPath)
|
|
158
|
+
: fs.createReadStream(bodyPath)
|
|
159
|
+
: '';
|
|
160
|
+
|
|
161
|
+
const headers = new Headers(metaJson.headers || {});
|
|
162
|
+
// @ts-expect-error - createReadStream is supported in node
|
|
163
|
+
const response = new Response(bodyInit, {
|
|
164
|
+
status: metaJson.status,
|
|
165
|
+
statusText: metaJson.statusText,
|
|
166
|
+
headers,
|
|
167
|
+
});
|
|
156
168
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
if (metaJson.status > 400) {
|
|
170
|
+
throw new HTTPException(metaJson.status, { res: response, message: metaJson.statusText });
|
|
171
|
+
}
|
|
160
172
|
|
|
161
|
-
|
|
173
|
+
return response;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (e instanceof HTTPException) {
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
context.header('Content-Type', 'application/vnd.api+json');
|
|
179
|
+
context.status(500);
|
|
180
|
+
return context.body(
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
errors: [
|
|
183
|
+
{
|
|
184
|
+
status: '500',
|
|
185
|
+
code: 'MOCK_SERVER_ERROR',
|
|
186
|
+
title: 'Mock Replay Failed',
|
|
187
|
+
detail: `Failed to create the response for ${context.req.method} ${context.req.url}.\n\n\n${e.message}\n${e.stack}`,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
}
|
|
162
193
|
}
|
|
163
194
|
|
|
164
195
|
function createTestHandler(projectRoot) {
|
|
@@ -208,7 +239,7 @@ function createTestHandler(projectRoot) {
|
|
|
208
239
|
);
|
|
209
240
|
}
|
|
210
241
|
|
|
211
|
-
if (req.method === 'POST'
|
|
242
|
+
if (req.method === 'POST' && niceUrl === '__record') {
|
|
212
243
|
const payload = await req.json();
|
|
213
244
|
const { url, headers, method, status, statusText, body, response } = payload;
|
|
214
245
|
const cacheKey = generateFilepath({
|
|
@@ -216,7 +247,7 @@ function createTestHandler(projectRoot) {
|
|
|
216
247
|
testId,
|
|
217
248
|
url,
|
|
218
249
|
method,
|
|
219
|
-
body
|
|
250
|
+
body,
|
|
220
251
|
testRequestNumber,
|
|
221
252
|
});
|
|
222
253
|
const compressedResponse = compress(JSON.stringify(response));
|
|
@@ -252,24 +283,34 @@ function createTestHandler(projectRoot) {
|
|
|
252
283
|
fs.writeFileSync(`${cacheKey}.body.br`, compressedResponse);
|
|
253
284
|
}
|
|
254
285
|
|
|
255
|
-
context.status(
|
|
256
|
-
return context.body(
|
|
286
|
+
context.status(201);
|
|
287
|
+
return context.body(
|
|
288
|
+
JSON.stringify({
|
|
289
|
+
message: `Recorded ${method} ${url} for test ${testId} request #${testRequestNumber}`,
|
|
290
|
+
cacheKey,
|
|
291
|
+
cacheDir,
|
|
292
|
+
})
|
|
293
|
+
);
|
|
257
294
|
} else {
|
|
258
|
-
const body = req.body;
|
|
295
|
+
const body = req.raw.body ? await req.text() : null;
|
|
259
296
|
const cacheKey = generateFilepath({
|
|
260
297
|
projectRoot,
|
|
261
298
|
testId,
|
|
262
299
|
url: niceUrl,
|
|
263
300
|
method: req.method,
|
|
264
|
-
body: body ?
|
|
301
|
+
body: body ? body : null,
|
|
265
302
|
testRequestNumber,
|
|
266
303
|
});
|
|
267
304
|
return replayRequest(context, cacheKey);
|
|
268
305
|
}
|
|
269
306
|
} catch (e) {
|
|
270
307
|
if (e instanceof HTTPException) {
|
|
308
|
+
console.log(`HTTPException Encountered`);
|
|
309
|
+
console.error(e);
|
|
271
310
|
throw e;
|
|
272
311
|
}
|
|
312
|
+
console.log(`500 MOCK_SERVER_ERROR Encountered`);
|
|
313
|
+
console.error(e);
|
|
273
314
|
context.header('Content-Type', 'application/vnd.api+json');
|
|
274
315
|
context.status(500);
|
|
275
316
|
return context.body(
|
|
@@ -302,6 +343,7 @@ export function startNodeServer() {
|
|
|
302
343
|
export function startWorker() {
|
|
303
344
|
// listen for launch message
|
|
304
345
|
globalThis.onmessage = async (event) => {
|
|
346
|
+
console.log('starting holodeck worker');
|
|
305
347
|
const { options } = event.data;
|
|
306
348
|
|
|
307
349
|
const { server } = await _createServer(options);
|
|
@@ -317,6 +359,16 @@ export function startWorker() {
|
|
|
317
359
|
};
|
|
318
360
|
}
|
|
319
361
|
|
|
362
|
+
async function waitForLog(server, logMessage) {
|
|
363
|
+
for await (const chunk of server.stdout) {
|
|
364
|
+
process.stdout.write(chunk);
|
|
365
|
+
const txt = new TextDecoder().decode(chunk);
|
|
366
|
+
if (txt.includes(logMessage)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
320
372
|
/*
|
|
321
373
|
{ port?: number, projectRoot: string }
|
|
322
374
|
*/
|
|
@@ -324,14 +376,16 @@ export async function createServer(options, useBun = false) {
|
|
|
324
376
|
if (!useBun) {
|
|
325
377
|
const CURRENT_FILE = new URL(import.meta.url).pathname;
|
|
326
378
|
const START_FILE = path.join(CURRENT_FILE, '../start-node.js');
|
|
327
|
-
const server = Bun.spawn(['node',
|
|
328
|
-
env: process.env,
|
|
379
|
+
const server = Bun.spawn(['node', START_FILE, JSON.stringify(options)], {
|
|
380
|
+
env: Object.assign({}, process.env, { FORCE_COLOR: 1 }),
|
|
329
381
|
cwd: process.cwd(),
|
|
330
382
|
stdin: 'inherit',
|
|
331
|
-
stdout: '
|
|
383
|
+
stdout: 'pipe',
|
|
332
384
|
stderr: 'inherit',
|
|
333
385
|
});
|
|
334
386
|
|
|
387
|
+
await waitForLog(server, 'Serving Holodeck HTTP Mocks');
|
|
388
|
+
|
|
335
389
|
return {
|
|
336
390
|
terminate() {
|
|
337
391
|
server.kill();
|
|
@@ -340,6 +394,7 @@ export async function createServer(options, useBun = false) {
|
|
|
340
394
|
};
|
|
341
395
|
}
|
|
342
396
|
|
|
397
|
+
console.log('starting holodeck worker');
|
|
343
398
|
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
344
399
|
|
|
345
400
|
worker.postMessage({
|
|
@@ -347,7 +402,15 @@ export async function createServer(options, useBun = false) {
|
|
|
347
402
|
options,
|
|
348
403
|
});
|
|
349
404
|
|
|
350
|
-
return
|
|
405
|
+
return new Promise((resolve) => {
|
|
406
|
+
// @ts-expect-error
|
|
407
|
+
worker.onmessage((v) => {
|
|
408
|
+
console.log('worker message received', v);
|
|
409
|
+
if (v.data === 'launched') {
|
|
410
|
+
resolve(worker);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
351
414
|
}
|
|
352
415
|
|
|
353
416
|
async function _createServer(options) {
|
|
@@ -383,9 +446,13 @@ async function _createServer(options) {
|
|
|
383
446
|
});
|
|
384
447
|
|
|
385
448
|
console.log(
|
|
386
|
-
`\
|
|
449
|
+
`\tServing Holodeck HTTP Mocks from ${chalk.yellow('https://') + chalk.magenta((options.hostname ?? 'localhost') + ':') + chalk.yellow(options.port ?? DEFAULT_PORT)}\n`
|
|
387
450
|
);
|
|
388
451
|
|
|
452
|
+
if (typeof threadId === 'number' && threadId !== 0) {
|
|
453
|
+
parentPort.postMessage('launched');
|
|
454
|
+
}
|
|
455
|
+
|
|
389
456
|
return { app, server };
|
|
390
457
|
}
|
|
391
458
|
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocument } from '@ember-data/request';\n\nimport type { ScaffoldGenerator } from './mock';\n\nconst TEST_IDS = new WeakMap<object, { id: string; request: number; mock: number }>();\n\nlet HOST = 'https://localhost:1135/';\nexport function setConfig({ host }: { host: string }) {\n HOST = host.endsWith('/') ? host : `${host}/`;\n}\n\nexport function setTestId(context: object, str: string | null) {\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, { id: str, request: 0, mock: 0 });\n } else {\n TEST_IDS.delete(context);\n }\n}\n\nlet IS_RECORDING = false;\nexport function setIsRecording(value: boolean) {\n IS_RECORDING = Boolean(value);\n}\nexport function getIsRecording() {\n return IS_RECORDING;\n}\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 test = TEST_IDS.get(this.owner);\n if (!test) {\n throw new Error(\n `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test`\n );\n }\n\n const request: RequestInfo = Object.assign({}, context.request);\n const isRecording = request.url!.endsWith('/__record');\n const firstChar = request.url!.includes('?') ? '&' : '?';\n const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${\n isRecording ? test.mock++ : test.request++\n }`;\n request.url = request.url + queryForTest;\n\n request.mode = 'cors';\n request.credentials = 'omit';\n request.referrerPolicy = '';\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\nexport async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean) {\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 testMockNum = test.mock++;\n if (getIsRecording() || isRecording) {\n const port = window.location.port ? `:${window.location.port}` : '';\n const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`;\n await fetch(url, {\n method: 'POST',\n body: JSON.stringify(generate()),\n mode: 'cors',\n credentials: 'omit',\n referrerPolicy: '',\n });\n }\n}\n"],"names":["TEST_IDS","WeakMap","HOST","setConfig","host","endsWith","setTestId","context","str","has","Error","set","id","request","mock","delete","IS_RECORDING","setIsRecording","value","Boolean","getIsRecording","MockServerHandler","constructor","owner","next","test","get","Object","assign","isRecording","url","firstChar","includes","queryForTest","mode","credentials","referrerPolicy","future","setStream","getStream","e","DOMException","message","replace","generate","testMockNum","fetch","method","body","JSON","stringify"],"mappings":"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"}
|
package/dist/mock.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mock.js","sources":["../src/mock.ts"],"sourcesContent":["import { getIsRecording, mock } from '.';\n\nexport interface Scaffold {\n status: number;\n statusText?: string;\n headers: Record<string, string>;\n body: Record<string, string> | string | null;\n method: string;\n url: string;\n response: Record<string, unknown>;\n}\n\nexport type ScaffoldGenerator = () => Scaffold;\nexport type ResponseGenerator = () => Record<string, unknown>;\n\n/**\n * Sets up Mocking for a GET request on the mock server\n * for the supplied url.\n *\n * The response body is generated by the supplied response function.\n *\n * Available options:\n * - status: the status code to return (default: 200)\n * - headers: the headers to return (default: {})\n * - body: the body to match against for the request (default: null)\n * - RECORD: whether to record the request (default: false)\n *\n * @param url the url to mock, relative to the mock server host (e.g. `users/1`)\n * @param response a function which generates the response to return\n * @param options status, headers for the response, body to match against for the request, and whether to record the request\n * @return\n */\nexport function GET(\n owner: object,\n url: string,\n response: ResponseGenerator,\n options?: Partial<Omit<Scaffold, 'response' | 'url' | 'method'>> & { RECORD?: boolean }\n): Promise<void> {\n return mock(\n owner,\n () => ({\n status: options?.status ?? 200,\n statusText: options?.statusText ?? 'OK',\n headers: options?.headers ?? {},\n body: options?.body ?? null,\n method: 'GET',\n url,\n response: response(),\n }),\n getIsRecording() || (options?.RECORD ?? false)\n );\n}\nexport function POST() {}\nexport function PUT() {}\nexport function PATCH() {}\nexport function DELETE() {}\nexport function QUERY() {}\n"],"names":["GET","owner","url","response","options","mock","status","statusText","headers","body","method","getIsRecording","RECORD","POST","PUT","PATCH","DELETE","QUERY"],"mappings":";;AAeA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,GAAGA,CACjBC,KAAa,EACbC,GAAW,EACXC,QAA2B,EAC3BC,OAAuF,EACxE;AACf,EAAA,OAAOC,IAAI,CACTJ,KAAK,EACL,OAAO;AACLK,IAAAA,MAAM,EAAEF,OAAO,EAAEE,MAAM,IAAI,GAAG;AAC9BC,IAAAA,UAAU,EAAEH,OAAO,EAAEG,UAAU,IAAI,IAAI;AACvCC,IAAAA,OAAO,EAAEJ,OAAO,EAAEI,OAAO,IAAI,EAAE;AAC/BC,IAAAA,IAAI,EAAEL,OAAO,EAAEK,IAAI,IAAI,IAAI;AAC3BC,IAAAA,MAAM,EAAE,KAAK;IACbR,GAAG;IACHC,QAAQ,EAAEA,QAAQ;AACpB,GAAC,CAAC,EACFQ,cAAc,EAAE,KAAKP,OAAO,EAAEQ,MAAM,IAAI,KAAK,CAC/C,CAAC;AACH;AACO,SAASC,IAAIA,GAAG;AAChB,SAASC,GAAGA,GAAG;AACf,SAASC,KAAKA,GAAG;AACjB,SAASC,MAAMA,GAAG;AAClB,SAASC,KAAKA,GAAG;;;;"}
|