diff --git a/src/mockttp.ts b/src/mockttp.ts index a4f9c3241..b68143228 100644 --- a/src/mockttp.ts +++ b/src/mockttp.ts @@ -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), @@ -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. @@ -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. diff --git a/src/rules/matchers.ts b/src/rules/matchers.ts index 26b44c4e0..e439b79ae 100644 --- a/src/rules/matchers.ts +++ b/src/rules/matchers.ts @@ -11,7 +11,9 @@ import normalizeUrl from "../util/normalize-url"; export type MatcherData = ( WildcardMatcherData | - SimpleMatcherData | + MethodMatcherData | + SimplePathMatcherData | + RegexPathMatcherData | HeaderMatcherData | FormDataMatcherData ); @@ -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 } @@ -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'; @@ -83,16 +103,31 @@ type MatcherBuilder = (data: D) => RequestMatcher const matcherBuilders: { [T in MatcherType]: MatcherBuilder } = { 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 => { diff --git a/src/rules/mock-rule-builder.ts b/src/rules/mock-rule-builder.ts index dbe22215a..e00acfe49 100644 --- a/src/rules/mock-rule-builder.ts +++ b/src/rules/mock-rule-builder.ts @@ -18,8 +18,10 @@ import { } from "./completion-checkers"; import { - SimpleMatcherData, - MatcherData, + MatcherData, + MethodMatcherData, + SimplePathMatcherData, + RegexPathMatcherData, HeaderMatcherData, FormDataMatcherData, WildcardMatcherData @@ -57,19 +59,27 @@ export default class MockRuleBuilder { constructor(addRule: (rule: MockRuleData) => Promise) constructor( method: Method, - path: string, + path: string | RegExp, addRule: (rule: MockRuleData) => Promise ) constructor( methodOrAddRule: Method | ((rule: MockRuleData) => Promise), - path?: string, + path?: string | RegExp, addRule?: (rule: MockRuleData) => Promise ) { 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!; } } diff --git a/test/integration/explanations.spec.ts b/test/integration/explanations.spec.ts index f00ad00d8..f42bafd25 100644 --- a/test/integration/explanations.spec.ts +++ b/test/integration/explanations.spec.ts @@ -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). `); }); @@ -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). `); }); @@ -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). `); }); @@ -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). `); }); }); diff --git a/test/integration/matchers/method-path-matching.spec.ts b/test/integration/matchers/method-path-matching.spec.ts index 3a2c0862f..57f7d0965 100644 --- a/test/integration/matchers/method-path-matching.spec.ts +++ b/test/integration/matchers/method-path-matching.spec.ts @@ -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"); @@ -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");