Skip to content

Commit 5d57c32

Browse files
authored
feat(core): Filter requests matched by a route handler (#189)
1 parent e9678c4 commit 5d57c32

File tree

6 files changed

+242
-76
lines changed

6 files changed

+242
-76
lines changed

docs/server/route-handler.md

+48-15
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,37 @@ server.any('/session').passthrough();
170170
server.get('/session/1').passthrough(false);
171171
```
172172

173-
### recordingName
173+
### filter
174174

175-
Override the recording name for the given route. This allows for grouping common
176-
requests to share a single recording which can drastically help de-clutter test
177-
recordings.
175+
Filter requests matched by the route handler with a predicate callback function.
176+
This can be useful when trying to match a request by a part of the url, a header,
177+
and/or parts of the request body.
178178

179-
For example, if your tests always make a `/users` or `/session` call, instead of
180-
having each of those requests be recorded for every single test, you can use
181-
this to create a common recording file for them.
179+
The callback will receive the [Request](server/request)
180+
as an argument. Return `true` to match the request, `false` otherwise.
182181

183-
| Param | Type | Description |
184-
| ------------- | -------- | ------------------------------------------------------------------------- |
185-
| recordingName | `String` | Name of the [recording](api#recordingName) to store the recordings under. |
182+
?> Multiple filters can be chained together. They must all return `true` for the route handler to match the given request.
183+
184+
| Param | Type | Description |
185+
| -------- | ---------- | ----------------------------- |
186+
| callback | `Function` | The filter predicate function |
186187

187188
**Example**
188189

189190
```js
190-
server.any('/session').recordingName('User Session');
191-
192-
server.get('/users/:id').recordingName('User Data');
191+
server
192+
.any()
193+
.filter(req => req.hasHeader('Authentication'));
194+
.on('request', req => {
195+
res.setHeader('Authentication', 'test123')
196+
});
193197

194198
server
195-
.get('/users/1')
196-
.recordingName(); /* Fallback to the polly instance's recording name */
199+
.get('/users/:id')
200+
.filter(req => req.params.id === '1');
201+
.intercept((req, res) => {
202+
res.status(200).json({ email: '[email protected]' });
203+
});
197204
```
198205

199206
### configure
@@ -217,3 +224,29 @@ server.get('/users/:id').configure({ timing: Timing.relative(3.0) });
217224

218225
server.get('/users/1').configure({ logging: true });
219226
```
227+
228+
### recordingName
229+
230+
Override the recording name for the given route. This allows for grouping common
231+
requests to share a single recording which can drastically help de-clutter test
232+
recordings.
233+
234+
For example, if your tests always make a `/users` or `/session` call, instead of
235+
having each of those requests be recorded for every single test, you can use
236+
this to create a common recording file for them.
237+
238+
| Param | Type | Description |
239+
| ------------- | -------- | ------------------------------------------------------------------------- |
240+
| recordingName | `String` | Name of the [recording](api#recordingName) to store the recordings under. |
241+
242+
**Example**
243+
244+
```js
245+
server.any('/session').recordingName('User Session');
246+
247+
server.get('/users/:id').recordingName('User Data');
248+
249+
server
250+
.get('/users/1')
251+
.recordingName(); /* Fallback to the polly instance's recording name */
252+
```

packages/@pollyjs/adapter-fetch/tests/integration/server-test.js

+148-30
Original file line numberDiff line numberDiff line change
@@ -49,40 +49,158 @@ describe('Integration | Server', function() {
4949
expect(numIntercepts).to.equal(2);
5050
});
5151

52-
it('merges all configs', async function() {
53-
const { server } = this.polly;
54-
let config;
55-
56-
server.any().configure({ foo: 'foo' });
57-
server.any().configure({ bar: 'bar' });
58-
server
59-
.get('/ping')
60-
.configure({ foo: 'baz' })
61-
.intercept((req, res) => {
62-
config = req.config;
63-
res.sendStatus(200);
64-
});
52+
describe('API', function() {
53+
it('.configure() - merges all configs', async function() {
54+
const { server } = this.polly;
55+
let config;
6556

66-
expect((await fetch('/ping')).status).to.equal(200);
67-
expect(config).to.include({ foo: 'baz', bar: 'bar' });
68-
});
57+
server.any().configure({ foo: 'foo' });
58+
server.any().configure({ bar: 'bar' });
59+
server
60+
.get('/ping')
61+
.configure({ foo: 'baz' })
62+
.intercept((req, res) => {
63+
config = req.config;
64+
res.sendStatus(200);
65+
});
6966

70-
it('should throw when trying to override certain options', async function() {
71-
const { server } = this.polly;
67+
expect((await fetch('/ping')).status).to.equal(200);
68+
expect(config).to.include({ foo: 'baz', bar: 'bar' });
69+
});
70+
71+
it('.configure() - should throw when trying to override certain options', async function() {
72+
const { server } = this.polly;
73+
74+
// The following options cannot be overridden on a per request basis
75+
[
76+
'mode',
77+
'adapters',
78+
'adapterOptions',
79+
'persister',
80+
'persisterOptions'
81+
].forEach(key =>
82+
expect(() => server.any().configure({ [key]: 'foo' })).to.throw(
83+
Error,
84+
/Invalid configuration option/
85+
)
86+
);
87+
});
88+
89+
it('.recordingName()', async function() {
90+
const { server } = this.polly;
91+
let recordingName;
92+
93+
server
94+
.get('/ping')
95+
.recordingName('Override')
96+
.intercept((req, res) => {
97+
recordingName = req.recordingName;
98+
res.sendStatus(200);
99+
});
100+
101+
expect((await fetch('/ping')).status).to.equal(200);
102+
expect(recordingName).to.equal('Override');
103+
});
104+
105+
it('.recordingName() - should reset when called with no arguments', async function() {
106+
const { server } = this.polly;
107+
let recordingName;
108+
109+
server.any().recordingName('Override');
110+
111+
server
112+
.get('/ping')
113+
.recordingName()
114+
.intercept((req, res) => {
115+
recordingName = req.recordingName;
116+
res.sendStatus(200);
117+
});
118+
119+
expect((await fetch('/ping')).status).to.equal(200);
120+
expect(recordingName).to.not.equal('Override');
121+
});
122+
123+
it('.filter()', async function() {
124+
const { server } = this.polly;
72125

73-
// The following options cannot be overridden on a per request basis
74-
[
75-
'mode',
76-
'adapters',
77-
'adapterOptions',
78-
'persister',
79-
'persisterOptions'
80-
].forEach(key =>
81-
expect(() => server.any().configure({ [key]: 'foo' })).to.throw(
126+
server
127+
.get('/ping')
128+
.filter(req => req.query.num === '1')
129+
.intercept((req, res) => res.sendStatus(201));
130+
131+
server
132+
.get('/ping')
133+
.filter(req => req.query.num === '2')
134+
.intercept((req, res) => res.sendStatus(202));
135+
136+
expect((await fetch('/ping?num=1')).status).to.equal(201);
137+
expect((await fetch('/ping?num=2')).status).to.equal(202);
138+
});
139+
140+
it('.filter() + events', async function() {
141+
const { server } = this.polly;
142+
let num;
143+
144+
server
145+
.get('/ping')
146+
.filter(req => req.query.num === '1')
147+
.on('request', req => (num = req.query.num))
148+
.intercept((req, res) => res.sendStatus(201));
149+
150+
server
151+
.get('/ping')
152+
.filter(req => req.query.num === '2')
153+
.on('request', req => (num = req.query.num))
154+
.intercept((req, res) => res.sendStatus(202));
155+
156+
expect((await fetch('/ping?num=1')).status).to.equal(201);
157+
expect(num).to.equal('1');
158+
});
159+
160+
it('.filter() - multiple', async function() {
161+
const { server } = this.polly;
162+
163+
server
164+
.get('/ping')
165+
.filter(req => req.query.foo === 'foo')
166+
.filter(req => req.query.bar === 'bar')
167+
.intercept((req, res) => res.sendStatus(201));
168+
169+
server
170+
.get('/ping')
171+
.filter(req => req.query.foo === 'foo')
172+
.filter(req => req.query.baz === 'baz')
173+
.intercept((req, res) => res.sendStatus(202));
174+
175+
expect((await fetch('/ping?foo=foo&bar=bar')).status).to.equal(201);
176+
expect((await fetch('/ping?foo=foo&baz=baz')).status).to.equal(202);
177+
});
178+
179+
it('.filter() - can access params', async function() {
180+
const { server } = this.polly;
181+
let id;
182+
183+
server
184+
.get('/ping/:id')
185+
.filter(req => {
186+
id = req.params.id;
187+
188+
return true;
189+
})
190+
.intercept((req, res) => res.sendStatus(201));
191+
192+
expect((await fetch('/ping/1')).status).to.equal(201);
193+
expect(id).to.equal('1');
194+
});
195+
196+
it('.filter() - should throw when not passed a function', async function() {
197+
const { server } = this.polly;
198+
199+
expect(() => server.any().filter()).to.throw(
82200
Error,
83-
/Invalid configuration option/
84-
)
85-
);
201+
/Invalid filter callback provided/
202+
);
203+
});
86204
});
87205

88206
describe('Events & Middleware', function() {

packages/@pollyjs/core/src/-private/request.js

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export default class PollyRequest extends HTTPBase {
5757
// Lookup the associated route for this request
5858
this[ROUTE] = polly.server.lookup(this.method, this.url);
5959

60+
// Filter all matched route handlers by this request
61+
this[ROUTE].applyFiltersWithArgs(this);
62+
6063
// Handle config overrides defined by the route
6164
this._configure(this[ROUTE].config());
6265

packages/@pollyjs/core/src/server/handler.js

+12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default class Handler extends Map {
1111
super();
1212

1313
this.set('config', {});
14+
this.set('filters', new Set());
1415
this._eventEmitter = new EventEmitter({
1516
eventNames: [
1617
'error',
@@ -79,4 +80,15 @@ export default class Handler extends Map {
7980

8081
return this;
8182
}
83+
84+
filter(fn) {
85+
assert(
86+
`Invalid filter callback provided. Expected function, received: "${typeof fn}".`,
87+
typeof fn === 'function'
88+
);
89+
90+
this.get('filters').add(fn);
91+
92+
return this;
93+
}
8294
}

0 commit comments

Comments
 (0)