@warp-drive/holodeck 0.0.0-beta.11 → 0.0.0-beta.13

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 CHANGED
@@ -94,6 +94,152 @@ and `brotli` minification in a way that can be replayed over and over again.
94
94
 
95
95
  Basically, pay the cost when you write the test. Forever after skip the cost until you need to edit the test again.
96
96
 
97
+ ## Setup
98
+
99
+ ### Use with WarpDrive
100
+
101
+ First, you will need to add the holodeck handler to the request manager chain prior to `Fetch` (or any equivalent handler that proceeds to network).
102
+
103
+ For instance:
104
+
105
+ ```ts
106
+ import RequestManager from '@ember-data/request';
107
+ import Fetch from '@ember-data/request/fetch';
108
+ import { MockServerHandler } from '@warp-drive/holodeck';
109
+
110
+ const manager = new RequestManager();
111
+ manager.use([new MockServerHandler(testContext), Fetch]);
112
+ ```
113
+
114
+ From within a test this might look like:
115
+
116
+ ```ts
117
+ import RequestManager from '@ember-data/request';
118
+ import Fetch from '@ember-data/request/fetch';
119
+ import { MockServerHandler } from '@warp-drive/holodeck';
120
+ import { module, test } from 'qunit';
121
+
122
+ module('my module', function() {
123
+ test('my test', async function() {
124
+ const manager = new RequestManager();
125
+ manager.use([new MockServerHandler(this), Fetch]);
126
+ });
127
+ });
128
+ ```
129
+
130
+ Next, you will need to configure holodeck to understand your tests contexts. For qunit and diagnostic
131
+ in a project using Ember this is typically done in `tests/test-helper.js`
132
+
133
+ #### With Diagnostic
134
+
135
+ ```ts
136
+ import { setupGlobalHooks } from '@warp-drive/diagnostic';
137
+ import { setConfig, setTestId } from '@warp-drive/holodeck';
138
+
139
+ // if not proxying the port / set port to the correct value here
140
+ const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`;
141
+
142
+ setConfig({ host: MockHost });
143
+
144
+ setupGlobalHooks((hooks) => {
145
+ hooks.beforeEach(function (assert) {
146
+ setTestId(this, assert.test.testId);
147
+ });
148
+ hooks.afterEach(function () {
149
+ setTestId(this, null);
150
+ });
151
+ });
152
+ ```
153
+
154
+ #### With QUnit
155
+
156
+ ```ts
157
+ import * as QUnit from 'qunit';
158
+ import { setConfig, setTestId } from '@warp-drive/holodeck';
159
+
160
+ // if not proxying the port / set port to the correct value here
161
+ const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`;
162
+
163
+ setConfig({ host: MockHost });
164
+
165
+ QUnit.hooks.beforeEach(function (assert) {
166
+ setTestId(assert.test.testId);
167
+ });
168
+ QUnit.hooks.afterEach(function (assert) {
169
+ setTestId(null);
170
+ });
171
+ ```
172
+
173
+ ### Testem
174
+
175
+ You can integrate holodeck with Testem using testem's [async config capability](https://github.com/testem/testem/blob/master/docs/config_file.md#returning-a-promise-from-testemjs):
176
+
177
+ ```ts
178
+ module.exports = async function () {
179
+ const holodeck = (await import('@warp-drive/holodeck')).default;
180
+ await holodeck.launchProgram({
181
+ port: 7373,
182
+ });
183
+
184
+ process.on('beforeExit', async () => {
185
+ await holodeck.endProgram();
186
+ });
187
+
188
+ return {
189
+ // ... testem config
190
+ };
191
+ };
192
+ ```
193
+
194
+ If you need the API mock to run on the same port as the test suite, you can use Testem's [API Proxy](https://github.com/testem/testem/tree/master?tab=readme-ov-file#api-proxy)
195
+
196
+ ```ts
197
+ module.exports = async function () {
198
+ const holodeck = (await import('@warp-drive/holodeck')).default;
199
+ await holodeck.launchProgram({
200
+ port: 7373,
201
+ });
202
+
203
+ process.on('beforeExit', async () => {
204
+ await holodeck.endProgram();
205
+ });
206
+
207
+ return {
208
+ "proxies": {
209
+ "/api": {
210
+ // holodeck always runs on https
211
+ // the proxy is transparent so this means /api/v1 will route to https://localhost:7373/api/v1
212
+ "target": "https://localhost:7373",
213
+ // "onlyContentTypes": ["xml", "json"],
214
+ // if test suite is on http, set this to false
215
+ // "secure": false,
216
+ },
217
+ }
218
+ };
219
+ };
220
+ ```
221
+
222
+ ### Diagnostic
223
+
224
+ holodeck can be launched and cleaned up using the lifecycle hooks in the launch config
225
+ for diagnostic in `diagnostic.js`:
226
+
227
+ ```ts
228
+ import launch from '@warp-drive/diagnostic/server/default-setup.js';
229
+ import holodeck from '@warp-drive/holodeck';
230
+
231
+ await launch({
232
+ async setup(options) {
233
+ await holodeck.launchProgram({
234
+ port: options.port + 1,
235
+ });
236
+ },
237
+ async cleanup() {
238
+ await holodeck.endProgram();
239
+ },
240
+ });
241
+ ```
242
+
97
243
  ### ♥️ Credits
98
244
 
99
245
  <details>
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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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"}
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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@warp-drive/holodeck",
3
3
  "description": "⚡️ Simple, Fast HTTP Mocking for Tests",
4
- "version": "0.0.0-beta.11",
4
+ "version": "0.0.0-beta.13",
5
5
  "license": "MIT",
6
6
  "author": "Chris Thoburn <runspired@users.noreply.github.com>",
7
7
  "repository": {
@@ -12,7 +12,7 @@
12
12
  "homepage": "https://github.com/emberjs/data",
13
13
  "bugs": "https://github.com/emberjs/data/issues",
14
14
  "engines": {
15
- "node": ">= 18.20.3"
15
+ "node": ">= 18.20.4"
16
16
  },
17
17
  "keywords": [
18
18
  "http-mock"
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "@hono/node-server": "^1.11.1",
25
25
  "chalk": "^5.3.0",
26
- "hono": "^4.3.6"
26
+ "hono": "^4.6.5"
27
27
  },
28
28
  "type": "module",
29
29
  "files": [
@@ -38,14 +38,9 @@
38
38
  "bin": {
39
39
  "ensure-cert": "./server/ensure-cert.js"
40
40
  },
41
- "scripts": {
42
- "check:pkg-types": "tsc --noEmit",
43
- "build:pkg": "vite build;",
44
- "sync-hardlinks": "bun run sync-dependencies-meta-injected"
45
- },
46
41
  "peerDependencies": {
47
- "@ember-data/request": "5.4.0-beta.11",
48
- "@warp-drive/core-types": "0.0.0-beta.11"
42
+ "@ember-data/request": "5.4.0-beta.13",
43
+ "@warp-drive/core-types": "0.0.0-beta.13"
49
44
  },
50
45
  "devDependencies": {
51
46
  "@babel/core": "^7.24.5",
@@ -53,11 +48,11 @@
53
48
  "@babel/preset-env": "^7.24.5",
54
49
  "@babel/preset-typescript": "^7.24.1",
55
50
  "@babel/runtime": "^7.24.5",
56
- "@ember-data/request": "5.4.0-beta.11",
57
- "@warp-drive/core-types": "0.0.0-beta.11",
58
- "@warp-drive/internal-config": "5.4.0-beta.11",
51
+ "@ember-data/request": "5.4.0-beta.13",
52
+ "@warp-drive/core-types": "0.0.0-beta.13",
53
+ "@warp-drive/internal-config": "5.4.0-beta.13",
59
54
  "pnpm-sync-dependencies-meta-injected": "0.0.14",
60
- "typescript": "^5.4.5",
55
+ "typescript": "^5.7.2",
61
56
  "vite": "^5.2.11"
62
57
  },
63
58
  "exports": {
@@ -84,5 +79,10 @@
84
79
  "@warp-drive/core-types": {
85
80
  "injected": true
86
81
  }
82
+ },
83
+ "scripts": {
84
+ "check:pkg-types": "tsc --noEmit",
85
+ "build:pkg": "vite build;",
86
+ "sync-hardlinks": "bun run sync-dependencies-meta-injected"
87
87
  }
88
- }
88
+ }
@@ -23,18 +23,26 @@ function main() {
23
23
  let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
24
24
  const configFilePath = getShellConfigFilePath();
25
25
 
26
- if (!CERT_PATH) {
27
- CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
28
- process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
29
- execSync(`echo '\nexport HOLODECK_SSL_CERT_PATH="${CERT_PATH}"' >> ${configFilePath}`);
30
- console.log(`Added HOLODECK_SSL_CERT_PATH to ${configFilePath}`);
31
- }
32
-
33
- if (!KEY_PATH) {
34
- KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
35
- process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
36
- execSync(`echo '\nexport HOLODECK_SSL_KEY_PATH="${KEY_PATH}"' >> ${configFilePath}`);
37
- console.log(`Added HOLODECK_SSL_KEY_PATH to ${configFilePath}`);
26
+ if (!CERT_PATH || !KEY_PATH) {
27
+ console.log(`Environment variables not found, updating the environment config file...\n`);
28
+
29
+ if (!CERT_PATH) {
30
+ CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
31
+ process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
32
+ execSync(`echo '\nexport HOLODECK_SSL_CERT_PATH="${CERT_PATH}"' >> ${configFilePath}`);
33
+ console.log(`Added HOLODECK_SSL_CERT_PATH to ${configFilePath}`);
34
+ }
35
+
36
+ if (!KEY_PATH) {
37
+ KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
38
+ process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
39
+ execSync(`echo '\nexport HOLODECK_SSL_KEY_PATH="${KEY_PATH}"' >> ${configFilePath}`);
40
+ console.log(`Added HOLODECK_SSL_KEY_PATH to ${configFilePath}`);
41
+ }
42
+
43
+ console.log(
44
+ `\n*** Please restart your terminal session to apply the changes or run \`source ${configFilePath}\`. ***\n`
45
+ );
38
46
  }
39
47
 
40
48
  if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
package/server/index.js CHANGED
@@ -5,12 +5,11 @@ import { Hono } from 'hono';
5
5
  import { cors } from 'hono/cors';
6
6
  import { HTTPException } from 'hono/http-exception';
7
7
  import { logger } from 'hono/logger';
8
- import { execSync } from 'node:child_process';
9
8
  import crypto from 'node:crypto';
10
9
  import fs from 'node:fs';
11
10
  import http2 from 'node:http2';
12
11
  import zlib from 'node:zlib';
13
- import { homedir, userInfo } from 'os';
12
+ import { homedir } from 'os';
14
13
  import path from 'path';
15
14
 
16
15
  /** @type {import('bun-types')} */
@@ -18,41 +17,30 @@ const isBun = typeof Bun !== 'undefined';
18
17
  const DEBUG = process.env.DEBUG?.includes('holodeck') || process.env.DEBUG === '*';
19
18
  const CURRENT_FILE = new URL(import.meta.url).pathname;
20
19
 
21
- function getShellConfigFilePath() {
22
- const shell = userInfo().shell;
23
- switch (shell) {
24
- case '/bin/zsh':
25
- return path.join(homedir(), '.zshrc');
26
- case '/bin/bash':
27
- return path.join(homedir(), '.bashrc');
28
- default:
29
- throw Error(
30
- `Unable to determine configuration file for shell: ${shell}. Manual SSL Cert Setup Required for Holodeck.`
31
- );
32
- }
33
- }
34
-
35
20
  function getCertInfo() {
36
21
  let CERT_PATH = process.env.HOLODECK_SSL_CERT_PATH;
37
22
  let KEY_PATH = process.env.HOLODECK_SSL_KEY_PATH;
38
- const configFilePath = getShellConfigFilePath();
39
23
 
40
24
  if (!CERT_PATH) {
41
25
  CERT_PATH = path.join(homedir(), 'holodeck-localhost.pem');
42
26
  process.env.HOLODECK_SSL_CERT_PATH = CERT_PATH;
43
- execSync(`echo '\nexport HOLODECK_SSL_CERT_PATH="${CERT_PATH}"' >> ${configFilePath}`);
44
- console.log(`Added HOLODECK_SSL_CERT_PATH to ${configFilePath}`);
27
+
28
+ console.log(
29
+ `HOLODECK_SSL_CERT_PATH was not found in the current environment. Setting it to default value of ${CERT_PATH}`
30
+ );
45
31
  }
46
32
 
47
33
  if (!KEY_PATH) {
48
34
  KEY_PATH = path.join(homedir(), 'holodeck-localhost-key.pem');
49
35
  process.env.HOLODECK_SSL_KEY_PATH = KEY_PATH;
50
- execSync(`echo '\nexport HOLODECK_SSL_KEY_PATH="${KEY_PATH}"' >> ${configFilePath}`);
51
- console.log(`Added HOLODECK_SSL_KEY_PATH to ${configFilePath}`);
36
+
37
+ console.log(
38
+ `HOLODECK_SSL_KEY_PATH was not found in the current environment. Setting it to default value of ${KEY_PATH}`
39
+ );
52
40
  }
53
41
 
54
42
  if (!fs.existsSync(CERT_PATH) || !fs.existsSync(KEY_PATH)) {
55
- throw new Error('SSL certificate or key not found, you may need to run `npx -p @warp-drive/holodeck ensure-cert`');
43
+ throw new Error('SSL certificate or key not found, you may need to run `pnpx @warp-drive/holodeck ensure-cert`');
56
44
  }
57
45
 
58
46
  return {
@@ -147,96 +135,116 @@ function replayRequest(context, cacheKey) {
147
135
 
148
136
  function createTestHandler(projectRoot) {
149
137
  const TestHandler = async (context) => {
150
- const { req } = context;
151
-
152
- const testId = req.query('__xTestId');
153
- const testRequestNumber = req.query('__xTestRequestNumber');
154
- const niceUrl = getNiceUrl(req.url);
138
+ try {
139
+ const { req } = context;
140
+
141
+ const testId = req.query('__xTestId');
142
+ const testRequestNumber = req.query('__xTestRequestNumber');
143
+ const niceUrl = getNiceUrl(req.url);
144
+
145
+ if (!testId) {
146
+ context.header('Content-Type', 'application/vnd.api+json');
147
+ context.status(400);
148
+ return context.body(
149
+ JSON.stringify({
150
+ errors: [
151
+ {
152
+ status: '400',
153
+ code: 'MISSING_X_TEST_ID_HEADER',
154
+ title: 'Request to the http mock server is missing the `X-Test-Id` header',
155
+ detail:
156
+ "The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
157
+ source: { header: 'X-Test-Id' },
158
+ },
159
+ ],
160
+ })
161
+ );
162
+ }
155
163
 
156
- if (!testId) {
157
- context.header('Content-Type', 'application/vnd.api+json');
158
- context.status(400);
159
- return context.body(
160
- JSON.stringify({
161
- errors: [
162
- {
163
- status: '400',
164
- code: 'MISSING_X_TEST_ID_HEADER',
165
- title: 'Request to the http mock server is missing the `X-Test-Id` header',
166
- detail:
167
- "The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
168
- source: { header: 'X-Test-Id' },
169
- },
170
- ],
171
- })
172
- );
173
- }
164
+ if (!testRequestNumber) {
165
+ context.header('Content-Type', 'application/vnd.api+json');
166
+ context.status(400);
167
+ return context.body(
168
+ JSON.stringify({
169
+ errors: [
170
+ {
171
+ status: '400',
172
+ code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
173
+ title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
174
+ detail:
175
+ "The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
176
+ source: { header: 'X-Test-Request-Number' },
177
+ },
178
+ ],
179
+ })
180
+ );
181
+ }
174
182
 
175
- if (!testRequestNumber) {
183
+ if (req.method === 'POST' || niceUrl === '__record') {
184
+ const payload = await req.json();
185
+ const { url, headers, method, status, statusText, body, response } = payload;
186
+ const cacheKey = generateFilepath({
187
+ projectRoot,
188
+ testId,
189
+ url,
190
+ method,
191
+ body: body ? JSON.stringify(body) : null,
192
+ testRequestNumber,
193
+ });
194
+ // allow Content-Type to be overridden
195
+ headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
196
+ // We always compress and chunk the response
197
+ headers['Content-Encoding'] = 'br';
198
+ // we don't cache since tests will often reuse similar urls for different payload
199
+ headers['Cache-Control'] = 'no-store';
200
+
201
+ const cacheDir = generateFileDir({
202
+ projectRoot,
203
+ testId,
204
+ url,
205
+ method,
206
+ testRequestNumber,
207
+ });
208
+
209
+ fs.mkdirSync(cacheDir, { recursive: true });
210
+ fs.writeFileSync(
211
+ `${cacheKey}.meta.json`,
212
+ JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2)
213
+ );
214
+ fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response)));
215
+ context.status(204);
216
+ return context.body(null);
217
+ } else {
218
+ const body = await req.text();
219
+ const cacheKey = generateFilepath({
220
+ projectRoot,
221
+ testId,
222
+ url: niceUrl,
223
+ method: req.method,
224
+ body,
225
+ testRequestNumber,
226
+ });
227
+ return replayRequest(context, cacheKey);
228
+ }
229
+ } catch (e) {
230
+ if (e instanceof HTTPException) {
231
+ throw e;
232
+ }
176
233
  context.header('Content-Type', 'application/vnd.api+json');
177
- context.status(400);
234
+ context.status(500);
178
235
  return context.body(
179
236
  JSON.stringify({
180
237
  errors: [
181
238
  {
182
- status: '400',
183
- code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER',
184
- title: 'Request to the http mock server is missing the `X-Test-Request-Number` header',
185
- detail:
186
- "The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.",
187
- source: { header: 'X-Test-Request-Number' },
239
+ status: '500',
240
+ code: 'MOCK_SERVER_ERROR',
241
+ title: 'Mock Server Error during Request',
242
+ detail: e.message,
188
243
  },
189
244
  ],
190
245
  })
191
246
  );
192
247
  }
193
-
194
- if (req.method === 'POST' || niceUrl === '__record') {
195
- const payload = await req.json();
196
- const { url, headers, method, status, statusText, body, response } = payload;
197
- const cacheKey = generateFilepath({
198
- projectRoot,
199
- testId,
200
- url,
201
- method,
202
- body: body ? JSON.stringify(body) : null,
203
- testRequestNumber,
204
- });
205
- // allow Content-Type to be overridden
206
- headers['Content-Type'] = headers['Content-Type'] || 'application/vnd.api+json';
207
- // We always compress and chunk the response
208
- headers['Content-Encoding'] = 'br';
209
- // we don't cache since tests will often reuse similar urls for different payload
210
- headers['Cache-Control'] = 'no-store';
211
-
212
- const cacheDir = generateFileDir({
213
- projectRoot,
214
- testId,
215
- url,
216
- method,
217
- testRequestNumber,
218
- });
219
-
220
- fs.mkdirSync(cacheDir, { recursive: true });
221
- fs.writeFileSync(
222
- `${cacheKey}.meta.json`,
223
- JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2)
224
- );
225
- fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response)));
226
- context.status(204);
227
- return context.body(null);
228
- } else {
229
- const body = await req.text();
230
- const cacheKey = generateFilepath({
231
- projectRoot,
232
- testId,
233
- url: niceUrl,
234
- method: req.method,
235
- body,
236
- testRequestNumber,
237
- });
238
- return replayRequest(context, cacheKey);
239
- }
240
248
  };
241
249
 
242
250
  return TestHandler;
@@ -342,11 +350,11 @@ export default {
342
350
  async endProgram() {
343
351
  console.log(chalk.grey(`\n\tEnding Subroutines (mode:${chalk.cyan(isBun ? 'bun' : 'node')})`));
344
352
  const projectRoot = process.cwd();
345
- const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then(
346
- (pkg) => pkg.name
347
- );
348
353
 
349
354
  if (!servers.has(projectRoot)) {
355
+ const name = await import(path.join(projectRoot, 'package.json'), { with: { type: 'json' } }).then(
356
+ (pkg) => pkg.name
357
+ );
350
358
  throw new Error(`Holodeck was not running for project '${name}' at '${projectRoot}'`);
351
359
  }
352
360