Skip to content

Commit

Permalink
feat(core): Filter requests matched by a route handler (#189)
Browse files Browse the repository at this point in the history
  • Loading branch information
offirgolan authored Feb 26, 2019
1 parent e9678c4 commit 5d57c32
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 76 deletions.
63 changes: 48 additions & 15 deletions docs/server/route-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,30 +170,37 @@ server.any('/session').passthrough();
server.get('/session/1').passthrough(false);
```

### recordingName
### filter

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

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

| Param | Type | Description |
| ------------- | -------- | ------------------------------------------------------------------------- |
| recordingName | `String` | Name of the [recording](api#recordingName) to store the recordings under. |
?> Multiple filters can be chained together. They must all return `true` for the route handler to match the given request.

| Param | Type | Description |
| -------- | ---------- | ----------------------------- |
| callback | `Function` | The filter predicate function |

**Example**

```js
server.any('/session').recordingName('User Session');

server.get('/users/:id').recordingName('User Data');
server
.any()
.filter(req => req.hasHeader('Authentication'));
.on('request', req => {
res.setHeader('Authentication', 'test123')
});

server
.get('/users/1')
.recordingName(); /* Fallback to the polly instance's recording name */
.get('/users/:id')
.filter(req => req.params.id === '1');
.intercept((req, res) => {
res.status(200).json({ email: '[email protected]' });
});
```

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

server.get('/users/1').configure({ logging: true });
```

### recordingName

Override the recording name for the given route. This allows for grouping common
requests to share a single recording which can drastically help de-clutter test
recordings.

For example, if your tests always make a `/users` or `/session` call, instead of
having each of those requests be recorded for every single test, you can use
this to create a common recording file for them.

| Param | Type | Description |
| ------------- | -------- | ------------------------------------------------------------------------- |
| recordingName | `String` | Name of the [recording](api#recordingName) to store the recordings under. |

**Example**

```js
server.any('/session').recordingName('User Session');

server.get('/users/:id').recordingName('User Data');

server
.get('/users/1')
.recordingName(); /* Fallback to the polly instance's recording name */
```
178 changes: 148 additions & 30 deletions packages/@pollyjs/adapter-fetch/tests/integration/server-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,40 +49,158 @@ describe('Integration | Server', function() {
expect(numIntercepts).to.equal(2);
});

it('merges all configs', async function() {
const { server } = this.polly;
let config;

server.any().configure({ foo: 'foo' });
server.any().configure({ bar: 'bar' });
server
.get('/ping')
.configure({ foo: 'baz' })
.intercept((req, res) => {
config = req.config;
res.sendStatus(200);
});
describe('API', function() {
it('.configure() - merges all configs', async function() {
const { server } = this.polly;
let config;

expect((await fetch('/ping')).status).to.equal(200);
expect(config).to.include({ foo: 'baz', bar: 'bar' });
});
server.any().configure({ foo: 'foo' });
server.any().configure({ bar: 'bar' });
server
.get('/ping')
.configure({ foo: 'baz' })
.intercept((req, res) => {
config = req.config;
res.sendStatus(200);
});

it('should throw when trying to override certain options', async function() {
const { server } = this.polly;
expect((await fetch('/ping')).status).to.equal(200);
expect(config).to.include({ foo: 'baz', bar: 'bar' });
});

it('.configure() - should throw when trying to override certain options', async function() {
const { server } = this.polly;

// The following options cannot be overridden on a per request basis
[
'mode',
'adapters',
'adapterOptions',
'persister',
'persisterOptions'
].forEach(key =>
expect(() => server.any().configure({ [key]: 'foo' })).to.throw(
Error,
/Invalid configuration option/
)
);
});

it('.recordingName()', async function() {
const { server } = this.polly;
let recordingName;

server
.get('/ping')
.recordingName('Override')
.intercept((req, res) => {
recordingName = req.recordingName;
res.sendStatus(200);
});

expect((await fetch('/ping')).status).to.equal(200);
expect(recordingName).to.equal('Override');
});

it('.recordingName() - should reset when called with no arguments', async function() {
const { server } = this.polly;
let recordingName;

server.any().recordingName('Override');

server
.get('/ping')
.recordingName()
.intercept((req, res) => {
recordingName = req.recordingName;
res.sendStatus(200);
});

expect((await fetch('/ping')).status).to.equal(200);
expect(recordingName).to.not.equal('Override');
});

it('.filter()', async function() {
const { server } = this.polly;

// The following options cannot be overridden on a per request basis
[
'mode',
'adapters',
'adapterOptions',
'persister',
'persisterOptions'
].forEach(key =>
expect(() => server.any().configure({ [key]: 'foo' })).to.throw(
server
.get('/ping')
.filter(req => req.query.num === '1')
.intercept((req, res) => res.sendStatus(201));

server
.get('/ping')
.filter(req => req.query.num === '2')
.intercept((req, res) => res.sendStatus(202));

expect((await fetch('/ping?num=1')).status).to.equal(201);
expect((await fetch('/ping?num=2')).status).to.equal(202);
});

it('.filter() + events', async function() {
const { server } = this.polly;
let num;

server
.get('/ping')
.filter(req => req.query.num === '1')
.on('request', req => (num = req.query.num))
.intercept((req, res) => res.sendStatus(201));

server
.get('/ping')
.filter(req => req.query.num === '2')
.on('request', req => (num = req.query.num))
.intercept((req, res) => res.sendStatus(202));

expect((await fetch('/ping?num=1')).status).to.equal(201);
expect(num).to.equal('1');
});

it('.filter() - multiple', async function() {
const { server } = this.polly;

server
.get('/ping')
.filter(req => req.query.foo === 'foo')
.filter(req => req.query.bar === 'bar')
.intercept((req, res) => res.sendStatus(201));

server
.get('/ping')
.filter(req => req.query.foo === 'foo')
.filter(req => req.query.baz === 'baz')
.intercept((req, res) => res.sendStatus(202));

expect((await fetch('/ping?foo=foo&bar=bar')).status).to.equal(201);
expect((await fetch('/ping?foo=foo&baz=baz')).status).to.equal(202);
});

it('.filter() - can access params', async function() {
const { server } = this.polly;
let id;

server
.get('/ping/:id')
.filter(req => {
id = req.params.id;

return true;
})
.intercept((req, res) => res.sendStatus(201));

expect((await fetch('/ping/1')).status).to.equal(201);
expect(id).to.equal('1');
});

it('.filter() - should throw when not passed a function', async function() {
const { server } = this.polly;

expect(() => server.any().filter()).to.throw(
Error,
/Invalid configuration option/
)
);
/Invalid filter callback provided/
);
});
});

describe('Events & Middleware', function() {
Expand Down
3 changes: 3 additions & 0 deletions packages/@pollyjs/core/src/-private/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default class PollyRequest extends HTTPBase {
// Lookup the associated route for this request
this[ROUTE] = polly.server.lookup(this.method, this.url);

// Filter all matched route handlers by this request
this[ROUTE].applyFiltersWithArgs(this);

// Handle config overrides defined by the route
this._configure(this[ROUTE].config());

Expand Down
12 changes: 12 additions & 0 deletions packages/@pollyjs/core/src/server/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class Handler extends Map {
super();

this.set('config', {});
this.set('filters', new Set());
this._eventEmitter = new EventEmitter({
eventNames: [
'error',
Expand Down Expand Up @@ -79,4 +80,15 @@ export default class Handler extends Map {

return this;
}

filter(fn) {
assert(
`Invalid filter callback provided. Expected function, received: "${typeof fn}".`,
typeof fn === 'function'
);

this.get('filters').add(fn);

return this;
}
}
Loading

0 comments on commit 5d57c32

Please sign in to comment.