@scenarist/msw-adapter 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { HttpHandler } from "msw";
|
|
2
|
-
import type { ActiveScenario, ScenaristScenario, ResponseSelector } from "@scenarist/core";
|
|
2
|
+
import type { ActiveScenario, ScenaristScenario, ResponseSelector, ErrorBehaviors, Logger } from "@scenarist/core";
|
|
3
3
|
export type DynamicHandlerOptions = {
|
|
4
4
|
readonly getTestId: (request: Request) => string;
|
|
5
5
|
readonly getActiveScenario: (testId: string) => ActiveScenario | undefined;
|
|
6
6
|
readonly getScenarioDefinition: (scenarioId: string) => ScenaristScenario | undefined;
|
|
7
7
|
readonly strictMode: boolean;
|
|
8
8
|
readonly responseSelector: ResponseSelector;
|
|
9
|
+
readonly errorBehaviors?: ErrorBehaviors;
|
|
10
|
+
readonly logger?: Logger;
|
|
9
11
|
};
|
|
10
12
|
export declare const createDynamicHandler: (options: DynamicHandlerOptions) => HttpHandler;
|
|
11
13
|
//# sourceMappingURL=dynamic-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/dynamic-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAIjB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"dynamic-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/dynamic-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EAIjB,gBAAgB,EAChB,cAAc,EACd,MAAM,EACP,MAAM,iBAAiB,CAAC;AAKzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACjD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAC3E,QAAQ,CAAC,qBAAqB,EAAE,CAC9B,UAAU,EAAE,MAAM,KACf,iBAAiB,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAiGF,eAAO,MAAM,oBAAoB,GAC/B,SAAS,qBAAqB,KAC7B,WAiIF,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { http, passthrough } from "msw";
|
|
2
|
+
import { ScenaristError, ErrorCodes, LogCategories } from "@scenarist/core";
|
|
2
3
|
import { buildResponse } from "../conversion/response-builder.js";
|
|
3
4
|
import { matchesUrl } from "../matching/url-matcher.js";
|
|
4
5
|
/**
|
|
@@ -84,20 +85,87 @@ const getMocksFromScenarios = (activeScenario, getScenarioDefinition, method, ur
|
|
|
84
85
|
export const createDynamicHandler = (options) => {
|
|
85
86
|
return http.all("*", async ({ request }) => {
|
|
86
87
|
const testId = options.getTestId(request);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
try {
|
|
89
|
+
// Check for missing test ID
|
|
90
|
+
if (!testId) {
|
|
91
|
+
if (options.errorBehaviors?.onMissingTestId === "throw") {
|
|
92
|
+
throw new ScenaristError("Missing test ID header. Ensure your test setup sends the x-scenarist-test-id header with each request.", {
|
|
93
|
+
code: ErrorCodes.MISSING_TEST_ID,
|
|
94
|
+
context: {
|
|
95
|
+
requestInfo: {
|
|
96
|
+
method: request.method,
|
|
97
|
+
url: request.url,
|
|
98
|
+
},
|
|
99
|
+
hint: "This typically means: 1) Test didn't call switchScenario() before making requests, 2) Request originated outside the test context, or 3) Header forwarding is misconfigured.",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (options.errorBehaviors?.onMissingTestId === "warn" &&
|
|
104
|
+
options.logger) {
|
|
105
|
+
options.logger.warn(LogCategories.REQUEST, "Missing test ID header. Using default scenario.", {
|
|
106
|
+
requestUrl: request.url,
|
|
107
|
+
requestMethod: request.method,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// For 'ignore' or when no errorBehaviors set, silently continue with empty testId
|
|
111
|
+
}
|
|
112
|
+
const activeScenario = options.getActiveScenario(testId);
|
|
113
|
+
const scenarioId = activeScenario?.scenarioId ?? "default";
|
|
114
|
+
// Extract request context for matching
|
|
115
|
+
const context = await extractHttpRequestContext(request);
|
|
116
|
+
// Get candidate mocks from active or default scenario
|
|
117
|
+
const mocks = getMocksFromScenarios(activeScenario, options.getScenarioDefinition, request.method, request.url);
|
|
118
|
+
// Use injected ResponseSelector to find matching mock
|
|
119
|
+
const result = options.responseSelector.selectResponse(testId, scenarioId, context, mocks);
|
|
120
|
+
if (result.success) {
|
|
121
|
+
return buildResponse(result.data);
|
|
122
|
+
}
|
|
123
|
+
// Determine which error behavior to use based on error code
|
|
124
|
+
const errorCode = result.error.code;
|
|
125
|
+
const errorBehavior = errorCode === ErrorCodes.SEQUENCE_EXHAUSTED
|
|
126
|
+
? options.errorBehaviors?.onSequenceExhausted
|
|
127
|
+
: options.errorBehaviors?.onNoMockFound;
|
|
128
|
+
// Handle error based on configured behavior
|
|
129
|
+
if (errorBehavior === "throw") {
|
|
130
|
+
throw result.error;
|
|
131
|
+
}
|
|
132
|
+
if (errorBehavior === "warn" && options.logger) {
|
|
133
|
+
options.logger.warn("matching", result.error.message, {
|
|
134
|
+
testId,
|
|
135
|
+
scenarioId,
|
|
136
|
+
requestUrl: context.url,
|
|
137
|
+
requestMethod: context.method,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (options.strictMode) {
|
|
141
|
+
return new Response(null, { status: 501 });
|
|
142
|
+
}
|
|
143
|
+
return passthrough();
|
|
97
144
|
}
|
|
98
|
-
|
|
99
|
-
|
|
145
|
+
catch (error) {
|
|
146
|
+
// Log the error via Logger if available
|
|
147
|
+
if (options.logger) {
|
|
148
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
149
|
+
options.logger.error(LogCategories.REQUEST, `Handler error: ${errorMessage}`, {
|
|
150
|
+
testId,
|
|
151
|
+
requestUrl: request.url,
|
|
152
|
+
requestMethod: request.method,
|
|
153
|
+
}, {
|
|
154
|
+
errorName: error instanceof Error ? error.name : "Unknown",
|
|
155
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Use specific error code from ScenaristError, or fallback to HANDLER_ERROR
|
|
159
|
+
const errorCode = error instanceof ScenaristError ? error.code : "HANDLER_ERROR";
|
|
160
|
+
// Return a 500 error response with error details
|
|
161
|
+
return new Response(JSON.stringify({
|
|
162
|
+
error: "Internal mock server error",
|
|
163
|
+
message: error instanceof Error ? error.message : String(error),
|
|
164
|
+
code: errorCode,
|
|
165
|
+
}), {
|
|
166
|
+
status: 500,
|
|
167
|
+
headers: { "Content-Type": "application/json" },
|
|
168
|
+
});
|
|
100
169
|
}
|
|
101
|
-
return passthrough();
|
|
102
170
|
});
|
|
103
171
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenarist/msw-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Internal: MSW integration layer for Scenarist framework adapters",
|
|
5
5
|
"author": "Paul Hammond (citypaul) <paul@packsoftware.co.uk>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"path-to-regexp": "^6.3.0",
|
|
45
|
-
"@scenarist/core": "0.
|
|
45
|
+
"@scenarist/core": "0.3.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"msw": "^2.0.0"
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"eslint": "^9.39.1",
|
|
54
54
|
"msw": "^2.12.3",
|
|
55
55
|
"typescript": "^5.9.3",
|
|
56
|
-
"vitest": "^4.0.
|
|
56
|
+
"vitest": "^4.0.15",
|
|
57
57
|
"@scenarist/eslint-config": "^0.0.0",
|
|
58
58
|
"@scenarist/typescript-config": "0.0.0"
|
|
59
59
|
},
|