@fluojs/testing 1.0.0-beta.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +117 -0
  3. package/README.md +115 -0
  4. package/dist/app.d.ts +16 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +54 -0
  7. package/dist/babel-decorators-plugin.d.ts +36 -0
  8. package/dist/babel-decorators-plugin.d.ts.map +1 -0
  9. package/dist/babel-decorators-plugin.js +67 -0
  10. package/dist/conformance/fetch-style-websocket-conformance.d.ts +14 -0
  11. package/dist/conformance/fetch-style-websocket-conformance.d.ts.map +1 -0
  12. package/dist/conformance/fetch-style-websocket-conformance.js +34 -0
  13. package/dist/conformance/platform-conformance.d.ts +42 -0
  14. package/dist/conformance/platform-conformance.d.ts.map +1 -0
  15. package/dist/conformance/platform-conformance.js +193 -0
  16. package/dist/http.d.ts +73 -0
  17. package/dist/http.d.ts.map +1 -0
  18. package/dist/http.js +239 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +3 -0
  22. package/dist/mock.d.ts +26 -0
  23. package/dist/mock.d.ts.map +1 -0
  24. package/dist/mock.js +60 -0
  25. package/dist/module.d.ts +45 -0
  26. package/dist/module.d.ts.map +1 -0
  27. package/dist/module.js +405 -0
  28. package/dist/portability/http-adapter-portability.d.ts +83 -0
  29. package/dist/portability/http-adapter-portability.d.ts.map +1 -0
  30. package/dist/portability/http-adapter-portability.js +528 -0
  31. package/dist/portability/web-runtime-adapter-portability.d.ts +26 -0
  32. package/dist/portability/web-runtime-adapter-portability.d.ts.map +1 -0
  33. package/dist/portability/web-runtime-adapter-portability.js +260 -0
  34. package/dist/types.d.ts +76 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +1 -0
  37. package/dist/vitest.d.ts +9 -0
  38. package/dist/vitest.d.ts.map +1 -0
  39. package/dist/vitest.js +11 -0
  40. package/package.json +102 -0
@@ -0,0 +1,193 @@
1
+ const DEFAULT_FORBIDDEN_KEY_PATTERNS = [/secret/i, /password/i, /token/i, /credential/i, /api[-_]?key/i];
2
+ const DEFAULT_REQUIRED_FIX_HINT_SEVERITIES = ['error'];
3
+ function isRecord(value) {
4
+ return typeof value === 'object' && value !== null;
5
+ }
6
+ function normalizeForComparison(value) {
7
+ if (Array.isArray(value)) {
8
+ return value.map(entry => normalizeForComparison(entry));
9
+ }
10
+ if (!isRecord(value)) {
11
+ return value;
12
+ }
13
+ const normalizedEntries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForComparison(entry)]);
14
+ return Object.fromEntries(normalizedEntries);
15
+ }
16
+ function defaultCompare(left, right) {
17
+ return JSON.stringify(normalizeForComparison(left)) === JSON.stringify(normalizeForComparison(right));
18
+ }
19
+ function toErrorMessage(error) {
20
+ return error instanceof Error ? error.message : String(error);
21
+ }
22
+ async function captureOutcome(action) {
23
+ try {
24
+ await action();
25
+ return {
26
+ ok: true
27
+ };
28
+ } catch (error) {
29
+ return {
30
+ message: toErrorMessage(error),
31
+ ok: false
32
+ };
33
+ }
34
+ }
35
+ function collectForbiddenKeyPaths(value, patterns, allowPatterns, currentPath, violations) {
36
+ if (Array.isArray(value)) {
37
+ value.forEach((entry, index) => {
38
+ collectForbiddenKeyPaths(entry, patterns, allowPatterns, `${currentPath}[${index}]`, violations);
39
+ });
40
+ return;
41
+ }
42
+ if (!isRecord(value)) {
43
+ return;
44
+ }
45
+ for (const [key, entry] of Object.entries(value)) {
46
+ const nextPath = currentPath.length > 0 ? `${currentPath}.${key}` : key;
47
+ const isForbidden = patterns.some(pattern => pattern.test(key));
48
+ const isAllowed = allowPatterns.some(pattern => pattern.test(nextPath));
49
+ if (isForbidden && !isAllowed) {
50
+ violations.push(nextPath);
51
+ }
52
+ collectForbiddenKeyPaths(entry, patterns, allowPatterns, nextPath, violations);
53
+ }
54
+ }
55
+ export class PlatformConformanceHarness {
56
+ constructor(options) {
57
+ this.options = options;
58
+ }
59
+ async assertValidationHasNoLongLivedSideEffects() {
60
+ const component = this.options.createComponent();
61
+ const beforeState = component.state();
62
+ const beforeEffects = this.options.captureValidationSideEffects ? await this.options.captureValidationSideEffects(component) : undefined;
63
+ await component.validate();
64
+ const afterState = component.state();
65
+ if (beforeState !== afterState) {
66
+ throw new Error(`validate() must not transition component state. Expected "${beforeState}" but received "${afterState}".`);
67
+ }
68
+ if (!this.options.captureValidationSideEffects) {
69
+ return;
70
+ }
71
+ const compare = this.options.snapshot?.compare ?? defaultCompare;
72
+ const afterEffects = await this.options.captureValidationSideEffects(component);
73
+ if (!compare(beforeEffects, afterEffects)) {
74
+ throw new Error('validate() introduced long-lived side effects.');
75
+ }
76
+ }
77
+ async assertStartIsDeterministic() {
78
+ const component = this.options.createComponent();
79
+ const compare = this.options.snapshot?.compare ?? defaultCompare;
80
+ const firstStart = await captureOutcome(() => component.start());
81
+ const firstSnapshot = component.snapshot();
82
+ const secondStart = await captureOutcome(() => component.start());
83
+ const secondSnapshot = component.snapshot();
84
+ if (firstStart.ok !== secondStart.ok) {
85
+ throw new Error('start() is not deterministic: first and second calls had different outcomes.');
86
+ }
87
+ if (!firstStart.ok && !secondStart.ok && firstStart.message !== secondStart.message) {
88
+ throw new Error('start() rejection messages changed across duplicate calls.');
89
+ }
90
+ if (firstStart.ok && secondStart.ok && !compare(firstSnapshot, secondSnapshot)) {
91
+ throw new Error('start() is not idempotent: duplicate calls changed component snapshot output.');
92
+ }
93
+ await captureOutcome(() => component.stop());
94
+ }
95
+ async assertStopIsIdempotent() {
96
+ const component = this.options.createComponent();
97
+ const compare = this.options.snapshot?.compare ?? defaultCompare;
98
+ const startOutcome = await captureOutcome(() => component.start());
99
+ if (!startOutcome.ok) {
100
+ throw new Error(`stop() idempotency check requires a startable component: ${startOutcome.message}`);
101
+ }
102
+ const firstStop = await captureOutcome(() => component.stop());
103
+ if (!firstStop.ok) {
104
+ throw new Error(`first stop() call failed: ${firstStop.message}`);
105
+ }
106
+ const firstState = component.state();
107
+ const firstSnapshot = component.snapshot();
108
+ const secondStop = await captureOutcome(() => component.stop());
109
+ if (!secondStop.ok) {
110
+ throw new Error(`stop() is not idempotent: second call failed with "${secondStop.message}".`);
111
+ }
112
+ const secondState = component.state();
113
+ const secondSnapshot = component.snapshot();
114
+ if (firstState !== secondState) {
115
+ throw new Error(`stop() changed state across duplicate calls (${firstState} -> ${secondState}).`);
116
+ }
117
+ if (!compare(firstSnapshot, secondSnapshot)) {
118
+ throw new Error('stop() is not idempotent: duplicate calls changed component snapshot output.');
119
+ }
120
+ }
121
+ async assertSnapshotSafeInDegradedAndFailedStates() {
122
+ const scenarios = this.options.scenarios;
123
+ if (!scenarios) {
124
+ throw new Error('Conformance scenarios are required. Provide degraded and failed snapshot scenarios.');
125
+ }
126
+ for (const scenario of [scenarios.degraded, scenarios.failed]) {
127
+ const component = scenario.createComponent();
128
+ await scenario.enterState(component);
129
+ if (scenario.expectedState !== undefined && component.state() !== scenario.expectedState) {
130
+ throw new Error(`Scenario "${scenario.name}" expected state "${scenario.expectedState}" but received "${component.state()}".`);
131
+ }
132
+ try {
133
+ component.snapshot();
134
+ } catch (error) {
135
+ throw new Error(`snapshot() must be safe in "${scenario.name}" state: ${toErrorMessage(error)}`);
136
+ } finally {
137
+ await captureOutcome(() => component.stop());
138
+ }
139
+ }
140
+ }
141
+ async assertStableDiagnostics() {
142
+ const component = this.options.createComponent();
143
+ const validation = await component.validate();
144
+ const diagnostics = [...validation.issues, ...(validation.warnings ?? [])];
145
+ if (this.options.diagnostics?.collect) {
146
+ const extra = await this.options.diagnostics.collect(component, validation);
147
+ diagnostics.push(...extra);
148
+ }
149
+ const requiredFixHintSeverities = this.options.diagnostics?.requireFixHintForSeverities ?? DEFAULT_REQUIRED_FIX_HINT_SEVERITIES;
150
+ for (const issue of diagnostics) {
151
+ if (issue.code.trim().length === 0) {
152
+ throw new Error('Diagnostics must provide a stable non-empty code.');
153
+ }
154
+ if (requiredFixHintSeverities.includes(issue.severity) && (!issue.fixHint || issue.fixHint.trim().length === 0)) {
155
+ throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint.`);
156
+ }
157
+ }
158
+ const expectedCodes = this.options.diagnostics?.expectedCodes;
159
+ if (!expectedCodes) {
160
+ return;
161
+ }
162
+ const normalizeCodes = codes => [...new Set(codes)].sort();
163
+ const actualCodes = normalizeCodes(diagnostics.map(diagnostic => diagnostic.code));
164
+ const normalizedExpectedCodes = normalizeCodes(expectedCodes);
165
+ if (!defaultCompare(actualCodes, normalizedExpectedCodes)) {
166
+ throw new Error(`Diagnostic code set changed. Expected [${normalizedExpectedCodes.join(', ')}] but received [${actualCodes.join(', ')}].`);
167
+ }
168
+ }
169
+ async assertSnapshotSanitized() {
170
+ const component = this.options.createComponent();
171
+ const snapshot = component.snapshot();
172
+ const sanitize = this.options.snapshot?.sanitize;
173
+ const candidate = sanitize ? sanitize(snapshot) : snapshot;
174
+ const forbiddenPatterns = this.options.snapshot?.forbiddenKeyPatterns ?? DEFAULT_FORBIDDEN_KEY_PATTERNS;
175
+ const allowPatterns = this.options.snapshot?.allowKeyPatterns ?? [];
176
+ const violations = [];
177
+ collectForbiddenKeyPaths(candidate, forbiddenPatterns, allowPatterns, '', violations);
178
+ if (violations.length > 0) {
179
+ throw new Error(`snapshot() contains unsanitized keys: ${violations.join(', ')}`);
180
+ }
181
+ }
182
+ async assertAll() {
183
+ await this.assertValidationHasNoLongLivedSideEffects();
184
+ await this.assertStartIsDeterministic();
185
+ await this.assertStopIsIdempotent();
186
+ await this.assertSnapshotSafeInDegradedAndFailedStates();
187
+ await this.assertStableDiagnostics();
188
+ await this.assertSnapshotSanitized();
189
+ }
190
+ }
191
+ export function createPlatformConformanceHarness(options) {
192
+ return new PlatformConformanceHarness(options);
193
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type { Dispatcher, Middleware } from '@fluojs/http';
2
+ /**
3
+ * Principal payload used by testing request helpers.
4
+ */
5
+ export interface TestPrincipal {
6
+ subject?: string;
7
+ issuer?: string;
8
+ audience?: string | string[];
9
+ roles?: string[];
10
+ scopes?: string[];
11
+ claims?: Record<string, unknown>;
12
+ [key: string]: unknown;
13
+ }
14
+ /**
15
+ * Minimal request input shape accepted by testing request helpers.
16
+ */
17
+ export interface TestRequest {
18
+ method?: string;
19
+ path: string;
20
+ body?: unknown;
21
+ headers?: Record<string, string>;
22
+ query?: Record<string, string | string[]>;
23
+ principal?: TestPrincipal;
24
+ }
25
+ /**
26
+ * Normalized test request shape used internally by dispatch helpers.
27
+ */
28
+ export interface TestRequestWithOptions extends TestRequest {
29
+ principal?: TestPrincipal;
30
+ }
31
+ /**
32
+ * Serialized test response returned by helper dispatch calls.
33
+ */
34
+ export interface TestResponse {
35
+ status: number;
36
+ body: unknown;
37
+ headers: Record<string, string | string[]>;
38
+ }
39
+ /**
40
+ * Fluent builder for constructing and dispatching test requests.
41
+ */
42
+ export interface RequestBuilder {
43
+ method(value: string): RequestBuilder;
44
+ path(value: string): RequestBuilder;
45
+ body(value: unknown): RequestBuilder;
46
+ header(name: string, value: string): RequestBuilder;
47
+ query(key: string, value: string | string[]): RequestBuilder;
48
+ principal(value: TestPrincipal): RequestBuilder;
49
+ send(): Promise<TestResponse>;
50
+ }
51
+ /**
52
+ * Creates a fluent request builder around a dispatcher.
53
+ *
54
+ * @param dispatcher Dispatcher that should execute the synthetic request.
55
+ * @param request Initial request state for the fluent builder.
56
+ * @returns A builder that can mutate the request before dispatching it.
57
+ */
58
+ export declare function createRequestBuilder(dispatcher: Dispatcher, request: TestRequestWithOptions): RequestBuilder;
59
+ /**
60
+ * Middleware that maps test-request principal data into `RequestContext.principal`.
61
+ *
62
+ * @returns Middleware that injects synthetic principal data before the next handler runs.
63
+ */
64
+ export declare function createTestRequestContextMiddleware(): Middleware;
65
+ /**
66
+ * Dispatches one synthetic request through a Fluo dispatcher and captures the serialized response.
67
+ *
68
+ * @param dispatcher Dispatcher that should handle the request.
69
+ * @param req Normalized synthetic request payload.
70
+ * @returns The captured status, body, and headers produced by the dispatcher.
71
+ */
72
+ export declare function makeRequest(dispatcher: Dispatcher, req: TestRequestWithOptions): Promise<TestResponse>;
73
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAmD,UAAU,EAAa,MAAM,cAAc,CAAC;AAEvH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpC,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAAC;IACrC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,CAAC;IACpD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,cAAc,CAAC;IAC7D,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc,CAAC;IAChD,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;CAC/B;AA2ED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,sBAAsB,GAAG,cAAc,CAqD5G;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,UAAU,CAa/D;AAuFD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAO5G"}
package/dist/http.js ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Principal payload used by testing request helpers.
3
+ */
4
+
5
+ /**
6
+ * Minimal request input shape accepted by testing request helpers.
7
+ */
8
+
9
+ /**
10
+ * Normalized test request shape used internally by dispatch helpers.
11
+ */
12
+
13
+ /**
14
+ * Serialized test response returned by helper dispatch calls.
15
+ */
16
+
17
+ /**
18
+ * Fluent builder for constructing and dispatching test requests.
19
+ */
20
+
21
+ function isRecord(value) {
22
+ return typeof value === 'object' && value !== null;
23
+ }
24
+ function toRecord(value) {
25
+ if (isRecord(value)) {
26
+ return value;
27
+ }
28
+ return {};
29
+ }
30
+ function normalizePrincipal(principal) {
31
+ if (!principal) {
32
+ return undefined;
33
+ }
34
+ const {
35
+ subject,
36
+ issuer,
37
+ audience,
38
+ roles,
39
+ scopes,
40
+ claims: principalClaims,
41
+ ...additionalClaims
42
+ } = principal;
43
+ const subjectValue = typeof subject === 'string' ? subject : typeof additionalClaims.id === 'string' ? String(additionalClaims.id) : 'test';
44
+ const normalizedClaims = {
45
+ subject: subjectValue,
46
+ audience,
47
+ claims: {
48
+ ...toRecord(principalClaims),
49
+ ...additionalClaims
50
+ }
51
+ };
52
+ if (issuer !== undefined) {
53
+ normalizedClaims.issuer = issuer;
54
+ }
55
+ if (roles !== undefined) {
56
+ normalizedClaims.roles = roles;
57
+ }
58
+ if (scopes !== undefined) {
59
+ normalizedClaims.scopes = scopes;
60
+ }
61
+ return normalizedClaims;
62
+ }
63
+
64
+ /**
65
+ * Creates a fluent request builder around a dispatcher.
66
+ *
67
+ * @param dispatcher Dispatcher that should execute the synthetic request.
68
+ * @param request Initial request state for the fluent builder.
69
+ * @returns A builder that can mutate the request before dispatching it.
70
+ */
71
+ export function createRequestBuilder(dispatcher, request) {
72
+ let current = {
73
+ method: request.method,
74
+ path: request.path,
75
+ body: request.body,
76
+ headers: request.headers ? {
77
+ ...request.headers
78
+ } : undefined,
79
+ query: request.query ? {
80
+ ...request.query
81
+ } : undefined,
82
+ principal: request.principal
83
+ };
84
+ return {
85
+ method(value) {
86
+ current = {
87
+ ...current,
88
+ method: value
89
+ };
90
+ return this;
91
+ },
92
+ path(value) {
93
+ current = {
94
+ ...current,
95
+ path: value
96
+ };
97
+ return this;
98
+ },
99
+ body(value) {
100
+ current = {
101
+ ...current,
102
+ body: value
103
+ };
104
+ return this;
105
+ },
106
+ header(name, value) {
107
+ current = {
108
+ ...current,
109
+ headers: {
110
+ ...(current.headers ?? {}),
111
+ [name]: value
112
+ }
113
+ };
114
+ return this;
115
+ },
116
+ query(key, value) {
117
+ current = {
118
+ ...current,
119
+ query: {
120
+ ...(current.query ?? {}),
121
+ [key]: value
122
+ }
123
+ };
124
+ return this;
125
+ },
126
+ principal(value) {
127
+ current = {
128
+ ...current,
129
+ principal: value
130
+ };
131
+ return this;
132
+ },
133
+ async send() {
134
+ return makeRequest(dispatcher, current);
135
+ }
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Middleware that maps test-request principal data into `RequestContext.principal`.
141
+ *
142
+ * @returns Middleware that injects synthetic principal data before the next handler runs.
143
+ */
144
+ export function createTestRequestContextMiddleware() {
145
+ return {
146
+ async handle(context, next) {
147
+ const request = context.request;
148
+ const principal = normalizePrincipal(request.principal);
149
+ if (principal !== undefined) {
150
+ context.requestContext.principal = principal;
151
+ }
152
+ await next();
153
+ }
154
+ };
155
+ }
156
+ function buildFrameworkRequest(req) {
157
+ const method = (req.method ?? 'GET').toUpperCase();
158
+ const queryString = req.query ? `?${new URLSearchParams(Object.entries(req.query).flatMap(([key, value]) => Array.isArray(value) ? value.map(v => [key, v]) : [[key, value]])).toString()}` : '';
159
+ return {
160
+ method,
161
+ path: req.path,
162
+ url: req.path + queryString,
163
+ headers: req.headers ?? {},
164
+ query: req.query ?? {},
165
+ cookies: {},
166
+ params: {},
167
+ body: req.body,
168
+ raw: req,
169
+ principal: req.principal
170
+ };
171
+ }
172
+ function buildFrameworkResponse() {
173
+ const result = {
174
+ status: 200,
175
+ body: undefined,
176
+ headers: {}
177
+ };
178
+ const mergeHeaderValue = (current, incoming) => {
179
+ const nextValues = Array.isArray(incoming) ? incoming : [incoming];
180
+ if (current === undefined) {
181
+ return nextValues.length === 1 ? nextValues[0] : [...nextValues];
182
+ }
183
+ const currentValues = Array.isArray(current) ? current : [current];
184
+ const merged = [...currentValues, ...nextValues];
185
+ return merged.length === 1 ? merged[0] : merged;
186
+ };
187
+ const response = {
188
+ statusCode: undefined,
189
+ headers: {},
190
+ committed: false,
191
+ setStatus(code) {
192
+ result.status = code;
193
+ this.statusCode = code;
194
+ this.statusSet = true;
195
+ },
196
+ setHeader(name, value) {
197
+ const lowerName = name.toLowerCase();
198
+ const responseHeaders = this.headers;
199
+ if (lowerName === 'set-cookie') {
200
+ result.headers[name] = mergeHeaderValue(result.headers[name], value);
201
+ responseHeaders[name] = mergeHeaderValue(responseHeaders[name], value);
202
+ return;
203
+ }
204
+ result.headers[name] = value;
205
+ responseHeaders[name] = value;
206
+ },
207
+ redirect(status, location) {
208
+ this.setStatus(status);
209
+ this.setHeader('location', location);
210
+ this.committed = true;
211
+ },
212
+ async send(body) {
213
+ result.body = body;
214
+ this.committed = true;
215
+ },
216
+ statusSet: false
217
+ };
218
+ return {
219
+ response,
220
+ result
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Dispatches one synthetic request through a Fluo dispatcher and captures the serialized response.
226
+ *
227
+ * @param dispatcher Dispatcher that should handle the request.
228
+ * @param req Normalized synthetic request payload.
229
+ * @returns The captured status, body, and headers produced by the dispatcher.
230
+ */
231
+ export async function makeRequest(dispatcher, req) {
232
+ const frameworkRequest = buildFrameworkRequest(req);
233
+ const {
234
+ response,
235
+ result
236
+ } = buildFrameworkResponse();
237
+ await dispatcher.dispatch(frameworkRequest, response);
238
+ return result;
239
+ }
@@ -0,0 +1,4 @@
1
+ export * from './app.js';
2
+ export { Test, createTestingModule, extractModuleProviders, extractModuleControllers, extractModuleImports } from './module.js';
3
+ export * from './types.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,IAAI,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAChI,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './app.js';
2
+ export { Test, createTestingModule, extractModuleProviders, extractModuleControllers, extractModuleImports } from './module.js';
3
+ export * from './types.js';
package/dist/mock.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { Mock } from 'vitest';
2
+ import type { Token } from '@fluojs/core';
3
+ import type { ValueProvider } from '@fluojs/di';
4
+ import type { DeepMocked } from './types.js';
5
+ export type MockedMethods<T> = {
6
+ [K in keyof T]: T[K] extends (...args: never[]) => unknown ? Mock<T[K]> : T[K];
7
+ };
8
+ /**
9
+ * Creates a proxy mock object with optional strict missing-property checks.
10
+ */
11
+ export declare function createMock<T extends object>(partial?: Partial<MockedMethods<T>>, options?: {
12
+ strict?: boolean;
13
+ }): MockedMethods<T>;
14
+ /**
15
+ * Casts a function to a strongly typed Vitest mock.
16
+ */
17
+ export declare function asMock<T extends (...args: never[]) => unknown>(fn: T): Mock<T>;
18
+ /**
19
+ * Creates a deep mock by replacing prototype methods with `vi.fn()` spies.
20
+ */
21
+ export declare function createDeepMock<T extends object>(type: new (...args: unknown[]) => T): DeepMocked<T>;
22
+ /**
23
+ * Creates a `useValue` provider for overriding a token in tests.
24
+ */
25
+ export declare function mockToken<T>(token: Token<T>, partial?: Partial<T>): ValueProvider<T>;
26
+ //# sourceMappingURL=mock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../src/mock.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEnC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI;KAC5B,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAC/E,CAAC;AAEF;;GAEG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EACzC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAM,EACvC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,aAAa,CAAC,CAAC,CAAC,CAsBlB;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAE9E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAkBnG;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,OAAO,CAAC,CAAC,CAAM,GAAG,aAAa,CAAC,CAAC,CAAC,CAExF"}
package/dist/mock.js ADDED
@@ -0,0 +1,60 @@
1
+ import { vi } from 'vitest';
2
+ /**
3
+ * Creates a proxy mock object with optional strict missing-property checks.
4
+ */
5
+ export function createMock(partial = {}, options = {}) {
6
+ const autoMocks = new Map();
7
+ return new Proxy({
8
+ ...partial
9
+ }, {
10
+ get(target, prop, receiver) {
11
+ if (Reflect.has(target, prop)) {
12
+ return Reflect.get(target, prop, receiver);
13
+ }
14
+ if (options.strict) {
15
+ throw new Error(`createMock: strict mode — property "${String(prop)}" is not declared in the partial mock. Add it to the partial or disable strict mode.`);
16
+ }
17
+ if (!autoMocks.has(prop)) {
18
+ autoMocks.set(prop, vi.fn());
19
+ }
20
+ return autoMocks.get(prop);
21
+ }
22
+ });
23
+ }
24
+
25
+ /**
26
+ * Casts a function to a strongly typed Vitest mock.
27
+ */
28
+ export function asMock(fn) {
29
+ return vi.mocked(fn);
30
+ }
31
+
32
+ /**
33
+ * Creates a deep mock by replacing prototype methods with `vi.fn()` spies.
34
+ */
35
+ export function createDeepMock(type) {
36
+ const spies = {};
37
+ let proto = type.prototype;
38
+ while (proto !== null && proto !== Object.prototype) {
39
+ for (const key of Reflect.ownKeys(proto)) {
40
+ if (key === 'constructor') continue;
41
+ if (key in spies) continue;
42
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
43
+ if (descriptor && typeof descriptor.value === 'function') {
44
+ spies[key] = vi.fn();
45
+ }
46
+ }
47
+ proto = Object.getPrototypeOf(proto);
48
+ }
49
+ return spies;
50
+ }
51
+
52
+ /**
53
+ * Creates a `useValue` provider for overriding a token in tests.
54
+ */
55
+ export function mockToken(token, partial = {}) {
56
+ return {
57
+ provide: token,
58
+ useValue: partial
59
+ };
60
+ }
@@ -0,0 +1,45 @@
1
+ import { type ClassType, type Provider } from '@fluojs/di';
2
+ import type { ModuleType } from '@fluojs/runtime';
3
+ import type { TestingModuleBuilder, TestingModuleOptions } from './types.js';
4
+ /**
5
+ * Returns providers declared on a module metadata definition.
6
+ *
7
+ * @param moduleType Module type whose `providers` metadata should be inspected.
8
+ * @returns The declared provider list, or an empty array when the module has none.
9
+ */
10
+ export declare function extractModuleProviders(moduleType: ModuleType): Provider[];
11
+ /**
12
+ * Returns controllers declared on a module metadata definition.
13
+ *
14
+ * @param moduleType Module type whose `controllers` metadata should be inspected.
15
+ * @returns The declared controller list, or an empty array when the module has none.
16
+ */
17
+ export declare function extractModuleControllers(moduleType: ModuleType): ClassType[];
18
+ /**
19
+ * Returns imported modules declared on a module metadata definition.
20
+ *
21
+ * @param moduleType Module type whose `imports` metadata should be inspected.
22
+ * @returns The declared import list, or an empty array when the module has none.
23
+ */
24
+ export declare function extractModuleImports(moduleType: ModuleType): ModuleType[];
25
+ /**
26
+ * Creates a fluent testing-module builder for overriding providers and compiling a test graph.
27
+ *
28
+ * @param options Bootstrap options plus the root module that should be compiled for the test.
29
+ * @returns A builder that supports provider and module overrides before compilation.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const module = await createTestingModule({ rootModule: AppModule })
34
+ * .overrideProvider(USER_REPOSITORY, fakeUserRepository)
35
+ * .compile();
36
+ * ```
37
+ */
38
+ export declare function createTestingModule(options: TestingModuleOptions): TestingModuleBuilder;
39
+ /**
40
+ * Namespace-style access point for `createTestingModule(...)`.
41
+ */
42
+ export declare const Test: {
43
+ createTestingModule: typeof createTestingModule;
44
+ };
45
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,SAAS,EAId,KAAK,QAAQ,EACd,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAqC,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMrF,OAAO,KAAK,EAA2B,oBAAoB,EAAE,oBAAoB,EAAoB,MAAM,YAAY,CAAC;AAExH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,EAAE,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,CAQ5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,CAQzE;AAiaD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAEvF;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;;CAEhB,CAAC"}