@pactflow/pact-msw-adapter
Advanced tools
Comparing version 2.0.1 to 3.0.0
@@ -5,2 +5,13 @@ # Changelog | ||
## [3.0.0](https://github.com/pactflow/pact-msw-adapter/compare/v2.0.1...v3.0.0) (2023-11-09) | ||
### ⚠ BREAKING CHANGES | ||
* make compatible and consistent with msw v2 | ||
* update dependencies including msw to v2 | ||
* make compatible and consistent with msw v2 ([63c98f6](https://github.com/pactflow/pact-msw-adapter/commit/63c98f67d911ed391e60860ec203f6cd14fa8b22)) | ||
* update dependencies including msw to v2 ([37cf49d](https://github.com/pactflow/pact-msw-adapter/commit/37cf49d79b4e44a2d0a0ba796a60bd117f22b7fe)) | ||
### [2.0.1](https://github.com/pactflow/pact-msw-adapter/compare/v2.0.0...v2.0.1) (2023-11-07) | ||
@@ -7,0 +18,0 @@ |
@@ -1,9 +0,11 @@ | ||
import { PactFile, MswMatch } from "./pactMswAdapter"; | ||
import { PactFile, MatchedRequest } from "./pactMswAdapter"; | ||
import { JSONValue } from "./utils/utils"; | ||
export declare const readBody: (input: Request | Response) => Promise<JSONValue | FormData | undefined>; | ||
export declare const convertMswMatchToPact: ({ consumer, provider, matches, headers, }: { | ||
consumer: string; | ||
provider: string; | ||
matches: MswMatch[]; | ||
matches: MatchedRequest[]; | ||
headers?: { | ||
excludeHeaders: string[] | undefined; | ||
} | undefined; | ||
}) => PactFile; | ||
}) => Promise<PactFile>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.convertMswMatchToPact = void 0; | ||
exports.convertMswMatchToPact = exports.readBody = void 0; | ||
const lodash_1 = require("lodash"); | ||
const pjson = require("../package.json"); | ||
const convertMswMatchToPact = ({ consumer, provider, matches, headers, }) => { | ||
const readBody = async (input) => { | ||
// so we don't reread body somewhere | ||
const clone = input.clone(); | ||
if (clone.body === null) | ||
return undefined; | ||
const contentType = clone.headers.get("content-type"); | ||
if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("application/json")) { | ||
return clone.json(); | ||
} | ||
else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("multipart/form-data")) { | ||
return clone.formData(); | ||
} | ||
// default to text | ||
return clone.text(); | ||
}; | ||
exports.readBody = readBody; | ||
const convertMswMatchToPact = async ({ consumer, provider, matches, headers, }) => { | ||
const pactFile = { | ||
consumer: { name: consumer }, | ||
provider: { name: provider }, | ||
interactions: matches.map((match) => { | ||
var _a; | ||
interactions: await Promise.all(matches.map(async (match) => { | ||
var _a, _b, _c; | ||
return ({ | ||
description: match.request.id, | ||
description: match.requestId, | ||
providerState: "", | ||
request: { | ||
method: match.request.method, | ||
path: match.request.url.pathname, | ||
headers: (headers === null || headers === void 0 ? void 0 : headers.excludeHeaders) | ||
? (0, lodash_1.omit)(Object.fromEntries(match.request.headers.entries()), headers.excludeHeaders) | ||
: Object.fromEntries(match.request.headers.entries()), | ||
body: match.request.body || undefined, | ||
query: match.request.url.search | ||
? match.request.url.search.split("?")[1] | ||
: undefined, | ||
path: new URL(match.request.url).pathname, | ||
headers: (0, lodash_1.omit)(Object.fromEntries(match.request.headers.entries()), (_a = headers === null || headers === void 0 ? void 0 : headers.excludeHeaders) !== null && _a !== void 0 ? _a : []), | ||
body: await (0, exports.readBody)(match.request), | ||
query: (_b = new URL(match.request.url).search) === null || _b === void 0 ? void 0 : _b.split("?")[1] | ||
}, | ||
response: { | ||
status: match.response.status, | ||
headers: (headers === null || headers === void 0 ? void 0 : headers.excludeHeaders) | ||
? (0, lodash_1.omit)(Object.fromEntries(match.response.headers.entries()), headers.excludeHeaders) | ||
: Object.fromEntries(match.response.headers.entries()), | ||
body: match.body | ||
? ((_a = match.response.headers.get("content-type")) === null || _a === void 0 ? void 0 : _a.includes("json")) | ||
? JSON.parse(match.body) | ||
: match.body | ||
: undefined, | ||
headers: (0, lodash_1.omit)(Object.fromEntries(match.response.headers.entries()), (_c = headers === null || headers === void 0 ? void 0 : headers.excludeHeaders) !== null && _c !== void 0 ? _c : []), | ||
body: await (0, exports.readBody)(match.response), | ||
}, | ||
}); | ||
}), | ||
})), | ||
metadata: { | ||
@@ -40,0 +46,0 @@ pactSpecification: { |
/// <reference types="node" /> | ||
import { DefaultRequestBody, MockedRequest, SetupWorkerApi } from "msw"; | ||
import { Logger } from "./utils/utils"; | ||
import { SetupWorker } from "msw/browser"; | ||
import { SetupServer } from "msw/node"; | ||
import { JSONValue, Logger } from "./utils/utils"; | ||
import { convertMswMatchToPact } from "./convertMswMatchToPact"; | ||
import { EventEmitter } from "events"; | ||
import { SetupServerApi } from "msw/lib/types/node/glossary"; | ||
import { IsomorphicResponse } from "@mswjs/interceptors"; | ||
export interface PactMswAdapterOptions { | ||
@@ -15,3 +14,3 @@ timeout?: number; | ||
[name: string]: string[]; | ||
} | ((match: MockedRequest) => string | null); | ||
} | ((event: PendingRequest) => string | null); | ||
includeUrl?: string[]; | ||
@@ -29,3 +28,3 @@ excludeUrl?: string[]; | ||
[name: string]: string[]; | ||
} | ((match: MockedRequest) => string | null); | ||
} | ((event: PendingRequest) => string | null); | ||
includeUrl?: string[]; | ||
@@ -45,4 +44,4 @@ excludeUrl?: string[]; | ||
options: PactMswAdapterOptions; | ||
worker?: SetupWorkerApi | undefined; | ||
server?: SetupServerApi | undefined; | ||
worker?: SetupWorker | undefined; | ||
server?: SetupServer | undefined; | ||
}) => PactMswAdapter; | ||
@@ -57,3 +56,3 @@ export { convertMswMatchToPact }; | ||
headers: any; | ||
body: DefaultRequestBody; | ||
body: JSONValue | FormData | undefined; | ||
query?: string; | ||
@@ -90,11 +89,12 @@ }; | ||
} | ||
export interface MswMatch { | ||
request: MockedRequest; | ||
response: IsomorphicResponse | Response; | ||
body: string | undefined; | ||
export interface PendingRequest { | ||
request: Request; | ||
requestId: string; | ||
} | ||
export interface ExpiredRequest { | ||
reqId: string; | ||
export interface MatchedRequest extends PendingRequest { | ||
response: Response; | ||
} | ||
export interface ExpiredRequest extends PendingRequest { | ||
startTime: number; | ||
duration?: number; | ||
} |
@@ -40,44 +40,36 @@ "use strict"; | ||
const matches = []; // Completed request-response pairs | ||
mswMocker.events.on("request:match", (req) => { | ||
if (!(0, utils_1.checkUrlFilters)(req, options)) | ||
mswMocker.events.on("request:match", ({ request, requestId }) => { | ||
if (!(0, utils_1.checkUrlFilters)({ request, requestId }, options)) | ||
return; | ||
if (options.debug) { | ||
(0, utils_1.logGroup)(["Matching request", req], { endGroup: true, mode: "debug", logger: options.logger }); | ||
(0, utils_1.logGroup)(["Matching request", request], { endGroup: true, mode: "debug", logger: options.logger }); | ||
} | ||
const startTime = Date.now(); | ||
pendingRequests.push(req); | ||
activeRequestIds.push(req.id); | ||
pendingRequests.push({ request, requestId }); | ||
activeRequestIds.push(requestId); | ||
setTimeout(() => { | ||
const activeIdx = activeRequestIds.indexOf(req.id); | ||
emitter.emit("pact-msw-adapter:expired", req); | ||
const expired = { requestId, startTime, request }; | ||
const activeIdx = activeRequestIds.indexOf(requestId); | ||
emitter.emit("pact-msw-adapter:expired", expired); | ||
if (activeIdx >= 0) { | ||
// Could be removed if completed or the test ended | ||
activeRequestIds.splice(activeIdx, 1); | ||
expiredRequests.push({ | ||
reqId: req.id, | ||
startTime, | ||
}); | ||
expiredRequests.push(expired); | ||
} | ||
}, options.timeout); | ||
}); | ||
mswMocker.events.on("response:mocked", async (response, reqId) => { | ||
// https://mswjs.io/docs/extensions/life-cycle-events#responsemocked | ||
// Note that the res instance differs between the browser and Node.js. | ||
// Take this difference into account when operating with it. | ||
const responseBody = isWorker | ||
? await response.text() | ||
: response.body; | ||
mswMocker.events.on("response:mocked", async ({ response, requestId }) => { | ||
(0, utils_1.logGroup)(JSON.stringify(response), { endGroup: true, logger: options.logger }); | ||
const reqIdx = pendingRequests.findIndex((req) => req.id === reqId); | ||
const reqIdx = pendingRequests.findIndex((pending) => pending.requestId === requestId); | ||
if (reqIdx < 0) | ||
return; // Filtered and (expired and cleared) requests | ||
const endTime = Date.now(); | ||
const request = pendingRequests.splice(reqIdx, 1)[0]; | ||
const activeReqIdx = activeRequestIds.indexOf(reqId); | ||
const { request } = pendingRequests.splice(reqIdx, 1)[0]; | ||
const activeReqIdx = activeRequestIds.indexOf(requestId); | ||
if (activeReqIdx < 0) { | ||
// Expired requests and responses from previous tests | ||
const oldReqId = oldRequestIds.find((id) => id === reqId); | ||
const expiredReq = expiredRequests.find((expired) => expired.reqId === reqId); | ||
const oldReqId = oldRequestIds.find((id) => id === requestId); | ||
const expiredReq = expiredRequests.find((expired) => expired.requestId === requestId); | ||
if (oldReqId) { | ||
orphanResponses.push(request.url.toString()); | ||
orphanResponses.push(request.url); | ||
(0, utils_1.log)(`Orphan response: ${request.url}`, { | ||
@@ -91,3 +83,4 @@ mode: "warn", | ||
if (!oldReqId) { | ||
(0, utils_1.log)(`Expired request to ${request.url.pathname}`, { | ||
const pathname = new URL(request.url).pathname; | ||
(0, utils_1.log)(`Expired request to ${pathname}`, { | ||
mode: "warn", | ||
@@ -112,4 +105,4 @@ group: true, | ||
request, | ||
requestId, | ||
response, | ||
body: responseBody, | ||
}; | ||
@@ -119,8 +112,7 @@ emitter.emit("pact-msw-adapter:match", match); | ||
}); | ||
mswMocker.events.on("request:unhandled", (req) => { | ||
const url = req.url.toString(); | ||
if (!(0, utils_1.checkUrlFilters)(req, options)) | ||
mswMocker.events.on("request:unhandled", ({ request, requestId }) => { | ||
if (!(0, utils_1.checkUrlFilters)({ request, requestId }, options)) | ||
return; | ||
unhandledRequests.push(url); | ||
(0, utils_1.log)(`Unhandled request: ${url}`, { mode: "warn", logger: options.logger }); | ||
unhandledRequests.push(request.url); | ||
(0, utils_1.log)(`Unhandled request: ${request.url}`, { mode: "warn", logger: options.logger }); | ||
}); | ||
@@ -144,6 +136,9 @@ return { | ||
expired, | ||
req: pendingRequests.find((req) => req.id === expired.reqId), | ||
pending: pendingRequests.find((pending) => pending.requestId === expired.requestId), | ||
})) | ||
.filter(({ expired, req }) => expired && req) | ||
.map(({ expired, req }) => `${req.url.pathname}${expired.duration ? `took ${expired.duration}ms and` : ""} timed out after ${options.timeout}ms`) | ||
.filter(({ expired, pending }) => expired && pending) | ||
.map(({ expired, pending }) => { | ||
const pathname = new URL(pending.request.url).pathname; | ||
return `${pathname}${expired.duration ? `took ${expired.duration}ms and` : ""} timed out after ${options.timeout}ms`; | ||
}) | ||
.join("\n")}\n`; | ||
@@ -228,9 +223,8 @@ expiredRequests.length = 0; | ||
const pactFiles = []; | ||
const matchProvider = (request) => { | ||
const matchProvider = (match) => { | ||
var _a; | ||
if (typeof options.providers === "function") | ||
return options.providers(request); | ||
const url = request.url.toString(); | ||
return options.providers(match); | ||
return (_a = Object.entries(options.providers) | ||
.find(([_, paths]) => paths.some((path) => url.includes(path)))) === null || _a === void 0 ? void 0 : _a[0]; | ||
.find(([_, paths]) => paths.some((path) => match.request.url.includes(path)))) === null || _a === void 0 ? void 0 : _a[0]; | ||
}; | ||
@@ -240,3 +234,3 @@ const matchesByProvider = {}; | ||
var _a; | ||
const provider = (_a = matchProvider(match.request)) !== null && _a !== void 0 ? _a : "unknown"; | ||
const provider = (_a = matchProvider(match)) !== null && _a !== void 0 ? _a : "unknown"; | ||
if (!matchesByProvider[provider]) | ||
@@ -247,3 +241,3 @@ matchesByProvider[provider] = []; | ||
for (const [provider, providerMatches] of Object.entries(matchesByProvider)) { | ||
const pactFile = (0, convertMswMatchToPact_1.convertMswMatchToPact)({ | ||
const pactFile = await (0, convertMswMatchToPact_1.convertMswMatchToPact)({ | ||
consumer: options.consumer, | ||
@@ -250,0 +244,0 @@ provider, |
@@ -1,3 +0,2 @@ | ||
import { MockedRequest } from "msw"; | ||
import { PactMswAdapterOptionsInternal } from "../pactMswAdapter"; | ||
import { PactMswAdapterOptionsInternal, PendingRequest } from "../pactMswAdapter"; | ||
type LogLevel = 'debug' | 'info' | 'warn' | 'error'; | ||
@@ -16,4 +15,7 @@ export type Logger = Pick<typeof console, LogLevel | 'groupEnd' | 'groupCollapsed'>; | ||
declare const createWriter: (options: PactMswAdapterOptionsInternal) => (filePath: string, data: Object) => void; | ||
declare const checkUrlFilters: (request: MockedRequest, options: PactMswAdapterOptionsInternal) => boolean; | ||
declare const checkUrlFilters: (pending: PendingRequest, options: PactMswAdapterOptionsInternal) => boolean; | ||
declare const addTimeout: <T>(promise: Promise<T>, label: string, timeout: number) => Promise<void | T>; | ||
export type JSONValue = string | number | boolean | null | { | ||
[key: string]: JSONValue; | ||
} | JSONValue[]; | ||
export { log, logGroup, createWriter, checkUrlFilters, addTimeout }; |
@@ -64,12 +64,12 @@ "use strict"; | ||
exports.createWriter = createWriter; | ||
const hasProvider = (request, options) => { | ||
const hasProvider = (pending, options) => { | ||
var _a; | ||
if (typeof options.providers === 'function') { | ||
return options.providers(request) !== null; | ||
return options.providers(pending) !== null; | ||
} | ||
return (_a = Object.values(options.providers)) === null || _a === void 0 ? void 0 : _a.some(validPaths => validPaths.some(path => request.url.toString().includes(path))); | ||
return (_a = Object.values(options.providers)) === null || _a === void 0 ? void 0 : _a.some(validPaths => validPaths.some(path => pending.request.url.includes(path))); | ||
}; | ||
const checkUrlFilters = (request, options) => { | ||
const urlString = request.url.toString(); | ||
const providerFilter = hasProvider(request, options); | ||
const checkUrlFilters = (pending, options) => { | ||
const urlString = pending.request.url.toString(); | ||
const providerFilter = hasProvider(pending, options); | ||
const includeFilter = !options.includeUrl || options.includeUrl.some(inc => urlString.includes(inc)); | ||
@@ -76,0 +76,0 @@ const excludeFilter = !options.excludeUrl || !options.excludeUrl.some(exc => urlString.includes(exc)); |
{ | ||
"name": "@pactflow/pact-msw-adapter", | ||
"version": "2.0.1", | ||
"version": "3.0.0", | ||
"main": "./dist/pactMswAdapter.js", | ||
@@ -23,6 +23,6 @@ "keywords": [ | ||
"peerDependencies": { | ||
"msw": ">=0.35.0" | ||
"msw": ">=2.0.0" | ||
}, | ||
"engines": { | ||
"node": ">=16" | ||
"node": ">=18" | ||
}, | ||
@@ -47,19 +47,18 @@ "scripts": { | ||
"devDependencies": { | ||
"@babel/preset-env": "7.16.11", | ||
"@pact-foundation/pact": "9.17.2", | ||
"@types/axios": "0.14.0", | ||
"@types/jest": "27.4.1", | ||
"@typescript-eslint/eslint-plugin": "5.56.0", | ||
"@typescript-eslint/parser": "5.59.5", | ||
"axios": "0.26.1", | ||
"babel-jest": "27.5.1", | ||
"eslint": "8.10.0", | ||
"jest": "27.5.1", | ||
"msw": "0.36.8", | ||
"nock": "13.2.4", | ||
"regenerator-runtime": "0.13.11", | ||
"rimraf": "5.0.1", | ||
"standard-version": "9.5.0", | ||
"ts-jest": "27.1.3", | ||
"typescript": "4.9.5" | ||
"@babel/preset-env": "^7.23.2", | ||
"@pact-foundation/pact": "^12.1.0", | ||
"@types/jest": "^29.5.7", | ||
"@typescript-eslint/eslint-plugin": "^6.9.1", | ||
"@typescript-eslint/parser": "^6.9.1", | ||
"axios": "^1.6.0", | ||
"babel-jest": "^29.7.0", | ||
"eslint": "^8.52.0", | ||
"jest": "^29.7.0", | ||
"msw": "^2.0.2", | ||
"nock": "^13.3.7", | ||
"regenerator-runtime": "^0.14.0", | ||
"rimraf": "^5.0.5", | ||
"standard-version": "^9.5.0", | ||
"ts-jest": "^29.1.1", | ||
"typescript": "^5.2.2" | ||
}, | ||
@@ -66,0 +65,0 @@ "dependencies": { |
@@ -25,2 +25,9 @@ # pact-msw-adapter | ||
## Compatibility table | ||
| pact msw version | msw version | node version | migration guide | | ||
|------------------|-------------|--------------|-------------------------------------------------------| | ||
| `^2` | `<=1` | `>=16 <=20` | | | ||
| `^3` | `^2` | `>=18` | [v2 to v3](#migrating-pact-msw-adapter-from-v2-to-v3) | | ||
## Getting started | ||
@@ -46,3 +53,3 @@ | ||
```js | ||
import { setupWorker } from "msw"; | ||
import { setupWorker } from "msw/browser"; | ||
import { setupPactMswAdapter } from "@pactflow/pact-msw-adapter"; | ||
@@ -71,8 +78,8 @@ ``` | ||
| - | - | - | - | - | | ||
| server | false | `SetupServerApi` | | server provided by msw - a server or worker must be provided| | ||
| worker | false | `SetupWorkerApi` | | worker provided by msw - a server or worker must be provided| | ||
| timeout | `false` | `number` | 200 | Time in ms for a network request to expire, `verifyTest` will fail after twice this amount of time. | | ||
| server | `false` | `SetupServer` | | server provided by msw - a server or worker must be provided| | ||
| worker | `false` | `SetupWorker` | | worker provided by msw - a server or worker must be provided| | ||
| timeout | `false` | `number` | `200` | Time in ms for a network request to expire, `verifyTest` will fail after twice this amount of time. | | ||
| consumer | `true` | `string` | | name of the consumer running the tests | | ||
| providers | `true` | `{ [string]: string[] } | (request: MockedRequest) => string | null` | | names and filters for each provider or function that returns name of provider for given request | | ||
| pactOutDir | `false` | `string` | ./msw_generated_pacts/ | path to write pact files into | | ||
| providers | `true` | `{ [string]: string[] } \| ({ request: Request; requestId: string }) => string \| null` | | names and filters for each provider or function that returns name of provider for given request | | ||
| pactOutDir | `false` | `string` | `./msw_generated_pacts/` | path to write pact files into | | ||
| includeUrl | `false` | `string[]` | | inclusive filters for network calls | | ||
@@ -225,2 +232,20 @@ | excludeUrl | `false` | `string[]` | | exclusive filters for network calls | | ||
## Migrating pact-msw-adapter from v2 to v3 | ||
In [October 2023 msw released new version 2](https://mswjs.io/blog/introducing-msw-2.0) that bring significant changes to the msw interface. To reflect these changes we've released pact-msw-adapter@v3 that's compatible with msw@v2. | ||
To migrate you'll need to update `msw to >=2.0` and migrate your usage of the library ([migration guide here](https://mswjs.io/docs/migrations/1.x-to-2.x)). | ||
Breaking changes on pact-msw-adapter side: | ||
- minimal required version of Node is v18 | ||
- some exported types were renamed and extended to match msw behaviour | ||
- `MswMatch` is now `MatchedRequest` and is `{ request: Request; requestId: string; response: Response }` | ||
- `ExpiredRequest` still called the same and is `{ request: Request; requestId: string; startTime: number; duration?: number }` | ||
- added `PendingRequest` as `{ request: Request; requestId: string; }` | ||
- `PactMswAdapterOptions.providers` function variant is now consistent with msw and has signature `(event: PendingRequest) => string | null` | ||
- `PactMswAdapter.emitter` events are slightly updated to match msw behaviour | ||
- `pact-msw-adapter:expired` handler must have signature `(event: ExpiredRequest) => void` | ||
- `pact-msw-adapter:match` handler must have signature `(event: MatchedRequest) => void` | ||
- `convertMswMatchToPact` is now async function | ||
## Contributors | ||
@@ -227,0 +252,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45948
16
531
267