@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/package.json
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
"name": "@hellaweb3/wiremock-testcontainers-nodejs",
|
|
3
3
|
"description": "A Node.js library for using WireMock with Testcontainers, providing a fluent API for easy setup and integration testing.",
|
|
4
4
|
"homepage": "https://github.com/hella-web3/wiremock-testcontainers-nodejs#readme",
|
|
5
|
-
"repository":
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/hella-web3/wiremock-testcontainers-nodejs.git"
|
|
8
|
+
},
|
|
6
9
|
"bugs": "https://github.com/hella-web3/wiremock-testcontainers-nodejs/issues",
|
|
7
10
|
"keywords": [
|
|
8
11
|
"testcontainers",
|
|
@@ -11,7 +14,7 @@
|
|
|
11
14
|
"mock",
|
|
12
15
|
"api"
|
|
13
16
|
],
|
|
14
|
-
"version": "0.
|
|
17
|
+
"version": "0.2.0",
|
|
15
18
|
"publishConfig": {
|
|
16
19
|
"access": "public"
|
|
17
20
|
},
|
|
@@ -19,11 +22,13 @@
|
|
|
19
22
|
"author": "Chris Anatalio",
|
|
20
23
|
"files": [
|
|
21
24
|
"dist",
|
|
25
|
+
"!dist/*.js.map",
|
|
22
26
|
"src",
|
|
23
27
|
"README.md",
|
|
24
28
|
"LICENSE"
|
|
25
29
|
],
|
|
26
30
|
"type": "module",
|
|
31
|
+
"sideEffects": false,
|
|
27
32
|
"main": "./dist/index.cjs",
|
|
28
33
|
"module": "./dist/index.js",
|
|
29
34
|
"types": "./dist/index.d.ts",
|
|
@@ -41,50 +46,30 @@
|
|
|
41
46
|
"./package.json": "./package.json"
|
|
42
47
|
},
|
|
43
48
|
"dependencies": {
|
|
44
|
-
"testcontainers": "^
|
|
49
|
+
"testcontainers": "^12.2.0"
|
|
45
50
|
},
|
|
46
51
|
"devDependencies": {
|
|
47
|
-
"@
|
|
48
|
-
"@types/node": "^
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"typescript": "^
|
|
54
|
-
"vitest": "^
|
|
55
|
-
},
|
|
56
|
-
"jest": {
|
|
57
|
-
"collectCoverageFrom": [
|
|
58
|
-
"**/*.(t|j)s"
|
|
59
|
-
],
|
|
60
|
-
"coverageDirectory": "../coverage",
|
|
61
|
-
"coveragePathIgnorePatterns": [
|
|
62
|
-
"app.module.ts",
|
|
63
|
-
"main.ts",
|
|
64
|
-
".*\\.module\\.ts",
|
|
65
|
-
".*\\.dto\\.ts"
|
|
66
|
-
],
|
|
67
|
-
"moduleFileExtensions": [
|
|
68
|
-
"js",
|
|
69
|
-
"json",
|
|
70
|
-
"ts"
|
|
71
|
-
],
|
|
72
|
-
"rootDir": ".",
|
|
73
|
-
"testEnvironment": "node",
|
|
74
|
-
"testRegex": "test/.*\\.test\\.ts$",
|
|
75
|
-
"transform": {
|
|
76
|
-
"^.+\\.(t|j)s$": "ts-jest"
|
|
77
|
-
}
|
|
52
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
53
|
+
"@types/node": "^24.19.2",
|
|
54
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
55
|
+
"oxlint": "^1.87.0",
|
|
56
|
+
"publint": "^0.3.24",
|
|
57
|
+
"tsdown": "^0.23.0",
|
|
58
|
+
"typescript": "^6.0.3",
|
|
59
|
+
"vitest": "^5.1.0"
|
|
78
60
|
},
|
|
79
61
|
"engines": {
|
|
80
|
-
"node": ">=22"
|
|
62
|
+
"node": ">=22.22"
|
|
81
63
|
},
|
|
82
64
|
"scripts": {
|
|
83
|
-
"dev": "
|
|
84
|
-
"build": "
|
|
85
|
-
"test": "
|
|
86
|
-
"
|
|
65
|
+
"dev": "tsdown --watch",
|
|
66
|
+
"build": "tsdown",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:unit": "vitest run test/unit",
|
|
69
|
+
"test:e2e": "vitest run test/index.test.ts",
|
|
70
|
+
"test:coverage": "vitest run --coverage",
|
|
71
|
+
"lint": "oxlint --deny-warnings",
|
|
87
72
|
"typecheck": "tsc --noEmit",
|
|
88
|
-
"check": "pnpm build && pnpm typecheck && pnpm lint && pnpm test"
|
|
73
|
+
"check": "pnpm build && pnpm typecheck && pnpm lint && pnpm test:coverage"
|
|
89
74
|
}
|
|
90
75
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join, posix, resolve } from "node:path";
|
|
1
3
|
import {
|
|
2
4
|
AbstractStartedContainer,
|
|
3
5
|
GenericContainer,
|
|
4
|
-
StartedTestContainer,
|
|
6
|
+
type StartedTestContainer,
|
|
5
7
|
Wait,
|
|
6
8
|
} from "testcontainers";
|
|
9
|
+
import {
|
|
10
|
+
containerName,
|
|
11
|
+
fail,
|
|
12
|
+
inlineJson,
|
|
13
|
+
isRecord,
|
|
14
|
+
listFiles,
|
|
15
|
+
resolveFile,
|
|
16
|
+
stubIds,
|
|
17
|
+
stubIdsOf,
|
|
18
|
+
within,
|
|
19
|
+
type WiremockMappingSource,
|
|
20
|
+
type WiremockStaticFile,
|
|
21
|
+
} from "./sources";
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
WiremockFileSource,
|
|
25
|
+
WiremockMappingSource,
|
|
26
|
+
WiremockPath,
|
|
27
|
+
WiremockStaticFile,
|
|
28
|
+
WiremockStubMapping,
|
|
29
|
+
} from "./sources";
|
|
30
|
+
|
|
31
|
+
const MAPPINGS = "/home/wiremock/mappings";
|
|
32
|
+
const FILES = "/home/wiremock/__files";
|
|
33
|
+
const INLINE = "inline JSON";
|
|
34
|
+
|
|
35
|
+
/** A file a registration places in the container. */
|
|
36
|
+
interface Claim {
|
|
37
|
+
readonly target: string;
|
|
38
|
+
/** The absolute source path, or {@link INLINE}. */
|
|
39
|
+
readonly origin: string;
|
|
40
|
+
readonly ids?: readonly string[];
|
|
41
|
+
}
|
|
7
42
|
|
|
8
43
|
/**
|
|
9
|
-
* A Testcontainer for Wiremock.
|
|
44
|
+
* A Testcontainer for Wiremock. Stubs are registered with {@link withMappings} (a directory),
|
|
45
|
+
* {@link withMapping} (one named stub) and {@link withFiles} (response bodies), in any
|
|
46
|
+
* combination. Registrations are validated immediately and may not overwrite one another.
|
|
10
47
|
*
|
|
11
48
|
* @example
|
|
12
49
|
* ```typescript
|
|
@@ -14,6 +51,11 @@ import {
|
|
|
14
51
|
* ```
|
|
15
52
|
*/
|
|
16
53
|
export class WiremockContainer extends GenericContainer {
|
|
54
|
+
/** Container path → origin of every registered file. */
|
|
55
|
+
#origins = new Map<string, string>();
|
|
56
|
+
/** Stub id → container path declaring it: WireMock refuses to start with duplicates. */
|
|
57
|
+
#stubIds = new Map<string, string>();
|
|
58
|
+
|
|
17
59
|
/**
|
|
18
60
|
* Creates a new WiremockContainer.
|
|
19
61
|
* @example
|
|
@@ -36,18 +78,74 @@ export class WiremockContainer extends GenericContainer {
|
|
|
36
78
|
* .withMappings("./test/__mocks__/wiremock")
|
|
37
79
|
* .start();
|
|
38
80
|
* ```
|
|
39
|
-
* @param directory The directory containing
|
|
81
|
+
* @param directory The directory containing `mappings/` and, optionally, `__files/`.
|
|
82
|
+
* @throws If `mappings/` is not a directory in `directory`, or a file would overwrite an earlier registration.
|
|
40
83
|
*/
|
|
41
|
-
public withMappings(directory: string) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
public withMappings(directory: string): this {
|
|
85
|
+
within("WiremockContainer.withMappings", () => {
|
|
86
|
+
const mappings = join(directory, "mappings");
|
|
87
|
+
if (!existsSync(mappings)) fail(`"${mappings}" does not exist`);
|
|
88
|
+
if (!statSync(mappings).isDirectory()) fail(`"${mappings}" is not a directory`);
|
|
89
|
+
// `__files/` is optional: mappings may define their response bodies inline.
|
|
90
|
+
const files = join(directory, "__files");
|
|
91
|
+
const dirs = [{ source: mappings, target: MAPPINGS }, ...(existsSync(files) ? [{ source: files, target: FILES }] : [])];
|
|
92
|
+
this.#claim(
|
|
93
|
+
dirs.flatMap(({ source, target }) =>
|
|
94
|
+
listFiles(source).map((name) => ({
|
|
95
|
+
target: posix.join(target, name),
|
|
96
|
+
origin: resolve(source, name),
|
|
97
|
+
ids: target === MAPPINGS ? stubIdsOf(join(source, name)) : [],
|
|
98
|
+
})),
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
this.withCopyDirectoriesToContainer(dirs);
|
|
102
|
+
});
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
50
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Registers one stub at `mappings/<name>.json`, validated and copied byte-for-byte.
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* new WiremockContainer()
|
|
111
|
+
* .withMapping("cards", "./test/fixtures/cards.json")
|
|
112
|
+
* .withMapping("user", { package: "@acme/fixtures", file: "wiremock/user.json" })
|
|
113
|
+
* .withMapping("balance", { module: import.meta.url, file: "./balance.json" })
|
|
114
|
+
* .withMapping("ping", { json: { request: { url: "/ping" }, response: { status: 200 } } });
|
|
115
|
+
* ```
|
|
116
|
+
* @param name File name below `mappings/`, may contain `/`; `.json` is appended unless present.
|
|
117
|
+
* @throws If the name or source is invalid, the source is not a WireMock mapping, or it would overwrite an earlier registration.
|
|
118
|
+
*/
|
|
119
|
+
public withMapping(name: string, source: WiremockMappingSource): this {
|
|
120
|
+
within(`WiremockContainer.withMapping("${name}")`, () => {
|
|
121
|
+
const file = containerName(name);
|
|
122
|
+
const target = posix.join(MAPPINGS, file.endsWith(".json") ? file : `${file}.json`);
|
|
123
|
+
const inline = isRecord(source) && "json" in source;
|
|
124
|
+
const origin = inline ? INLINE : resolveFile(source);
|
|
125
|
+
const content = inline ? inlineJson(source.json) : readFileSync(origin);
|
|
126
|
+
const ids = within(origin, () => stubIds(content.toString()));
|
|
127
|
+
this.withCopyContentToContainer(this.#claim([{ target, origin, ids }]).map(() => ({ content, target })));
|
|
128
|
+
});
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Registers response bodies at `__files/<name>` (default: the base name), copied as-is.
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* new WiremockContainer().withFiles("./test/fixtures/body.json", { file: "./logo.png", name: "img/logo.png" });
|
|
137
|
+
* ```
|
|
138
|
+
* @throws If a source or name is invalid, or a file would overwrite an earlier registration; nothing is registered then.
|
|
139
|
+
*/
|
|
140
|
+
public withFiles(...files: WiremockStaticFile[]): this {
|
|
141
|
+
within("WiremockContainer.withFiles", () => {
|
|
142
|
+
const claims = files.map((file) => {
|
|
143
|
+
const origin = resolveFile(file);
|
|
144
|
+
const name = isRecord(file) && file.name !== undefined ? containerName(file.name) : basename(origin);
|
|
145
|
+
return { target: posix.join(FILES, name), origin };
|
|
146
|
+
});
|
|
147
|
+
this.withCopyFilesToContainer(this.#claim(claims).map(({ origin, target }) => ({ source: origin, target })));
|
|
148
|
+
});
|
|
51
149
|
return this;
|
|
52
150
|
}
|
|
53
151
|
|
|
@@ -62,13 +160,36 @@ export class WiremockContainer extends GenericContainer {
|
|
|
62
160
|
`http://${startedContainer.getHost()}:${startedContainer.getMappedPort(8080)}`,
|
|
63
161
|
);
|
|
64
162
|
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Records `claims` – all or nothing – unless one would overwrite a file from another origin or
|
|
166
|
+
* reuse a stub id. Returns the claims not already registered from the same file.
|
|
167
|
+
*/
|
|
168
|
+
#claim<T extends Claim>(claims: readonly T[]): T[] {
|
|
169
|
+
const origins = new Map(this.#origins);
|
|
170
|
+
const owners = new Map(this.#stubIds);
|
|
171
|
+
const fresh = claims.filter(({ target, origin, ids = [] }) => {
|
|
172
|
+
const existing = origins.get(target);
|
|
173
|
+
if (existing === origin && origin !== INLINE) return false;
|
|
174
|
+
if (existing) fail(`"${target}" is already registered from ${existing}; registering ${origin} would overwrite it`);
|
|
175
|
+
for (const id of ids) {
|
|
176
|
+
const owner = owners.get(id);
|
|
177
|
+
if (owner) fail(`stub id "${id}" in ${origin} is already used by "${owner}"; WireMock refuses to start with duplicate stub ids`);
|
|
178
|
+
owners.set(id, target);
|
|
179
|
+
}
|
|
180
|
+
origins.set(target, origin);
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
[this.#origins, this.#stubIds] = [origins, owners];
|
|
184
|
+
return fresh;
|
|
185
|
+
}
|
|
65
186
|
}
|
|
66
187
|
|
|
67
188
|
/**
|
|
68
189
|
* A started Wiremock container with helper methods for interacting with the node.
|
|
69
190
|
*/
|
|
70
191
|
export class StartedWiremockContainer extends AbstractStartedContainer {
|
|
71
|
-
|
|
192
|
+
readonly #rpcUrl: string;
|
|
72
193
|
|
|
73
194
|
/**
|
|
74
195
|
* Creates a new StartedWiremockContainer.
|
|
@@ -77,13 +198,13 @@ export class StartedWiremockContainer extends AbstractStartedContainer {
|
|
|
77
198
|
*/
|
|
78
199
|
constructor(startedTestContainer: StartedTestContainer, url: string) {
|
|
79
200
|
super(startedTestContainer);
|
|
80
|
-
this
|
|
201
|
+
this.#rpcUrl = url;
|
|
81
202
|
}
|
|
82
203
|
|
|
83
204
|
/**
|
|
84
205
|
* Gets the URL of the Wiremock node.
|
|
85
206
|
*/
|
|
86
|
-
get rpcUrl() {
|
|
87
|
-
return this
|
|
207
|
+
get rpcUrl(): string {
|
|
208
|
+
return this.#rpcUrl;
|
|
88
209
|
}
|
|
89
210
|
}
|
package/src/sources.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** A path (relative to the working directory) or a `file:` URL. */
|
|
7
|
+
export type WiremockPath = string | URL;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A fixture file: a local path or `file:` URL, `{ file }`, a file inside an installed npm
|
|
11
|
+
* package `{ package, file }` (its `exports` are ignored), or a file next to a module
|
|
12
|
+
* `{ module: import.meta.url | __filename | __dirname, file }`.
|
|
13
|
+
*/
|
|
14
|
+
export type WiremockFileSource =
|
|
15
|
+
| WiremockPath
|
|
16
|
+
| { readonly file: WiremockPath }
|
|
17
|
+
| { readonly package: string; readonly file: string }
|
|
18
|
+
| { readonly module: WiremockPath; readonly file: string };
|
|
19
|
+
|
|
20
|
+
/** A WireMock stub; fields besides `request` and `response` are passed through. */
|
|
21
|
+
export interface WiremockStubMapping {
|
|
22
|
+
readonly request: object;
|
|
23
|
+
readonly response: object;
|
|
24
|
+
readonly [field: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A file source, or inline JSON: a string, a stub or `{ mappings: [...] }`. */
|
|
28
|
+
export type WiremockMappingSource =
|
|
29
|
+
| WiremockFileSource
|
|
30
|
+
| { readonly json: string | WiremockStubMapping | { readonly mappings: readonly WiremockStubMapping[] } };
|
|
31
|
+
|
|
32
|
+
/** A file source; object forms may set `name`, the path below `__files/` (default: the base name). */
|
|
33
|
+
export type WiremockStaticFile = WiremockPath | (Exclude<WiremockFileSource, WiremockPath> & { readonly name?: string });
|
|
34
|
+
|
|
35
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;
|
|
36
|
+
|
|
37
|
+
export function fail(message: string): never {
|
|
38
|
+
throw new Error(message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Runs `fn`, prefixing the message of any error it throws with `context`. */
|
|
42
|
+
export function within<T>(context: string, fn: () => T): T {
|
|
43
|
+
try {
|
|
44
|
+
return fn();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new Error(`${context}: ${(error as Error).message}`, { cause: error });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
51
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
|
|
53
|
+
const show = (value: unknown): string => (value instanceof URL ? value.href : (JSON.stringify(value) ?? String(value)));
|
|
54
|
+
|
|
55
|
+
const text = (value: unknown, field: string): string =>
|
|
56
|
+
typeof value === "string" && value.trim() !== "" ? value : fail(`"${field}" must be a non-empty string, got ${show(value)}`);
|
|
57
|
+
|
|
58
|
+
const toPath = (value: unknown, field: string): string => {
|
|
59
|
+
if (!(value instanceof URL) && !text(value, field).startsWith("file:")) return resolve(value as string);
|
|
60
|
+
try {
|
|
61
|
+
return fileURLToPath(value as WiremockPath);
|
|
62
|
+
} catch {
|
|
63
|
+
return fail(`"${field}" must be a path or a file: URL, got ${show(value)}`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const existingFile = (path: string): string => {
|
|
68
|
+
const stats = statSync(path, { throwIfNoEntry: false });
|
|
69
|
+
return stats?.isFile() ? path : fail(`"${path}" ${stats ? "is not a file" : "does not exist"}`);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Finds an installed package the way `require()` would from the working directory. */
|
|
73
|
+
const packageRoot = (name: string): string => {
|
|
74
|
+
if (!PACKAGE_NAME.test(name)) fail(`"${name}" is not a valid npm package name`);
|
|
75
|
+
const searchPaths = createRequire(join(process.cwd(), "noop.js")).resolve.paths(name) ?? [];
|
|
76
|
+
const root = searchPaths.map((dir) => join(dir, name)).find((dir) => existsSync(join(dir, "package.json")));
|
|
77
|
+
return root ?? fail(`package "${name}" is not installed (searched from "${process.cwd()}")`);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Resolves a {@link WiremockFileSource} to the absolute path of an existing file. */
|
|
81
|
+
export function resolveFile(source: unknown): string {
|
|
82
|
+
if (typeof source === "string" || source instanceof URL) return existingFile(toPath(source, "source"));
|
|
83
|
+
if (!isRecord(source)) return fail(`unsupported source ${show(source)}; expected a path, a file: URL, { file }, { package, file } or { module, file }`);
|
|
84
|
+
const { file, package: pkg, module } = source;
|
|
85
|
+
if (pkg !== undefined && module !== undefined) fail('a source may set "package" or "module", not both');
|
|
86
|
+
if (pkg !== undefined) {
|
|
87
|
+
const root = packageRoot(text(pkg, "package"));
|
|
88
|
+
const path = resolve(root, text(file, "file"));
|
|
89
|
+
return relative(root, path).split(sep)[0] === ".." ? fail(`"${String(file)}" must point to a file inside package "${pkg}"`) : existingFile(path);
|
|
90
|
+
}
|
|
91
|
+
if (module === undefined) return existingFile(toPath(file, "file"));
|
|
92
|
+
const anchor = toPath(module, "module");
|
|
93
|
+
return existingFile(resolve(statSync(anchor, { throwIfNoEntry: false })?.isDirectory() ? anchor : dirname(anchor), text(file, "file")));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Validates a `/`-separated relative container name (no empty, `.` or `..` segments). */
|
|
97
|
+
export const containerName = (name: unknown): string => {
|
|
98
|
+
const value = text(name, "name");
|
|
99
|
+
const invalid = value.split("/").some((part) => ["", ".", ".."].includes(part) || /[\\\0]/u.test(part));
|
|
100
|
+
return invalid ? fail(`name "${value}" must be a relative path using "/" separators, without empty, "." or ".." segments`) : value;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Inline mapping JSON: strings verbatim, anything else pretty-printed. */
|
|
104
|
+
export const inlineJson = (json: unknown): string =>
|
|
105
|
+
typeof json === "string" ? json : (JSON.stringify(json, null, 2) ?? fail('"json" must be a JSON string or an object'));
|
|
106
|
+
|
|
107
|
+
const parseJson = (json: string): unknown => {
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(json);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return fail(`invalid JSON (${(error as Error).message})`);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** Validates a mapping file – one stub or `{ mappings: [...] }` – and returns its stub ids. */
|
|
116
|
+
export function stubIds(json: string): string[] {
|
|
117
|
+
const value = parseJson(json);
|
|
118
|
+
const many = isRecord(value) && "mappings" in value;
|
|
119
|
+
const stubs = many ? value.mappings : [value];
|
|
120
|
+
if (!Array.isArray(stubs) || stubs.length === 0) fail('"mappings" must be a non-empty array of stub mappings');
|
|
121
|
+
return stubs.flatMap((stub: unknown, index) => {
|
|
122
|
+
if (!isRecord(stub) || !isRecord(stub.request) || !isRecord(stub.response)) {
|
|
123
|
+
fail(`${many ? `mappings[${index}]` : "the mapping"} is not a WireMock stub mapping ("request" and "response" objects are required)`);
|
|
124
|
+
}
|
|
125
|
+
const id = stub.id ?? stub.uuid;
|
|
126
|
+
return typeof id === "string" ? [id] : [];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** {@link stubIds} of a file in a mounted directory; malformed files are left for WireMock to report. */
|
|
131
|
+
export const stubIdsOf = (path: string): string[] => {
|
|
132
|
+
try {
|
|
133
|
+
return path.endsWith(".json") ? stubIds(readFileSync(path, "utf8")) : [];
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Files below `dir`, as `/`-separated relative paths. */
|
|
140
|
+
export const listFiles = (dir: string): string[] =>
|
|
141
|
+
readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
142
|
+
.filter((entry) => !entry.isDirectory())
|
|
143
|
+
.map((entry) => relative(dir, join(entry.parentPath, entry.name)).split(sep).join("/"));
|