diff --git a/README.md b/README.md index 23176a3..ba00747 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Let's start by listing it's methods: | 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. | | consumer | `true` | `string` | | name of the consumer running the tests | -| providers | `true` | `{ [string]: string[] }` | | names and filters for each provider | +| 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 | | includeUrl | `false` | `string[]` | | inclusive filters for network calls | | excludeUrl | `false` | `string[]` | | exclusive filters for network calls | @@ -89,6 +89,8 @@ This mechanism has three layers, in order of priority: The first two layers can be skipped by setting it’s value to `undefined`. The third layer is mandatory. +`providers` can be also a function that returns name of provider for given request. If no provider is matched it should return `null`. This allows dynamically matching providers based on url patterns. + ## Header filtering By default pact-msw-adapter captures and serialises all request and response headers captured, in the generated pact file. diff --git a/examples/react/src/api.js b/examples/react/src/api.js index 45d72a5..787a5c5 100644 --- a/examples/react/src/api.js +++ b/examples/react/src/api.js @@ -53,6 +53,16 @@ export class API { }) .then(r => r.data); } + + async getUser(params) { + return axios.get(this.withPath("/user"), { + params, + headers: { + "Authorization": this.generateAuthToken() + } + }) + .then(r => r.data); + } } export default new API(process.env.REACT_APP_API_BASE_URL); diff --git a/src/pactFromMswServerWithProviderFn.msw.spec.ts b/src/pactFromMswServerWithProviderFn.msw.spec.ts new file mode 100644 index 0000000..86c9cb8 --- /dev/null +++ b/src/pactFromMswServerWithProviderFn.msw.spec.ts @@ -0,0 +1,69 @@ +import API from "../examples/react/src/api"; +import { rest } from "msw"; +import { setupServer } from "msw/node"; +import { PactFile, setupPactMswAdapter } from "./pactMswAdapter"; + +const server = setupServer(); +const pactMswAdapter = setupPactMswAdapter({ + server, + options: { + consumer: "testDynamicProvidersConsumer", + providers: (req) => { + // first segment of the path is the provider name + return req.url.pathname.match(/\/([\w\d]+)\/?.*/)?.[1] ?? null + }, + debug: true, + excludeHeaders: ["x-powered-by", "cookie"], + }, +}); + +describe("API - With MSW mock generating a pact for dynamic providers", () => { + beforeAll(async () => { + server.listen(); + pactMswAdapter.clear(); + }); + + beforeEach(async () => { + pactMswAdapter.newTest(); + }); + + afterEach(async () => { + pactMswAdapter.verifyTest(); + server.resetHandlers(); + }); + + afterAll(async () => { + await pactMswAdapter.writeToFile(); // writes the pacts to a file + pactMswAdapter.clear(); + server.close() + }); + + test("get all products and user", async () => { + const products = [ + { id: "09", type: "CREDIT_CARD", name: "Gem Visa" }, + { id: "10", type: "CREDIT_CARD", name: "28 Degrees" }, + ]; + const user = { name: "John Doe" }; + server.use( + rest.get(API.url + "/products", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(products)); + }), + rest.get(API.url + "/user", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(user)); + }), + ); + + const respProducts = await API.getAllProducts(); + const respUser = await API.getUser(); + expect(respProducts).toEqual(products); + expect(respUser).toEqual(user); + + let pactResults: PactFile[] = []; + await pactMswAdapter.writeToFile((path, data) => { + pactResults.push(data as PactFile); + }); // writes the pacts to a file + expect(pactResults.length).toEqual(2); + expect(pactResults[0].provider.name).toEqual("products"); + expect(pactResults[1].provider.name).toEqual("user"); + }); +}) \ No newline at end of file diff --git a/src/pactMswAdapter.ts b/src/pactMswAdapter.ts index 2836d78..61e3115 100644 --- a/src/pactMswAdapter.ts +++ b/src/pactMswAdapter.ts @@ -16,7 +16,7 @@ export interface PactMswAdapterOptions { debug?: boolean; pactOutDir?: string; consumer: string; - providers: { [name: string]: string[] }; + providers: { [name: string]: string[] } | ((match: MockedRequest) => string | null); includeUrl?: string[]; excludeUrl?: string[]; excludeHeaders?: string[]; @@ -27,7 +27,7 @@ export interface PactMswAdapterOptionsInternal { debug: boolean; pactOutDir: string; consumer: string; - providers: { [name: string]: string[] }; + providers: { [name: string]: string[] } | ((match: MockedRequest) => string | null); includeUrl?: string[]; excludeUrl?: string[]; excludeHeaders?: string[]; @@ -89,8 +89,7 @@ export const setupPactMswAdapter = ({ const matches: MswMatch[] = []; // Completed request-response pairs mswMocker.events.on("request:match", (req) => { - const url = req.url.toString(); - if (!checkUrlFilters(url, options)) return; + if (!checkUrlFilters(req, options)) return; if (options.debug) { logGroup(["Matching request", req], { endGroup: true, mode: "debug", logger: options.logger }); } @@ -185,7 +184,7 @@ export const setupPactMswAdapter = ({ mswMocker.events.on("request:unhandled", (req) => { const url = req.url.toString(); - if (!checkUrlFilters(url, options)) return; + if (!checkUrlFilters(req, options)) return; unhandledRequests.push(url); log(`Unhandled request: ${url}`, { mode: "warn", logger: options.logger }); @@ -333,14 +332,19 @@ const transformMswToPact = async ( ); const pactFiles: PactFile[] = []; - const providers = Object.entries(options.providers); + + const matchProvider = (request: MockedRequest) => { + if (typeof options.providers === "function") return options.providers(request); + const url = request.url.toString(); + return Object.entries(options.providers) + .find(([_, paths]) => + paths.some((path) => url.includes(path)) + )?.[0]; + } + const matchesByProvider: { [key: string]: MswMatch[] } = {}; matches.forEach((match) => { - const url = match.request.url.toString(); - const provider = - providers.find(([_, paths]) => - paths.some((path) => url.includes(path)) - )?.[0] || "unknown"; + const provider = matchProvider(match.request) ?? "unknown"; if (!matchesByProvider[provider]) matchesByProvider[provider] = []; matchesByProvider[provider].push(match); }); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index af2492d..342cc90 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,3 +1,4 @@ +import { MockedRequest } from "msw"; import { PactMswAdapterOptionsInternal } from "../pactMswAdapter"; var path = require("path"); let fs: any; // dynamic import @@ -62,9 +63,17 @@ const createWriter = (options: PactMswAdapterOptionsInternal) => (filePath: stri } }; -const checkUrlFilters = (urlString: string, options: PactMswAdapterOptionsInternal) => { - const providerFilter = Object.values(options.providers) - ?.some(validPaths => validPaths.some(path => urlString.includes(path))); +const hasProvider = (request: MockedRequest, options: PactMswAdapterOptionsInternal) => { + if (typeof options.providers === 'function') { + return options.providers(request) !== null; + } + return Object.values(options.providers) + ?.some(validPaths => validPaths.some(path => request.url.toString().includes(path))); +}; + +const checkUrlFilters = (request: MockedRequest, options: PactMswAdapterOptionsInternal) => { + const urlString = request.url.toString(); + const providerFilter = hasProvider(request, options); const includeFilter = !options.includeUrl || options.includeUrl.some(inc => urlString.includes(inc)); const excludeFilter = !options.excludeUrl || !options.excludeUrl.some(exc => urlString.includes(exc)); const matchIsAllowed = includeFilter && excludeFilter && providerFilter