Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose

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

# Response Injection (Tap Feature)

The injection/tap feature allows you to "tap into" the request flow and inject custom responses for specific requests. This is particularly useful for:
- **Offline testing** - Mock external API responses without network access
- **Testing edge cases** - Simulate errors, timeouts, or specific response scenarios
- **Development** - Work on your application without depending on external services

## What is a "Tap"?

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.

## Basic Usage

```javascript
import { mockhttp } from '@jaredwray/mockhttp';

const mock = new mockhttp();
await mock.start();

// Inject a simple response
const tap = mock.taps.inject(
{
response: "Hello, World!",
statusCode: 200,
headers: { "Content-Type": "text/plain" }
},
{
url: "/api/greeting",
method: "GET"
}
);

// Make requests - they will get the injected response
const response = await fetch('http://localhost:3000/api/greeting');
console.log(await response.text()); // "Hello, World!"

// Remove the injection when done
mock.taps.removeInjection(tap);

await mock.close();
```

## Advanced Examples

### Inject JSON Response

```javascript
const tap = mock.taps.inject(
{
response: { message: "Success", data: { id: 123 } },
statusCode: 200
},
{ url: "/api/users/123" }
);
```

### Wildcard URL Matching

```javascript
// Match all requests under /api/
const tap = mock.taps.inject(
{
response: "API is mocked",
statusCode: 503
},
{ url: "/api/*" }
);
```

### Multiple Injections

```javascript
const tap1 = mock.taps.inject(
{ response: "Users data" },
{ url: "/api/users" }
);

const tap2 = mock.taps.inject(
{ response: "Posts data" },
{ url: "/api/posts" }
);

// View all active injections
console.log(mock.taps.injections); // Map of all active taps

// Remove specific injections
mock.taps.removeInjection(tap1);
mock.taps.removeInjection(tap2);
```

### Match by HTTP Method

```javascript
// Only intercept POST requests
const tap = mock.taps.inject(
{ response: "Created", statusCode: 201 },
{ url: "/api/users", method: "POST" }
);
```

### Match by Headers

```javascript
const tap = mock.taps.inject(
{ response: "Authenticated response" },
{
url: "/api/secure",
headers: {
"authorization": "Bearer token123"
}
}
);
```

### Catch-All Injection

```javascript
// Match ALL requests (no matcher specified)
const tap = mock.taps.inject({
response: "Server is in maintenance mode",
statusCode: 503
});
```

# API Reference

## MockHttp Class

### Constructor

```javascript
new MockHttp(options?)
```

**Parameters:**
- `options?` (MockHttpOptions):
- `port?`: number - The port to listen on (default: 3000)
- `host?`: string - The host to listen on (default: '0.0.0.0')
- `autoDetectPort?`: boolean - Auto-detect next available port if in use (default: true)
- `helmet?`: boolean - Use Helmet for security headers (default: true)
- `apiDocs?`: boolean - Enable Swagger API documentation (default: true)
- `httpBin?`: HttpBinOptions - Configure which httpbin routes to enable
- `hookOptions?`: HookifiedOptions - Hookified options

### Properties

- `port`: number - Get/set the server port
- `host`: string - Get/set the server host
- `autoDetectPort`: boolean - Get/set auto-detect port behavior
- `helmet`: boolean - Get/set Helmet security headers
- `apiDocs`: boolean - Get/set API documentation
- `httpBin`: HttpBinOptions - Get/set httpbin route options
- `server`: FastifyInstance - Get/set the Fastify server instance
- `taps`: TapManager - Get/set the TapManager instance

### Methods

#### `async start()`

Start the Fastify server. If already running, it will be closed and restarted.

#### `async close()`

Stop the Fastify server.

#### `async detectPort()`

Detect the next available port.

**Returns:** number - The available port

#### `async registerApiDocs(fastifyInstance?)`

Register Swagger API documentation routes.

#### `async registerHttpMethods(fastifyInstance?)`

Register HTTP method routes (GET, POST, PUT, PATCH, DELETE).

#### `async registerStatusCodeRoutes(fastifyInstance?)`

Register status code routes.

#### `async registerRequestInspectionRoutes(fastifyInstance?)`

Register request inspection routes (headers, ip, user-agent).

#### `async registerResponseInspectionRoutes(fastifyInstance?)`

Register response inspection routes (cache, etag, response-headers).

#### `async registerResponseFormatRoutes(fastifyInstance?)`

Register response format routes (json, xml, html, etc.).

#### `async registerRedirectRoutes(fastifyInstance?)`

Register redirect routes (absolute, relative, redirect-to).

#### `async registerCookieRoutes(fastifyInstance?)`

Register cookie routes (get, set, delete).

#### `async registerAnythingRoutes(fastifyInstance?)`

Register "anything" catch-all routes.

#### `async registerAuthRoutes(fastifyInstance?)`

Register authentication routes (basic, bearer, digest, hidden-basic).

## Taps (Response Injection)

Access the TapManager via `mockHttp.taps` to inject custom responses.

### `taps.inject(response, matcher?)`

Injects a custom response for requests matching the criteria.

**Parameters:**
- `response` (InjectionResponse):
- `response`: string | object | Buffer - The response body
- `statusCode?`: number - HTTP status code (default: 200)
- `headers?`: object - Response headers

- `matcher?` (InjectionMatcher) - Optional matching criteria:
- `url?`: string - URL path (supports wildcards with `*`)
- `method?`: string - HTTP method (GET, POST, etc.)
- `hostname?`: string - Hostname to match
- `headers?`: object - Headers that must be present

**Returns:** `InjectionTap` - A tap object with a unique `id` that can be used to remove the injection

## `taps.removeInjection(tapOrId)`

Removes an injection.

**Parameters:**
- `tapOrId`: InjectionTap | string - The tap object or tap ID to remove

**Returns:** boolean - `true` if removed, `false` if not found

### `taps.injections`

A getter that returns a Map of all active injection taps.

**Returns:** `Map<string, InjectionTap>` - Map of all active injections with tap IDs as keys

## `taps.clear()`

Removes all injections.

## `taps.hasInjections`

A getter that returns whether there are any active injections.

**Returns:** boolean - `true` if there are active injections, `false` otherwise

# About mockhttp.org

[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.
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
/* c8 ignore end */

export { MockHttp as default, MockHttp as mockhttp } from "./mock-http.js";
export type {
InjectionMatcher,
InjectionResponse,
InjectionTap,
} from "./tap-manager.js";
37 changes: 37 additions & 0 deletions src/mock-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
import { sitemapRoute } from "./routes/sitemap.js";
import { statusCodeRoute } from "./routes/status-codes/index.js";
import { fastifySwaggerConfig, registerSwaggerUi } from "./swagger.js";
import { TapManager } from "./tap-manager.js";

export type HttpBinOptions = {
httpMethods?: boolean;
Expand Down Expand Up @@ -110,6 +111,7 @@ export class MockHttp extends Hookified {
};

private _server: FastifyInstance = Fastify();
private _taps: TapManager = new TapManager();

constructor(options?: MockHttpOptions) {
super(options?.hookOptions);
Expand Down Expand Up @@ -245,6 +247,20 @@ export class MockHttp extends Hookified {
this._server = server;
}

/**
* The TapManager instance for managing injection taps.
*/
public get taps(): TapManager {
return this._taps;
}

/**
* The TapManager instance for managing injection taps.
*/
public set taps(taps: TapManager) {
this._taps = taps;
}

/**
* Start the Fastify server. If the server is already running, it will be closed and restarted.
*/
Expand All @@ -256,6 +272,27 @@ export class MockHttp extends Hookified {

this._server = Fastify(fastifyConfig);

// Register injection hook to intercept requests
this._server.addHook("onRequest", async (request, reply) => {
const matchedTap = this._taps.matchRequest(request);
if (matchedTap) {
const { response, statusCode = 200, headers } = matchedTap.response;

// Set status code
reply.code(statusCode);

// Set headers if provided
if (headers) {
for (const [key, value] of Object.entries(headers)) {
reply.header(key, value);
}
}

// Send the response and prevent further processing
return reply.send(response);
}
});

// Register Scalar API client
await this._server.register(fastifyStatic, {
root: path.resolve("./node_modules/@scalar/api-reference/dist"),
Expand Down
Loading
Loading