@hellaweb3/wiremock-testcontainers-nodejs 0.1.2 → 0.2.0
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 +298 -18
- package/dist/index.cjs +190 -72
- package/dist/index.d.cts +101 -44
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +101 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -70
- package/package.json +25 -40
- package/src/index.ts +137 -16
- package/src/sources.ts +143 -0
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
<p>A library for easily using WireMock with Testcontainers in Node.js applications.</p>
|
|
7
7
|
|
|
8
|
-
<a href="https://github.com/hella-web3/wiremock-testcontainers-nodejs/actions/workflows/main.yml
|
|
9
|
-
<img alt="Build Status" src="https://github.com/hella-web3/wiremock-testcontainers-nodejs/actions/workflows/main.yml" />
|
|
8
|
+
<a href="https://github.com/hella-web3/wiremock-testcontainers-nodejs/actions/workflows/main.yml ">
|
|
9
|
+
<img alt="Build Status" src="https://github.com/hella-web3/wiremock-testcontainers-nodejs/actions/workflows/main.yml/badge.svg" />
|
|
10
10
|
</a>
|
|
11
11
|
|
|
12
12
|
<a href="https://www.npmjs.com/package/@hellaweb3/wiremock-testcontainers-nodejs">
|
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
a [WireMock](https://wiremock.org/)
|
|
26
26
|
instance with [Testcontainers](https://node.testcontainers.org/). It provides a
|
|
27
27
|
fluent API to configure and start WireMock
|
|
28
|
-
containers
|
|
29
|
-
|
|
28
|
+
containers from recorded WireMock directories, individual named mappings
|
|
29
|
+
(local files, npm packages, module-relative files or inline JSON) and static
|
|
30
|
+
files – an API modelled on the official
|
|
31
|
+
[Java module](https://github.com/wiremock/wiremock-testcontainers-java).
|
|
30
32
|
|
|
31
33
|
## Installation
|
|
32
34
|
|
|
@@ -34,35 +36,79 @@ Wiremock sessions.
|
|
|
34
36
|
pnpm add -D @hellaweb3/wiremock-testcontainers-nodejs testcontainers
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
Requires Node.js >= 22.22 and a reachable Docker daemon (anything
|
|
40
|
+
[Testcontainers](https://node.testcontainers.org/supported-container-runtimes/)
|
|
41
|
+
supports).
|
|
42
|
+
|
|
43
|
+
**What you get:** a ~17 kB package – dual ESM (`dist/index.js`) and CJS
|
|
44
|
+
(`dist/index.cjs`) builds with matching declarations, `sideEffects: false`
|
|
45
|
+
for tree-shaking, `testcontainers` as the single runtime dependency (never
|
|
46
|
+
bundled), and the TypeScript source for go-to-definition.
|
|
47
|
+
|
|
48
|
+
**pnpm users:** pnpm >= 10 may report `Ignored build scripts: ssh2, protobufjs,
|
|
49
|
+
cpu-features`. These are transitive dependencies of `testcontainers`; the
|
|
50
|
+
scripts only build an *optional* native crypto binding for `ssh2`, which falls
|
|
51
|
+
back to pure JavaScript. It is safe to leave them ignored.
|
|
52
|
+
|
|
37
53
|
## Usage
|
|
38
54
|
|
|
39
55
|
### Basic Example
|
|
40
56
|
|
|
41
|
-
|
|
57
|
+
1. Record or hand-write your stubs in the standard WireMock layout, e.g.
|
|
58
|
+
`test/__mocks__/wiremock/mappings/*.json` and
|
|
59
|
+
`test/__mocks__/wiremock/__files/*.json`.
|
|
60
|
+
2. Start a `WiremockContainer` once per suite, point it at that directory and
|
|
61
|
+
use `rpcUrl` as the base URL of the API under test.
|
|
62
|
+
3. Stop the container when the suite finishes.
|
|
42
63
|
|
|
43
64
|
```typescript
|
|
44
|
-
import {
|
|
65
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest"; // or jest
|
|
66
|
+
import {
|
|
67
|
+
StartedWiremockContainer,
|
|
68
|
+
WiremockContainer,
|
|
69
|
+
} from "@hellaweb3/wiremock-testcontainers-nodejs";
|
|
45
70
|
|
|
46
|
-
describe("
|
|
47
|
-
let container;
|
|
71
|
+
describe("Cards API", () => {
|
|
72
|
+
let container: StartedWiremockContainer;
|
|
48
73
|
|
|
49
74
|
beforeAll(async () => {
|
|
50
|
-
container = await new WiremockContainer()
|
|
51
|
-
.withMappings("./test/__mocks__/wiremock")
|
|
52
|
-
.start();
|
|
53
|
-
},
|
|
75
|
+
container = await new WiremockContainer() // wiremock/wiremock:latest
|
|
76
|
+
.withMappings("./test/__mocks__/wiremock") // mappings/ + __files/
|
|
77
|
+
.start(); // waits for port 8080
|
|
78
|
+
}, 60_000); // image pull can be slow
|
|
54
79
|
|
|
55
80
|
afterAll(async () => {
|
|
56
81
|
await container.stop();
|
|
57
82
|
});
|
|
58
83
|
|
|
59
|
-
it("
|
|
60
|
-
const
|
|
61
|
-
|
|
84
|
+
it("serves the recorded GET /cards stub", async () => {
|
|
85
|
+
const response = await fetch(`${container.rpcUrl}/cards`);
|
|
86
|
+
expect(response.status).toBe(200);
|
|
87
|
+
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
expect(body.items).toHaveLength(6);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("exposes the WireMock admin API", async () => {
|
|
93
|
+
const response = await fetch(`${container.rpcUrl}/__admin/mappings`);
|
|
94
|
+
const { meta } = await response.json();
|
|
95
|
+
expect(meta.total).toBe(2);
|
|
62
96
|
});
|
|
63
97
|
});
|
|
64
98
|
```
|
|
65
99
|
|
|
100
|
+
`WiremockContainer` extends Testcontainers' `GenericContainer`, so every
|
|
101
|
+
builder method is available – for example to pin an image, name the container
|
|
102
|
+
or pass WireMock CLI flags:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const container = await new WiremockContainer("wiremock/wiremock:3.13.0")
|
|
106
|
+
.withName("wiremock-cards-api")
|
|
107
|
+
.withCommand(["--verbose", "--global-response-templating"])
|
|
108
|
+
.withMappings("./test/__mocks__/wiremock")
|
|
109
|
+
.start();
|
|
110
|
+
```
|
|
111
|
+
|
|
66
112
|
### With Mappings and Files
|
|
67
113
|
|
|
68
114
|
WireMock uses a specific directory structure for mappings and static files. This
|
|
@@ -88,21 +134,139 @@ const container = await new WiremockContainer()
|
|
|
88
134
|
.start();
|
|
89
135
|
```
|
|
90
136
|
|
|
137
|
+
### Individual mappings and files
|
|
138
|
+
|
|
139
|
+
`withMapping(name, source)` registers one stub at `mappings/<name>.json` and
|
|
140
|
+
`withFiles(...files)` registers response bodies in `__files/` – no recorded
|
|
141
|
+
directory required. A mapping file holds one WireMock stub, or several under
|
|
142
|
+
`"mappings"`:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{ "request": { "method": "GET", "url": "/health" }, "response": { "status": 200, "jsonBody": { "status": "UP" } } }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### Mapping sources
|
|
149
|
+
|
|
150
|
+
**Local file** – a path relative to the working directory, or absolute:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
new WiremockContainer().withMapping("health", "./test/fixtures/health.json"); // → mappings/health.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**`file:` URL** – e.g. relative to the current module:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
new WiremockContainer().withMapping("health", new URL("./fixtures/health.json", import.meta.url));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**`{ file }`** – a path or URL in object form:
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
new WiremockContainer().withMapping("health", { file: "./test/fixtures/health.json" });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**`{ package, file }`** – a file inside an installed npm package, e.g. shared
|
|
169
|
+
fixtures. Resolved like `require()` from the working directory; the file does
|
|
170
|
+
not need to be exported and may not leave the package:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
new WiremockContainer().withMapping("cards/list", { package: "@acme/api-fixtures", file: "wiremock/cards.json" }); // → mappings/cards/list.json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**`{ module, file }`** – a file relative to a module, like a Java classpath
|
|
177
|
+
resource:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
new WiremockContainer().withMapping("balance", { module: import.meta.url, file: "./__mocks__/balance.json" }); // ESM
|
|
181
|
+
new WiremockContainer().withMapping("balance", { module: __dirname, file: "./__mocks__/balance.json" }); // CJS (or __filename)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**`{ json }` (object)** – an inline stub, serialised as-is:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
new WiremockContainer().withMapping("ping", {
|
|
188
|
+
json: { request: { method: "GET", url: "/ping" }, response: { status: 200, body: "pong" } },
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**`{ json }` (string)** – inline JSON, copied verbatim:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
new WiremockContainer().withMapping("ping", { json: '{ "request": { "url": "/ping" }, "response": { "status": 200 } }' });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Several stubs in one mapping** – any source may hold `{ "mappings": [...] }`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
new WiremockContainer().withMapping("greetings", {
|
|
202
|
+
json: {
|
|
203
|
+
mappings: [
|
|
204
|
+
{ request: { url: "/hello" }, response: { status: 200, body: "Hello" } },
|
|
205
|
+
{ request: { url: "/bye" }, response: { status: 200, body: "Bye" } },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Static files
|
|
212
|
+
|
|
213
|
+
`withFiles()` takes the same file sources (not `{ json }`). A file keeps its
|
|
214
|
+
base name unless an object source sets `name`; mappings reference it as
|
|
215
|
+
`bodyFileName`:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
new WiremockContainer().withFiles(
|
|
219
|
+
"./test/fixtures/body-cards.json", // → __files/body-cards.json
|
|
220
|
+
{ file: "./test/fixtures/logo.png", name: "images/logo.png" }, // → __files/images/logo.png
|
|
221
|
+
{ package: "@acme/api-fixtures", file: "wiremock/__files/user.json" }, // → __files/user.json
|
|
222
|
+
);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Combining them
|
|
226
|
+
|
|
227
|
+
All three methods chain with each other and with every Testcontainers builder:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const container = await new WiremockContainer()
|
|
231
|
+
.withMappings("./test/__mocks__/wiremock")
|
|
232
|
+
.withMapping("health", "./test/fixtures/health.json")
|
|
233
|
+
.withMapping("cards/list", { package: "@acme/api-fixtures", file: "wiremock/cards.json" })
|
|
234
|
+
.withFiles({ file: "./test/fixtures/cards.json", name: "cards/body.json" })
|
|
235
|
+
.start();
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Registrations are validated immediately, before any image is pulled: sources
|
|
239
|
+
must exist, mappings must be WireMock stubs, and no registration may overwrite a
|
|
240
|
+
file of another or reuse a stub `id` (WireMock would refuse to start).
|
|
241
|
+
Registering the same file at the same path again is a no-op. Sources are only
|
|
242
|
+
read – mappings are copied byte-for-byte as of registration.
|
|
243
|
+
|
|
91
244
|
## API
|
|
92
245
|
|
|
93
246
|
### `WiremockContainer`
|
|
94
247
|
|
|
95
248
|
- `constructor(image?: string)`: Create a new container. Defaults to
|
|
96
249
|
`wiremock/wiremock`.
|
|
97
|
-
- `withMappings(directory: string)`:
|
|
98
|
-
specified local directory
|
|
250
|
+
- `withMappings(directory: string)`: Copies `mappings/` (required) and
|
|
251
|
+
`__files/` (optional) from the specified local directory into the container.
|
|
252
|
+
Throws immediately if `mappings/` does not exist, so a typo fails before any
|
|
253
|
+
image is pulled.
|
|
254
|
+
- `withMapping(name: string, source: WiremockMappingSource)`: Registers one stub
|
|
255
|
+
at `mappings/<name>.json` (`name` may contain `/`; `.json` is optional).
|
|
256
|
+
- `withFiles(...files: WiremockStaticFile[])`: Registers files at
|
|
257
|
+
`__files/<name>` – the base name unless an object source sets `name`. A call
|
|
258
|
+
registers all files or none.
|
|
99
259
|
- `start()`: Starts the container and returns a `StartedWiremockContainer`.
|
|
260
|
+
Runtime failures (no daemon, image pull denied, start-up timeout, name or
|
|
261
|
+
port conflicts) are propagated unchanged.
|
|
100
262
|
|
|
101
263
|
### `StartedWiremockContainer`
|
|
102
264
|
|
|
103
265
|
- `rpcUrl`: Returns the base URL of the running WireMock instance (e.g.,
|
|
104
266
|
`http://localhost:32768`).
|
|
105
267
|
- `stop()`: Stops the container (inherited from Testcontainers).
|
|
268
|
+
- `[Symbol.asyncDispose]`: `await using container = await new WiremockContainer().start();`
|
|
269
|
+
stops the container automatically when the scope exits (TypeScript 5.2+).
|
|
106
270
|
|
|
107
271
|
## Best Practices
|
|
108
272
|
|
|
@@ -111,11 +275,127 @@ const container = await new WiremockContainer()
|
|
|
111
275
|
2. **Mapping Organization**: Keep your mappings organized in a dedicated
|
|
112
276
|
directory structure that mirrors your API endpoints.
|
|
113
277
|
3. **Timeouts**: Docker container startup can sometimes exceed the default test
|
|
114
|
-
runner timeout (e.g
|
|
278
|
+
runner timeout (e.g. 5 s in Jest, 10 s in Vitest). Always set a higher timeout for
|
|
115
279
|
`beforeAll` hooks that start containers.
|
|
116
280
|
4. **Resource Management**: Always ensure `container.stop()` is called in an
|
|
117
281
|
`afterAll` or `afterEach` block to prevent orphaned containers.
|
|
118
282
|
|
|
283
|
+
## Development
|
|
284
|
+
|
|
285
|
+
Tooling: [Vitest](https://vitest.dev/) (tests + v8 coverage),
|
|
286
|
+
[oxlint](https://oxc.rs/docs/guide/usage/linter) (lint), `tsc` (typecheck)
|
|
287
|
+
and [tsdown](https://tsdown.dev/) (build – rolldown + oxc, emitting
|
|
288
|
+
declarations natively via `isolatedDeclarations` in `tsconfig.build.json`).
|
|
289
|
+
Node version: see `.nvmrc`.
|
|
290
|
+
|
|
291
|
+
### Scripts
|
|
292
|
+
|
|
293
|
+
| Command | What it does | Needs Docker |
|
|
294
|
+
|----------------------|--------------------------------------------------------------------------|--------------|
|
|
295
|
+
| `pnpm build` | ESM + CJS + declarations into `dist/`, then `publint` + `attw` checks | No |
|
|
296
|
+
| `pnpm typecheck` | `tsc --noEmit` over `src/` and `test/` | No |
|
|
297
|
+
| `pnpm lint` | `oxlint --deny-warnings` with the rules in `.oxlintrc.json` | No |
|
|
298
|
+
| `pnpm test:unit` | `test/unit/**` – container wrapper logic, registration API and fixture integrity checks | No |
|
|
299
|
+
| `pnpm test:e2e` | `test/index.test.ts` – a real WireMock container serving the fixtures | **Yes** |
|
|
300
|
+
| `pnpm test` | Both suites, no coverage (fast local loop) | **Yes** |
|
|
301
|
+
| `pnpm test:coverage` | Both suites with coverage of `src/`, thresholds enforced | **Yes** |
|
|
302
|
+
| `pnpm check` | `build` + `typecheck` + `lint` + `test:coverage` – the full CI gate | **Yes** |
|
|
303
|
+
|
|
304
|
+
Append `--coverage` to any test script to collect coverage for that run, e.g.
|
|
305
|
+
`pnpm test:unit --coverage`. Coverage thresholds are **95 %** for statements,
|
|
306
|
+
branches, functions and lines; the unit suite alone currently reaches 100 %
|
|
307
|
+
and completes in well under two seconds of test time.
|
|
308
|
+
|
|
309
|
+
### Running the E2E suite in containerised environments
|
|
310
|
+
|
|
311
|
+
The E2E suite starts a real container through Testcontainers, so the test
|
|
312
|
+
process needs a container runtime it can talk to. Without one, `pnpm test:e2e`,
|
|
313
|
+
`pnpm test` and `pnpm check` fail in the E2E suite with:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
Could not find a working container runtime strategy
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
In that situation the Docker-free loop still covers build, types, lint and
|
|
320
|
+
100 % of `src/`:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit --coverage
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Testcontainers resolves a runtime in this order; pick whichever your
|
|
327
|
+
environment can provide:
|
|
328
|
+
|
|
329
|
+
| Environment | How to enable E2E |
|
|
330
|
+
|-----------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
331
|
+
| GitHub Codespaces / VS Code Dev Containers | Use the provided `.devcontainer/devcontainer.json` – it adds the `docker-in-docker` feature, so a daemon runs inside the dev container. |
|
|
332
|
+
| Dev container with the host socket mounted | Add the `docker-outside-of-docker` feature (or mount `/var/run/docker.sock`). Set `TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal` if port lookups fail. |
|
|
333
|
+
| Remote or rootless daemon | `DOCKER_HOST=tcp://host:2375` (or `unix:///path/to/docker.sock`). |
|
|
334
|
+
| Podman | `DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock` and `TESTCONTAINERS_RYUK_DISABLED=true` (or run Ryuk privileged). |
|
|
335
|
+
| No runtime can be installed (locked-down sandbox) | [Testcontainers Cloud](https://testcontainers.com/cloud/): run the agent and export `TC_CLOUD_TOKEN`; containers start remotely, tests run locally. |
|
|
336
|
+
|
|
337
|
+
Sandboxes that have **no runtime binary and no socket at all** (as is the case
|
|
338
|
+
for some hosted dev VMs) cannot run the E2E suite; the suite states this in its
|
|
339
|
+
header comment and `pnpm test:unit` is the supported loop there.
|
|
340
|
+
|
|
341
|
+
CI (`.github/workflows/main.yml`) runs build, typecheck, lint, unit tests
|
|
342
|
+
(with coverage) and E2E tests as separate steps on `ubuntu-latest`, which
|
|
343
|
+
ships with Docker, for every active Node LTS line.
|
|
344
|
+
|
|
345
|
+
### Supply-chain controls
|
|
346
|
+
|
|
347
|
+
- `pnpm install --frozen-lockfile` everywhere; the lockfile is the source of truth.
|
|
348
|
+
- `pnpm-workspace.yaml` allow-lists the only packages permitted to run install
|
|
349
|
+
scripts (`ssh2`, `cpu-features`, `protobufjs` – all transitive via
|
|
350
|
+
`testcontainers`) and sets `minimumReleaseAge: 1440` so a version must be at
|
|
351
|
+
least 24 h old before it can be resolved.
|
|
352
|
+
- CI runs `pnpm audit --prod --audit-level=high` on every push and PR;
|
|
353
|
+
Dependabot (`.github/dependabot.yml`) proposes weekly updates with a 3-day
|
|
354
|
+
cooldown, grouping dev tooling and leaving `testcontainers` bumps individual.
|
|
355
|
+
- `pnpm build` runs [publint](https://publint.dev/) and
|
|
356
|
+
[Are the types wrong?](https://arethetypeswrong.github.io/) against the
|
|
357
|
+
emitted package; `size.yml` fails a PR if the tarball exceeds 25 kB.
|
|
358
|
+
- Releases publish with npm provenance (`--provenance`, OIDC) after a
|
|
359
|
+
Docker-free verify step.
|
|
360
|
+
|
|
361
|
+
### Fixtures and schema-based assertions
|
|
362
|
+
|
|
363
|
+
The recorded WireMock session lives in `test/__mocks__/wiremock`:
|
|
364
|
+
|
|
365
|
+
- `mappings/*.json` – one stub per endpoint (`request.url`, `response.status`,
|
|
366
|
+
`response.headers`, `response.bodyFileName`).
|
|
367
|
+
- `__files/*.json` – the response bodies referenced by the mappings.
|
|
368
|
+
|
|
369
|
+
`test/helpers/fixtures.ts` loads these from disk and declares the TypeScript
|
|
370
|
+
shapes of every JSON payload. `test/helpers/assertions.ts` declares a
|
|
371
|
+
`Schema<T>` per payload – stub mappings, the admin API response and each
|
|
372
|
+
recorded body. The compiler requires a schema to cover **every** field of
|
|
373
|
+
`T`; `expectObject` then asserts each field's shape, its recorded value and
|
|
374
|
+
the exact key set (`partialOf` relaxes the key set for third-party shapes).
|
|
375
|
+
|
|
376
|
+
The E2E suite runs a mapping-driven contract test for every file in
|
|
377
|
+
`mappings/` (status, headers and full JSON body), so adding a new recorded
|
|
378
|
+
mapping automatically puts it under test. Endpoint-specific suites add
|
|
379
|
+
schema assertions, which the unit suite also runs against the fixture files
|
|
380
|
+
themselves to catch drift early.
|
|
381
|
+
|
|
382
|
+
Negative cases are hand-written stubs alongside the recorded ones:
|
|
383
|
+
|
|
384
|
+
- `mapping-cards-bad-request-400.json` – `GET /cards?page=0` → 400 with a
|
|
385
|
+
validation error envelope.
|
|
386
|
+
- `mapping-account-balance-server-error-500.json` – `GET /account/balance`
|
|
387
|
+
→ 500 **only** when the request carries `X-Mock-Scenario: server-error`.
|
|
388
|
+
It sets `"priority": 1` so it wins over the recorded 200 stub for the same
|
|
389
|
+
URL; without the header the 200 stub is still served. This is the standard
|
|
390
|
+
WireMock pattern for simulating outages on demand.
|
|
391
|
+
|
|
392
|
+
Unmatched requests (unknown path, wrong method) get WireMock's default 404.
|
|
393
|
+
|
|
394
|
+
`test/__mocks__/standalone` holds loose fixtures for `withMapping()` and
|
|
395
|
+
`withFiles()`. Their unit tests need no Docker: `test/helpers/container.ts`
|
|
396
|
+
reads the tar archive Testcontainers would upload and asserts exactly which
|
|
397
|
+
files land where.
|
|
398
|
+
|
|
119
399
|
## Maintainers
|
|
120
400
|
|
|
121
401
|
Maintained by [Hella Labs](https://hellaweb3.com/).
|
package/dist/index.cjs
CHANGED
|
@@ -1,76 +1,194 @@
|
|
|
1
|
-
Object.defineProperty(exports,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* ```
|
|
12
|
-
*/ class WiremockContainer extends testcontainers.GenericContainer {
|
|
13
|
-
/**
|
|
14
|
-
* Creates a new WiremockContainer.
|
|
15
|
-
* @example
|
|
16
|
-
* ```typescript
|
|
17
|
-
* const container = await new WiremockContainer().start();
|
|
18
|
-
* ```
|
|
19
|
-
* @param image The docker image to use. Defaults to "wiremock/wiremock".
|
|
20
|
-
*/ constructor(image = "wiremock/wiremock"){
|
|
21
|
-
super(image);
|
|
22
|
-
this.withExposedPorts(8080);
|
|
23
|
-
this.withWaitStrategy(testcontainers.Wait.forListeningPorts());
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Adds mappings to the Wiremock container.
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* const container = await new WiremockContainer()
|
|
30
|
-
* .withMappings("./test/__mocks__/wiremock")
|
|
31
|
-
* .start();
|
|
32
|
-
* ```
|
|
33
|
-
* @param directory The directory containing the mappings and __files.
|
|
34
|
-
*/ withMappings(directory) {
|
|
35
|
-
this.withCopyDirectoriesToContainer([
|
|
36
|
-
{
|
|
37
|
-
source: `${directory}/__files`,
|
|
38
|
-
target: "/home/wiremock/__files"
|
|
39
|
-
}
|
|
40
|
-
]);
|
|
41
|
-
this.withCopyDirectoriesToContainer([
|
|
42
|
-
{
|
|
43
|
-
source: `${directory}/mappings`,
|
|
44
|
-
target: "/home/wiremock/mappings"
|
|
45
|
-
}
|
|
46
|
-
]);
|
|
47
|
-
return this;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Starts the container and returns a {@link StartedWiremockContainer}.
|
|
51
|
-
* @returns A promise that resolves to the started container.
|
|
52
|
-
*/ async start() {
|
|
53
|
-
const startedContainer = await super.start();
|
|
54
|
-
return new StartedWiremockContainer(startedContainer, `http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`);
|
|
55
|
-
}
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
let testcontainers = require("testcontainers");
|
|
5
|
+
let node_module = require("node:module");
|
|
6
|
+
let node_url = require("node:url");
|
|
7
|
+
// src/sources.ts
|
|
8
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;
|
|
9
|
+
function fail(message) {
|
|
10
|
+
throw new Error(message);
|
|
56
11
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* @param url The URL of the started container.
|
|
64
|
-
*/ constructor(startedTestContainer, url){
|
|
65
|
-
super(startedTestContainer);
|
|
66
|
-
this._rpcUrl = url;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Gets the URL of the Wiremock node.
|
|
70
|
-
*/ get rpcUrl() {
|
|
71
|
-
return this._rpcUrl;
|
|
72
|
-
}
|
|
12
|
+
function within(context, fn) {
|
|
13
|
+
try {
|
|
14
|
+
return fn();
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error(`${context}: ${error.message}`, { cause: error });
|
|
17
|
+
}
|
|
73
18
|
}
|
|
74
|
-
|
|
19
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
const show = (value) => value instanceof URL ? value.href : JSON.stringify(value) ?? String(value);
|
|
21
|
+
const text = (value, field) => typeof value === "string" && value.trim() !== "" ? value : fail(`"${field}" must be a non-empty string, got ${show(value)}`);
|
|
22
|
+
const toPath = (value, field) => {
|
|
23
|
+
if (!(value instanceof URL) && !text(value, field).startsWith("file:")) return (0, node_path.resolve)(value);
|
|
24
|
+
try {
|
|
25
|
+
return (0, node_url.fileURLToPath)(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return fail(`"${field}" must be a path or a file: URL, got ${show(value)}`);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const existingFile = (path) => {
|
|
31
|
+
const stats = (0, node_fs.statSync)(path, { throwIfNoEntry: false });
|
|
32
|
+
return stats?.isFile() ? path : fail(`"${path}" ${stats ? "is not a file" : "does not exist"}`);
|
|
33
|
+
};
|
|
34
|
+
const packageRoot = (name) => {
|
|
35
|
+
if (!PACKAGE_NAME.test(name)) fail(`"${name}" is not a valid npm package name`);
|
|
36
|
+
return ((0, node_module.createRequire)((0, node_path.join)(process.cwd(), "noop.js")).resolve.paths(name) ?? []).map((dir) => (0, node_path.join)(dir, name)).find((dir) => (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) ?? fail(`package "${name}" is not installed (searched from "${process.cwd()}")`);
|
|
37
|
+
};
|
|
38
|
+
function resolveFile(source) {
|
|
39
|
+
if (typeof source === "string" || source instanceof URL) return existingFile(toPath(source, "source"));
|
|
40
|
+
if (!isRecord(source)) return fail(`unsupported source ${show(source)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);
|
|
41
|
+
const { file, package: pkg, module } = source;
|
|
42
|
+
if (pkg !== void 0 && module !== void 0) fail("a source may set \"package\" or \"module\", not both");
|
|
43
|
+
if (pkg !== void 0) {
|
|
44
|
+
const root = packageRoot(text(pkg, "package"));
|
|
45
|
+
const path = (0, node_path.resolve)(root, text(file, "file"));
|
|
46
|
+
return (0, node_path.relative)(root, path).split(node_path.sep)[0] === ".." ? fail(`"${String(file)}" must point to a file inside package "${pkg}"`) : existingFile(path);
|
|
47
|
+
}
|
|
48
|
+
if (module === void 0) return existingFile(toPath(file, "file"));
|
|
49
|
+
const anchor = toPath(module, "module");
|
|
50
|
+
return existingFile((0, node_path.resolve)((0, node_fs.statSync)(anchor, { throwIfNoEntry: false })?.isDirectory() ? anchor : (0, node_path.dirname)(anchor), text(file, "file")));
|
|
51
|
+
}
|
|
52
|
+
const containerName = (name) => {
|
|
53
|
+
const value = text(name, "name");
|
|
54
|
+
return value.split("/").some((part) => [
|
|
55
|
+
"",
|
|
56
|
+
".",
|
|
57
|
+
".."
|
|
58
|
+
].includes(part) || /[\\\0]/u.test(part)) ? fail(`name "${value}" must be a relative path using "/" separators, without empty, "." or ".." segments`) : value;
|
|
59
|
+
};
|
|
60
|
+
const inlineJson = (json) => typeof json === "string" ? json : JSON.stringify(json, null, 2) ?? fail("\"json\" must be a JSON string or an object");
|
|
61
|
+
const parseJson = (json) => {
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(json);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return fail(`invalid JSON (${error.message})`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function stubIds(json) {
|
|
69
|
+
const value = parseJson(json);
|
|
70
|
+
const many = isRecord(value) && "mappings" in value;
|
|
71
|
+
const stubs = many ? value.mappings : [value];
|
|
72
|
+
if (!Array.isArray(stubs) || stubs.length === 0) fail("\"mappings\" must be a non-empty array of stub mappings");
|
|
73
|
+
return stubs.flatMap((stub, index) => {
|
|
74
|
+
if (!isRecord(stub) || !isRecord(stub.request) || !isRecord(stub.response)) fail(`${many ? `mappings[${index}]` : "the mapping"} is not a WireMock stub mapping ("request" and "response" objects are required)`);
|
|
75
|
+
const id = stub.id ?? stub.uuid;
|
|
76
|
+
return typeof id === "string" ? [id] : [];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const stubIdsOf = (path) => {
|
|
80
|
+
try {
|
|
81
|
+
return path.endsWith(".json") ? stubIds((0, node_fs.readFileSync)(path, "utf8")) : [];
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const listFiles = (dir) => (0, node_fs.readdirSync)(dir, {
|
|
87
|
+
recursive: true,
|
|
88
|
+
withFileTypes: true
|
|
89
|
+
}).filter((entry) => !entry.isDirectory()).map((entry) => (0, node_path.relative)(dir, (0, node_path.join)(entry.parentPath, entry.name)).split(node_path.sep).join("/"));
|
|
90
|
+
// src/index.ts
|
|
91
|
+
const MAPPINGS = "/home/wiremock/mappings";
|
|
92
|
+
const FILES = "/home/wiremock/__files";
|
|
93
|
+
const INLINE = "inline JSON";
|
|
94
|
+
var WiremockContainer = class extends testcontainers.GenericContainer {
|
|
95
|
+
#origins = /* @__PURE__ */ new Map();
|
|
96
|
+
#stubIds = /* @__PURE__ */ new Map();
|
|
97
|
+
constructor(image = "wiremock/wiremock") {
|
|
98
|
+
super(image);
|
|
99
|
+
this.withExposedPorts(8080);
|
|
100
|
+
this.withWaitStrategy(testcontainers.Wait.forListeningPorts());
|
|
101
|
+
}
|
|
102
|
+
withMappings(directory) {
|
|
103
|
+
within("WiremockContainer.withMappings", () => {
|
|
104
|
+
const mappings = (0, node_path.join)(directory, "mappings");
|
|
105
|
+
if (!(0, node_fs.existsSync)(mappings)) fail(`"${mappings}" does not exist`);
|
|
106
|
+
if (!(0, node_fs.statSync)(mappings).isDirectory()) fail(`"${mappings}" is not a directory`);
|
|
107
|
+
const files = (0, node_path.join)(directory, "__files");
|
|
108
|
+
const dirs = [{
|
|
109
|
+
source: mappings,
|
|
110
|
+
target: MAPPINGS
|
|
111
|
+
}, ...(0, node_fs.existsSync)(files) ? [{
|
|
112
|
+
source: files,
|
|
113
|
+
target: FILES
|
|
114
|
+
}] : []];
|
|
115
|
+
this.#claim(dirs.flatMap(({ source, target }) => listFiles(source).map((name) => ({
|
|
116
|
+
target: node_path.posix.join(target, name),
|
|
117
|
+
origin: (0, node_path.resolve)(source, name),
|
|
118
|
+
ids: target === MAPPINGS ? stubIdsOf((0, node_path.join)(source, name)) : []
|
|
119
|
+
}))));
|
|
120
|
+
this.withCopyDirectoriesToContainer(dirs);
|
|
121
|
+
});
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
withMapping(name, source) {
|
|
125
|
+
within(`WiremockContainer.withMapping("${name}")`, () => {
|
|
126
|
+
const file = containerName(name);
|
|
127
|
+
const target = node_path.posix.join(MAPPINGS, file.endsWith(".json") ? file : `${file}.json`);
|
|
128
|
+
const inline = isRecord(source) && "json" in source;
|
|
129
|
+
const origin = inline ? INLINE : resolveFile(source);
|
|
130
|
+
const content = inline ? inlineJson(source.json) : (0, node_fs.readFileSync)(origin);
|
|
131
|
+
const ids = within(origin, () => stubIds(content.toString()));
|
|
132
|
+
this.withCopyContentToContainer(this.#claim([{
|
|
133
|
+
target,
|
|
134
|
+
origin,
|
|
135
|
+
ids
|
|
136
|
+
}]).map(() => ({
|
|
137
|
+
content,
|
|
138
|
+
target
|
|
139
|
+
})));
|
|
140
|
+
});
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
withFiles(...files) {
|
|
144
|
+
within("WiremockContainer.withFiles", () => {
|
|
145
|
+
const claims = files.map((file) => {
|
|
146
|
+
const origin = resolveFile(file);
|
|
147
|
+
const name = isRecord(file) && file.name !== void 0 ? containerName(file.name) : (0, node_path.basename)(origin);
|
|
148
|
+
return {
|
|
149
|
+
target: node_path.posix.join(FILES, name),
|
|
150
|
+
origin
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
this.withCopyFilesToContainer(this.#claim(claims).map(({ origin, target }) => ({
|
|
154
|
+
source: origin,
|
|
155
|
+
target
|
|
156
|
+
})));
|
|
157
|
+
});
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
async start() {
|
|
161
|
+
const startedContainer = await super.start();
|
|
162
|
+
return new StartedWiremockContainer(startedContainer, `http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`);
|
|
163
|
+
}
|
|
164
|
+
#claim(claims) {
|
|
165
|
+
const origins = new Map(this.#origins);
|
|
166
|
+
const owners = new Map(this.#stubIds);
|
|
167
|
+
const fresh = claims.filter(({ target, origin, ids = [] }) => {
|
|
168
|
+
const existing = origins.get(target);
|
|
169
|
+
if (existing === origin && origin !== INLINE) return false;
|
|
170
|
+
if (existing) fail(`"${target}" is already registered from ${existing}; registering ${origin} would overwrite it`);
|
|
171
|
+
for (const id of ids) {
|
|
172
|
+
const owner = owners.get(id);
|
|
173
|
+
if (owner) fail(`stub id "${id}" in ${origin} is already used by "${owner}"; WireMock refuses to start with duplicate stub ids`);
|
|
174
|
+
owners.set(id, target);
|
|
175
|
+
}
|
|
176
|
+
origins.set(target, origin);
|
|
177
|
+
return true;
|
|
178
|
+
});
|
|
179
|
+
[this.#origins, this.#stubIds] = [origins, owners];
|
|
180
|
+
return fresh;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var StartedWiremockContainer = class extends testcontainers.AbstractStartedContainer {
|
|
184
|
+
#rpcUrl;
|
|
185
|
+
constructor(startedTestContainer, url) {
|
|
186
|
+
super(startedTestContainer);
|
|
187
|
+
this.#rpcUrl = url;
|
|
188
|
+
}
|
|
189
|
+
get rpcUrl() {
|
|
190
|
+
return this.#rpcUrl;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
75
193
|
exports.StartedWiremockContainer = StartedWiremockContainer;
|
|
76
194
|
exports.WiremockContainer = WiremockContainer;
|