Skip to content

Commit

Permalink
Allow matching paths by regular expression (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed Jun 12, 2018
1 parent b1d0e1b commit 04fe22e
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 45 deletions.
36 changes: 24 additions & 12 deletions src/mockttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,39 @@ export interface Mockttp {
anyRequest(): MockRuleBuilder;
/**
* Get a builder for a mock rule that will match GET requests for the given path.
*
* The path can be either a string, or a regular expression to match against.
*/
get(url: string): MockRuleBuilder;
get(url: string | RegExp): MockRuleBuilder;
/**
* Get a builder for a mock rule that will match POST requests for the given path.
*
* The path can be either a string, or a regular expression to match against.
*/
post(url: string): MockRuleBuilder;
post(url: string | RegExp): MockRuleBuilder;
/**
* Get a builder for a mock rule that will match PUT requests for the given path.
*
* The path can be either a string, or a regular expression to match against.
*/
put(url: string): MockRuleBuilder;
put(url: string | RegExp): MockRuleBuilder;
/**
* Get a builder for a mock rule that will match DELETE requests for the given path.
*
* The path can be either a string, or a regular expression to match against.
*/
delete(url: string): MockRuleBuilder;
delete(url: string | RegExp): MockRuleBuilder;
/**
* Get a builder for a mock rule that will match PATCH requests for the given path.
*
* The path can be either a string, or a regular expression to match against.
*/
patch(url: string): MockRuleBuilder;
patch(url: string | RegExp): MockRuleBuilder;
/**
* Get a builder for a mock rule that will match OPTIONS requests for the given path.
*
* The path can be either a string, or a regular expression to match against.
*
* This can only be used if the `cors` option has been set to false.
*
* If cors is true (the default when using a remote client, e.g. in the browser),
Expand All @@ -108,7 +120,7 @@ export interface Mockttp {
* but if you're testing in a browser you will need to ensure you mock all OPTIONS
* requests appropriately so that the browser allows your other requests to be sent.
*/
options(url: string): MockRuleBuilder;
options(url: string | RegExp): MockRuleBuilder;

/**
* Subscribe to hear about request details as they're received.
Expand Down Expand Up @@ -160,27 +172,27 @@ export abstract class AbstractMockttp {
return new MockRuleBuilder(this.addRule);
}

get(url: string): MockRuleBuilder {
get(url: string | RegExp): MockRuleBuilder {
return new MockRuleBuilder(Method.GET, url, this.addRule);
}

post(url: string): MockRuleBuilder {
post(url: string | RegExp): MockRuleBuilder {
return new MockRuleBuilder(Method.POST, url, this.addRule);
}

put(url: string): MockRuleBuilder {
put(url: string | RegExp): MockRuleBuilder {
return new MockRuleBuilder(Method.PUT, url, this.addRule);
}

delete(url: string): MockRuleBuilder {
delete(url: string | RegExp): MockRuleBuilder {
return new MockRuleBuilder(Method.DELETE, url, this.addRule);
}

patch(url: string): MockRuleBuilder {
patch(url: string | RegExp): MockRuleBuilder {
return new MockRuleBuilder(Method.PATCH, url, this.addRule);
}

options(url: string): MockRuleBuilder {
options(url: string | RegExp): MockRuleBuilder {
if (this.cors) {
throw new Error(`Cannot mock OPTIONS requests with CORS enabled.
Expand Down
53 changes: 44 additions & 9 deletions src/rules/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import normalizeUrl from "../util/normalize-url";

export type MatcherData = (
WildcardMatcherData |
SimpleMatcherData |
MethodMatcherData |
SimplePathMatcherData |
RegexPathMatcherData |
HeaderMatcherData |
FormDataMatcherData
);
Expand All @@ -20,7 +22,9 @@ export type MatcherType = MatcherData['type'];

export type MatcherDataLookup = {
'wildcard': WildcardMatcherData,
'simple': SimpleMatcherData,
'method': MethodMatcherData,
'simple-path': SimplePathMatcherData,
'regex-path': RegexPathMatcherData,
'header': HeaderMatcherData,
'form-data': FormDataMatcherData
}
Expand All @@ -29,15 +33,31 @@ export class WildcardMatcherData {
readonly type: 'wildcard' = 'wildcard';
}

export class SimpleMatcherData {
readonly type: 'simple' = 'simple';
export class MethodMatcherData {
readonly type: 'method' = 'method';

constructor(
public method: Method
) {}
}

export class SimplePathMatcherData {
readonly type: 'simple-path' = 'simple-path';

constructor(
public method: Method,
public path: string
) {}
}

export class RegexPathMatcherData {
readonly type: 'regex-path' = 'regex-path';
readonly regexString: string;

constructor(regex: RegExp) {
this.regexString = regex.source;
}
}

export class HeaderMatcherData {
readonly type: 'header' = 'header';

Expand Down Expand Up @@ -83,16 +103,31 @@ type MatcherBuilder<D extends MatcherData> = (data: D) => RequestMatcher

const matcherBuilders: { [T in MatcherType]: MatcherBuilder<MatcherDataLookup[T]> } = {
wildcard: (): RequestMatcher => {
return _.assign(() => true, { explain: () => 'for any method and path' })
return _.assign(() => true, { explain: () => 'for anything' })
},

simple: (data: SimpleMatcherData): RequestMatcher => {
method: (data: MethodMatcherData): RequestMatcher => {
let methodName = Method[data.method];

return _.assign((request: OngoingRequest) =>
request.method === methodName
, { explain: () => `making ${methodName}s` });
},

'simple-path': (data: SimplePathMatcherData): RequestMatcher => {
let url = normalizeUrl(data.path);

return _.assign((request: OngoingRequest) =>
request.method === methodName && normalizeUrl(request.url) === url
, { explain: () => `making ${methodName}s for ${data.path}` });
normalizeUrl(request.url) === url
, { explain: () => `for ${data.path}` });
},

'regex-path': (data: RegexPathMatcherData): RequestMatcher => {
let url = new RegExp(data.regexString);

return _.assign((request: OngoingRequest) =>
url.test(normalizeUrl(request.url))
, { explain: () => `for paths matching /${data.regexString}/` });
},

header: (data: HeaderMatcherData): RequestMatcher => {
Expand Down
20 changes: 15 additions & 5 deletions src/rules/mock-rule-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
} from "./completion-checkers";

import {
SimpleMatcherData,
MatcherData,
MatcherData,
MethodMatcherData,
SimplePathMatcherData,
RegexPathMatcherData,
HeaderMatcherData,
FormDataMatcherData,
WildcardMatcherData
Expand Down Expand Up @@ -57,19 +59,27 @@ export default class MockRuleBuilder {
constructor(addRule: (rule: MockRuleData) => Promise<MockedEndpoint>)
constructor(
method: Method,
path: string,
path: string | RegExp,
addRule: (rule: MockRuleData) => Promise<MockedEndpoint>
)
constructor(
methodOrAddRule: Method | ((rule: MockRuleData) => Promise<MockedEndpoint>),
path?: string,
path?: string | RegExp,
addRule?: (rule: MockRuleData) => Promise<MockedEndpoint>
) {
if (methodOrAddRule instanceof Function) {
this.matchers.push(new WildcardMatcherData());
this.addRule = methodOrAddRule;
return;
}

this.matchers.push(new MethodMatcherData(methodOrAddRule));

if (path instanceof RegExp) {
this.matchers.push(new RegexPathMatcherData(path));
this.addRule = addRule!;
} else {
this.matchers.push(new SimpleMatcherData(methodOrAddRule, path!));
this.matchers.push(new SimplePathMatcherData(path!));
this.addRule = addRule!;
}
}
Expand Down
32 changes: 16 additions & 16 deletions test/integration/explanations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ describe("Mockttp explanation messages", function () {
let responseText = await response.text();

expect(responseText).to.include(`
Match requests making GETs for /endpoint, and then respond with status 200 and body "1", once (seen 0).
Match requests making GETs for /endpoint, and then respond with status 200 and body "2/3", twice (seen 0).
Match requests making GETs for /endpoint, and then respond with status 200 and body "4/5/6", thrice (seen 0).
Match requests making GETs for /endpoint, and then respond with status 200 and body "7/8/9/10", 4 times (seen 0).
Match requests making GETs for /endpoint, and then respond with status 200 and body "forever", always (seen 0).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "1", once (seen 0).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "2/3", twice (seen 0).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "4/5/6", thrice (seen 0).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "7/8/9/10", 4 times (seen 0).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "forever", always (seen 0).
`);
});

Expand All @@ -42,11 +42,11 @@ Match requests making GETs for /endpoint, and then respond with status 200 and b
let responseText = await response.text();

expect(responseText).to.include(`
Match requests making GETs for /endpoint, and then respond with status 200 and body "1", once (done).
Match requests making GETs for /endpoint, and then respond with status 200 and body "2/3", twice (done).
Match requests making GETs for /endpoint, and then respond with status 200 and body "4/5/6", thrice (done).
Match requests making GETs for /endpoint, and then respond with status 200 and body "7/8/9/10", 4 times (seen 2).
Match requests making GETs for /endpoint, and then respond with status 200 and body "forever", always (seen 0).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "1", once (done).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "2/3", twice (done).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "4/5/6", thrice (done).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "7/8/9/10", 4 times (seen 2).
Match requests making GETs, and for /endpoint, and then respond with status 200 and body "forever", always (seen 0).
`);
});

Expand All @@ -63,10 +63,10 @@ Match requests making GETs for /endpoint, and then respond with status 200 and b

expect(text).to.include(`No rules were found matching this request.`);
expect(text).to.include(`The configured rules are:
Match requests for any method and path, and with headers including {"h":"v"}, and then respond with status 200.
Match requests making GETs for /endpointA, and then respond with status 200 and body "nice request!", once (done).
Match requests making POSTs for /endpointB, and with form data including {"key":"value"}, and then respond with status 500.
Match requests making PUTs for /endpointC, and then respond with status 200 and body "good headers", always (seen 0).
Match requests for anything, and with headers including {"h":"v"}, and then respond with status 200.
Match requests making GETs, and for /endpointA, and then respond with status 200 and body "nice request!", once (done).
Match requests making POSTs, for /endpointB, and with form data including {"key":"value"}, and then respond with status 500.
Match requests making PUTs, and for /endpointC, and then respond with status 200 and body "good headers", always (seen 0).
`);
});

Expand All @@ -79,8 +79,8 @@ Match requests making PUTs for /endpointC, and then respond with status 200 and
let text = await response.text();

expect(text).to.include(`The configured rules are:
Match requests making POSTs for /endpointA, and then respond using provided callback.
Match requests making POSTs for /endpointB, and then respond using provided callback (handleRequest).
Match requests making POSTs, and for /endpointA, and then respond using provided callback.
Match requests making POSTs, and for /endpointB, and then respond using provided callback (handleRequest).
`);
});
});
Expand Down
24 changes: 21 additions & 3 deletions test/integration/matchers/method-path-matching.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,23 @@ responses by hand.`);
methods.forEach((methodName: Method) => {
it(`should match requests by ${methodName}`, async () => {
await server[methodName]('/').thenReply(200, methodName);
return expect(fetch(server.url, {

let result = await fetch(server.url, {
method: methodName.toUpperCase(),
})).to.have.responseText(methodName);
});

expect(result).to.have.responseText(methodName);
});
});

it("should match requests for a matching regex path", async () => {
await server.get(/.*.txt/).thenReply(200, 'Fake file');

let result = await fetch(server.urlFor('/matching-file.txt'));

expect(result).to.have.responseText('Fake file');
});

it("should reject requests for the wrong path", async () => {
await server.get("/specific-endpoint").thenReply(200, "mocked data");

Expand All @@ -47,6 +57,14 @@ responses by hand.`);
expect(result.status).to.equal(503);
});

it("should reject requests that don't match a regex path", async () => {
await server.get(/.*.txt/).thenReply(200, 'Fake file');

let result = await fetch(server.urlFor('/non-matching-file.css'));

expect(result.status).to.equal(503);
});

it("should allowing matching all requests, with a wildcard", async () => {
await server.anyRequest().thenReply(200, "wildcard response");

Expand Down

0 comments on commit 04fe22e

Please sign in to comment.