Skip to content

Commit

Permalink
Merge pull request #93 from cultureamp/providers-fn
Browse files Browse the repository at this point in the history
feat: Allow dynamic provider matching
  • Loading branch information
YOU54F authored May 22, 2023
2 parents f4e9f18 + 76bb1bb commit 94a441d
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 15 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions examples/react/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
69 changes: 69 additions & 0 deletions src/pactFromMswServerWithProviderFn.msw.spec.ts
Original file line number Diff line number Diff line change
@@ -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");
});
})
26 changes: 15 additions & 11 deletions src/pactMswAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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[];
Expand Down Expand Up @@ -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 });
}
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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);
});
Expand Down
15 changes: 12 additions & 3 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MockedRequest } from "msw";
import { PactMswAdapterOptionsInternal } from "../pactMswAdapter";
var path = require("path");
let fs: any; // dynamic import
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 94a441d

Please sign in to comment.