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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### [0.7.1](https://github.com/ike18t/wiremock_mapper_node/compare/v0.5.0...v0.7.1) (2025-07-15)


### Features

* add ability to delete requests ([aa5d6b5](https://github.com/ike18t/wiremock_mapper_node/commit/aa5d6b538e518246dff1b0f5e739956490e1a195))
* add e2e tests with testcontainers ([786221a](https://github.com/ike18t/wiremock_mapper_node/commit/786221a297644a9a308b3744753d0da1ed90584a))
* add Jest matchers with global configuration support ([987703c](https://github.com/ike18t/wiremock_mapper_node/commit/987703cf025209269f5aa94f7a40c834192f61f8))

## 0.7.0 (2024-10-11)


Expand Down
140 changes: 140 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,146 @@ await WireMockMapper.getRequests({ stubId: 'some_stub_id' });

The interface for the returned object can be found [here](https://github.com/ike18t/wiremock_mapper_node/blob/master/lib/requests_response.ts#L1-L7).

## Jest Matchers

The library provides custom Jest matchers for testing WireMock interactions with built-in retry logic for handling asynchronous operations.

### Setup

```typescript
import { wiremockMapperMatchers } from 'wiremock-mapper';

// Extend Jest with WireMock matchers
expect.extend(wiremockMapperMatchers);
```

### Available Matchers

#### `toHaveBeenRequested(options?)`

Verifies that a stub received at least one request.

```typescript
const stubId = await WireMockMapper.createMapping((req, res) => {
req.isAGet.withUrlPath.equalTo('/api/users');
res.withJsonBody([]).withStatus(200);
});

// Make API call
await fetch('http://localhost:8080/api/users');

// Verify the stub was called
await expect(stubId).toHaveBeenRequested();

// With custom retry options
await expect(stubId).toHaveBeenRequested({ retries: 5, delay: 100 });
```

#### `toHaveBeenRequestedWith(expected, options?)`

Verifies that a stub received a request with the specified payload.

```typescript
const stubId = await WireMockMapper.createMapping((req, res) => {
req.isAPost.withUrlPath.equalTo('/api/users');
res.withJsonBody({ id: 123 }).withStatus(201);
});

const userData = { name: 'John Doe', email: 'john@example.com' };

// Make API call with payload
await fetch('http://localhost:8080/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});

// Verify the stub was called with expected payload
await expect(stubId).toHaveBeenRequestedWith(userData);
```

#### `toHaveBeenRequestedTimes(expectedCount, options?)`

Verifies that a stub received exactly the specified number of requests.

```typescript
const stubId = await WireMockMapper.createMapping((req, res) => {
req.isAGet.withUrlPath.equalTo('/api/health');
res.withBody('OK').withStatus(200);
});

// Make multiple API calls
await fetch('http://localhost:8080/api/health');
await fetch('http://localhost:8080/api/health');
await fetch('http://localhost:8080/api/health');

// Verify the stub was called exactly 3 times
await expect(stubId).toHaveBeenRequestedTimes(3);
```

### Global Matcher Configuration

You can configure global defaults for all matchers:

```typescript
import { Configuration } from 'wiremock-mapper';

// Set global retry options
Configuration.setMatcherOptions({
retries: 10, // Number of retry attempts (default: 15)
delay: 100 // Delay between retries in milliseconds (default: 200)
});
```

Individual matchers can override global settings:

```typescript
// Override global settings for this specific assertion
await expect(stubId).toHaveBeenRequested({ retries: 3, delay: 50 });
```

### Matcher Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `retries` | `number` | `15` | Number of retry attempts when waiting for requests |
| `delay` | `number` | `200` | Delay in milliseconds between retry attempts |

### Example Test

```typescript
import { WireMockMapper, Configuration, wiremockMapperMatchers } from 'wiremock-mapper';

expect.extend(wiremockMapperMatchers);

describe('API Tests', () => {
beforeAll(() => {
Configuration.wireMockBaseUrl = 'http://localhost:8080';
});

beforeEach(async () => {
await WireMockMapper.clearAllMappings();
});

it('should handle user creation', async () => {
const stubId = await WireMockMapper.createMapping((req, res) => {
req.isAPost.withUrlPath.equalTo('/api/users');
res.withJsonBody({ id: 123, name: 'John Doe' }).withStatus(201);
});

const userData = { name: 'John Doe', email: 'john@example.com' };

await fetch('http://localhost:8080/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});

await expect(stubId).toHaveBeenRequestedWith(userData);
});
});
```

## Troubleshooting

### Common Issues
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default [
pluginJs.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintConfigPrettier,
{ ignores: ['jest.config.ts', 'eslint.config.mjs', 'coverage'] },
{ ignores: ['jest.config.ts', 'eslint.config.mjs', 'coverage', 'dist'] },
{
languageOptions: {
parserOptions: {
Expand Down
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ export { ResponseBuilder } from './lib/builders/response_builder';
export { ScenarioBuilder } from './lib/builders/scenario_builder';
export { WireMockMapper } from './lib/wiremock_mapper';
export { WireMockMapping } from './lib/wiremock_mapping';

// Optional Jest utilities - only import if using Jest
export { wiremockMapperMatchers } from './lib/jest-matchers';
14 changes: 14 additions & 0 deletions lib/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { RequestBuilderImpl } from './builders/request_builder';
import { ResponseBuilderImpl } from './builders/response_builder';
import { GlobalWireMockMapping } from './wiremock_mapping';

export type MatcherOptions = {
retries: number;
delay: number;
};

export class Configuration {
public static get requestBuilder(): RequestBuilderImpl {
return Configuration.requestBuilderImpl;
Expand Down Expand Up @@ -36,6 +41,15 @@ export class Configuration {
wireMockMapping(this.requestBuilder, this.responseBuilder);
}

public static matcherOptions: MatcherOptions = {
retries: 15,
delay: 200
};

public static setMatcherOptions(options: Partial<MatcherOptions>) {
Configuration.matcherOptions = { ...Configuration.matcherOptions, ...options };
}

public static reset() {
Configuration.requestBuilderImpl = new RequestBuilderImpl();
Configuration.responseBuilderImpl = new ResponseBuilderImpl();
Expand Down
151 changes: 151 additions & 0 deletions lib/jest-matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { diff } from 'jest-diff';

import { WireMockMapper } from './wiremock_mapper';
import { Configuration, MatcherOptions } from './configuration';

interface JestMatcherResult {
pass: boolean;
message: () => string;
}

const mergeOptions = (options?: Partial<MatcherOptions>): MatcherOptions => {
return { ...Configuration.matcherOptions, ...options };
};

export const wiremockMapperMatchers = {
async toHaveBeenRequested(
stubId: string,
options?: Partial<MatcherOptions>
): Promise<JestMatcherResult> {
const { retries: maxRetries, delay } = mergeOptions(options);
let retries = maxRetries;

while (retries > 0) {
try {
const { requests } = await WireMockMapper.getRequests({
stubId
});
if (requests.length === 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
retries -= 1;

continue;
} else {
return {
message: () => 'Request found',
pass: true
};
}
} catch (_error) {
retries -= 1;
}
}
return {
message: () => 'Request not found',
pass: false
};
},

async toHaveBeenRequestedWith(
stubId: string,
expected: unknown,
options?: Partial<MatcherOptions>
): Promise<JestMatcherResult> {
const { retries: maxRetries, delay } = mergeOptions(options);
let retries = maxRetries;

while (retries > 0) {
try {
const { requests } = await WireMockMapper.getRequests({
stubId
});
if (requests.length === 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
retries -= 1;

continue;
} else {
const diffResult = diff(
JSON.parse(requests[0].request.body),
JSON.parse(JSON.stringify(expected))
);

return {
message: () => diffResult || 'no visual difference',
pass:
diffResult === null || diffResult.includes('no visual difference')
};
}
} catch (_error) {
retries -= 1;
}
}
return {
message: () => 'Request not found',
pass: false
};
},

async toHaveBeenRequestedTimes(
stubId: string,
expectedCount: number,
options?: Partial<MatcherOptions>
): Promise<JestMatcherResult> {
const { retries: maxRetries, delay } = mergeOptions(options);
let retries = maxRetries;

while (retries > 0) {
try {
const { requests } = await WireMockMapper.getRequests({
stubId
});

if (requests.length === expectedCount) {
return {
message: () =>
`Expected stub ${stubId} not to have been requested ${expectedCount} time(s), but it was`,
pass: true
};
}

await new Promise((resolve) => setTimeout(resolve, delay));
retries -= 1;
} catch (_error) {
retries -= 1;
}
}

// Final check to get actual count for error message
try {
const { requests } = await WireMockMapper.getRequests({ stubId });
const actualCount = requests.length;

return {
message: () =>
`Expected stub ${stubId} to have been requested ${expectedCount} time(s), but it was requested ${actualCount} time(s)`,
pass: false
};
} catch (_error) {
return {
message: () =>
`Expected stub ${stubId} to have been requested ${expectedCount} time(s), but could not retrieve requests`,
pass: false
};
}
}
};

// Type declarations for TypeScript users
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R> {
toHaveBeenRequested(options?: Partial<MatcherOptions>): Promise<R>;
toHaveBeenRequestedWith(expected: unknown, options?: Partial<MatcherOptions>): Promise<R>;
toHaveBeenRequestedTimes(
expectedCount: number,
options?: Partial<MatcherOptions>
): Promise<R>;
}
}
}
Loading