Skip to content

Commit b3fefac

Browse files
authored
Merge pull request #85 from jaredwray/feat-adding-in-injection-taps
feat: adding in injection taps
2 parents 1be46df + 1fded83 commit b3fefac

6 files changed

Lines changed: 1193 additions & 0 deletions

File tree

README.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
1212

1313
# Features
1414
* All the features of [httpbin](https://httpbin.org/)
15+
* Taps - Inject custom responses for testing and develepment
1516
* `@fastify/helmet` built in by default
1617
* Built with `nodejs`, `typescript`, and `fastify`
1718
* Deploy via `docker` or `nodejs`
@@ -63,6 +64,264 @@ console.log(reaponse);
6364
await mockhttp.stop(); // stop the server
6465
```
6566

67+
# Response Injection (Tap Feature)
68+
69+
The injection/tap feature allows you to "tap into" the request flow and inject custom responses for specific requests. This is particularly useful for:
70+
- **Offline testing** - Mock external API responses without network access
71+
- **Testing edge cases** - Simulate errors, timeouts, or specific response scenarios
72+
- **Development** - Work on your application without depending on external services
73+
74+
## What is a "Tap"?
75+
76+
A "tap" is a reference to an injected response, similar to "wiretapping" - you're intercepting requests and returning predefined responses. Each tap can be removed when you're done with it, restoring normal server behavior.
77+
78+
## Basic Usage
79+
80+
```javascript
81+
import { mockhttp } from '@jaredwray/mockhttp';
82+
83+
const mock = new mockhttp();
84+
await mock.start();
85+
86+
// Inject a simple response
87+
const tap = mock.taps.inject(
88+
{
89+
response: "Hello, World!",
90+
statusCode: 200,
91+
headers: { "Content-Type": "text/plain" }
92+
},
93+
{
94+
url: "/api/greeting",
95+
method: "GET"
96+
}
97+
);
98+
99+
// Make requests - they will get the injected response
100+
const response = await fetch('http://localhost:3000/api/greeting');
101+
console.log(await response.text()); // "Hello, World!"
102+
103+
// Remove the injection when done
104+
mock.taps.removeInjection(tap);
105+
106+
await mock.close();
107+
```
108+
109+
## Advanced Examples
110+
111+
### Inject JSON Response
112+
113+
```javascript
114+
const tap = mock.taps.inject(
115+
{
116+
response: { message: "Success", data: { id: 123 } },
117+
statusCode: 200
118+
},
119+
{ url: "/api/users/123" }
120+
);
121+
```
122+
123+
### Wildcard URL Matching
124+
125+
```javascript
126+
// Match all requests under /api/
127+
const tap = mock.taps.inject(
128+
{
129+
response: "API is mocked",
130+
statusCode: 503
131+
},
132+
{ url: "/api/*" }
133+
);
134+
```
135+
136+
### Multiple Injections
137+
138+
```javascript
139+
const tap1 = mock.taps.inject(
140+
{ response: "Users data" },
141+
{ url: "/api/users" }
142+
);
143+
144+
const tap2 = mock.taps.inject(
145+
{ response: "Posts data" },
146+
{ url: "/api/posts" }
147+
);
148+
149+
// View all active injections
150+
console.log(mock.taps.injections); // Map of all active taps
151+
152+
// Remove specific injections
153+
mock.taps.removeInjection(tap1);
154+
mock.taps.removeInjection(tap2);
155+
```
156+
157+
### Match by HTTP Method
158+
159+
```javascript
160+
// Only intercept POST requests
161+
const tap = mock.taps.inject(
162+
{ response: "Created", statusCode: 201 },
163+
{ url: "/api/users", method: "POST" }
164+
);
165+
```
166+
167+
### Match by Headers
168+
169+
```javascript
170+
const tap = mock.taps.inject(
171+
{ response: "Authenticated response" },
172+
{
173+
url: "/api/secure",
174+
headers: {
175+
"authorization": "Bearer token123"
176+
}
177+
}
178+
);
179+
```
180+
181+
### Catch-All Injection
182+
183+
```javascript
184+
// Match ALL requests (no matcher specified)
185+
const tap = mock.taps.inject({
186+
response: "Server is in maintenance mode",
187+
statusCode: 503
188+
});
189+
```
190+
191+
# API Reference
192+
193+
## MockHttp Class
194+
195+
### Constructor
196+
197+
```javascript
198+
new MockHttp(options?)
199+
```
200+
201+
**Parameters:**
202+
- `options?` (MockHttpOptions):
203+
- `port?`: number - The port to listen on (default: 3000)
204+
- `host?`: string - The host to listen on (default: '0.0.0.0')
205+
- `autoDetectPort?`: boolean - Auto-detect next available port if in use (default: true)
206+
- `helmet?`: boolean - Use Helmet for security headers (default: true)
207+
- `apiDocs?`: boolean - Enable Swagger API documentation (default: true)
208+
- `httpBin?`: HttpBinOptions - Configure which httpbin routes to enable
209+
- `hookOptions?`: HookifiedOptions - Hookified options
210+
211+
### Properties
212+
213+
- `port`: number - Get/set the server port
214+
- `host`: string - Get/set the server host
215+
- `autoDetectPort`: boolean - Get/set auto-detect port behavior
216+
- `helmet`: boolean - Get/set Helmet security headers
217+
- `apiDocs`: boolean - Get/set API documentation
218+
- `httpBin`: HttpBinOptions - Get/set httpbin route options
219+
- `server`: FastifyInstance - Get/set the Fastify server instance
220+
- `taps`: TapManager - Get/set the TapManager instance
221+
222+
### Methods
223+
224+
#### `async start()`
225+
226+
Start the Fastify server. If already running, it will be closed and restarted.
227+
228+
#### `async close()`
229+
230+
Stop the Fastify server.
231+
232+
#### `async detectPort()`
233+
234+
Detect the next available port.
235+
236+
**Returns:** number - The available port
237+
238+
#### `async registerApiDocs(fastifyInstance?)`
239+
240+
Register Swagger API documentation routes.
241+
242+
#### `async registerHttpMethods(fastifyInstance?)`
243+
244+
Register HTTP method routes (GET, POST, PUT, PATCH, DELETE).
245+
246+
#### `async registerStatusCodeRoutes(fastifyInstance?)`
247+
248+
Register status code routes.
249+
250+
#### `async registerRequestInspectionRoutes(fastifyInstance?)`
251+
252+
Register request inspection routes (headers, ip, user-agent).
253+
254+
#### `async registerResponseInspectionRoutes(fastifyInstance?)`
255+
256+
Register response inspection routes (cache, etag, response-headers).
257+
258+
#### `async registerResponseFormatRoutes(fastifyInstance?)`
259+
260+
Register response format routes (json, xml, html, etc.).
261+
262+
#### `async registerRedirectRoutes(fastifyInstance?)`
263+
264+
Register redirect routes (absolute, relative, redirect-to).
265+
266+
#### `async registerCookieRoutes(fastifyInstance?)`
267+
268+
Register cookie routes (get, set, delete).
269+
270+
#### `async registerAnythingRoutes(fastifyInstance?)`
271+
272+
Register "anything" catch-all routes.
273+
274+
#### `async registerAuthRoutes(fastifyInstance?)`
275+
276+
Register authentication routes (basic, bearer, digest, hidden-basic).
277+
278+
## Taps (Response Injection)
279+
280+
Access the TapManager via `mockHttp.taps` to inject custom responses.
281+
282+
### `taps.inject(response, matcher?)`
283+
284+
Injects a custom response for requests matching the criteria.
285+
286+
**Parameters:**
287+
- `response` (InjectionResponse):
288+
- `response`: string | object | Buffer - The response body
289+
- `statusCode?`: number - HTTP status code (default: 200)
290+
- `headers?`: object - Response headers
291+
292+
- `matcher?` (InjectionMatcher) - Optional matching criteria:
293+
- `url?`: string - URL path (supports wildcards with `*`)
294+
- `method?`: string - HTTP method (GET, POST, etc.)
295+
- `hostname?`: string - Hostname to match
296+
- `headers?`: object - Headers that must be present
297+
298+
**Returns:** `InjectionTap` - A tap object with a unique `id` that can be used to remove the injection
299+
300+
## `taps.removeInjection(tapOrId)`
301+
302+
Removes an injection.
303+
304+
**Parameters:**
305+
- `tapOrId`: InjectionTap | string - The tap object or tap ID to remove
306+
307+
**Returns:** boolean - `true` if removed, `false` if not found
308+
309+
### `taps.injections`
310+
311+
A getter that returns a Map of all active injection taps.
312+
313+
**Returns:** `Map<string, InjectionTap>` - Map of all active injections with tap IDs as keys
314+
315+
## `taps.clear()`
316+
317+
Removes all injections.
318+
319+
## `taps.hasInjections`
320+
321+
A getter that returns whether there are any active injections.
322+
323+
**Returns:** boolean - `true` if there are active injections, `false` otherwise
324+
66325
# About mockhttp.org
67326

68327
[mockhttp.org](https://mockhttp.org) is a free service that runs this codebase and allows you to use it for testing purposes. It is a simple way to mock HTTP responses for testing purposes. It is globally available has some limitations on it to prevent abuse such as requests per second. It is ran via [Cloudflare](https://cloudflare.com) and [Google Cloud Run](https://cloud.google.com/run/) across 7 regions globally and can do millions of requests per second.

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
2727
/* c8 ignore end */
2828

2929
export { MockHttp as default, MockHttp as mockhttp } from "./mock-http.js";
30+
export type {
31+
InjectionMatcher,
32+
InjectionResponse,
33+
InjectionTap,
34+
} from "./tap-manager.js";

src/mock-http.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
import { sitemapRoute } from "./routes/sitemap.js";
4747
import { statusCodeRoute } from "./routes/status-codes/index.js";
4848
import { fastifySwaggerConfig, registerSwaggerUi } from "./swagger.js";
49+
import { TapManager } from "./tap-manager.js";
4950

5051
export type HttpBinOptions = {
5152
httpMethods?: boolean;
@@ -110,6 +111,7 @@ export class MockHttp extends Hookified {
110111
};
111112

112113
private _server: FastifyInstance = Fastify();
114+
private _taps: TapManager = new TapManager();
113115

114116
constructor(options?: MockHttpOptions) {
115117
super(options?.hookOptions);
@@ -245,6 +247,20 @@ export class MockHttp extends Hookified {
245247
this._server = server;
246248
}
247249

250+
/**
251+
* The TapManager instance for managing injection taps.
252+
*/
253+
public get taps(): TapManager {
254+
return this._taps;
255+
}
256+
257+
/**
258+
* The TapManager instance for managing injection taps.
259+
*/
260+
public set taps(taps: TapManager) {
261+
this._taps = taps;
262+
}
263+
248264
/**
249265
* Start the Fastify server. If the server is already running, it will be closed and restarted.
250266
*/
@@ -256,6 +272,27 @@ export class MockHttp extends Hookified {
256272

257273
this._server = Fastify(fastifyConfig);
258274

275+
// Register injection hook to intercept requests
276+
this._server.addHook("onRequest", async (request, reply) => {
277+
const matchedTap = this._taps.matchRequest(request);
278+
if (matchedTap) {
279+
const { response, statusCode = 200, headers } = matchedTap.response;
280+
281+
// Set status code
282+
reply.code(statusCode);
283+
284+
// Set headers if provided
285+
if (headers) {
286+
for (const [key, value] of Object.entries(headers)) {
287+
reply.header(key, value);
288+
}
289+
}
290+
291+
// Send the response and prevent further processing
292+
return reply.send(response);
293+
}
294+
});
295+
259296
// Register Scalar API client
260297
await this._server.register(fastifyStatic, {
261298
root: path.resolve("./node_modules/@scalar/api-reference/dist"),

0 commit comments

Comments
 (0)