@asenajs/asena 0.7.1 → 0.8.1
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 +2 -2
- package/dist/lib/adapter/AsenaAdapter.d.ts +2 -2
- package/dist/lib/adapter/AsenaAdapter.js.map +1 -1
- package/dist/lib/adapter/types/ServeOptions.d.ts +16 -0
- package/dist/lib/adapter/types/ValidationError.d.ts +53 -0
- package/dist/lib/adapter/types/ValidationError.js +24 -0
- package/dist/lib/adapter/types/ValidationError.js.map +1 -0
- package/dist/lib/adapter/types/index.d.ts +1 -0
- package/dist/lib/adapter/types/index.js +1 -0
- package/dist/lib/adapter/types/index.js.map +1 -1
- package/dist/lib/ioc/Container.d.ts +25 -0
- package/dist/lib/ioc/Container.js +47 -0
- package/dist/lib/ioc/Container.js.map +1 -1
- package/dist/lib/ioc/CoreContainer.d.ts +5 -3
- package/dist/lib/ioc/CoreContainer.js +9 -3
- package/dist/lib/ioc/CoreContainer.js.map +1 -1
- package/dist/lib/ioc/IocEngine.d.ts +50 -4
- package/dist/lib/ioc/IocEngine.js +138 -12
- package/dist/lib/ioc/IocEngine.js.map +1 -1
- package/dist/lib/ioc/component/componentRegistry.d.ts +22 -0
- package/dist/lib/ioc/component/componentRegistry.js +119 -0
- package/dist/lib/ioc/component/componentRegistry.js.map +1 -0
- package/dist/lib/ioc/component/componentUtils.js +4 -0
- package/dist/lib/ioc/component/componentUtils.js.map +1 -1
- package/dist/lib/ioc/component/decorators/Inject.js +16 -0
- package/dist/lib/ioc/component/decorators/Inject.js.map +1 -1
- package/dist/lib/ioc/component/index.d.ts +1 -0
- package/dist/lib/ioc/component/index.js +1 -0
- package/dist/lib/ioc/component/index.js.map +1 -1
- package/dist/lib/ioc/constants/ComponentConstants.d.ts +4 -0
- package/dist/lib/ioc/constants/ComponentConstants.js +5 -0
- package/dist/lib/ioc/constants/ComponentConstants.js.map +1 -1
- package/dist/lib/ioc/helper/fileHelper.d.ts +9 -0
- package/dist/lib/ioc/helper/fileHelper.js +41 -11
- package/dist/lib/ioc/helper/fileHelper.js.map +1 -1
- package/dist/lib/ioc/types/ICoreServiceNamesType.d.ts +1 -0
- package/dist/lib/ioc/types/ICoreServiceNamesType.js +1 -0
- package/dist/lib/ioc/types/ICoreServiceNamesType.js.map +1 -1
- package/dist/lib/ioc/types/InjectableComponent.d.ts +10 -0
- package/dist/lib/ioc/types/IocConfig.d.ts +7 -0
- package/dist/lib/ioc/types/decorators/ComponentType.d.ts +2 -1
- package/dist/lib/ioc/types/decorators/ComponentType.js +1 -0
- package/dist/lib/ioc/types/decorators/ComponentType.js.map +1 -1
- package/dist/lib/server/AsenaServer.d.ts +47 -3
- package/dist/lib/server/AsenaServer.js +167 -28
- package/dist/lib/server/AsenaServer.js.map +1 -1
- package/dist/lib/server/AsenaServerFactory.d.ts +38 -3
- package/dist/lib/server/AsenaServerFactory.js +21 -2
- package/dist/lib/server/AsenaServerFactory.js.map +1 -1
- package/dist/lib/server/config/AsenaConfig.d.ts +82 -4
- package/dist/lib/server/config/AsenaConfig.js +39 -1
- package/dist/lib/server/config/AsenaConfig.js.map +1 -1
- package/dist/lib/server/decorators/components/MessageController.d.ts +49 -0
- package/dist/lib/server/decorators/components/MessageController.js +62 -0
- package/dist/lib/server/decorators/components/MessageController.js.map +1 -0
- package/dist/lib/server/decorators/components/index.d.ts +1 -0
- package/dist/lib/server/decorators/components/index.js +1 -0
- package/dist/lib/server/decorators/components/index.js.map +1 -1
- package/dist/lib/server/event/EventDispatchService.d.ts +3 -7
- package/dist/lib/server/event/EventDispatchService.js +9 -40
- package/dist/lib/server/event/EventDispatchService.js.map +1 -1
- package/dist/lib/server/event/PatternHandlerIndex.d.ts +53 -0
- package/dist/lib/server/event/PatternHandlerIndex.js +100 -0
- package/dist/lib/server/event/PatternHandlerIndex.js.map +1 -0
- package/dist/lib/server/event/decorators/On.d.ts +4 -0
- package/dist/lib/server/event/decorators/On.js +6 -0
- package/dist/lib/server/event/decorators/On.js.map +1 -1
- package/dist/lib/server/event/index.d.ts +1 -0
- package/dist/lib/server/event/index.js +2 -0
- package/dist/lib/server/event/index.js.map +1 -1
- package/dist/lib/server/event/types.d.ts +12 -0
- package/dist/lib/server/health/HealthServer.d.ts +26 -0
- package/dist/lib/server/health/HealthServer.js +63 -0
- package/dist/lib/server/health/HealthServer.js.map +1 -0
- package/dist/lib/server/health/index.d.ts +1 -0
- package/dist/lib/server/health/index.js +2 -0
- package/dist/lib/server/health/index.js.map +1 -0
- package/dist/lib/server/messaging/Ulak.d.ts +156 -0
- package/dist/lib/server/messaging/Ulak.js +174 -0
- package/dist/lib/server/messaging/Ulak.js.map +1 -1
- package/dist/lib/server/messaging/types.d.ts +5 -1
- package/dist/lib/server/messaging/types.js +5 -0
- package/dist/lib/server/messaging/types.js.map +1 -1
- package/dist/lib/server/microservice/InMemoryTransport.d.ts +45 -0
- package/dist/lib/server/microservice/InMemoryTransport.js +124 -0
- package/dist/lib/server/microservice/InMemoryTransport.js.map +1 -0
- package/dist/lib/server/microservice/MicroserviceTransport.d.ts +132 -0
- package/dist/lib/server/microservice/MicroserviceTransport.js +2 -0
- package/dist/lib/server/microservice/MicroserviceTransport.js.map +1 -0
- package/dist/lib/server/microservice/decorators/EventPattern.d.ts +38 -0
- package/dist/lib/server/microservice/decorators/EventPattern.js +59 -0
- package/dist/lib/server/microservice/decorators/EventPattern.js.map +1 -0
- package/dist/lib/server/microservice/decorators/MessagePattern.d.ts +31 -0
- package/dist/lib/server/microservice/decorators/MessagePattern.js +55 -0
- package/dist/lib/server/microservice/decorators/MessagePattern.js.map +1 -0
- package/dist/lib/server/microservice/decorators/index.d.ts +2 -0
- package/dist/lib/server/microservice/decorators/index.js +3 -0
- package/dist/lib/server/microservice/decorators/index.js.map +1 -0
- package/dist/lib/server/microservice/index.d.ts +5 -0
- package/dist/lib/server/microservice/index.js +11 -0
- package/dist/lib/server/microservice/index.js.map +1 -0
- package/dist/lib/server/microservice/interceptorChain.d.ts +20 -0
- package/dist/lib/server/microservice/interceptorChain.js +39 -0
- package/dist/lib/server/microservice/interceptorChain.js.map +1 -0
- package/dist/lib/server/microservice/types.d.ts +142 -0
- package/dist/lib/server/microservice/types.js +5 -0
- package/dist/lib/server/microservice/types.js.map +1 -0
- package/dist/lib/server/src/services/PrepareEventService.d.ts +4 -1
- package/dist/lib/server/src/services/PrepareEventService.js +10 -3
- package/dist/lib/server/src/services/PrepareEventService.js.map +1 -1
- package/dist/lib/server/src/services/PrepareMicroserviceService.d.ts +66 -0
- package/dist/lib/server/src/services/PrepareMicroserviceService.js +193 -0
- package/dist/lib/server/src/services/PrepareMicroserviceService.js.map +1 -0
- package/dist/lib/test/factory/deepMock.d.ts +25 -0
- package/dist/lib/test/factory/deepMock.js +105 -0
- package/dist/lib/test/factory/deepMock.js.map +1 -0
- package/dist/lib/test/factory/ulakStub.d.ts +44 -0
- package/dist/lib/test/factory/ulakStub.js +40 -0
- package/dist/lib/test/factory/ulakStub.js.map +1 -0
- package/dist/lib/test/harness/createTestApp.d.ts +33 -0
- package/dist/lib/test/harness/createTestApp.js +105 -0
- package/dist/lib/test/harness/createTestApp.js.map +1 -0
- package/dist/lib/test/harness/createWebTest.d.ts +28 -0
- package/dist/lib/test/harness/createWebTest.js +127 -0
- package/dist/lib/test/harness/createWebTest.js.map +1 -0
- package/dist/lib/test/harness/silentLogger.d.ts +27 -0
- package/dist/lib/test/harness/silentLogger.js +54 -0
- package/dist/lib/test/harness/silentLogger.js.map +1 -0
- package/dist/lib/test/harness/types.d.ts +147 -0
- package/dist/lib/test/harness/types.js +2 -0
- package/dist/lib/test/harness/types.js.map +1 -0
- package/dist/lib/test/http/TestHttpCall.d.ts +78 -0
- package/dist/lib/test/http/TestHttpCall.js +149 -0
- package/dist/lib/test/http/TestHttpCall.js.map +1 -0
- package/dist/lib/test/http/TestHttpResponse.d.ts +46 -0
- package/dist/lib/test/http/TestHttpResponse.js +70 -0
- package/dist/lib/test/http/TestHttpResponse.js.map +1 -0
- package/dist/lib/test/index.d.ts +9 -0
- package/dist/lib/test/index.js +8 -0
- package/dist/lib/test/index.js.map +1 -1
- package/dist/lib/test/metadata/discovery.d.ts +21 -0
- package/dist/lib/test/metadata/discovery.js +37 -8
- package/dist/lib/test/metadata/discovery.js.map +1 -1
- package/dist/lib/test/mockComponent.d.ts +25 -0
- package/dist/lib/test/mockComponent.js +39 -6
- package/dist/lib/test/mockComponent.js.map +1 -1
- package/dist/lib/test/types.d.ts +13 -0
- package/package.json +11 -7
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ComponentConstants, ComponentType, ICoreServiceNames } from '../../ioc';
|
|
2
|
+
import { getOwnTypedMetadata, getTypedMetadata } from '../../utils';
|
|
3
|
+
import { createMockFromClass } from '../factory/mockFactory';
|
|
4
|
+
import { discoverInjectedFieldsFromClass } from '../metadata/discovery';
|
|
5
|
+
import { createTestApp } from './createTestApp';
|
|
6
|
+
import { silentLogger } from './silentLogger';
|
|
7
|
+
/**
|
|
8
|
+
* The registered container name for a component
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
function componentName(Class) {
|
|
13
|
+
return getTypedMetadata(ComponentConstants.NameKey, Class) || Class.name;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Collects the classes the web layer cannot function without.
|
|
17
|
+
*
|
|
18
|
+
* Middlewares, validators and static-serve configs are resolved **by name from the
|
|
19
|
+
* container** at start-up (PrepareMiddlewareService / PrepareValidatorService) and throw
|
|
20
|
+
* when missing, so a slice test has to register them for real rather than mock them.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
function collectWebLayer(controllers) {
|
|
25
|
+
const webLayer = new Set(controllers);
|
|
26
|
+
for (const controller of controllers) {
|
|
27
|
+
for (const middleware of getTypedMetadata(ComponentConstants.MiddlewaresKey, controller) || []) {
|
|
28
|
+
webLayer.add(middleware);
|
|
29
|
+
}
|
|
30
|
+
const routes = getOwnTypedMetadata(ComponentConstants.RouteKey, controller) || {};
|
|
31
|
+
for (const params of Object.values(routes)) {
|
|
32
|
+
for (const middleware of params.middlewares || []) {
|
|
33
|
+
webLayer.add(middleware);
|
|
34
|
+
}
|
|
35
|
+
if (params.validator) {
|
|
36
|
+
webLayer.add(params.validator);
|
|
37
|
+
}
|
|
38
|
+
if (params.staticServe) {
|
|
39
|
+
webLayer.add(params.staticServe);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return webLayer;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Boots only the web layer of an application - the equivalent of Spring's `@WebMvcTest`.
|
|
47
|
+
*
|
|
48
|
+
* The controllers, their middlewares and their validators run for real. Every other
|
|
49
|
+
* dependency they inject is replaced by an auto-generated mock shaped like the real class,
|
|
50
|
+
* so a controller test never drags in a database or an HTTP client.
|
|
51
|
+
*
|
|
52
|
+
* Core services (the container, the logger, `ulak(...)`, the event emitter, ...) are always
|
|
53
|
+
* real - they are wired during bootstrap, before user components exist.
|
|
54
|
+
*
|
|
55
|
+
* @param options - Adapter, controllers and optional explicit overrides
|
|
56
|
+
* @returns The started app plus every double in play, keyed by service name
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const { app, mocks } = await createWebTest({ adapter, controllers: [UserController] });
|
|
61
|
+
*
|
|
62
|
+
* mocks.UserService.findById.mockResolvedValue({ id: '1', name: 'Ada' });
|
|
63
|
+
*
|
|
64
|
+
* await app.get('/users/1').expectStatus(200).expectJson({ id: '1', name: 'Ada' });
|
|
65
|
+
* expect(mocks.UserService.findById).toHaveBeenCalledWith('1');
|
|
66
|
+
*
|
|
67
|
+
* await app.stop();
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export async function createWebTest(options) {
|
|
71
|
+
const { adapter, controllers, components = [], overrides = {}, logger = silentLogger, port, dispatch } = options;
|
|
72
|
+
const controllerList = Array.isArray(controllers) ? controllers : [controllers];
|
|
73
|
+
if (!controllerList.length) {
|
|
74
|
+
throw new Error('createWebTest requires at least one @Controller class.');
|
|
75
|
+
}
|
|
76
|
+
for (const controller of controllerList) {
|
|
77
|
+
if (typeof controller !== 'function' || !getTypedMetadata(ComponentType.CONTROLLER, controller)) {
|
|
78
|
+
throw new Error(`createWebTest expects @Controller classes, but received '${controller?.name ?? typeof controller}'. ` +
|
|
79
|
+
'Pass services and middlewares through `components` instead.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const realComponents = collectWebLayer(controllerList);
|
|
83
|
+
for (const component of components) {
|
|
84
|
+
realComponents.add(component);
|
|
85
|
+
}
|
|
86
|
+
const realNames = new Set();
|
|
87
|
+
for (const component of realComponents) {
|
|
88
|
+
realNames.add(componentName(component));
|
|
89
|
+
}
|
|
90
|
+
const coreServiceNames = new Set(Object.values(ICoreServiceNames));
|
|
91
|
+
const autoMocks = {};
|
|
92
|
+
for (const component of realComponents) {
|
|
93
|
+
for (const field of discoverInjectedFieldsFromClass(component)) {
|
|
94
|
+
const { serviceName, serviceClass, fieldName } = field;
|
|
95
|
+
// Core services are wired during bootstrap and are always real - mocking them would
|
|
96
|
+
// also trip the factory's core-key guard. ulak(...) lands here.
|
|
97
|
+
if (coreServiceNames.has(serviceName)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Already satisfied - either registered for real or replaced by the caller
|
|
101
|
+
if (realNames.has(serviceName) || Object.hasOwn(overrides, serviceName)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// One shared mock per service, so two controllers injecting it see the same double
|
|
105
|
+
if (Object.hasOwn(autoMocks, serviceName)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!serviceClass) {
|
|
109
|
+
logger.warn(`createWebTest: '${serviceName}' (field '${fieldName}' on ${component.name}) was injected by name, ` +
|
|
110
|
+
'so its method shape is unknown and the mock will be an empty object. ' +
|
|
111
|
+
'Pass an explicit override if the test needs its methods.');
|
|
112
|
+
}
|
|
113
|
+
autoMocks[serviceName] = createMockFromClass(serviceClass);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const app = await createTestApp({
|
|
117
|
+
adapter,
|
|
118
|
+
logger,
|
|
119
|
+
port,
|
|
120
|
+
dispatch,
|
|
121
|
+
components: [...realComponents],
|
|
122
|
+
// Explicit overrides win over the generated ones
|
|
123
|
+
overrides: { ...autoMocks, ...overrides },
|
|
124
|
+
});
|
|
125
|
+
return { app, mocks: { ...autoMocks, ...overrides } };
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=createWebTest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createWebTest.js","sourceRoot":"","sources":["../../../../lib/test/harness/createWebTest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ServerLogger } from '../../logger';
|
|
2
|
+
export declare const silentLogger: ServerLogger;
|
|
3
|
+
/**
|
|
4
|
+
* A single captured log line.
|
|
5
|
+
*/
|
|
6
|
+
export interface CapturedLogEntry {
|
|
7
|
+
level: 'info' | 'warn' | 'error' | 'profile';
|
|
8
|
+
message: string;
|
|
9
|
+
meta?: any;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Creates a silent logger that also records every call, so tests can assert on warnings.
|
|
13
|
+
*
|
|
14
|
+
* @returns The logger plus the array it appends to (live, not a copy)
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const { logger, entries } = createCapturingLogger();
|
|
19
|
+
* const { app } = await createWebTest({ adapter, controllers: [UserController], logger });
|
|
20
|
+
*
|
|
21
|
+
* expect(entries.some((e) => e.level === 'warn' && e.message.includes('cannot be auto-mocked'))).toBe(true);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function createCapturingLogger(): {
|
|
25
|
+
logger: ServerLogger;
|
|
26
|
+
entries: CapturedLogEntry[];
|
|
27
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ServerLogger that discards everything.
|
|
3
|
+
*
|
|
4
|
+
* The default logger for test apps: booting a server prints the banner, the adapter name,
|
|
5
|
+
* a route table and a "server running" line, none of which belong in test output.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const [adapter] = createHonoAdapter({ logger: silentLogger });
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
const noop = () => {
|
|
13
|
+
// Intentionally empty - the whole point is to keep test output clean
|
|
14
|
+
};
|
|
15
|
+
export const silentLogger = {
|
|
16
|
+
info: noop,
|
|
17
|
+
warn: noop,
|
|
18
|
+
error: noop,
|
|
19
|
+
profile: noop,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Creates a silent logger that also records every call, so tests can assert on warnings.
|
|
23
|
+
*
|
|
24
|
+
* @returns The logger plus the array it appends to (live, not a copy)
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const { logger, entries } = createCapturingLogger();
|
|
29
|
+
* const { app } = await createWebTest({ adapter, controllers: [UserController], logger });
|
|
30
|
+
*
|
|
31
|
+
* expect(entries.some((e) => e.level === 'warn' && e.message.includes('cannot be auto-mocked'))).toBe(true);
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function createCapturingLogger() {
|
|
35
|
+
const entries = [];
|
|
36
|
+
return {
|
|
37
|
+
entries,
|
|
38
|
+
logger: {
|
|
39
|
+
info: (message, meta) => {
|
|
40
|
+
entries.push({ level: 'info', message, meta });
|
|
41
|
+
},
|
|
42
|
+
warn: (message, meta) => {
|
|
43
|
+
entries.push({ level: 'warn', message, meta });
|
|
44
|
+
},
|
|
45
|
+
error: (message, meta) => {
|
|
46
|
+
entries.push({ level: 'error', message, meta });
|
|
47
|
+
},
|
|
48
|
+
profile: (message) => {
|
|
49
|
+
entries.push({ level: 'profile', message });
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=silentLogger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silentLogger.js","sourceRoot":"","sources":["../../../../lib/test/harness/silentLogger.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,IAAI,GAAG,GAAS,EAAE;IACtB,qEAAqE;AACvE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,IAAI;CACd,CAAC;AAWF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,OAAO,GAAuB,EAAE,CAAC;IAEvC,OAAO;QACL,OAAO;QACP,MAAM,EAAE;YACN,IAAI,EAAE,CAAC,OAAe,EAAE,IAAU,EAAE,EAAE;gBACpC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,CAAC;YACD,IAAI,EAAE,CAAC,OAAe,EAAE,IAAU,EAAE,EAAE;gBACpC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACjD,CAAC;YACD,KAAK,EAAE,CAAC,OAAe,EAAE,IAAU,EAAE,EAAE;gBACrC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAClD,CAAC;YACD,OAAO,EAAE,CAAC,OAAe,EAAE,EAAE;gBAC3B,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;YAC9C,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { AsenaAdapter } from '../../adapter';
|
|
2
|
+
import type { Container } from '../../ioc';
|
|
3
|
+
import type { ServerLogger } from '../../logger';
|
|
4
|
+
import type { AsenaServer } from '../../server/AsenaServer';
|
|
5
|
+
import type { Class } from '../../server/types';
|
|
6
|
+
import type { TestHttpCall } from '../http/TestHttpCall';
|
|
7
|
+
/**
|
|
8
|
+
* How the test app accepts requests.
|
|
9
|
+
*
|
|
10
|
+
* - `server` - listens on a TCP port (default). Matches production exactly.
|
|
11
|
+
* - `socket` - listens on a unix domain socket instead. Still the adapter's real routing
|
|
12
|
+
* pipeline, but occupies no port, so parallel suites cannot collide. Requires an adapter
|
|
13
|
+
* that honours `AsenaStartOptions.unix`.
|
|
14
|
+
*/
|
|
15
|
+
export type DispatchMode = 'server' | 'socket';
|
|
16
|
+
/**
|
|
17
|
+
* Options for {@link createTestApp}
|
|
18
|
+
*/
|
|
19
|
+
export interface TestAppOptions<A extends AsenaAdapter<any, any> = AsenaAdapter<any, any>> {
|
|
20
|
+
/**
|
|
21
|
+
* The adapter instance to boot, e.g. `createHonoAdapter({ logger: silentLogger })[0]`
|
|
22
|
+
*/
|
|
23
|
+
adapter: A;
|
|
24
|
+
/**
|
|
25
|
+
* Components to register. Passing this list skips filesystem scanning entirely.
|
|
26
|
+
*/
|
|
27
|
+
components: Class[];
|
|
28
|
+
/**
|
|
29
|
+
* Replace registered components with test doubles, keyed by service name (`@MockBean`).
|
|
30
|
+
*
|
|
31
|
+
* Override services, not controllers: a plain object mock carries no `@Controller`
|
|
32
|
+
* metadata, so an overridden controller's routes would never be registered.
|
|
33
|
+
*/
|
|
34
|
+
overrides?: Record<string, object>;
|
|
35
|
+
/**
|
|
36
|
+
* Logger for the booted server
|
|
37
|
+
*
|
|
38
|
+
* @default silentLogger
|
|
39
|
+
*/
|
|
40
|
+
logger?: ServerLogger;
|
|
41
|
+
/**
|
|
42
|
+
* Port to bind. Ignored in `socket` dispatch.
|
|
43
|
+
*
|
|
44
|
+
* @default 0 - Bun picks a free ephemeral port, which removes the port race entirely
|
|
45
|
+
*/
|
|
46
|
+
port?: number;
|
|
47
|
+
/**
|
|
48
|
+
* @default 'server'
|
|
49
|
+
*/
|
|
50
|
+
dispatch?: DispatchMode;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A booted test application
|
|
54
|
+
*/
|
|
55
|
+
export interface TestApp {
|
|
56
|
+
/**
|
|
57
|
+
* The running server
|
|
58
|
+
*/
|
|
59
|
+
readonly server: AsenaServer<any>;
|
|
60
|
+
/**
|
|
61
|
+
* The user component container
|
|
62
|
+
*/
|
|
63
|
+
readonly container: Container;
|
|
64
|
+
/**
|
|
65
|
+
* The bound TCP port. `0` in `socket` dispatch, where no port is used.
|
|
66
|
+
*/
|
|
67
|
+
readonly port: number;
|
|
68
|
+
/**
|
|
69
|
+
* Base URL for HTTP calls. In `socket` dispatch the host is a placeholder - routing
|
|
70
|
+
* happens over {@link socketPath}.
|
|
71
|
+
*/
|
|
72
|
+
readonly baseUrl: string;
|
|
73
|
+
/**
|
|
74
|
+
* Path of the unix socket, when running in `socket` dispatch
|
|
75
|
+
*/
|
|
76
|
+
readonly socketPath?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Resolves a component from the container
|
|
79
|
+
*
|
|
80
|
+
* @param name - Registered service name
|
|
81
|
+
*/
|
|
82
|
+
resolve<T>(name: string): Promise<T>;
|
|
83
|
+
/**
|
|
84
|
+
* Builds a WebSocket URL for the given path, correct for the active dispatch mode
|
|
85
|
+
*
|
|
86
|
+
* @param path - Route path, e.g. `/ws/chat`
|
|
87
|
+
*/
|
|
88
|
+
wsUrl(path: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Starts a request with an explicit method in `init`
|
|
91
|
+
*
|
|
92
|
+
* @param path - Path relative to the app root
|
|
93
|
+
* @param init - Standard fetch init
|
|
94
|
+
*/
|
|
95
|
+
request(path: string, init?: RequestInit): TestHttpCall;
|
|
96
|
+
get(path: string, init?: RequestInit): TestHttpCall;
|
|
97
|
+
post(path: string, init?: RequestInit): TestHttpCall;
|
|
98
|
+
put(path: string, init?: RequestInit): TestHttpCall;
|
|
99
|
+
patch(path: string, init?: RequestInit): TestHttpCall;
|
|
100
|
+
delete(path: string, init?: RequestInit): TestHttpCall;
|
|
101
|
+
head(path: string, init?: RequestInit): TestHttpCall;
|
|
102
|
+
options(path: string, init?: RequestInit): TestHttpCall;
|
|
103
|
+
/**
|
|
104
|
+
* Stops the server and removes the unix socket if one was created. Idempotent.
|
|
105
|
+
*/
|
|
106
|
+
stop(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Enables `await using app = await createTestApp(...)`
|
|
109
|
+
*/
|
|
110
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Options for {@link createWebTest}
|
|
114
|
+
*/
|
|
115
|
+
export interface WebTestOptions<A extends AsenaAdapter<any, any> = AsenaAdapter<any, any>> {
|
|
116
|
+
adapter: A;
|
|
117
|
+
/**
|
|
118
|
+
* Controller(s) under test
|
|
119
|
+
*/
|
|
120
|
+
controllers: Class | Class[];
|
|
121
|
+
/**
|
|
122
|
+
* Extra components to register for real, alongside the controllers' middlewares and
|
|
123
|
+
* validators (which are always real, since the framework resolves them by name)
|
|
124
|
+
*/
|
|
125
|
+
components?: Class[];
|
|
126
|
+
/**
|
|
127
|
+
* Explicit doubles, keyed by service name. These win over the generated auto-mocks.
|
|
128
|
+
*/
|
|
129
|
+
overrides?: Record<string, object>;
|
|
130
|
+
logger?: ServerLogger;
|
|
131
|
+
port?: number;
|
|
132
|
+
dispatch?: DispatchMode;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Result of {@link createWebTest}
|
|
136
|
+
*/
|
|
137
|
+
export interface WebTestResult {
|
|
138
|
+
app: TestApp;
|
|
139
|
+
/**
|
|
140
|
+
* Every double in play - both auto-generated mocks and the explicit `overrides` - keyed
|
|
141
|
+
* by **service name** (not field name, unlike `mockComponent`).
|
|
142
|
+
*
|
|
143
|
+
* One shared mock per service, so two controllers injecting the same service see the
|
|
144
|
+
* same double.
|
|
145
|
+
*/
|
|
146
|
+
mocks: Record<string, any>;
|
|
147
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../lib/test/harness/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { TestHttpResponse } from './TestHttpResponse';
|
|
2
|
+
/**
|
|
3
|
+
* A pending HTTP call with a fluent assertion chain.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is sent until the call is awaited. Assertions queue up in the order they were
|
|
6
|
+
* chained and run against the buffered response; the send is memoized, so awaiting the
|
|
7
|
+
* same call twice does not issue a second request.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* await app.get('/users/1').expectStatus(200).expectJson({ id: '1', name: 'Ada' });
|
|
12
|
+
*
|
|
13
|
+
* const res = await app.post('/users', { body: JSON.stringify({ name: 'Ada' }) }).expectStatus(201);
|
|
14
|
+
* expect(res.json().id).toBeString();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare class TestHttpCall implements PromiseLike<TestHttpResponse> {
|
|
18
|
+
private readonly assertions;
|
|
19
|
+
private pending?;
|
|
20
|
+
private readonly url;
|
|
21
|
+
private readonly init?;
|
|
22
|
+
constructor(url: string, init?: RequestInit);
|
|
23
|
+
/**
|
|
24
|
+
* Asserts the response status code
|
|
25
|
+
*
|
|
26
|
+
* @param expected - Expected status
|
|
27
|
+
* @returns this, for chaining
|
|
28
|
+
*/
|
|
29
|
+
expectStatus(expected: number): this;
|
|
30
|
+
/**
|
|
31
|
+
* Asserts a response header
|
|
32
|
+
*
|
|
33
|
+
* @param name - Header name (case-insensitive)
|
|
34
|
+
* @param expected - Exact value, or a pattern to match
|
|
35
|
+
* @returns this, for chaining
|
|
36
|
+
*/
|
|
37
|
+
expectHeader(name: string, expected: string | RegExp): this;
|
|
38
|
+
/**
|
|
39
|
+
* Asserts the whole JSON body deep-equals the expected value
|
|
40
|
+
*
|
|
41
|
+
* @param expected - Expected body
|
|
42
|
+
* @returns this, for chaining
|
|
43
|
+
*/
|
|
44
|
+
expectJson<T>(expected: T): this;
|
|
45
|
+
/**
|
|
46
|
+
* Asserts the JSON body contains at least the given properties
|
|
47
|
+
*
|
|
48
|
+
* @param partial - Subset the body must match
|
|
49
|
+
* @returns this, for chaining
|
|
50
|
+
*/
|
|
51
|
+
expectJsonContains(partial: object): this;
|
|
52
|
+
/**
|
|
53
|
+
* Asserts the raw text body
|
|
54
|
+
*
|
|
55
|
+
* @param expected - Exact body, or a pattern to match
|
|
56
|
+
* @returns this, for chaining
|
|
57
|
+
*/
|
|
58
|
+
expectBody(expected: string | RegExp): this;
|
|
59
|
+
/**
|
|
60
|
+
* Escape hatch for assertions the chain does not cover
|
|
61
|
+
*
|
|
62
|
+
* @param assertion - Runs against the buffered response, may be async
|
|
63
|
+
* @returns this, for chaining
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* await app.get('/users').expect((res) => {
|
|
68
|
+
* expect(res.json<User[]>()).toHaveLength(3);
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
expect(assertion: (response: TestHttpResponse) => void | Promise<void>): this;
|
|
73
|
+
then<TResult1 = TestHttpResponse, TResult2 = never>(onfulfilled?: ((value: TestHttpResponse) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): PromiseLike<TResult1 | TResult2>;
|
|
74
|
+
private enqueue;
|
|
75
|
+
private send;
|
|
76
|
+
private run;
|
|
77
|
+
private describeRequest;
|
|
78
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { expect } from 'bun:test';
|
|
2
|
+
import { TestHttpResponse, truncate } from './TestHttpResponse';
|
|
3
|
+
/**
|
|
4
|
+
* A pending HTTP call with a fluent assertion chain.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is sent until the call is awaited. Assertions queue up in the order they were
|
|
7
|
+
* chained and run against the buffered response; the send is memoized, so awaiting the
|
|
8
|
+
* same call twice does not issue a second request.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* await app.get('/users/1').expectStatus(200).expectJson({ id: '1', name: 'Ada' });
|
|
13
|
+
*
|
|
14
|
+
* const res = await app.post('/users', { body: JSON.stringify({ name: 'Ada' }) }).expectStatus(201);
|
|
15
|
+
* expect(res.json().id).toBeString();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export class TestHttpCall {
|
|
19
|
+
assertions = [];
|
|
20
|
+
pending;
|
|
21
|
+
url;
|
|
22
|
+
init;
|
|
23
|
+
constructor(url, init) {
|
|
24
|
+
this.url = url;
|
|
25
|
+
this.init = init;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Asserts the response status code
|
|
29
|
+
*
|
|
30
|
+
* @param expected - Expected status
|
|
31
|
+
* @returns this, for chaining
|
|
32
|
+
*/
|
|
33
|
+
expectStatus(expected) {
|
|
34
|
+
// Deliberately not `expect(status).toBe(n)`: a bare number mismatch is the least
|
|
35
|
+
// useful failure in an HTTP test. The body almost always explains why.
|
|
36
|
+
return this.enqueue((response) => {
|
|
37
|
+
if (response.status !== expected) {
|
|
38
|
+
throw new Error(`Expected status ${expected} but received ${response.status} for ${this.describeRequest()}\n` +
|
|
39
|
+
`Body: ${truncate(response.text())}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Asserts a response header
|
|
45
|
+
*
|
|
46
|
+
* @param name - Header name (case-insensitive)
|
|
47
|
+
* @param expected - Exact value, or a pattern to match
|
|
48
|
+
* @returns this, for chaining
|
|
49
|
+
*/
|
|
50
|
+
expectHeader(name, expected) {
|
|
51
|
+
return this.enqueue((response) => {
|
|
52
|
+
const actual = response.headers.get(name);
|
|
53
|
+
if (actual === null) {
|
|
54
|
+
const received = [];
|
|
55
|
+
response.headers.forEach((_value, key) => received.push(key));
|
|
56
|
+
throw new Error(`Expected header '${name}' to be present for ${this.describeRequest()}, but it was not sent.\n` +
|
|
57
|
+
`Received headers: ${received.join(', ') || '<none>'}`);
|
|
58
|
+
}
|
|
59
|
+
if (expected instanceof RegExp) {
|
|
60
|
+
expect(actual).toMatch(expected);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
expect(actual).toBe(expected);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Asserts the whole JSON body deep-equals the expected value
|
|
69
|
+
*
|
|
70
|
+
* @param expected - Expected body
|
|
71
|
+
* @returns this, for chaining
|
|
72
|
+
*/
|
|
73
|
+
expectJson(expected) {
|
|
74
|
+
return this.enqueue((response) => {
|
|
75
|
+
expect(response.json()).toEqual(expected);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Asserts the JSON body contains at least the given properties
|
|
80
|
+
*
|
|
81
|
+
* @param partial - Subset the body must match
|
|
82
|
+
* @returns this, for chaining
|
|
83
|
+
*/
|
|
84
|
+
expectJsonContains(partial) {
|
|
85
|
+
return this.enqueue((response) => {
|
|
86
|
+
expect(response.json()).toMatchObject(partial);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Asserts the raw text body
|
|
91
|
+
*
|
|
92
|
+
* @param expected - Exact body, or a pattern to match
|
|
93
|
+
* @returns this, for chaining
|
|
94
|
+
*/
|
|
95
|
+
expectBody(expected) {
|
|
96
|
+
return this.enqueue((response) => {
|
|
97
|
+
if (expected instanceof RegExp) {
|
|
98
|
+
expect(response.text()).toMatch(expected);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
expect(response.text()).toBe(expected);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Escape hatch for assertions the chain does not cover
|
|
107
|
+
*
|
|
108
|
+
* @param assertion - Runs against the buffered response, may be async
|
|
109
|
+
* @returns this, for chaining
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* await app.get('/users').expect((res) => {
|
|
114
|
+
* expect(res.json<User[]>()).toHaveLength(3);
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
expect(assertion) {
|
|
119
|
+
return this.enqueue(assertion);
|
|
120
|
+
}
|
|
121
|
+
then(onfulfilled, onrejected) {
|
|
122
|
+
return this.send().then(onfulfilled, onrejected);
|
|
123
|
+
}
|
|
124
|
+
enqueue(assertion) {
|
|
125
|
+
if (this.pending !== undefined) {
|
|
126
|
+
throw new Error('Cannot add assertions to a TestHttpCall that has already been sent. ' +
|
|
127
|
+
'Chain every expect* call before awaiting the request.');
|
|
128
|
+
}
|
|
129
|
+
this.assertions.push(assertion);
|
|
130
|
+
return this;
|
|
131
|
+
}
|
|
132
|
+
send() {
|
|
133
|
+
if (this.pending === undefined) {
|
|
134
|
+
this.pending = this.run();
|
|
135
|
+
}
|
|
136
|
+
return this.pending;
|
|
137
|
+
}
|
|
138
|
+
async run() {
|
|
139
|
+
const response = await TestHttpResponse.from(await fetch(this.url, this.init));
|
|
140
|
+
for (const assertion of this.assertions) {
|
|
141
|
+
await assertion(response);
|
|
142
|
+
}
|
|
143
|
+
return response;
|
|
144
|
+
}
|
|
145
|
+
describeRequest() {
|
|
146
|
+
return `${(this.init?.method ?? 'GET').toUpperCase()} ${this.url}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=TestHttpCall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestHttpCall.js","sourceRoot":"","sources":["../../../../lib/test/http/TestHttpCall.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A response whose body has already been read.
|
|
3
|
+
*
|
|
4
|
+
* `Response` bodies can only be consumed once, which makes them awkward to assert against
|
|
5
|
+
* more than one way. This buffers the body at construction so `text()` and `json()` are
|
|
6
|
+
* synchronous and repeatable.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TestHttpResponse {
|
|
9
|
+
/**
|
|
10
|
+
* The underlying fetch Response. Its body stream is already consumed.
|
|
11
|
+
*/
|
|
12
|
+
readonly raw: Response;
|
|
13
|
+
private readonly body;
|
|
14
|
+
private constructor();
|
|
15
|
+
/**
|
|
16
|
+
* Buffers a fetch Response into a TestHttpResponse
|
|
17
|
+
*
|
|
18
|
+
* @param raw - Response to consume
|
|
19
|
+
* @returns The buffered response
|
|
20
|
+
*/
|
|
21
|
+
static from(raw: Response): Promise<TestHttpResponse>;
|
|
22
|
+
get status(): number;
|
|
23
|
+
get headers(): Headers;
|
|
24
|
+
/**
|
|
25
|
+
* The response body as text
|
|
26
|
+
*
|
|
27
|
+
* @returns Body string
|
|
28
|
+
*/
|
|
29
|
+
text(): string;
|
|
30
|
+
/**
|
|
31
|
+
* The response body parsed as JSON
|
|
32
|
+
*
|
|
33
|
+
* @returns Parsed body
|
|
34
|
+
* @throws When the body is not valid JSON - the message includes the raw body
|
|
35
|
+
*/
|
|
36
|
+
json<T = any>(): T;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Shortens a body for error messages so a large HTML page cannot bury the failure
|
|
40
|
+
*
|
|
41
|
+
* @param value - Body text
|
|
42
|
+
* @returns Truncated text
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function truncate(value: string, limit?: number): string;
|