@emseepea/create-soap-backed-server 0.0.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.
Files changed (25) hide show
  1. package/README.md +96 -0
  2. package/initializer-dist/LICENSE +21 -0
  3. package/initializer-dist/create.mjs +44 -0
  4. package/initializer-dist/template/README.md +68 -0
  5. package/initializer-dist/template/contracts/pea-service.wsdl +30 -0
  6. package/initializer-dist/template/contracts/pea-service.xsd +29 -0
  7. package/initializer-dist/template/contracts/soap-envelope.xsd +20 -0
  8. package/initializer-dist/template/eval/meaning.test.mjs +25 -0
  9. package/initializer-dist/template/package.json +35 -0
  10. package/initializer-dist/template/scripts/check-generated.mjs +15 -0
  11. package/initializer-dist/template/scripts/generate-types.mjs +42 -0
  12. package/initializer-dist/template/src/app.ts +26 -0
  13. package/initializer-dist/template/src/capabilities/context.ts +5 -0
  14. package/initializer-dist/template/src/capabilities/tool.get-pea-variety.ts +41 -0
  15. package/initializer-dist/template/src/generated/pea-service.schema-model.json +205 -0
  16. package/initializer-dist/template/src/generated/pea-service.ts +34 -0
  17. package/initializer-dist/template/src/server.ts +21 -0
  18. package/initializer-dist/template/src/soap-schema.ts +28 -0
  19. package/initializer-dist/template/src/validating-http-client.ts +125 -0
  20. package/initializer-dist/template/test/server.test.mjs +250 -0
  21. package/initializer-dist/template/test-support/soap-fixture.mjs +160 -0
  22. package/initializer-dist/template/test-types/generated-soap-types.ts +45 -0
  23. package/initializer-dist/template/tsconfig.json +13 -0
  24. package/initializer-dist/template/tsconfig.type-tests.json +8 -0
  25. package/package.json +59 -0
@@ -0,0 +1,205 @@
1
+ {
2
+ "targetNamespace": "http://schemas.xmlsoap.org/soap/envelope/",
3
+ "elementFormDefault": "qualified",
4
+ "attributeFormDefault": "unqualified",
5
+ "version": "",
6
+ "elements": {
7
+ "{http://schemas.xmlsoap.org/soap/envelope/}Envelope": {
8
+ "name": "Envelope",
9
+ "namespace": "http://schemas.xmlsoap.org/soap/envelope/",
10
+ "typeName": "{http://schemas.xmlsoap.org/soap/envelope/}SoapEnvelope",
11
+ "nillable": false,
12
+ "abstract": false,
13
+ "defaultValue": null,
14
+ "fixedValue": null,
15
+ "substitutionGroup": null,
16
+ "minOccurs": 1,
17
+ "maxOccurs": 1
18
+ },
19
+ "{urn:emseepea:pea-service}GetPeaRequest": {
20
+ "name": "GetPeaRequest",
21
+ "namespace": "urn:emseepea:pea-service",
22
+ "typeName": null,
23
+ "nillable": false,
24
+ "abstract": false,
25
+ "defaultValue": null,
26
+ "fixedValue": null,
27
+ "substitutionGroup": null,
28
+ "minOccurs": 1,
29
+ "maxOccurs": 1
30
+ },
31
+ "{urn:emseepea:pea-service}GetPeaResponse": {
32
+ "name": "GetPeaResponse",
33
+ "namespace": "urn:emseepea:pea-service",
34
+ "typeName": "{urn:emseepea:pea-service}PeaVariety",
35
+ "nillable": false,
36
+ "abstract": false,
37
+ "defaultValue": null,
38
+ "fixedValue": null,
39
+ "substitutionGroup": null,
40
+ "minOccurs": 1,
41
+ "maxOccurs": 1
42
+ }
43
+ },
44
+ "simpleTypes": {
45
+ "{urn:emseepea:pea-service}PeaType": {
46
+ "kind": "simple",
47
+ "name": "PeaType",
48
+ "namespace": "urn:emseepea:pea-service",
49
+ "baseTypeName": "xs:string",
50
+ "variety": "atomic",
51
+ "facets": [
52
+ {
53
+ "kind": "minLength",
54
+ "value": "1"
55
+ },
56
+ {
57
+ "kind": "maxLength",
58
+ "value": "40"
59
+ }
60
+ ],
61
+ "memberTypes": [],
62
+ "itemType": null
63
+ }
64
+ },
65
+ "complexTypes": {
66
+ "{http://schemas.xmlsoap.org/soap/envelope/}SoapBody": {
67
+ "kind": "complex",
68
+ "name": "SoapBody",
69
+ "namespace": "http://schemas.xmlsoap.org/soap/envelope/",
70
+ "abstract": false,
71
+ "mixed": false,
72
+ "contentType": "elementOnly",
73
+ "baseTypeName": null,
74
+ "derivationMethod": null,
75
+ "attributes": [],
76
+ "particle": {
77
+ "particleKind": "group",
78
+ "compositor": "choice",
79
+ "occurrence": {
80
+ "min": 1,
81
+ "max": 1
82
+ },
83
+ "particles": [
84
+ {
85
+ "particleKind": "element",
86
+ "occurrence": {
87
+ "min": 1,
88
+ "max": 1
89
+ },
90
+ "element": "GetPeaResponse"
91
+ },
92
+ {
93
+ "particleKind": "element",
94
+ "occurrence": {
95
+ "min": 1,
96
+ "max": 1
97
+ },
98
+ "element": "Fault"
99
+ }
100
+ ]
101
+ },
102
+ "simpleContent": null,
103
+ "final": [],
104
+ "block": []
105
+ },
106
+ "{http://schemas.xmlsoap.org/soap/envelope/}SoapEnvelope": {
107
+ "kind": "complex",
108
+ "name": "SoapEnvelope",
109
+ "namespace": "http://schemas.xmlsoap.org/soap/envelope/",
110
+ "abstract": false,
111
+ "mixed": false,
112
+ "contentType": "elementOnly",
113
+ "baseTypeName": null,
114
+ "derivationMethod": null,
115
+ "attributes": [],
116
+ "particle": {
117
+ "particleKind": "group",
118
+ "compositor": "sequence",
119
+ "occurrence": {
120
+ "min": 1,
121
+ "max": 1
122
+ },
123
+ "particles": [
124
+ {
125
+ "particleKind": "element",
126
+ "occurrence": {
127
+ "min": 1,
128
+ "max": 1
129
+ },
130
+ "element": "Body"
131
+ }
132
+ ]
133
+ },
134
+ "simpleContent": null,
135
+ "final": [],
136
+ "block": []
137
+ },
138
+ "{urn:emseepea:pea-service}PeaVariety": {
139
+ "kind": "complex",
140
+ "name": "PeaVariety",
141
+ "namespace": "urn:emseepea:pea-service",
142
+ "abstract": false,
143
+ "mixed": false,
144
+ "contentType": "elementOnly",
145
+ "baseTypeName": null,
146
+ "derivationMethod": null,
147
+ "attributes": [],
148
+ "particle": {
149
+ "particleKind": "group",
150
+ "compositor": "sequence",
151
+ "occurrence": {
152
+ "min": 1,
153
+ "max": 1
154
+ },
155
+ "particles": [
156
+ {
157
+ "particleKind": "element",
158
+ "occurrence": {
159
+ "min": 1,
160
+ "max": 1
161
+ },
162
+ "element": "name"
163
+ },
164
+ {
165
+ "particleKind": "element",
166
+ "occurrence": {
167
+ "min": 1,
168
+ "max": 1
169
+ },
170
+ "element": "peaType"
171
+ },
172
+ {
173
+ "particleKind": "element",
174
+ "occurrence": {
175
+ "min": 1,
176
+ "max": 1
177
+ },
178
+ "element": "daysToMaturity"
179
+ },
180
+ {
181
+ "particleKind": "element",
182
+ "occurrence": {
183
+ "min": 0,
184
+ "max": 1
185
+ },
186
+ "element": "note"
187
+ },
188
+ {
189
+ "particleKind": "element",
190
+ "occurrence": {
191
+ "min": 0,
192
+ "max": 5
193
+ },
194
+ "element": "trait"
195
+ }
196
+ ]
197
+ },
198
+ "simpleContent": null,
199
+ "final": [],
200
+ "block": []
201
+ }
202
+ },
203
+ "attributes": {},
204
+ "substitutionGroups": {}
205
+ }
@@ -0,0 +1,34 @@
1
+ // Generated from the local SOAP XSD graph. Do not edit.
2
+ /** XSD simpleType: PeaType */
3
+ export type PeaType = string;
4
+
5
+ /** XSD complexType: SoapBody */
6
+ export interface SoapBody {
7
+ GetPeaResponse: unknown;
8
+ Fault: unknown;
9
+ }
10
+
11
+ /** XSD complexType: SoapEnvelope */
12
+ export interface SoapEnvelope {
13
+ Body: SoapBody;
14
+ }
15
+
16
+ /** XSD complexType: PeaVariety */
17
+ export interface PeaVariety {
18
+ name: string;
19
+ peaType: PeaType;
20
+ daysToMaturity: number;
21
+ note?: string;
22
+ trait?: string[];
23
+ }
24
+
25
+ /** XSD element: Envelope */
26
+ export type Envelope = SoapEnvelope;
27
+
28
+ /** XSD element: GetPeaRequest */
29
+ export interface GetPeaRequest {
30
+ name: string;
31
+ }
32
+
33
+ /** XSD element: GetPeaResponse */
34
+ export type GetPeaResponse = PeaVariety;
@@ -0,0 +1,21 @@
1
+ import { serveEmseepea } from "@emseepea/server";
2
+ import { createSoapExample } from "./app.js";
3
+
4
+ const endpoint = process.env.PEA_SOAP_URL ?? "http://127.0.0.1:3999/soap";
5
+ const { app } = await createSoapExample(endpoint);
6
+ const running = await serveEmseepea(app, {
7
+ port: Number.parseInt(process.env.PORT ?? "3000", 10),
8
+ });
9
+
10
+ console.log(`Em See Pea SOAP-backed example listening at ${running.url}`);
11
+
12
+ let shuttingDown = false;
13
+ async function shutdown(): Promise<void> {
14
+ if (shuttingDown) return;
15
+ shuttingDown = true;
16
+ await running.close();
17
+ process.exitCode = 0;
18
+ }
19
+
20
+ process.once("SIGINT", () => void shutdown());
21
+ process.once("SIGTERM", () => void shutdown());
@@ -0,0 +1,28 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseXsd, parseXsdAsync, type SchemaModel } from "xml-xsd-engine";
3
+
4
+ const contractDirectory = new URL("../contracts/", import.meta.url);
5
+
6
+ export interface SoapSchemaGraph {
7
+ readonly envelope: SchemaModel;
8
+ readonly service: SchemaModel;
9
+ }
10
+
11
+ export async function loadSoapEnvelopeSchema(): Promise<SoapSchemaGraph> {
12
+ const service = await readFile(new URL("pea-service.xsd", contractDirectory), "utf8");
13
+ const envelope = await readFile(new URL("soap-envelope.xsd", contractDirectory), "utf8");
14
+ return parseSoapEnvelopeSchema(envelope, service);
15
+ }
16
+
17
+ export async function parseSoapEnvelopeSchema(
18
+ envelope: string,
19
+ service: string,
20
+ ): Promise<SoapSchemaGraph> {
21
+ const resolvedEnvelope = await parseXsdAsync(envelope, async (location, namespace) => {
22
+ if (location !== "pea-service.xsd" || namespace !== "urn:emseepea:pea-service") {
23
+ throw new Error("Blocked schema import");
24
+ }
25
+ return service;
26
+ });
27
+ return { envelope: resolvedEnvelope, service: parseXsd(service) };
28
+ }
@@ -0,0 +1,125 @@
1
+ import { request as requestHttp } from "node:http";
2
+ import { request as requestHttps } from "node:https";
3
+ import type { ClientRequest, IncomingMessage } from "node:http";
4
+ import type { IExOptions, IHeaders, IHttpClient } from "soap";
5
+ import { parseXml, validate, validateSubtree } from "xml-xsd-engine";
6
+ import type { SoapSchemaGraph } from "./soap-schema.js";
7
+
8
+ export const maximumSoapBytes = 65_536;
9
+
10
+ export class ValidatingHttpClient implements IHttpClient {
11
+ readonly #endpoint: URL;
12
+ readonly #schema: SoapSchemaGraph;
13
+ #validation = Promise.resolve();
14
+
15
+ constructor(endpoint: URL, schema: SoapSchemaGraph) {
16
+ this.#endpoint = endpoint;
17
+ this.#schema = schema;
18
+ }
19
+
20
+ request(
21
+ requestUrl: string,
22
+ data: unknown,
23
+ callback: Parameters<IHttpClient["request"]>[2],
24
+ headers: IHeaders = {},
25
+ options: IExOptions = {},
26
+ ): ReturnType<IHttpClient["request"]> {
27
+ const body = Buffer.isBuffer(data) ? data : Buffer.from(String(data ?? ""));
28
+ const pending = new Promise((resolve) => {
29
+ let finished = false;
30
+ let deadline: ReturnType<typeof setTimeout> | undefined;
31
+ const fail = (error: Error, request?: ClientRequest) => {
32
+ if (finished) return;
33
+ finished = true;
34
+ clearTimeout(deadline);
35
+ request?.destroy();
36
+ callback(error);
37
+ resolve({});
38
+ };
39
+ const succeed = (result: object, responseBody: string) => {
40
+ if (finished) return;
41
+ finished = true;
42
+ clearTimeout(deadline);
43
+ callback(null, result, responseBody);
44
+ resolve(result);
45
+ };
46
+ let target: URL;
47
+ try {
48
+ target = new URL(requestUrl);
49
+ if (target.href !== this.#endpoint.href) throw new Error("Blocked SOAP destination");
50
+ if (body.byteLength > maximumSoapBytes) throw new Error("SOAP request too large");
51
+ } catch (error) {
52
+ fail(error instanceof Error ? error : new Error("Invalid SOAP destination"));
53
+ return;
54
+ }
55
+ const transport = target.protocol === "https:" ? requestHttps : requestHttp;
56
+ const request = transport(target, {
57
+ method: "POST",
58
+ headers: Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)])),
59
+ signal: options.signal instanceof AbortSignal ? options.signal : undefined,
60
+ }, (response) => this.#receive(response, body, succeed, fail, request));
61
+ deadline = setTimeout(() => fail(new Error("SOAP request timed out"), request), 1_500);
62
+ request.once("error", (error) => fail(error, request));
63
+ request.end(body);
64
+ });
65
+ return pending as ReturnType<IHttpClient["request"]>;
66
+ }
67
+
68
+ #receive(
69
+ response: IncomingMessage,
70
+ requestBody: Buffer,
71
+ succeed: (result: object, body: string) => void,
72
+ fail: (error: Error, request?: ClientRequest) => void,
73
+ request: ClientRequest,
74
+ ) {
75
+ const status = response.statusCode ?? 500;
76
+ if (status < 200 || status >= 300) {
77
+ fail(new Error(
78
+ status >= 300 && status < 400 ? "SOAP redirects are blocked" : "SOAP request failed",
79
+ ), request);
80
+ return;
81
+ }
82
+ const chunks: Buffer[] = [];
83
+ let bytes = 0;
84
+ response.on("data", (chunk: Buffer) => {
85
+ bytes += chunk.byteLength;
86
+ if (bytes > maximumSoapBytes) fail(new Error("SOAP response too large"), request);
87
+ else chunks.push(chunk);
88
+ });
89
+ response.once("error", (error) => fail(error, request));
90
+ response.once("end", () => {
91
+ if (bytes > maximumSoapBytes) return;
92
+ const body = Buffer.concat(chunks).toString("utf8");
93
+ const validation = this.#validation.then(() => this.#validate(body));
94
+ this.#validation = validation.catch(() => {});
95
+ void validation.then(() => {
96
+ const result = {
97
+ config: { data: requestBody, headers: {} },
98
+ data: body,
99
+ headers: response.headers,
100
+ status: response.statusCode ?? 500,
101
+ statusText: response.statusMessage ?? "",
102
+ };
103
+ succeed(result, body);
104
+ }, (error: unknown) => fail(error instanceof Error ? error : new Error("Invalid SOAP response"), request));
105
+ });
106
+ }
107
+
108
+ #validate(source: string) {
109
+ if (/<!DOCTYPE|<!ENTITY/i.test(source)) throw new Error("DTD and entity declarations are blocked");
110
+ const document = parseXml(source, {
111
+ expandDtdEntities: false,
112
+ maxAttributes: 32,
113
+ maxDepth: 32,
114
+ maxNodeCount: 256,
115
+ maxTextLength: 8_192,
116
+ });
117
+ if (!validate(document, this.#schema.envelope, { mode: "strict" }).valid) {
118
+ throw new Error("SOAP response failed XSD validation");
119
+ }
120
+ const response = document.root?.getChild("Body")?.getChild("GetPeaResponse");
121
+ if (!response || !validateSubtree(response, this.#schema.service, { mode: "strict" }).valid) {
122
+ throw new Error("SOAP response failed XSD validation");
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,250 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import test from "node:test";
4
+
5
+ import { startMcpServer } from "@emseepea/testing";
6
+ import { createSoapExample } from "../dist/app.js";
7
+ import { artifactsFromSources } from "../scripts/generate-types.mjs";
8
+ import { parseSoapEnvelopeSchema } from "../dist/soap-schema.js";
9
+ import { maximumSoapBytes, ValidatingHttpClient } from "../dist/validating-http-client.js";
10
+ import { startSoapFixture } from "../test-support/soap-fixture.mjs";
11
+
12
+ const invalidSoapResponses = [
13
+ "Invalid XML",
14
+ "Invalid XSD",
15
+ "Zero Days",
16
+ "Negative Days",
17
+ "Fractional Days",
18
+ "Empty Type",
19
+ "Long Type",
20
+ "Too Many Traits",
21
+ "Missing Required Field",
22
+ "Wrong Namespace",
23
+ "DTD",
24
+ "Entity",
25
+ "Too Deep",
26
+ "Too Many Nodes",
27
+ "Too Many Attributes",
28
+ "Text Too Long",
29
+ "Oversized",
30
+ ];
31
+ const resourceLimitFailures = new Map([
32
+ ["Too Deep", /nesting depth \(32\) exceeded/],
33
+ ["Too Many Nodes", /node count exceeds limit \(256\)/],
34
+ ["Too Many Attributes", /Too many attributes.+limit: 32/],
35
+ ["Text Too Long", /Text node exceeds maximum allowed length.+8192/],
36
+ ["Oversized", /SOAP response too large/],
37
+ ]);
38
+
39
+ test("retrieves XSD-validated SOAP data through a described MCP schema", async (t) => {
40
+ const fixture = await startSoapFixture(t);
41
+ const running = await startMcpServer(t, new URL("../dist/server.js", import.meta.url), {
42
+ environment: { PEA_SOAP_URL: fixture.url.href },
43
+ });
44
+ const client = await running.connect();
45
+ const { tools } = await client.listTools();
46
+ assert.deepEqual(tools.map(({ name }) => name), ["get-pea-variety"]);
47
+ const [tool] = tools;
48
+ assert.equal(tool.inputSchema.properties.name.description, "Name of the pea variety to retrieve.");
49
+ for (const property of ["name", "peaType", "daysToMaturity", "note", "traits"]) {
50
+ assert.ok(tool.outputSchema.properties[property].description);
51
+ }
52
+ assert.equal(tool.outputSchema.properties.traits.maxItems, 5);
53
+
54
+ const result = await client.callTool({ name: "get-pea-variety", arguments: { name: "Sugar Ann" } });
55
+ assert.equal(result.isError, false);
56
+ assert.deepEqual(result.structuredContent, {
57
+ name: "Sugar Ann",
58
+ peaType: "snap",
59
+ daysToMaturity: 56,
60
+ note: "Compact plants with edible pods.",
61
+ traits: ["bush", "early"],
62
+ });
63
+ assert.equal(fixture.requests, 1);
64
+ assert.equal(fixture.lastRequest.method, "POST");
65
+ assert.equal(fixture.lastRequest.path, "/soap");
66
+ assert.match(fixture.lastRequest.soapAction, /GetPea/);
67
+ assert.match(fixture.lastRequest.body, /<(?:\w+:)?GetPeaRequest(?:\s|>)/);
68
+ assert.match(fixture.lastRequest.body, /<(?:\w+:)?name>Sugar Ann<\/(?:\w+:)?name>/);
69
+
70
+ const zeroTraits = await client.callTool({
71
+ name: "get-pea-variety",
72
+ arguments: { name: "Boundary Zero Traits" },
73
+ });
74
+ assert.deepEqual(zeroTraits.structuredContent, {
75
+ name: "Boundary Zero Traits",
76
+ peaType: "x",
77
+ daysToMaturity: 1,
78
+ traits: [],
79
+ });
80
+
81
+ const fiveTraits = await client.callTool({
82
+ name: "get-pea-variety",
83
+ arguments: { name: "Boundary Five Traits" },
84
+ });
85
+ assert.deepEqual(fiveTraits.structuredContent, {
86
+ name: "Boundary Five Traits",
87
+ peaType: "x".repeat(40),
88
+ daysToMaturity: 1,
89
+ traits: ["1", "2", "3", "4", "5"],
90
+ });
91
+ });
92
+
93
+ test("rejects unsafe and invalid SOAP responses with generic public failures", async (t) => {
94
+ const fixture = await startSoapFixture(t);
95
+ const running = await startMcpServer(t, new URL("../dist/server.js", import.meta.url), {
96
+ environment: { PEA_SOAP_URL: fixture.url.href },
97
+ });
98
+ const client = await running.connect();
99
+ for (const name of [
100
+ "SOAP Fault",
101
+ ...invalidSoapResponses,
102
+ "Valid Error Status",
103
+ "Redirect",
104
+ "Timeout",
105
+ "Trickle",
106
+ ]) {
107
+ const result = await client.callTool({ name: "get-pea-variety", arguments: { name } });
108
+ assert.equal(result.isError, true, name);
109
+ assert.equal(result.content[0].text, "Tool execution failed", name);
110
+ assert.doesNotMatch(JSON.stringify(result), /Private fixture detail|DOCTYPE|ENTITY|example\.com|ECONN/i, name);
111
+ }
112
+ });
113
+
114
+ test("invalid responses stop before the SOAP parser callback and mapper", async (t) => {
115
+ const fixture = await startSoapFixture(t);
116
+ const { schema } = await schemaSources();
117
+ const client = new ValidatingHttpClient(fixture.url, schema);
118
+ for (const name of invalidSoapResponses) {
119
+ const delivered = await rawTransportCall(client, fixture.url.href, name);
120
+ assert.ok(delivered.error instanceof Error, name);
121
+ if (resourceLimitFailures.has(name)) {
122
+ assert.match(delivered.error.message, resourceLimitFailures.get(name), name);
123
+ }
124
+ assert.equal(delivered.result, undefined, name);
125
+ assert.equal(delivered.body, undefined, name);
126
+ }
127
+ });
128
+
129
+ test("the transport blocks a different destination before sending", async (t) => {
130
+ const fixture = await startSoapFixture(t);
131
+ const { schema } = await schemaSources();
132
+ const client = new ValidatingHttpClient(fixture.url, schema);
133
+ const before = fixture.requests;
134
+ const error = await new Promise((resolve) => {
135
+ client.request("http://example.com/soap", "<request/>", (failure) => resolve(failure));
136
+ });
137
+ assert.ok(error instanceof Error);
138
+ assert.equal(fixture.requests, before);
139
+ await assert.rejects(createSoapExample("ftp://example.com/soap"));
140
+ });
141
+
142
+ test("the transport enforces an absolute deadline even while bytes arrive", { timeout: 3_000 }, async (t) => {
143
+ const fixture = await startSoapFixture(t);
144
+ const { schema } = await schemaSources();
145
+ const started = performance.now();
146
+ const delivered = await rawTransportCall(
147
+ new ValidatingHttpClient(fixture.url, schema),
148
+ fixture.url.href,
149
+ "Trickle",
150
+ );
151
+ assert.ok(delivered.error instanceof Error);
152
+ assert.match(delivered.error.message, /timed out/);
153
+ assert.ok(performance.now() - started < 2_000);
154
+ assert.equal(delivered.body, undefined);
155
+ });
156
+
157
+ test("schema loading is local-only and generated artefacts preserve their contract layers", async () => {
158
+ const { envelope, service } = await schemaSources();
159
+ await assert.rejects(parseSoapEnvelopeSchema(
160
+ envelope.replace("pea-service.xsd", "https://example.com/remote.xsd"),
161
+ service,
162
+ ));
163
+ const generated = await readFile(new URL("../src/generated/pea-service.ts", import.meta.url), "utf8");
164
+ assert.match(generated, /name: string;/);
165
+ assert.match(generated, /daysToMaturity: number;/);
166
+ assert.match(generated, /note\?: string;/);
167
+ assert.match(generated, /trait\?: string\[\];/);
168
+ const mapperType = generated.match(/export interface PeaVariety \{[\s\S]*?\n\}/)?.[0] ?? "";
169
+ assert.doesNotMatch(mapperType, /unknown/);
170
+ const model = JSON.parse(await readFile(
171
+ new URL("../src/generated/pea-service.schema-model.json", import.meta.url),
172
+ "utf8",
173
+ ));
174
+ assert.deepEqual(model.simpleTypes["{urn:emseepea:pea-service}PeaType"].facets, [
175
+ { kind: "minLength", value: "1" },
176
+ { kind: "maxLength", value: "40" },
177
+ ]);
178
+ const particles = model.complexTypes["{urn:emseepea:pea-service}PeaVariety"].particle.particles;
179
+ assert.deepEqual(particles.find(({ element }) => element === "trait").occurrence, { min: 0, max: 5 });
180
+ });
181
+
182
+ test("the response cap is below the total network ceiling", () => {
183
+ assert.equal(maximumSoapBytes, 64 * 1_024);
184
+ assert.equal(maximumSoapBytes * 2, 128 * 1_024);
185
+ });
186
+
187
+ test("an oversized request is rejected before the fixture receives it", async (t) => {
188
+ const fixture = await startSoapFixture(t);
189
+ const { schema } = await schemaSources();
190
+ const before = fixture.requests;
191
+ const delivered = await new Promise((resolve) => {
192
+ new ValidatingHttpClient(fixture.url, schema).request(
193
+ fixture.url.href,
194
+ "x".repeat(maximumSoapBytes + 1),
195
+ (error, result, body) => resolve({ error, result, body }),
196
+ );
197
+ });
198
+ assert.ok(delivered.error instanceof Error);
199
+ assert.match(delivered.error.message, /SOAP request too large/);
200
+ assert.equal(delivered.result, undefined);
201
+ assert.equal(delivered.body, undefined);
202
+ assert.equal(fixture.requests, before);
203
+ });
204
+
205
+ test("primitive and facet XSD changes affect the correct generated and runtime layers", async (t) => {
206
+ const fixture = await startSoapFixture(t);
207
+ const { envelope, service } = await schemaSources();
208
+ const original = await artifactsFromSources(envelope, service);
209
+
210
+ const primitiveService = service.replace('type="xs:positiveInteger"', 'type="xs:string"');
211
+ const primitive = await artifactsFromSources(envelope, primitiveService);
212
+ assert.match(original.types, /daysToMaturity: number;/);
213
+ assert.match(primitive.types, /daysToMaturity: string;/);
214
+ const primitiveTransport = new ValidatingHttpClient(
215
+ fixture.url,
216
+ await parseSoapEnvelopeSchema(envelope, primitiveService),
217
+ );
218
+ const primitiveResult = await rawTransportCall(primitiveTransport, fixture.url.href, "Invalid XSD");
219
+ assert.equal(primitiveResult.error, null);
220
+ assert.ok(primitiveResult.body);
221
+
222
+ const facetService = service.replace('<xs:maxLength value="40"/>', '<xs:maxLength value="3"/>');
223
+ const facet = await artifactsFromSources(envelope, facetService);
224
+ assert.equal(facet.types, original.types);
225
+ assert.notEqual(facet.model, original.model);
226
+ const facetTransport = new ValidatingHttpClient(
227
+ fixture.url,
228
+ await parseSoapEnvelopeSchema(envelope, facetService),
229
+ );
230
+ const facetResult = await rawTransportCall(facetTransport, fixture.url.href, "Facet Mutation");
231
+ assert.ok(facetResult.error instanceof Error);
232
+ assert.equal(facetResult.body, undefined);
233
+ });
234
+
235
+ async function schemaSources() {
236
+ const envelope = await readFile(new URL("../contracts/soap-envelope.xsd", import.meta.url), "utf8");
237
+ const service = await readFile(new URL("../contracts/pea-service.xsd", import.meta.url), "utf8");
238
+ return {
239
+ envelope,
240
+ service,
241
+ schema: await parseSoapEnvelopeSchema(envelope, service),
242
+ };
243
+ }
244
+
245
+ function rawTransportCall(client, url, name) {
246
+ const request = `<GetPeaRequest><name>${name}</name></GetPeaRequest>`;
247
+ return new Promise((resolve) => {
248
+ client.request(url, request, (error, result, body) => resolve({ error, result, body }));
249
+ });
250
+ }