@webpieces/http-server 0.3.234 → 0.3.235
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/package.json +2 -2
- package/src/InProcessApiClientFactory.d.ts +34 -0
- package/src/InProcessApiClientFactory.js +77 -0
- package/src/InProcessApiClientFactory.js.map +1 -0
- package/src/WebpiecesRouteCreator.d.ts +90 -0
- package/src/WebpiecesRouteCreator.js +157 -0
- package/src/WebpiecesRouteCreator.js.map +1 -0
- package/src/WebpiecesServerImpl.d.ts +2 -10
- package/src/WebpiecesServerImpl.js +12 -84
- package/src/WebpiecesServerImpl.js.map +1 -1
- package/src/filters/ContextFilter.d.ts +3 -3
- package/src/filters/ContextFilter.js +9 -15
- package/src/filters/ContextFilter.js.map +1 -1
- package/src/filters/LogApiFilter.d.ts +2 -2
- package/src/filters/LogApiFilter.js +8 -10
- package/src/filters/LogApiFilter.js.map +1 -1
- package/src/filters/RecordingFilter.d.ts +30 -0
- package/src/filters/RecordingFilter.js +91 -0
- package/src/filters/RecordingFilter.js.map +1 -0
- package/src/headers/WebpiecesCoreHeaders.d.ts +4 -37
- package/src/headers/WebpiecesCoreHeaders.js +5 -48
- package/src/headers/WebpiecesCoreHeaders.js.map +1 -1
- package/src/index.d.ts +8 -1
- package/src/index.js +18 -1
- package/src/index.js.map +1 -1
- package/src/modules/WebpiecesModule.js +9 -0
- package/src/modules/WebpiecesModule.js.map +1 -1
- package/src/recorder/SpecGenerator.d.ts +16 -0
- package/src/recorder/SpecGenerator.js +70 -0
- package/src/recorder/SpecGenerator.js.map +1 -0
- package/src/recorder/TestCaseRecorderImpl.d.ts +33 -0
- package/src/recorder/TestCaseRecorderImpl.js +75 -0
- package/src/recorder/TestCaseRecorderImpl.js.map +1 -0
- package/src/recorder/recordable.d.ts +16 -0
- package/src/recorder/recordable.js +52 -0
- package/src/recorder/recordable.js.map +1 -0
|
@@ -31,6 +31,15 @@ exports.WebpiecesModule = new inversify_1.ContainerModule((options) => {
|
|
|
31
31
|
// Bind extension for multiInject collection
|
|
32
32
|
// WebpiecesMiddleware will collect all PlatformHeadersExtension bindings via @multiInject
|
|
33
33
|
bind(http_api_1.HEADER_TYPES.PlatformHeadersExtension).toConstantValue(coreExtension);
|
|
34
|
+
// HeaderRegistry: the single source of truth for all platform headers.
|
|
35
|
+
// Collects EVERY module's PlatformHeadersExtension at first resolution and
|
|
36
|
+
// fail-fast validates duplicates/conflicts (port of Java HeaderTranslation).
|
|
37
|
+
bind(http_api_1.HeaderRegistry)
|
|
38
|
+
.toDynamicValue((ctx) => {
|
|
39
|
+
const extensions = ctx.getAll(http_api_1.HEADER_TYPES.PlatformHeadersExtension);
|
|
40
|
+
return new http_api_1.HeaderRegistry(extensions);
|
|
41
|
+
})
|
|
42
|
+
.inSingletonScope();
|
|
34
43
|
console.log(`[WebpiecesModule] Registered core platform headers extension with ${coreExtension.headers.length} headers`);
|
|
35
44
|
});
|
|
36
45
|
//# sourceMappingURL=WebpiecesModule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesModule.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/modules/WebpiecesModule.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"WebpiecesModule.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/modules/WebpiecesModule.ts"],"names":[],"mappings":";;;AAAA,yCAA+D;AAC/D,kDAA4G;AAC5G,0EAAuE;AAEvE;;;;;;;;;;;;;;;;;GAiBG;AACU,QAAA,eAAe,GAAG,IAAI,2BAAe,CAAC,CAAC,OAAO,EAAE,EAAE;IAC3D,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;IAEzB,qEAAqE;IACrE,IAAI,CAAgB,wBAAa,CAAC,CAAC,MAAM,EAAE,CAAC,gBAAgB,EAAE,CAAC;IAE/D,qCAAqC;IACrC,MAAM,aAAa,GAAG,IAAI,mCAAwB,CAAC,2CAAoB,CAAC,aAAa,EAAE,CAAC,CAAC;IAEzF,4CAA4C;IAC5C,0FAA0F;IAC1F,IAAI,CAA2B,uBAAY,CAAC,wBAAwB,CAAC,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;IAErG,uEAAuE;IACvE,2EAA2E;IAC3E,6EAA6E;IAC7E,IAAI,CAAiB,yBAAc,CAAC;SAC/B,cAAc,CAAC,CAAC,GAAsB,EAAE,EAAE;QACvC,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,CAA2B,uBAAY,CAAC,wBAAwB,CAAC,CAAC;QAC/F,OAAO,IAAI,yBAAc,CAAC,UAAU,CAAC,CAAC;IAC1C,CAAC,CAAC;SACD,gBAAgB,EAAE,CAAC;IAExB,OAAO,CAAC,GAAG,CAAC,qEAAqE,aAAa,CAAC,OAAO,CAAC,MAAM,UAAU,CAAC,CAAC;AAC7H,CAAC,CAAC,CAAC","sourcesContent":["import { ContainerModule, ResolutionContext } from 'inversify';\nimport { HEADER_TYPES, PlatformHeadersExtension, HeaderMethods, HeaderRegistry } from '@webpieces/http-api';\nimport { WebpiecesCoreHeaders } from '../headers/WebpiecesCoreHeaders';\n\n/**\n * WebpiecesModule - Framework-level DI bindings.\n *\n * This module is loaded by WebpiecesFactory BEFORE application modules.\n * It provides core framework services including platform headers.\n *\n * Platform Headers Pattern (Extension):\n * - Create PlatformHeadersExtension with array of headers\n * - Bind extension to HEADER_TYPES.PlatformHeadersExtension symbol\n * - Multiple modules bind their own extensions\n * - Consumer uses @multiInject to collect all extensions\n * - Pattern inspired by Guice Multibinder\n *\n * Module Loading Order:\n * 1. WebpiecesModule (framework headers) ← YOU ARE HERE\n * 2. CompanyModule (company-wide headers)\n * 3. InversifyModule (app-specific headers)\n */\nexport const WebpiecesModule = new ContainerModule((options) => {\n const { bind } = options;\n\n // Bind HeaderMethods as singleton (stateless utility, can be shared)\n bind<HeaderMethods>(HeaderMethods).toSelf().inSingletonScope();\n\n // Create extension with core headers\n const coreExtension = new PlatformHeadersExtension(WebpiecesCoreHeaders.getAllHeaders());\n\n // Bind extension for multiInject collection\n // WebpiecesMiddleware will collect all PlatformHeadersExtension bindings via @multiInject\n bind<PlatformHeadersExtension>(HEADER_TYPES.PlatformHeadersExtension).toConstantValue(coreExtension);\n\n // HeaderRegistry: the single source of truth for all platform headers.\n // Collects EVERY module's PlatformHeadersExtension at first resolution and\n // fail-fast validates duplicates/conflicts (port of Java HeaderTranslation).\n bind<HeaderRegistry>(HeaderRegistry)\n .toDynamicValue((ctx: ResolutionContext) => {\n const extensions = ctx.getAll<PlatformHeadersExtension>(HEADER_TYPES.PlatformHeadersExtension);\n return new HeaderRegistry(extensions);\n })\n .inSingletonScope();\n\n console.log(`[WebpiecesModule] Registered core platform headers extension with ${coreExtension.headers.length} headers`);\n});\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { RecordedTestCase } from '@webpieces/http-api';
|
|
2
|
+
/**
|
|
3
|
+
* SpecGenerator - Deterministic template that turns a RecordedTestCase into a
|
|
4
|
+
* small vitest spec (~30 lines) which loads the fixture JSON, primes a
|
|
5
|
+
* createMock per downstream api, invokes the endpoint in-process, and
|
|
6
|
+
* deep-equal asserts the response + the requests each mock received.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NO deep reflection over DTOs (the fragile part of the Java
|
|
9
|
+
* TestCaseRecorderImpl codegen) - all data lives in the fixture; the spec is
|
|
10
|
+
* pure plumbing. The fixture is also a stable artifact for an AI to write a
|
|
11
|
+
* richer spec from.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SpecGenerator {
|
|
14
|
+
generate(testCase: RecordedTestCase, fixtureFileName: string): string;
|
|
15
|
+
private uniqueApiNames;
|
|
16
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SpecGenerator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* SpecGenerator - Deterministic template that turns a RecordedTestCase into a
|
|
6
|
+
* small vitest spec (~30 lines) which loads the fixture JSON, primes a
|
|
7
|
+
* createMock per downstream api, invokes the endpoint in-process, and
|
|
8
|
+
* deep-equal asserts the response + the requests each mock received.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately NO deep reflection over DTOs (the fragile part of the Java
|
|
11
|
+
* TestCaseRecorderImpl codegen) - all data lives in the fixture; the spec is
|
|
12
|
+
* pure plumbing. The fixture is also a stable artifact for an AI to write a
|
|
13
|
+
* richer spec from.
|
|
14
|
+
*/
|
|
15
|
+
class SpecGenerator {
|
|
16
|
+
generate(testCase, fixtureFileName) {
|
|
17
|
+
const endpoint = testCase.serverEndpoint;
|
|
18
|
+
const downstreamApis = this.uniqueApiNames(testCase.downstreamCalls);
|
|
19
|
+
const mockDecls = downstreamApis
|
|
20
|
+
.map((api) => ` const mock${api} = createMock<${api}>('${api}');`)
|
|
21
|
+
.join('\n');
|
|
22
|
+
const mockPrimes = testCase.downstreamCalls
|
|
23
|
+
.map((call, i) => call.failureResponse
|
|
24
|
+
? ` mock${call.apiName}.mock.addExceptionToThrow('${call.methodName}', () => new Error(fixture.downstreamCalls[${i}].failureResponse!.message));`
|
|
25
|
+
: ` mock${call.apiName}.mock.addValueToReturn('${call.methodName}', fixture.downstreamCalls[${i}].successResponse);`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
const mockAsserts = testCase.downstreamCalls
|
|
28
|
+
.map((call, i) => ` expect(mock${call.apiName}.mock.getSingleRequestList('${call.methodName}')[0]).toEqual(fixture.downstreamCalls[${i}].args[0]);`)
|
|
29
|
+
.join('\n');
|
|
30
|
+
return `import 'reflect-metadata';
|
|
31
|
+
import * as fs from 'fs';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import { createMock } from '@webpieces/core-mock';
|
|
34
|
+
import { RecordedTestCase } from '@webpieces/http-api';
|
|
35
|
+
// TODO(generated): import your ServerMeta, the api class, DI tokens, and the
|
|
36
|
+
// downstream api types, then wire the appOverrides ContainerModule below.
|
|
37
|
+
|
|
38
|
+
describe('${endpoint.apiName}.${endpoint.methodName} (recorded ${testCase.recordedAt})', () => {
|
|
39
|
+
const fixture: RecordedTestCase = JSON.parse(
|
|
40
|
+
fs.readFileSync(path.join(__dirname, '${fixtureFileName}'), 'utf-8'),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
it('replays the recorded request against mocked downstream apis', async () => {
|
|
44
|
+
${mockDecls}
|
|
45
|
+
${mockPrimes}
|
|
46
|
+
// TODO(generated): boot the server with the mocks rebound, e.g.:
|
|
47
|
+
// const overrides = new ContainerModule(async (options) => {
|
|
48
|
+
// (await options.rebind(TYPES.RemoteApi)).toConstantValue(mockRemoteApi);
|
|
49
|
+
// });
|
|
50
|
+
// const server = await WebpiecesFactory.create(new ProdServerMeta(), new WebpiecesConfig(), overrides);
|
|
51
|
+
// const api = server.createApiClient(${endpoint.apiName});
|
|
52
|
+
// const response = await api.${endpoint.methodName}(fixture.serverEndpoint.args[0]);
|
|
53
|
+
// expect(response).toEqual(fixture.serverEndpoint.successResponse);
|
|
54
|
+
${mockAsserts}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
uniqueApiNames(calls) {
|
|
60
|
+
const names = [];
|
|
61
|
+
for (const call of calls) {
|
|
62
|
+
if (!names.includes(call.apiName)) {
|
|
63
|
+
names.push(call.apiName);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return names;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.SpecGenerator = SpecGenerator;
|
|
70
|
+
//# sourceMappingURL=SpecGenerator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpecGenerator.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/recorder/SpecGenerator.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;GAUG;AACH,MAAa,aAAa;IACtB,QAAQ,CAAC,QAA0B,EAAE,eAAuB;QACxD,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC;QACzC,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC;QAErE,MAAM,SAAS,GAAG,cAAc;aAC3B,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,iBAAiB,GAAG,iBAAiB,GAAG,MAAM,GAAG,KAAK,CAAC;aAC5E,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,MAAM,UAAU,GAAG,QAAQ,CAAC,eAAe;aACtC,GAAG,CAAC,CAAC,IAAsB,EAAE,CAAS,EAAE,EAAE,CACvC,IAAI,CAAC,eAAe;YAChB,CAAC,CAAC,WAAW,IAAI,CAAC,OAAO,8BAA8B,IAAI,CAAC,UAAU,8CAA8C,CAAC,+BAA+B;YACpJ,CAAC,CAAC,WAAW,IAAI,CAAC,OAAO,2BAA2B,IAAI,CAAC,UAAU,8BAA8B,CAAC,qBAAqB,CAAC;aAC/H,IAAI,CAAC,IAAI,CAAC,CAAC;QAChB,MAAM,WAAW,GAAG,QAAQ,CAAC,eAAe;aACvC,GAAG,CAAC,CAAC,IAAsB,EAAE,CAAS,EAAE,EAAE,CACvC,kBAAkB,IAAI,CAAC,OAAO,+BAA+B,IAAI,CAAC,UAAU,0CAA0C,CAAC,aAAa,CAAC;aACxI,IAAI,CAAC,IAAI,CAAC,CAAC;QAEhB,OAAO;;;;;;;;YAQH,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,UAAU,cAAc,QAAQ,CAAC,UAAU;;gDAEpC,eAAe;;;;EAI7D,SAAS;EACT,UAAU;;;;;;gDAMoC,QAAQ,CAAC,OAAO;wCACxB,QAAQ,CAAC,UAAU;;EAEzD,WAAW;;;CAGZ,CAAC;IACE,CAAC;IAEO,cAAc,CAAC,KAAyB;QAC5C,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBAChC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7B,CAAC;QACL,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AA1DD,sCA0DC","sourcesContent":["import { RecordedTestCase, RecordedEndpoint } from '@webpieces/http-api';\n\n/**\n * SpecGenerator - Deterministic template that turns a RecordedTestCase into a\n * small vitest spec (~30 lines) which loads the fixture JSON, primes a\n * createMock per downstream api, invokes the endpoint in-process, and\n * deep-equal asserts the response + the requests each mock received.\n *\n * Deliberately NO deep reflection over DTOs (the fragile part of the Java\n * TestCaseRecorderImpl codegen) - all data lives in the fixture; the spec is\n * pure plumbing. The fixture is also a stable artifact for an AI to write a\n * richer spec from.\n */\nexport class SpecGenerator {\n generate(testCase: RecordedTestCase, fixtureFileName: string): string {\n const endpoint = testCase.serverEndpoint;\n const downstreamApis = this.uniqueApiNames(testCase.downstreamCalls);\n\n const mockDecls = downstreamApis\n .map((api: string) => ` const mock${api} = createMock<${api}>('${api}');`)\n .join('\\n');\n const mockPrimes = testCase.downstreamCalls\n .map((call: RecordedEndpoint, i: number) =>\n call.failureResponse\n ? ` mock${call.apiName}.mock.addExceptionToThrow('${call.methodName}', () => new Error(fixture.downstreamCalls[${i}].failureResponse!.message));`\n : ` mock${call.apiName}.mock.addValueToReturn('${call.methodName}', fixture.downstreamCalls[${i}].successResponse);`)\n .join('\\n');\n const mockAsserts = testCase.downstreamCalls\n .map((call: RecordedEndpoint, i: number) =>\n ` expect(mock${call.apiName}.mock.getSingleRequestList('${call.methodName}')[0]).toEqual(fixture.downstreamCalls[${i}].args[0]);`)\n .join('\\n');\n\n return `import 'reflect-metadata';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createMock } from '@webpieces/core-mock';\nimport { RecordedTestCase } from '@webpieces/http-api';\n// TODO(generated): import your ServerMeta, the api class, DI tokens, and the\n// downstream api types, then wire the appOverrides ContainerModule below.\n\ndescribe('${endpoint.apiName}.${endpoint.methodName} (recorded ${testCase.recordedAt})', () => {\n const fixture: RecordedTestCase = JSON.parse(\n fs.readFileSync(path.join(__dirname, '${fixtureFileName}'), 'utf-8'),\n );\n\n it('replays the recorded request against mocked downstream apis', async () => {\n${mockDecls}\n${mockPrimes}\n // TODO(generated): boot the server with the mocks rebound, e.g.:\n // const overrides = new ContainerModule(async (options) => {\n // (await options.rebind(TYPES.RemoteApi)).toConstantValue(mockRemoteApi);\n // });\n // const server = await WebpiecesFactory.create(new ProdServerMeta(), new WebpiecesConfig(), overrides);\n // const api = server.createApiClient(${endpoint.apiName});\n // const response = await api.${endpoint.methodName}(fixture.serverEndpoint.args[0]);\n // expect(response).toEqual(fixture.serverEndpoint.successResponse);\n${mockAsserts}\n });\n});\n`;\n }\n\n private uniqueApiNames(calls: RecordedEndpoint[]): string[] {\n const names: string[] = [];\n for (const call of calls) {\n if (!names.includes(call.apiName)) {\n names.push(call.apiName);\n }\n }\n return names;\n }\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RecordedEndpoint, RecordedTestCase, TestCaseRecorder } from '@webpieces/http-api';
|
|
2
|
+
/**
|
|
3
|
+
* TestCaseRecorderImpl - Server-side recorder (port of Java
|
|
4
|
+
* TestCaseRecorderImpl, minus the fragile bean-reflection codegen).
|
|
5
|
+
*
|
|
6
|
+
* One instance per recorded request (created by RecordingFilter and placed in
|
|
7
|
+
* the RequestContext under RecorderKeys.RECORDER). Downstream hooks - the
|
|
8
|
+
* http-client proxy and recordable() wrappers - add every call they make.
|
|
9
|
+
*
|
|
10
|
+
* spitOutTestCase() emits:
|
|
11
|
+
* - a diffable JSON FIXTURE (the stable artifact: request, ctx snapshot,
|
|
12
|
+
* response, all downstream calls) - also perfect input for an AI to write
|
|
13
|
+
* a richer spec from
|
|
14
|
+
* - a small deterministic .spec.ts from SpecGenerator
|
|
15
|
+
* Both are always logged; written to recordingDir when configured.
|
|
16
|
+
* NEVER breaks production - the whole body is caught and logged.
|
|
17
|
+
*/
|
|
18
|
+
export declare class TestCaseRecorderImpl implements TestCaseRecorder {
|
|
19
|
+
private downstreamCalls;
|
|
20
|
+
private serializer;
|
|
21
|
+
private specGenerator;
|
|
22
|
+
addEndpointInfo(info: RecordedEndpoint): void;
|
|
23
|
+
getLastEndpointInfo(): RecordedEndpoint | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Emit the recorded test case. Called by RecordingFilter in its finally.
|
|
26
|
+
*
|
|
27
|
+
* @param serverEndpoint - The inbound endpoint capture (request + response)
|
|
28
|
+
* @param recordingDir - Directory to write fixture + spec files (optional)
|
|
29
|
+
* @returns The built RecordedTestCase, or undefined if emission failed
|
|
30
|
+
*/
|
|
31
|
+
spitOutTestCase(serverEndpoint: RecordedEndpoint, recordingDir?: string): RecordedTestCase | undefined;
|
|
32
|
+
private buildBaseName;
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TestCaseRecorderImpl = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const http_api_1 = require("@webpieces/http-api");
|
|
8
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
9
|
+
const SpecGenerator_1 = require("./SpecGenerator");
|
|
10
|
+
/**
|
|
11
|
+
* TestCaseRecorderImpl - Server-side recorder (port of Java
|
|
12
|
+
* TestCaseRecorderImpl, minus the fragile bean-reflection codegen).
|
|
13
|
+
*
|
|
14
|
+
* One instance per recorded request (created by RecordingFilter and placed in
|
|
15
|
+
* the RequestContext under RecorderKeys.RECORDER). Downstream hooks - the
|
|
16
|
+
* http-client proxy and recordable() wrappers - add every call they make.
|
|
17
|
+
*
|
|
18
|
+
* spitOutTestCase() emits:
|
|
19
|
+
* - a diffable JSON FIXTURE (the stable artifact: request, ctx snapshot,
|
|
20
|
+
* response, all downstream calls) - also perfect input for an AI to write
|
|
21
|
+
* a richer spec from
|
|
22
|
+
* - a small deterministic .spec.ts from SpecGenerator
|
|
23
|
+
* Both are always logged; written to recordingDir when configured.
|
|
24
|
+
* NEVER breaks production - the whole body is caught and logged.
|
|
25
|
+
*/
|
|
26
|
+
class TestCaseRecorderImpl {
|
|
27
|
+
downstreamCalls = [];
|
|
28
|
+
serializer = new http_api_1.RecordSerializer();
|
|
29
|
+
specGenerator = new SpecGenerator_1.SpecGenerator();
|
|
30
|
+
addEndpointInfo(info) {
|
|
31
|
+
this.downstreamCalls.push(info);
|
|
32
|
+
}
|
|
33
|
+
getLastEndpointInfo() {
|
|
34
|
+
return this.downstreamCalls[this.downstreamCalls.length - 1];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Emit the recorded test case. Called by RecordingFilter in its finally.
|
|
38
|
+
*
|
|
39
|
+
* @param serverEndpoint - The inbound endpoint capture (request + response)
|
|
40
|
+
* @param recordingDir - Directory to write fixture + spec files (optional)
|
|
41
|
+
* @returns The built RecordedTestCase, or undefined if emission failed
|
|
42
|
+
*/
|
|
43
|
+
spitOutTestCase(serverEndpoint, recordingDir) {
|
|
44
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- recording must NEVER break production requests
|
|
45
|
+
try {
|
|
46
|
+
const testCase = new http_api_1.RecordedTestCase(serverEndpoint, this.downstreamCalls, new Date().toISOString());
|
|
47
|
+
const fixtureJson = this.serializer.serialize(testCase);
|
|
48
|
+
const baseName = this.buildBaseName(serverEndpoint, testCase.recordedAt);
|
|
49
|
+
const specSource = this.specGenerator.generate(testCase, `${baseName}.fixture.json`);
|
|
50
|
+
console.log(`[TestCaseRecorder] Recorded ${serverEndpoint.apiName}.${serverEndpoint.methodName} ` +
|
|
51
|
+
`(${this.downstreamCalls.length} downstream calls)\n` +
|
|
52
|
+
`--- fixture (${baseName}.fixture.json) ---\n${fixtureJson}\n` +
|
|
53
|
+
`--- generated spec (${baseName}.spec.ts) ---\n${specSource}`);
|
|
54
|
+
if (recordingDir) {
|
|
55
|
+
fs.mkdirSync(recordingDir, { recursive: true });
|
|
56
|
+
fs.writeFileSync(path.join(recordingDir, `${baseName}.fixture.json`), fixtureJson);
|
|
57
|
+
fs.writeFileSync(path.join(recordingDir, `${baseName}.spec.ts`), specSource);
|
|
58
|
+
console.log(`[TestCaseRecorder] Wrote fixture + spec to ${recordingDir}/${baseName}.*`);
|
|
59
|
+
}
|
|
60
|
+
return testCase;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const error = (0, core_util_1.toError)(err);
|
|
64
|
+
console.error('[TestCaseRecorder] Failed to emit test case (request unaffected):', error.message);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
buildBaseName(serverEndpoint, recordedAt) {
|
|
69
|
+
// 2026-07-04T10:22:33.123Z -> 2026-07-04T10-22-33 (filesystem-safe)
|
|
70
|
+
const stamp = recordedAt.replace(/:/g, '-').replace(/\..*$/, '');
|
|
71
|
+
return `${serverEndpoint.apiName}.${serverEndpoint.methodName}.${stamp}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.TestCaseRecorderImpl = TestCaseRecorderImpl;
|
|
75
|
+
//# sourceMappingURL=TestCaseRecorderImpl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestCaseRecorderImpl.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/recorder/TestCaseRecorderImpl.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,kDAK6B;AAC7B,oDAA+C;AAC/C,mDAAgD;AAEhD;;;;;;;;;;;;;;;GAeG;AACH,MAAa,oBAAoB;IACrB,eAAe,GAAuB,EAAE,CAAC;IACzC,UAAU,GAAG,IAAI,2BAAgB,EAAE,CAAC;IACpC,aAAa,GAAG,IAAI,6BAAa,EAAE,CAAC;IAE5C,eAAe,CAAC,IAAsB;QAClC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,mBAAmB;QACf,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;OAMG;IACH,eAAe,CAAC,cAAgC,EAAE,YAAqB;QACnE,gHAAgH;QAChH,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,2BAAgB,CAAC,cAAc,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;YACtG,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YAExD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;YACzE,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,QAAQ,eAAe,CAAC,CAAC;YAErF,OAAO,CAAC,GAAG,CAAC,+BAA+B,cAAc,CAAC,OAAO,IAAI,cAAc,CAAC,UAAU,GAAG;gBAC7F,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,sBAAsB;gBACrD,gBAAgB,QAAQ,uBAAuB,WAAW,IAAI;gBAC9D,uBAAuB,QAAQ,kBAAkB,UAAU,EAAE,CAAC,CAAC;YAEnE,IAAI,YAAY,EAAE,CAAC;gBACf,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAChD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,QAAQ,eAAe,CAAC,EAAE,WAAW,CAAC,CAAC;gBACnF,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,QAAQ,UAAU,CAAC,EAAE,UAAU,CAAC,CAAC;gBAC7E,OAAO,CAAC,GAAG,CAAC,8CAA8C,YAAY,IAAI,QAAQ,IAAI,CAAC,CAAC;YAC5F,CAAC;YAED,OAAO,QAAQ,CAAC;QACpB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,mEAAmE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YAClG,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAEO,aAAa,CAAC,cAAgC,EAAE,UAAkB;QACtE,oEAAoE;QACpE,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACjE,OAAO,GAAG,cAAc,CAAC,OAAO,IAAI,cAAc,CAAC,UAAU,IAAI,KAAK,EAAE,CAAC;IAC7E,CAAC;CACJ;AAtDD,oDAsDC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n RecordedEndpoint,\n RecordedTestCase,\n RecordSerializer,\n TestCaseRecorder,\n} from '@webpieces/http-api';\nimport { toError } from '@webpieces/core-util';\nimport { SpecGenerator } from './SpecGenerator';\n\n/**\n * TestCaseRecorderImpl - Server-side recorder (port of Java\n * TestCaseRecorderImpl, minus the fragile bean-reflection codegen).\n *\n * One instance per recorded request (created by RecordingFilter and placed in\n * the RequestContext under RecorderKeys.RECORDER). Downstream hooks - the\n * http-client proxy and recordable() wrappers - add every call they make.\n *\n * spitOutTestCase() emits:\n * - a diffable JSON FIXTURE (the stable artifact: request, ctx snapshot,\n * response, all downstream calls) - also perfect input for an AI to write\n * a richer spec from\n * - a small deterministic .spec.ts from SpecGenerator\n * Both are always logged; written to recordingDir when configured.\n * NEVER breaks production - the whole body is caught and logged.\n */\nexport class TestCaseRecorderImpl implements TestCaseRecorder {\n private downstreamCalls: RecordedEndpoint[] = [];\n private serializer = new RecordSerializer();\n private specGenerator = new SpecGenerator();\n\n addEndpointInfo(info: RecordedEndpoint): void {\n this.downstreamCalls.push(info);\n }\n\n getLastEndpointInfo(): RecordedEndpoint | undefined {\n return this.downstreamCalls[this.downstreamCalls.length - 1];\n }\n\n /**\n * Emit the recorded test case. Called by RecordingFilter in its finally.\n *\n * @param serverEndpoint - The inbound endpoint capture (request + response)\n * @param recordingDir - Directory to write fixture + spec files (optional)\n * @returns The built RecordedTestCase, or undefined if emission failed\n */\n spitOutTestCase(serverEndpoint: RecordedEndpoint, recordingDir?: string): RecordedTestCase | undefined {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- recording must NEVER break production requests\n try {\n const testCase = new RecordedTestCase(serverEndpoint, this.downstreamCalls, new Date().toISOString());\n const fixtureJson = this.serializer.serialize(testCase);\n\n const baseName = this.buildBaseName(serverEndpoint, testCase.recordedAt);\n const specSource = this.specGenerator.generate(testCase, `${baseName}.fixture.json`);\n\n console.log(`[TestCaseRecorder] Recorded ${serverEndpoint.apiName}.${serverEndpoint.methodName} ` +\n `(${this.downstreamCalls.length} downstream calls)\\n` +\n `--- fixture (${baseName}.fixture.json) ---\\n${fixtureJson}\\n` +\n `--- generated spec (${baseName}.spec.ts) ---\\n${specSource}`);\n\n if (recordingDir) {\n fs.mkdirSync(recordingDir, { recursive: true });\n fs.writeFileSync(path.join(recordingDir, `${baseName}.fixture.json`), fixtureJson);\n fs.writeFileSync(path.join(recordingDir, `${baseName}.spec.ts`), specSource);\n console.log(`[TestCaseRecorder] Wrote fixture + spec to ${recordingDir}/${baseName}.*`);\n }\n\n return testCase;\n } catch (err: unknown) {\n const error = toError(err);\n console.error('[TestCaseRecorder] Failed to emit test case (request unaffected):', error.message);\n return undefined;\n }\n }\n\n private buildBaseName(serverEndpoint: RecordedEndpoint, recordedAt: string): string {\n // 2026-07-04T10:22:33.123Z -> 2026-07-04T10-22-33 (filesystem-safe)\n const stamp = recordedAt.replace(/:/g, '-').replace(/\\..*$/, '');\n return `${serverEndpoint.apiName}.${serverEndpoint.methodName}.${stamp}`;\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recordable - Wrap an IN-PROCESS api implementation (simulator, local impl)
|
|
3
|
+
* so its calls are captured by an active TestCaseRecorder, exactly like
|
|
4
|
+
* outbound HTTP client calls are. Port of Java ApiRecorder/ApiRecorderCreator.
|
|
5
|
+
*
|
|
6
|
+
* When no recorder is in the request context (the normal case) calls pass
|
|
7
|
+
* straight through with no overhead beyond one context read.
|
|
8
|
+
*
|
|
9
|
+
* Use at DI-binding time:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* bind<RemoteApi>(TYPES.RemoteApi)
|
|
12
|
+
* .toDynamicValue(() => recordable('RemoteApi', new RemoteServiceSimulator()))
|
|
13
|
+
* .inSingletonScope();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function recordable<T extends object>(apiName: string, impl: T): T;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordable = recordable;
|
|
4
|
+
const http_api_1 = require("@webpieces/http-api");
|
|
5
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
/**
|
|
7
|
+
* recordable - Wrap an IN-PROCESS api implementation (simulator, local impl)
|
|
8
|
+
* so its calls are captured by an active TestCaseRecorder, exactly like
|
|
9
|
+
* outbound HTTP client calls are. Port of Java ApiRecorder/ApiRecorderCreator.
|
|
10
|
+
*
|
|
11
|
+
* When no recorder is in the request context (the normal case) calls pass
|
|
12
|
+
* straight through with no overhead beyond one context read.
|
|
13
|
+
*
|
|
14
|
+
* Use at DI-binding time:
|
|
15
|
+
* ```typescript
|
|
16
|
+
* bind<RemoteApi>(TYPES.RemoteApi)
|
|
17
|
+
* .toDynamicValue(() => recordable('RemoteApi', new RemoteServiceSimulator()))
|
|
18
|
+
* .inSingletonScope();
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function recordable(apiName, impl) {
|
|
22
|
+
return new Proxy(impl, {
|
|
23
|
+
// webpieces-disable no-any-unknown -- Proxy get trap returns heterogeneous members
|
|
24
|
+
get(target, prop, receiver) {
|
|
25
|
+
const original = Reflect.get(target, prop, receiver);
|
|
26
|
+
if (typeof original !== 'function' || typeof prop !== 'string') {
|
|
27
|
+
return original;
|
|
28
|
+
}
|
|
29
|
+
// webpieces-disable no-any-unknown -- api method args/returns are type-erased in the proxy
|
|
30
|
+
return async (...args) => {
|
|
31
|
+
const recorder = core_context_1.RequestContext.getHeader(http_api_1.RecorderKeys.RECORDER);
|
|
32
|
+
if (!recorder) {
|
|
33
|
+
return await original.apply(target, args);
|
|
34
|
+
}
|
|
35
|
+
const recorded = new http_api_1.RecordedEndpoint(apiName, prop, args);
|
|
36
|
+
recorder.addEndpointInfo(recorded);
|
|
37
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged
|
|
38
|
+
try {
|
|
39
|
+
const response = await original.apply(target, args);
|
|
40
|
+
recorded.successResponse = response;
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const error = (0, http_api_1.toError)(err);
|
|
45
|
+
recorded.failureResponse = new http_api_1.RecordedError(error.name, error.message);
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=recordable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordable.js","sourceRoot":"","sources":["../../../../../../packages/http/http-server/src/recorder/recordable.ts"],"names":[],"mappings":";;AAwBA,gCAgCC;AAxDD,kDAM6B;AAC7B,0DAAyD;AAEzD;;;;;;;;;;;;;;GAcG;AACH,SAAgB,UAAU,CAAmB,OAAe,EAAE,IAAO;IACjE,OAAO,IAAI,KAAK,CAAC,IAAI,EAAE;QACnB,mFAAmF;QACnF,GAAG,CAAC,MAAS,EAAE,IAAqB,EAAE,QAAiB;YACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YACrD,IAAI,OAAO,QAAQ,KAAK,UAAU,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC7D,OAAO,QAAQ,CAAC;YACpB,CAAC;YAED,2FAA2F;YAC3F,OAAO,KAAK,EAAE,GAAG,IAAe,EAAoB,EAAE;gBAClD,MAAM,QAAQ,GAAG,6BAAc,CAAC,SAAS,CAAC,uBAAY,CAAC,QAAQ,CAAiC,CAAC;gBACjG,IAAI,CAAC,QAAQ,EAAE,CAAC;oBACZ,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;gBAC9C,CAAC;gBAED,MAAM,QAAQ,GAAG,IAAI,2BAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;gBAC3D,QAAQ,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;gBAEnC,4HAA4H;gBAC5H,IAAI,CAAC;oBACD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;oBACpD,QAAQ,CAAC,eAAe,GAAG,QAAQ,CAAC;oBACpC,OAAO,QAAQ,CAAC;gBACpB,CAAC;gBAAC,OAAO,GAAY,EAAE,CAAC;oBACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;oBAC3B,QAAQ,CAAC,eAAe,GAAG,IAAI,wBAAa,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;oBACxE,MAAM,GAAG,CAAC;gBACd,CAAC;YACL,CAAC,CAAC;QACN,CAAC;KACJ,CAAC,CAAC;AACP,CAAC","sourcesContent":["import {\n RecordedEndpoint,\n RecordedError,\n RecorderKeys,\n TestCaseRecorder,\n toError,\n} from '@webpieces/http-api';\nimport { RequestContext } from '@webpieces/core-context';\n\n/**\n * recordable - Wrap an IN-PROCESS api implementation (simulator, local impl)\n * so its calls are captured by an active TestCaseRecorder, exactly like\n * outbound HTTP client calls are. Port of Java ApiRecorder/ApiRecorderCreator.\n *\n * When no recorder is in the request context (the normal case) calls pass\n * straight through with no overhead beyond one context read.\n *\n * Use at DI-binding time:\n * ```typescript\n * bind<RemoteApi>(TYPES.RemoteApi)\n * .toDynamicValue(() => recordable('RemoteApi', new RemoteServiceSimulator()))\n * .inSingletonScope();\n * ```\n */\nexport function recordable<T extends object>(apiName: string, impl: T): T {\n return new Proxy(impl, {\n // webpieces-disable no-any-unknown -- Proxy get trap returns heterogeneous members\n get(target: T, prop: string | symbol, receiver: unknown): unknown {\n const original = Reflect.get(target, prop, receiver);\n if (typeof original !== 'function' || typeof prop !== 'string') {\n return original;\n }\n\n // webpieces-disable no-any-unknown -- api method args/returns are type-erased in the proxy\n return async (...args: unknown[]): Promise<unknown> => {\n const recorder = RequestContext.getHeader(RecorderKeys.RECORDER) as TestCaseRecorder | undefined;\n if (!recorder) {\n return await original.apply(target, args);\n }\n\n const recorded = new RecordedEndpoint(apiName, prop, args);\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await original.apply(target, args);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n };\n },\n });\n}\n"]}
|