diff --git a/README.md b/README.md index 7e7d663..022b6d3 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,24 @@ Replay.recordResponseControl = { }; ``` +## Removing unused fixtures +`replay` automatically records which fixtures have not been used during the test run. To auto-delete those, access `Replay.unmatchedFixtures` and remove them. + +Example: + +```javascript +const unlink = util.promisify(fs.unlink) + +afterAll(async () => { + const unlinkPromises = [] + Replay.unmatchedFixtures.map(matcher => { + unlinkPromises.push(unlink(matcher.fixturePath)) + }) + await Promise.all(unlinkPromises) + console.log(`Removed ${unlinkPromises.length} unused fixtures.`) +}) +``` + ## Geeking diff --git a/src/catalog.js b/src/catalog.js index 48d8116..c923633 100644 --- a/src/catalog.js +++ b/src/catalog.js @@ -193,20 +193,23 @@ module.exports = class Catalog { files = files.filter(f => !/^\./.test(f)); for (let file of files) { let mapping = this._read(`${pathname}/${file}`); - newMatchers.push(Matcher.fromMapping(host, mapping)); + newMatchers.push(Matcher.fromMapping(`${pathname}/${file}`, host, mapping)); } } else { const mapping = this._read(pathname); - newMatchers.push(Matcher.fromMapping(host, mapping)); + newMatchers.push(Matcher.fromMapping(`${pathname}`, host, mapping)); } return newMatchers; } + getUnmatchedFixtures() { + return Object.keys(this.matchers).map(host => { + return this.matchers[host].filter(matcher => !matcher.isMatched) + }).reduce((a, b) => a.concat(b), []); + } + save(host, request, response, callback) { - const matcher = Matcher.fromMapping(host, { request, response }); - const matchers = this.matchers[host] || []; - matchers.push(matcher); const requestHeaders = this.settings.headers; const uid = `${Date.now()}${Math.floor(Math.random() * 100000)}`; @@ -224,6 +227,10 @@ module.exports = class Catalog { } const filename = `${pathname}/${uid}`; + const matcher = Matcher.fromMapping(filename, host, { request, response }); + const matchers = this.matchers[host] || []; + matchers.push(matcher); + try { const file = File.createWriteStream(tmpfile, { encoding: 'utf-8' }); file.write(`${request.method.toUpperCase()} ${request.url.path || '/'}\n`); diff --git a/src/index.js b/src/index.js index c40a1a4..4493c7c 100644 --- a/src/index.js +++ b/src/index.js @@ -153,6 +153,9 @@ class Replay extends EventEmitter { this.catalog.setFixturesDir(dir); } + get unmatchedFixtures() { + return this.catalog.getUnmatchedFixtures(); + } } diff --git a/src/matcher.js b/src/matcher.js index b38d52f..32d1e94 100644 --- a/src/matcher.js +++ b/src/matcher.js @@ -1,6 +1,6 @@ // A matcher is a function that, given a request, returns an appropriate response or nothing. // -// The most common use case is to calling `Matcher.fromMapping(mapping)`. +// The most common use case is to calling `Matcher.fromMapping(filename, host, mapping)`. // // The request consists of: // url - URL object @@ -25,7 +25,7 @@ const jsStringEscape = require('js-string-escape'); // To create a matcher from request/response mapping use `fromMapping`. module.exports = class Matcher { - constructor(request, response) { + constructor(fixturePath, request, response) { // Map requests to object properties. We do this for quick matching. assert(request.url || request.regexp, 'I need at least a URL to match request to response'); if (request.regexp) { @@ -38,6 +38,9 @@ module.exports = class Matcher { this.path = url.path; } + this.fixturePath = fixturePath; + this.isMatched = false; + this.method = (request.method && request.method.toUpperCase()) || 'GET'; this.headers = {}; if (request.headers) @@ -103,12 +106,19 @@ module.exports = class Matcher { data += chunks[0]; data = jsStringEscape(data); - return this.body instanceof RegExp ? - this.body.test(data) : - this.body === data; + if (this.body instanceof RegExp && this.body.test(data)) { + this.isMatched = true; + return this.response; + } else if(this.body === data) { + this.isMatched = true; + return this.response; + } + return false } - return true; + this.isMatched = true; + + return this.response; } @@ -116,7 +126,7 @@ module.exports = class Matcher { // // Mapping can contain `request` and `response` object. As shortcut, mapping can specify `path` and `method` (optional) // directly, and also any of the response properties. - static fromMapping(host, mapping) { + static fromMapping(fileName, host, mapping) { assert(!!mapping.path ^ !!mapping.request, 'Mapping must specify path or request object'); let matchingRequest; @@ -141,11 +151,7 @@ module.exports = class Matcher { body: mapping.request.body }; - const matcher = new Matcher(matchingRequest, mapping.response || {}); - return function(request) { - if (matcher.match(request)) - return matcher.response; - }; + return new Matcher(fileName, matchingRequest, mapping.response || {}); } }; diff --git a/src/recorder.js b/src/recorder.js index 616d3e5..ec0139c 100644 --- a/src/recorder.js +++ b/src/recorder.js @@ -15,7 +15,7 @@ module.exports = function recorded(settings) { const matchers = catalog.find(host); if (matchers) for (let matcher of matchers) { - let response = matcher(request); + let response = matcher.match(request); if (response) { callback(null, response); return; diff --git a/test/replay_test.js b/test/replay_test.js index d4aaced..d72ad7d 100644 --- a/test/replay_test.js +++ b/test/replay_test.js @@ -21,11 +21,13 @@ describe('Replay', function() { describe('listeners', function() { let response; + let unmatchedFixtures; before(function(done) { HTTP .get(`http://example.com:${INACTIVE_PORT}/weather?c=94606`, function(_) { response = _; + unmatchedFixtures = Replay.unmatchedFixtures done(); }) .on('error', done); @@ -46,6 +48,9 @@ describe('Replay', function() { it('should return response trailers', function() { assert.deepEqual(response.trailers, { }); }); + it('should have matched', function() { + assert.equal(unmatchedFixtures.filter(matcher => matcher.path === '/weather?c=94606').length, 0); + }) }); describe('Old http status line format', function() {