diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2cdfb8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..3b74e3b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,17 @@ +{ + env: { + node: true + }, + rules: { + "no-console": 2, + "no-underscore-dangle": 0, + "no-use-before-define": [2, "nofunc"], + "max-len": [2, 150, 2], + "quotes": [2, "single"], + "space-after-keywords": [2, "always", { checkFunctionKeyword: true } ], + "no-multi-str": 0 + }, + "settings": { + "ecmascript": 6 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a941888 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - '0.10' +script: npm run lint && npm test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7efb36 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 M6Web + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c500e37 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ + +# superagent-mock [![Build Status](https://api.travis-ci.org/M6Web/superagent-mock.png?branch=master)](https://travis-ci.org/M6Web/superagent-mock) + +[superagent](https://github.com/visionmedia/superagent) plugin allowing to simulate HTTP calls by returning data fixtures based on the requested URL. + +**Note**: This plugin is developed for `superagent: ^v1.1.0`. + +## Installation + +Install with [npm](http://npmjs.org/): + +```sh +$ npm install superagent-mock +``` + +## Usage + +First, you have to define the URLs to mock in a configuration file: + +```js +// ./superagent-mock-config.js file +module.exports = [ + { + // regular expression of URL + pattern: 'https://domain.example/(\\w+)/', + + // callback that returns the data + fixtures: function () { + return 'Data fixtures'; + }, + + // `match`: result of the resolution of the regular expression + // `data`: data returns by `fixtures` attribute + callback: function (match, data) { + return { + body: data + }; + } + }, + ... +]; +``` + +Then use the plugin: + +```js +// ./server.js file +var request = require(‘superagent’); +var config = require(‘./superagent-mock-config); + +require(‘superagent-mock’)(request, config); +``` + +## Tests + +To run units tests: `npm test`. + +To check code style: `npm run lint`. + + +## Credits + +Developped by the [Cytron Team](http://cytron.fr/) of [M6 Web](http://tech.m6web.fr/). +Tested with [nodeunit](https://github.com/caolan/nodeunit). + +## License + +superagent-mock is licensed under the [MIT license](LICENSE). diff --git a/lib/superagent-mock.js b/lib/superagent-mock.js new file mode 100644 index 0000000..1652f5c --- /dev/null +++ b/lib/superagent-mock.js @@ -0,0 +1,77 @@ +'use strict'; + +var qs = require('qs'); + +/** + * Module exports. + */ +module.exports = mock; + +/** + * Installs the `mock` extension to superagent. + */ +function mock (superagent, config) { + var Request = superagent.Request; + var parsers = []; + + /** + * Keep the default methods + */ + var oldGet = superagent.get; + var oldEnd = Request.prototype.end; + + /** + * Override get function + */ + superagent.get = function (url, data, fn) { + var match = config.filter(function (parser) { + return new RegExp(parser.pattern, 'g').test(url); + })[0] || null; + + if (match) { + parsers[url] = match; + } + + var req; + if (parsers[url]) { + req = superagent('GET', url, data, fn); + } else { + req = oldGet.call(this, url, data, fn); + } + return req; + }; + + /** + * Override end function + */ + Request.prototype.end = function (fn) { + + var path = this.url; + var querystring = ''; + + if (this._query) { + querystring += this._query.join('&'); + } else { + if (this.qs) { + querystring += qs.stringify(this.qs); + } + if (this.qsRaw) { + querystring += this.qsRaw.join('&'); + } + } + + + if (querystring.length) { + path += (~path.indexOf('?') ? '&' : '?') + querystring; + } + + var parser = parsers[this.url]; + + if (parser) { + var match = new RegExp(parser.pattern, 'g').exec(path); + fn(null, parsers[this.url].callback(match, parser.fixtures())); + } else { + oldEnd.call(this, fn); + } + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cd57d8c --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "superagent-mock", + "description": "superagent plugin allowing to simulate HTTP calls by returning data fixtures based on the requested URL.", + "version": "1.0.0", + "keywords": [ + "superagent", + "mock", + "isomorphic" + ], + "bugs": "https://github.com/M6Web/superagent-mock/issues", + "license": "MIT", + "main": "./lib/superagent-mock.js", + "repository": { + "type": "git", + "url": "https://github.com/M6Web/superagent-mock" + }, + "dependencies": { + "qs": "^2.3.3", + "superagent": "^1.1.0" + }, + "devDependencies": { + "component-as-module": "0.3.0", + "eslint": "0.12.0", + "nodeunit": "0.9.0" + }, + "scripts": { + "test": "./node_modules/nodeunit/bin/nodeunit ./tests", + "lint": "./node_modules/eslint/bin/eslint.js ./lib" + } +} diff --git a/tests/client.spec.js b/tests/client.spec.js new file mode 100644 index 0000000..2be3c3e --- /dev/null +++ b/tests/client.spec.js @@ -0,0 +1,24 @@ +'use strict'; + +// Create specifics component require +var component = require('component-as-module'); + +// Get the "client" version of superagent +var request = component('node_modules/superagent', function (loader) { + loader.register('component-emitter', function () { + return require('superagent/node_modules/component-emitter'); + }); + + loader.register('component-reduce', function () { + return require('superagent/node_modules/reduce-component'); + }); + + loader.loadDependency('emitter'); +}); + +// Get the mock config and expectations +var config = require('./support/config'); +var expectations = require('./support/expectations'); + +// Expose the test cases +module.exports = expectations(request, config); diff --git a/tests/server.spec.js b/tests/server.spec.js new file mode 100644 index 0000000..84b55df --- /dev/null +++ b/tests/server.spec.js @@ -0,0 +1,11 @@ +'use strict'; + +// Get the "server" version of superagent +var request = require('superagent'); + +// Get the mock config and expectations +var config = require('./support/config'); +var expectations = require('./support/expectations'); + +// Expose the test cases +module.exports = expectations(request, config); diff --git a/tests/support/config.js b/tests/support/config.js new file mode 100644 index 0000000..dad9cbc --- /dev/null +++ b/tests/support/config.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = [ + { + pattern: 'https://domain.example/(\\w+)', + fixtures: function () { + return 'Fixture !'; + }, + callback: function (match, data) { + return {match: match, data: data}; + } + }, + { + pattern: 'https://domain.params.example/list(?:[?|&]((?:limit|offset)=[0-9]+))?(?:[?|&]((?:limit|offset)=[0-9]+))?', + fixtures: function () { + return 'Fixture !'; + }, + callback: function (match, data) { + return {match: match, data: data}; + } + }]; diff --git a/tests/support/expectations.js b/tests/support/expectations.js new file mode 100644 index 0000000..aff0c85 --- /dev/null +++ b/tests/support/expectations.js @@ -0,0 +1,103 @@ +'use strict'; + +module.exports = function (request, config) { + return { + + 'setUp': function (go) { + // Stubbing the superagent method + request.Request.prototype.end = function (fn) { + fn(null, 'Real call done'); + }; + + // Init module + require('./../../lib/superagent-mock')(request, config); + + go(); + }, + + 'Method GET': { + 'matching simple request': function (test) { + request.get('https://domain.example/666').end(function (err, result) { + test.ok(!err); + test.equal(result.match[1], '666'); + test.equal(result.data, 'Fixture !'); + test.done(); + }); + }, + + 'unmatching simple request': function (test) { + request.get('https://dummy.domain/666').end(function (err, result) { + test.ok(!err); + test.equal(result, 'Real call done'); + test.done(); + }); + }, + + 'matching parametrized request (object)': function (test) { + request.get('https://domain.params.example/list') + .query({limit: 10}) + .end(function (err, result) { + test.ok(!err); + test.notEqual(result.match.indexOf('limit=10'), -1); + test.equal(result.data, 'Fixture !'); + test.done(); + }); + }, + + 'matching double parametrized request (object)': function (test) { + request.get('https://domain.params.example/list') + .query({limit: 10, offset: 30}) + .end(function (err, result) { + test.ok(!err); + test.notEqual(result.match.indexOf('limit=10'), -1); + test.notEqual(result.match.indexOf('offset=30'), -1); + test.equal(result.data, 'Fixture !'); + test.done(); + }); + }, + + 'matching parametrized request (string)': function (test) { + request.get('https://domain.params.example/list') + .query('limit=10') + .end(function (err, result) { + test.ok(!err); + test.notEqual(result.match.indexOf('limit=10'), -1); + test.equal(result.data, 'Fixture !'); + test.done(); + }); + }, + + 'matching double parametrized request (string)': function (test) { + request.get('https://domain.params.example/list') + .query('limit=10&offset=40') + .end(function (err, result) { + test.ok(!err); + test.notEqual(result.match.indexOf('limit=10'), -1); + test.notEqual(result.match.indexOf('offset=40'), -1); + test.equal(result.data, 'Fixture !'); + test.done(); + }); + }, + + 'matching parametrized request (no parameters)': function (test) { + request.get('https://domain.params.example/list') + .end(function (err, result) { + test.ok(!err); + test.equal(result.data, 'Fixture !'); + test.done(); + }); + }, + + 'unmatching parametrized request (object)': function (test) { + request.get('https://dummy.domain.params.example/list') + .query({limit: 10}) + .end(function (err, result) { + test.ok(!err); + test.equal(result, 'Real call done'); + test.done(); + }); + } + } + + }; +};