@techspokes/typescript-wsdl-client 0.11.0 → 0.11.3
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/dist/app/generateApp.d.ts.map +1 -1
- package/dist/app/generateApp.js +3 -26
- package/dist/cli.js +22 -0
- package/dist/gateway/generators.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/pipeline.d.ts +4 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +17 -0
- package/dist/test/generateTests.d.ts +20 -0
- package/dist/test/generateTests.d.ts.map +1 -0
- package/dist/test/generateTests.js +136 -0
- package/dist/test/generators.d.ts +69 -0
- package/dist/test/generators.d.ts.map +1 -0
- package/dist/test/generators.js +630 -0
- package/dist/test/mockData.d.ts +68 -0
- package/dist/test/mockData.d.ts.map +1 -0
- package/dist/test/mockData.js +133 -0
- package/dist/util/imports.d.ts +17 -0
- package/dist/util/imports.d.ts.map +1 -0
- package/dist/util/imports.js +42 -0
- package/docs/cli-reference.md +21 -0
- package/docs/decisions/001-test-generation.md +36 -0
- package/docs/testing.md +70 -0
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Data Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic mock data trees from compiled catalog type metadata.
|
|
5
|
+
* Used by the test generator to create full default responses per operation
|
|
6
|
+
* so that generated tests pass out of the box.
|
|
7
|
+
*
|
|
8
|
+
* Pure logic — no I/O or side effects.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Generates a mock primitive value based on the TypeScript type and property name.
|
|
12
|
+
* Uses contextual defaults based on common property names.
|
|
13
|
+
*
|
|
14
|
+
* @param tsType - The TypeScript type (string, number, boolean)
|
|
15
|
+
* @param propName - The property name for contextual defaults
|
|
16
|
+
* @returns A mock value of the appropriate type
|
|
17
|
+
*/
|
|
18
|
+
export function generateMockPrimitive(tsType, propName) {
|
|
19
|
+
const lower = propName.toLowerCase();
|
|
20
|
+
if (tsType === "boolean") {
|
|
21
|
+
if (lower === "success")
|
|
22
|
+
return true;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (tsType === "number") {
|
|
26
|
+
if (lower.includes("id"))
|
|
27
|
+
return 1;
|
|
28
|
+
if (lower.includes("temperature") || lower.includes("temp"))
|
|
29
|
+
return 72;
|
|
30
|
+
if (lower.includes("humidity"))
|
|
31
|
+
return 65;
|
|
32
|
+
if (lower.includes("pressure"))
|
|
33
|
+
return 1013;
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
// string type — use contextual defaults
|
|
37
|
+
if (lower === "zip" || lower === "zipcode" || lower === "postalcode")
|
|
38
|
+
return "10001";
|
|
39
|
+
if (lower === "city")
|
|
40
|
+
return "New York";
|
|
41
|
+
if (lower === "state")
|
|
42
|
+
return "NY";
|
|
43
|
+
if (lower === "country")
|
|
44
|
+
return "US";
|
|
45
|
+
if (lower === "description" || lower === "desciption")
|
|
46
|
+
return "Sample description";
|
|
47
|
+
if (lower === "responsetext")
|
|
48
|
+
return "Data Found";
|
|
49
|
+
if (lower === "weatherstationcity")
|
|
50
|
+
return "White Plains";
|
|
51
|
+
if (lower.includes("url") || lower.includes("picture"))
|
|
52
|
+
return "https://example.com/image.png";
|
|
53
|
+
if (lower.includes("wind"))
|
|
54
|
+
return "CALM";
|
|
55
|
+
if (lower.includes("visibility"))
|
|
56
|
+
return "10 miles";
|
|
57
|
+
if (lower.includes("windchill"))
|
|
58
|
+
return "72";
|
|
59
|
+
if (lower.includes("remarks"))
|
|
60
|
+
return "";
|
|
61
|
+
if (lower.includes("date") || lower.includes("time"))
|
|
62
|
+
return "2026-01-15T12:00:00.000Z";
|
|
63
|
+
if (lower.includes("temperature") || lower === "morninglow" || lower === "daytimehigh")
|
|
64
|
+
return "72";
|
|
65
|
+
if (lower.includes("nighttime") || lower.includes("daytime"))
|
|
66
|
+
return "0";
|
|
67
|
+
return "sample";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Generates a mock data object for a given type by walking the catalog type metadata.
|
|
71
|
+
*
|
|
72
|
+
* @param typeName - The type name to generate mock data for
|
|
73
|
+
* @param catalog - The compiled catalog with type metadata
|
|
74
|
+
* @param opts - Optional generation options
|
|
75
|
+
* @param visited - Set of visited type names for cycle detection (internal)
|
|
76
|
+
* @param depth - Current recursion depth (internal)
|
|
77
|
+
* @returns Mock data object matching the type structure
|
|
78
|
+
*/
|
|
79
|
+
export function generateMockData(typeName, catalog, opts, visited, depth) {
|
|
80
|
+
const maxDepth = opts?.maxDepth ?? 10;
|
|
81
|
+
const currentDepth = depth ?? 0;
|
|
82
|
+
const currentVisited = visited ?? new Set();
|
|
83
|
+
if (currentDepth >= maxDepth || currentVisited.has(typeName)) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
const childTypes = catalog.meta?.childType?.[typeName];
|
|
87
|
+
if (!childTypes || Object.keys(childTypes).length === 0) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
const propMeta = catalog.meta?.propMeta?.[typeName] ?? {};
|
|
91
|
+
const newVisited = new Set(currentVisited);
|
|
92
|
+
newVisited.add(typeName);
|
|
93
|
+
const result = {};
|
|
94
|
+
for (const [propName, propType] of Object.entries(childTypes)) {
|
|
95
|
+
const meta = propMeta[propName];
|
|
96
|
+
const isArray = meta?.max === "unbounded" || (typeof meta?.max === "number" && meta.max > 1);
|
|
97
|
+
// Check if it's a primitive type
|
|
98
|
+
if (propType === "string" || propType === "number" || propType === "boolean") {
|
|
99
|
+
const value = generateMockPrimitive(propType, propName);
|
|
100
|
+
result[propName] = isArray ? [value] : value;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Complex type — recurse
|
|
104
|
+
const childData = generateMockData(propType, catalog, opts, newVisited, currentDepth + 1);
|
|
105
|
+
result[propName] = isArray ? [childData] : childData;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Generates mock request and response data for all operations in the catalog.
|
|
112
|
+
*
|
|
113
|
+
* Response data uses the pre-unwrap shape (e.g. { WeatherDescription: [{...}] }
|
|
114
|
+
* not [{...}]) since the generated route handler calls unwrapArrayWrappers() at runtime.
|
|
115
|
+
*
|
|
116
|
+
* @param catalog - The compiled catalog with operations and type metadata
|
|
117
|
+
* @returns Map from operation name to { request, response } mock data
|
|
118
|
+
*/
|
|
119
|
+
export function generateAllOperationMocks(catalog) {
|
|
120
|
+
const result = new Map();
|
|
121
|
+
if (!catalog.operations)
|
|
122
|
+
return result;
|
|
123
|
+
for (const op of catalog.operations) {
|
|
124
|
+
const request = op.inputTypeName
|
|
125
|
+
? generateMockData(op.inputTypeName, catalog)
|
|
126
|
+
: {};
|
|
127
|
+
const response = op.outputTypeName
|
|
128
|
+
? generateMockData(op.outputTypeName, catalog)
|
|
129
|
+
: {};
|
|
130
|
+
result.set(op.name, { request, response });
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the file extension for the import mode
|
|
3
|
+
*
|
|
4
|
+
* @param {string} imports - Import mode (js, ts, or bare)
|
|
5
|
+
* @returns {string} - File extension with leading dot or empty string for bare
|
|
6
|
+
*/
|
|
7
|
+
export declare function getImportExtension(imports: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Computes a relative import path from source to target
|
|
10
|
+
*
|
|
11
|
+
* @param {string} from - Source directory
|
|
12
|
+
* @param {string} to - Target file or directory
|
|
13
|
+
* @param {string} imports - Import mode (js, ts, or bare)
|
|
14
|
+
* @returns {string} - Relative import specifier with proper extension
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeRelativeImport(from: string, to: string, imports: string): string;
|
|
17
|
+
//# sourceMappingURL=imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/util/imports.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAavF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Import Path Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides functions for computing relative import paths and file extensions
|
|
5
|
+
* across different import modes (js, ts, bare). Used by app scaffold, test
|
|
6
|
+
* generation, and other modules that need to emit import specifiers.
|
|
7
|
+
*/
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
/**
|
|
10
|
+
* Returns the file extension for the import mode
|
|
11
|
+
*
|
|
12
|
+
* @param {string} imports - Import mode (js, ts, or bare)
|
|
13
|
+
* @returns {string} - File extension with leading dot or empty string for bare
|
|
14
|
+
*/
|
|
15
|
+
export function getImportExtension(imports) {
|
|
16
|
+
if (imports === "js")
|
|
17
|
+
return ".js";
|
|
18
|
+
if (imports === "ts")
|
|
19
|
+
return ".ts";
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Computes a relative import path from source to target
|
|
24
|
+
*
|
|
25
|
+
* @param {string} from - Source directory
|
|
26
|
+
* @param {string} to - Target file or directory
|
|
27
|
+
* @param {string} imports - Import mode (js, ts, or bare)
|
|
28
|
+
* @returns {string} - Relative import specifier with proper extension
|
|
29
|
+
*/
|
|
30
|
+
export function computeRelativeImport(from, to, imports) {
|
|
31
|
+
const rel = path.relative(from, to);
|
|
32
|
+
// Normalize to POSIX separators
|
|
33
|
+
const posix = rel.split(path.sep).join("/");
|
|
34
|
+
// Ensure it starts with ./ or ../
|
|
35
|
+
const prefixed = posix.startsWith(".") ? posix : `./${posix}`;
|
|
36
|
+
// Apply import extension rules
|
|
37
|
+
const ext = getImportExtension(imports);
|
|
38
|
+
if (ext) {
|
|
39
|
+
return prefixed + ext;
|
|
40
|
+
}
|
|
41
|
+
return prefixed;
|
|
42
|
+
}
|
package/docs/cli-reference.md
CHANGED
|
@@ -122,6 +122,13 @@ The catalog is auto-placed alongside the first available output directory: `{cli
|
|
|
122
122
|
| `--app-port` | `3000` | Default server port |
|
|
123
123
|
| `--app-prefix` | (empty) | Route prefix |
|
|
124
124
|
|
|
125
|
+
### Test Suite Flags
|
|
126
|
+
|
|
127
|
+
| Flag | Default | Description |
|
|
128
|
+
|------|---------|-------------|
|
|
129
|
+
| `--test-dir` | (none) | Generate Vitest test suite in this directory (requires client, gateway) |
|
|
130
|
+
| `--force-test` | `false` | Overwrite existing test files when using --test-dir |
|
|
131
|
+
|
|
125
132
|
### Pipeline Workflow
|
|
126
133
|
|
|
127
134
|
Steps execute in order:
|
|
@@ -133,6 +140,7 @@ Steps execute in order:
|
|
|
133
140
|
5. Generate OpenAPI (if --openapi-file)
|
|
134
141
|
6. Generate gateway (if --gateway-dir)
|
|
135
142
|
7. Scaffold app (if --init-app)
|
|
143
|
+
8. Generate test suite (if --test-dir)
|
|
136
144
|
|
|
137
145
|
All steps share the same parsed WSDL and compiled catalog.
|
|
138
146
|
|
|
@@ -163,6 +171,19 @@ npx wsdl-tsc pipeline \
|
|
|
163
171
|
--init-app
|
|
164
172
|
```
|
|
165
173
|
|
|
174
|
+
With generated test suite:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx wsdl-tsc pipeline \
|
|
178
|
+
--wsdl-source examples/minimal/weather.wsdl \
|
|
179
|
+
--client-dir tmp/client \
|
|
180
|
+
--openapi-file tmp/openapi.json \
|
|
181
|
+
--gateway-dir tmp/gateway \
|
|
182
|
+
--gateway-service-name weather \
|
|
183
|
+
--gateway-version-prefix v1 \
|
|
184
|
+
--test-dir tmp/tests
|
|
185
|
+
```
|
|
186
|
+
|
|
166
187
|
Client and OpenAPI only:
|
|
167
188
|
|
|
168
189
|
```bash
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# ADR 001: Generated Test Suite for Gateway Artifacts
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Users of generated Fastify gateways have no automated way to verify the generated code works. The only testing path is to manually write Vitest tests following the pattern in this project's own `test/integration/gateway-routes.test.ts`. The FST_ERR_FAILED_ERROR_SERIALIZATION bug (fixed in 0.11.1) proved that generated code can have subtle runtime issues that only surface under specific conditions. Generated tests would catch these immediately.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Add an opt-in `--test-dir` flag to the pipeline command that generates a complete, runnable Vitest test suite validating all generated gateway artifacts.
|
|
14
|
+
|
|
15
|
+
### Why generated tests (not a test library)
|
|
16
|
+
|
|
17
|
+
A reusable test library would require users to configure imports, mock data, and test structure themselves. Generated tests are zero-config: they import the exact generated modules with correct relative paths, use mock data derived from the actual catalog types, and run immediately with `npx vitest run`.
|
|
18
|
+
|
|
19
|
+
### Why full mocks (not placeholders)
|
|
20
|
+
|
|
21
|
+
Placeholder mocks (empty objects, `TODO` comments) require manual effort before tests pass. Full mocks generated from catalog type metadata produce passing tests out of the box. Property names and types come from the compiled catalog's `meta.childType` map, so mock data matches the actual SOAP response shape.
|
|
22
|
+
|
|
23
|
+
### Why skip-if-exists
|
|
24
|
+
|
|
25
|
+
Test files are meant to be customized after generation. Re-running the pipeline should not overwrite user modifications. The `--force-test` flag provides an explicit override when regeneration is desired.
|
|
26
|
+
|
|
27
|
+
### Relationship to operations.ts interface
|
|
28
|
+
|
|
29
|
+
The generated `{ClassName}Operations` interface (from `operations.ts`) is the natural seam for mock injection. The mock client helper implements this interface with full default responses per operation. This matches the pattern established in `test/integration/gateway-routes.test.ts`.
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
- Users get immediate test coverage for generated gateway code
|
|
34
|
+
- Mock data stays in sync with WSDL schema changes (regenerate to update)
|
|
35
|
+
- Test files can be customized without risk of overwrite
|
|
36
|
+
- Additional CLI flag and pipeline step increase the surface area to maintain
|
package/docs/testing.md
CHANGED
|
@@ -39,6 +39,7 @@ Unit tests cover pure functions with no I/O or side effects:
|
|
|
39
39
|
- **`primitives.test.ts`** — `xsdToTsPrimitive()` covering all XSD types (string-like, boolean, integers, decimals, floats, dates, any)
|
|
40
40
|
- **`errors.test.ts`** — `WsdlCompilationError` construction and `toUserMessage()` formatting
|
|
41
41
|
- **`schema-alignment.test.ts`** — Cross-validates TypeScript types, JSON schemas, and catalog.json for consistency
|
|
42
|
+
- **`mock-data.test.ts`** — `generateMockPrimitive()`, `generateMockData()`, `generateAllOperationMocks()` with cycle detection and array wrapping
|
|
42
43
|
|
|
43
44
|
### Writing Unit Tests
|
|
44
45
|
|
|
@@ -180,10 +181,79 @@ When `--openapi-flatten-array-wrappers false` is used, ArrayOf* types are emitte
|
|
|
180
181
|
|
|
181
182
|
The `classifyError()` function puts `err.message` (a string) in the `details` field for connection and timeout errors, but the error JSON schema defines `details` as `object | null`. This causes Fastify serialization failures for 503/504 error responses. Test error classification directly via `classifyError()` rather than through Fastify's `inject()` for these error types.
|
|
182
183
|
|
|
184
|
+
## Generated Test Suite
|
|
185
|
+
|
|
186
|
+
The `--test-dir` flag generates a complete, runnable Vitest test suite that validates all generated gateway artifacts out of the box. This is the recommended way to verify generated code in consumer projects.
|
|
187
|
+
|
|
188
|
+
### Generating Tests
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx wsdl-tsc pipeline \
|
|
192
|
+
--wsdl-source service.wsdl \
|
|
193
|
+
--client-dir ./generated/client \
|
|
194
|
+
--openapi-file ./generated/openapi.json \
|
|
195
|
+
--gateway-dir ./generated/gateway \
|
|
196
|
+
--gateway-service-name myservice \
|
|
197
|
+
--gateway-version-prefix v1 \
|
|
198
|
+
--test-dir ./generated/tests
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Generated Structure
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
{test-dir}/
|
|
205
|
+
vitest.config.ts
|
|
206
|
+
helpers/
|
|
207
|
+
mock-client.ts createMockClient() with full default responses per operation
|
|
208
|
+
test-app.ts createTestApp() Fastify bootstrap helper
|
|
209
|
+
gateway/
|
|
210
|
+
routes.test.ts per-operation happy path (one test per route)
|
|
211
|
+
errors.test.ts 400/500/502/503/504 through Fastify
|
|
212
|
+
envelope.test.ts SUCCESS/ERROR structure assertions
|
|
213
|
+
validation.test.ts invalid payloads rejected per route
|
|
214
|
+
runtime/
|
|
215
|
+
classify-error.test.ts classifyError() unit tests
|
|
216
|
+
envelope-builders.test.ts buildSuccessEnvelope/buildErrorEnvelope
|
|
217
|
+
unwrap.test.ts unwrapArrayWrappers (only when ArrayOf* wrappers exist)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Running Generated Tests
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npx vitest run --config ./generated/tests/vitest.config.ts
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Skip-if-Exists Behavior
|
|
227
|
+
|
|
228
|
+
Test files that already exist are skipped by default. This allows you to customize generated tests without losing changes on regeneration. Use `--force-test` to overwrite all test files.
|
|
229
|
+
|
|
230
|
+
### Mock Client
|
|
231
|
+
|
|
232
|
+
The generated `helpers/mock-client.ts` creates a fully typed mock client with default responses for every operation. Override individual methods for specific test scenarios:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { createMockClient } from "./helpers/mock-client.js";
|
|
236
|
+
|
|
237
|
+
const client = createMockClient({
|
|
238
|
+
GetCityWeatherByZIP: async () => ({
|
|
239
|
+
response: { GetCityWeatherByZIPResult: { Success: false, WeatherID: 0 } },
|
|
240
|
+
headers: {},
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Mock responses use the pre-unwrap SOAP wrapper shape. The generated `unwrapArrayWrappers()` function handles conversion at runtime.
|
|
246
|
+
|
|
183
247
|
## For Consumer Projects
|
|
184
248
|
|
|
185
249
|
If you're using wsdl-tsc as a dependency and want to test your integration:
|
|
186
250
|
|
|
251
|
+
1. Generate code with `npx wsdl-tsc pipeline --test-dir ./tests` to get a ready-to-run test suite
|
|
252
|
+
2. Run `npx vitest run --config ./tests/vitest.config.ts` to verify everything works
|
|
253
|
+
3. Customize generated tests as needed (they won't be overwritten on regeneration)
|
|
254
|
+
|
|
255
|
+
Alternatively, write tests manually:
|
|
256
|
+
|
|
187
257
|
1. Generate code with `npx wsdl-tsc pipeline`
|
|
188
258
|
2. Import the operations interface from `operations.ts`
|
|
189
259
|
3. Create a mock client implementing the interface
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techspokes/typescript-wsdl-client",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Generate type-safe TypeScript SOAP clients, OpenAPI 3.1 specs, and production-ready Fastify REST gateways from WSDL/XSD definitions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wsdl",
|