diff --git a/.istanbul.yml b/.istanbul.yml new file mode 100644 index 0000000000000..dc16e2061de43 --- /dev/null +++ b/.istanbul.yml @@ -0,0 +1,2 @@ +instrumentation: + excludes: ['lib/suggest.js', 'vendor/**/*.js'] diff --git a/.travis.yml b/.travis.yml index 774a8576fda9f..5add6284a6de2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ node_js: script: - npm run lint - npm run test:js + - if [ "$TRAVIS_EVENT_TYPE" == cron ]; then npm run test:services; fi + - if [ "$TRAVIS_EVENT_TYPE" == pull_request ]; then npm run test:services -- --pr; fi branches: except: @@ -14,6 +16,12 @@ notifications: email: - thaddee.tyl@gmail.com irc: "irc.freenode.org#shields" + slack: + rooms: + # One builds on badges/shields, the other for paulmelnikow/shields + secure: VyTRZfcRAIrYTgfYG5RNJrKd0boDYNBfWEhzm20aLJvDeI2TgwAB2bSClkjjBe9KD3vlEjW6WbOdXfc/pBEdiIYgxl0vjg1qv3qJLJ4a6oacJg3UukJim28SucxZL2yhyseahHt12HyuPdUtwy+QzhJRp6Jo7T7M7ClKh0/qI4M= + secure: Z494jqFmkiIrBEmhTW7zKTlewskUo/lFCs/IWgEA/zUUEPHIDYNJjjE47Lkd7WlKoD3k2Cpb5/HJ8vwXAGmOdMG7JAxzlL0etJYLgrE/MJ0+TDDAj9jCyS6G5OUzxAbcThSadQgC8umtQmZ+IzEyo2rBPh5VknfL/SN4QS8L83f8mdo/JKnfYR81Z05g+CXfBWQALDJzlRT0nzgSSWUjrIJKMCNlvpNnL/rrAeHhc1eYernUsZpiOVJVlkMWxuBmQHwntWp7aV9/yCuzz5w+tlYlHwYJySE2JP3LUb9z0vuueYL1YEXQMdcBcvb3SLBg1ZUPRKk+OwEI4U1U002BOZnr/uIqYGsAWL90W6AfjVA2mNwf0wMgD981SZLuqovZadvQ3+hxjl0oyb8eVc4uDXnKyrCuwI4gqc/T6RomfCs1bXlHsQrGp03l0IT5ADkzfTDS0VFHYYB5Og3cJodC0SF/m2s9ZnEhMtWnTdz4sOhRFV1GccODI+5lz6F/XVdaY2zon9zkRjpoZKXsjCpPMc/OTXKVwV90Y0vots3vlyaO3eJvlH/qkFkZyavHeSSl9vo50QUfcVGY5AgoNuaKwz4RwzklGTsW06TV4jHpw8SMBnapNDVziMUzSQ4nUvQYtJofmpxT2K/2JImtIyfvO489uQZjgr5WkXaPMUaGWdc= + on_success: change git: depth: 10 diff --git a/doc/TUTORIAL.md b/doc/TUTORIAL.md index 4f57abc9170e7..b769d167652bf 100644 --- a/doc/TUTORIAL.md +++ b/doc/TUTORIAL.md @@ -217,6 +217,24 @@ Edit [try.html][tryhtml] in the right section (Build, Downloads, ...) and add yo Save, restart and you can see it [locally][try]. +## (4.4) Write Tests + +When creating a badge for a new service or changing a badge's behavior, tests +should be included. They serve several purposes: + +1. They speed up future contributors when they are debugging or improving a + badge. +2. If a contributors like to change your badge, chances are, they forget + edge cases and break your code. + Tests may give hints in such cases. +3. The contributor and reviewer can easily verify the code works as + intended. +4. When a badge stops working on the live server, maintainers can find out + right away. + +There is a dedicated [tutorial for tests in the service-tests folder][tests-tutorial]. +Please follow it to include tests on your pull-request. + ## (5) Create a Pull Request You have implemented changes in `server.js`, `try.html` and `index.html`. @@ -249,4 +267,5 @@ These files can also be of help for creating your own badge. [new-badge]: https://github.com/badges/shields/pulls?q=is%3Apr+label%3Anew-badge [docker-example]: https://github.com/badges/shields/blob/bf373d11cd522835f198b50b4e1719027a0a2184/server.js#L5014 [travis-example]: https://github.com/badges/shields/blob/bf373d11cd522835f198b50b4e1719027a0a2184/server.js#L431 -[regex]: https://www.w3schools.com/jsref/jsref_obj_regexp.asp \ No newline at end of file +[regex]: https://www.w3schools.com/jsref/jsref_obj_regexp.asp +[tests-tutorial]: ../service-tests/#readme diff --git a/package.json b/package.json index 48037cdf12be7..937a0ad215a57 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,15 @@ "xml2js": "~0.4.16" }, "scripts": { + "coverage:test:js": "istanbul cover _mocha 'test/**/*.spec.js' --dir coverage/js", + "coverage:test:services": "istanbul cover _mocha --delay service-tests/runner/cli.js --dir coverage/services", + "coverage:test": "npm run coverage:test:js && npm run coverage:test:services", + "coverage:report": "istanbul report", + "coverage:report:reopen": "opn coverage/lcov-report/index.html", + "coverage:report:open": "npm run coverage:report && npm run coverage:report:reopen", "lint": "eslint '**/*.js'", "test:js": "mocha 'test/**/*.spec.js'", + "test:services": "mocha --delay service-tests/runner/cli.js", "test": "npm run lint && npm run test:js" }, "bin": { @@ -56,9 +63,17 @@ ], "devDependencies": { "eslint": "^3.18.0", - "is-png": "^1.0.0", + "glob": "^7.1.1", + "icedfrisby": "^1.1.0", + "icedfrisby-nock": "^0.3.0", + "is-png": "^1.1.0", "is-svg": "^2.1.0", + "istanbul": "^0.4.5", + "lodash.difference": "^4.5.0", "mocha": "^3.2.0", + "nock": "^9.0.13", + "node-fetch": "^1.6.3", + "opn-cli": "^3.1.0", "sinon": "^2.1.0" } } diff --git a/service-tests/README.md b/service-tests/README.md new file mode 100644 index 0000000000000..1d6a198e422f2 --- /dev/null +++ b/service-tests/README.md @@ -0,0 +1,355 @@ +Service tests +============= + +When creating a badge for a new service or changing a badge's behavior, +automated tests should be included. They serve three purposes: + +1. The contributor and reviewer can easily verify the code works as + intended. + +2. When a badge stops working on the live server, maintainers can find out + right away. + +3. They speed up future contributors when they are debugging or improving a + badge. + +Contributors should take care to cover each part of a badge's functionality, +and ideally, all code branches: + +1. Typical case + - File is present + - Build fails/succeeds +2. Expected resource not found + - Service may provide 200 error code with different response format + - Service may return a 404 or other >= 400 status code +3. Customization + - Non-default parameters like tags and branches +4. Server errors and other malformed responses + - Service may return status code 500 and higher + - Invalid JSON + - Attributes missing or have incorrect types + - Headers missing +5. Connection errors + +Tutorial +-------- + +In this tutorial, we'll write tests for the Travis badge. +Here, you can see the [source code][travis-example]: + +```js +camp.route(/^\/travis(-ci)?\/([^\/]+\/[^\/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, +cache(function(data, match, sendBadge, request) { + var userRepo = match[2]; // eg, espadrine/sc + var branch = match[3]; + var format = match[4]; + var options = { + method: 'HEAD', + uri: 'https://api.travis-ci.org/' + userRepo + '.svg', + }; + if (branch != null) { + options.uri += '?branch=' + branch; // (3) + } + var badgeData = getBadgeData('build', data); + request(options, function(err, res) { + if (err != null) { + console.error('Travis error: ' + err.stack); // 5 + if (res) { console.error(''+res); } + badgeData.text[1] = 'invalid'; + sendBadge(format, badgeData); + return; + } + try { + var state = res.headers['content-disposition'] + .match(/filename="(.+)\.svg"/)[1]; + badgeData.text[1] = state; + if (state === 'passing') { + badgeData.colorscheme = 'brightgreen'; // 1 + } else if (state === 'failing') { + badgeData.colorscheme = 'red'; // 1 + } else { + badgeData.text[1] = state; // 1, 2, 3 + } + sendBadge(format, badgeData); + + } catch(e) { + badgeData.text[1] = 'invalid'; // 4 + sendBadge(format, badgeData); + } + }); +})); +``` + +It handles the typical cases (1), resource not found (2), customization (3), +malformed responses (4), and connection errors (5). + +Before getting started, install the project dependencies if you haven't +already: + +``` +npm i +``` + +We'll start by creating a new module, `service-tests/travis.js`, using this +boilerplate: + +```js +'use strict'; + +const Joi = require('joi'); // 1 +const ServiceTester = require('./runner/service-tester'); // 2 + +const t = new ServiceTester({ id: 'travis', title: 'Travis CI' }) // 3 +module.exports = t; // 4 +``` + +We'll import [Joi][] (1) which will help with our assertions. We'll add all +our tests to this ServiceTester object (2), which gets exported from the +module (4). The first attribute passed to the constructor (3) is the id of +a service, which is used to identify it on the command line or in a pull +request. The tester will prepend the id to the URIs you provide later, which +saves copying and pasting. The second attribute is the human-readable title +of the service, which prints when you run the tests. + +Next we'll add a test for the typical case. + +[![](https://img.shields.io/badge/build-passing-brightgreen.svg)]() + +The JSON format for this badge is `{ name: 'build', value: 'passing' }`. + +Here's what our first test looks like: + +```js +t.create('build status on default branch') + .get('/rust-lang/rust.json') + .expectJSONTypes(Joi.object().keys({ + name: Joi.equal('build'), + value: Joi.equal('failing', 'passing', 'unknown') + })); +``` + +We need a real project to use for our tests. We'll use the programming +language [Rust][], though we could have chosen any stable project with a +Travis build. + +The `create()` method gives the tester a new test. The chained-on calls come +from the API testing framework [IcedFrisby][]. Here's a [longer example][] and +the complete [API guide][IcedFrisby API]. + +`expectJSONTypes()` is an IcedFrisby method which accepts a [Joi][] schema. +Joi is a validation library that is build into IcedFrisby which you can use to +match based on a set of allowed strings, regexes, or specific values. You can +refer to their [API reference][Joi API]. + +Since we don't know whether rust will be passing or not at the time the test +runs, we use `Joi.equal()`, which accepts any of the values passed in. + +Notice we don't have to specify `/travis` again, or even `localhost`. The test +runner handles that for us. + +When defining an IcedFrisby test, typically you would invoke the `toss()` +method, to register the test. This is not necessary, because the Shields test +harness will call it for you. + +[Rust]: https://www.rust-lang.org/en-US/ +[IcedFrisby]: https://github.com/MarkHerhold/IcedFrisby +[longer example]: https://github.com/MarkHerhold/IcedFrisby/#show-me-some-code +[IcedFrisby API]: https://github.com/MarkHerhold/IcedFrisby/blob/master/API.md +[Joi]: https://github.com/hapijs/joi +[Joi API]: https://github.com/hapijs/joi/blob/master/API.md + +Run the test: + +``` +npm run test:services -- --only=travis +``` + +The `--only=` option indicates which service or services you want to test. You +can provide a comma-separated list of ids. + +The `--` tells the NPM CLI to pass the remaining arguments through to the test +runner. + +Here's the output: + +``` +http://localhost:1111/try.html + Travis CI + build status on default branch + ✓ + [ GET http://localhost:1111/travis/rust-lang/rust.json ] (265ms) + + + 1 passing (1s) +``` + +That's looking good! + +Next we'll add a second test for a branch build. + +```js +t.create('build status on named branch') + .get('/rust-lang/rust/stable.json') + .expectJSONTypes(Joi.object().keys({ + name: Joi.equal('build'), + value: Joi.equal('failing', 'passing', 'unknown') + })); +``` + +``` +http://localhost:1111/try.html + Travis CI + build status on default branch + ✓ + [ GET http://localhost:1111/travis/rust-lang/rust.json ] (220ms) + build status on named branch + ✓ + [ GET http://localhost:1111/travis/rust-lang/rust/stable.json ] (100ms) + + + 2 passing (1s) +``` + +Having covered the typical and customize cases, we'll move on to errors. + +First, a nonexistent repo, which Travis reports as having an `unknown` status: + +```js +t.create('unknown repo') + .get('/this-repo/does-not-exist.json') + .expectJSON({ name: 'build', value: 'unknown' }); +``` + +Since in this case we know the exact badge which should be returned, we can +use the more concise `expectJSON()` in place of `expectJSONTypes()`. + +Next, we want to cover the code in the `catch` block. To do this, we need to +trigger an exception. After studying the code, we realize this could happen on +a request without a Content-Disposition header. + +Since we don't have an easy way to get the server to return a real repository +request without a Content-Disposition header, we will intercept the request +and provide our own mock response. We use the `intercept()` method provided by +the [icedfrisby-nock plugin][icedfrisby-nock]. It takes a setup function, +which returns an interceptor, and exposes the full API of the HTTP mocking +library [Nock][]. + +```js +t.create('missing content-disposition header') + .get('/foo/bar.json') + .intercept(nock => nock('https://api.travis-ci.org') + .head('/foo/bar.svg') + .reply(200)) + .expectJSON({ name: 'build', value: 'invalid' }); +``` + +Nock is fussy. All parts of a request must match perfectly for the mock to +take effect, including the method (in this case HEAD), scheme (https), host, +and path. + +[icedfrisby-nock]: https://github.com/paulmelnikow/icedfrisby-nock#usage +[Nock]: https://github.com/node-nock/nock + + +Code coverage +------------- + +By checking code coverage, we can make sure we've covered all our bases. + +We can generate a coverage report and open it: + +``` +npm run coverage:test:services -- -- --only=travis +npm run coverage:report:open +``` + +Note the two sets of double dashes. + +After searching `server.js` for the Travis code, we see that we've missed a +big block: the error branch in the request callback. To test that, we simulate +network connection errors on any unmocked requests. + +```js +t.create('connection error') + .get('/foo/bar.json') + .networkOff() + .expectJSON({ name: 'build', value: 'invalid' }); +``` + + +Pull requests +------------- + +The affected service ids should be included in brackets in the pull request +title. That way, Travis will run those service tests. When a pull request +affects multiple services, they should be separated with spaces. The test +runner is case-insensitive, so they should be capitalized for readability. + +For example: + +- [Travis] Fix timeout issues +- [Travis Sonar] Support user token authentication +- [CRAN CPAN CTAN] Add test coverage + + +Getting help +------------ + +If you have questions about how to write your tests, please open an issue. If +there's already an issue open for the badge you're working on, you can post a +comment there instead. + + +Complete example +---------------- + +```js +'use strict'; + +const Joi = require('joi'); +const ServiceTester = require('./runner/service-tester'); + +const t = new ServiceTester('Travis', '/travis'); +module.exports = t; + +t.create('build status on default branch') + .get('/rust-lang/rust.json') + .expectJSONTypes(Joi.object().keys({ + name: Joi.equal('build'), + value: Joi.equal('failing', 'passing', 'unknown') + })); + +t.create('build status on named branch') + .get('/rust-lang/rust/stable.json') + .expectJSONTypes(Joi.object().keys({ + name: Joi.equal('build'), + value: Joi.equal('failing', 'passing', 'unknown') + })); + +t.create('unknown repo') + .get('/this-repo/does-not-exist.json') + .expectJSON({ name: 'build', value: 'unknown' }); + +t.create('missing content-disposition header') + .get('/foo/bar.json') + .intercept(nock => nock('https://api.travis-ci.org') + .head('/foo/bar.svg') + .reply(200)) + .expectJSON({ name: 'build', value: 'invalid' }); + +t.create('connection error') + .get('/foo/bar.json') + .networkOff() + .expectJSON({ name: 'build', value: 'invalid' }); +``` + + +Further reading +--------------- + +- [IcedFrisby API][] +- [Joi API][] +- [icedfrisby-nock][] +- [Nock API](https://github.com/node-nock/nock#use) + +[travis-example]: https://github.com/badges/shields/blob/bf373d11cd522835f198b50b4e1719027a0a2184/server.js#L431 diff --git a/service-tests/cran.js b/service-tests/cran.js new file mode 100644 index 0000000000000..d12fd01c5d042 --- /dev/null +++ b/service-tests/cran.js @@ -0,0 +1,46 @@ +'use strict'; + +const Joi = require('joi'); +const ServiceTester = require('./runner/service-tester'); + +const t = new ServiceTester({ id: 'cran', title: 'CRAN/METACRAN' }); +module.exports = t; + +t.create('version') + .get('/v/devtools.json') + .expectJSONTypes(Joi.object().keys({ + name: Joi.equal('cran'), + value: Joi.string().regex(/^v\d+\.\d+\.\d+$/) + })); + +t.create('specified license') + .get('/l/devtools.json') + .expectJSON({ name: 'license', value: 'GPL (>= 2)' }); + +t.create('unknown package') + .get('/l/some-bogus-package.json') + .expectJSON({ name: 'cran', value: 'not found' }); + +t.create('unknown info') + .get('/z/devtools.json') + .expectStatus(404) + .expectJSON({ name: '404', value: 'badge not found' }); + +t.create('malformed response') + .get('/v/foobar.json') + .intercept(nock => nock('http://crandb.r-pkg.org') + .get('/foobar') + .reply(200)) // JSON without Version. + .expectJSON({ name: 'cran', value: 'invalid' }); + +t.create('connection error') + .get('/v/foobar.json') + .networkOff() + .expectJSON({ name: 'cran', value: 'inaccessible' }); + +t.create('unspecified license') + .get('/l/foobar.json') + .intercept(nock => nock('http://crandb.r-pkg.org') + .get('/foobar') + .reply(200, {})) // JSON without License. + .expectJSON({ name: 'license', value: 'unknown' }); diff --git a/service-tests/runner/cli.js b/service-tests/runner/cli.js new file mode 100644 index 0000000000000..decbee65c3d80 --- /dev/null +++ b/service-tests/runner/cli.js @@ -0,0 +1,102 @@ +// Usage: +// +// Run all services: +// npm run test:services +// +// Run some services: +// npm run test:services -- --only=service1,service2,service3 +// +// Infer the current PR from the Travis environment, and look for bracketed, +// space-separated service names in the pull request title. If none are found, +// do not run any tests. For example: +// Pull request title: [travis sonar] Support user token authentication +// npm run test:services -- --pr +// is equivalent to +// npm run test:services -- --only=travis,sonar + +'use strict'; + +const difference = require('lodash.difference'); +const fetch = require('node-fetch'); +const minimist = require('minimist'); +const Runner = require('./runner'); +const serverHelpers = require('../../test/in-process-server-helpers'); + +function getTitle (repoSlug, pullRequest) { + const uri = `https://api.github.com/repos/${repoSlug}/pulls/${pullRequest}`; + const options = { headers: { 'User-Agent': 'badges/shields' } }; + return fetch(uri, options) + .then(res => { + if (! res.ok) { + throw Error(`${res.status} ${res.statusText}`); + } + + return res.json(); + }) + .then(json => json.title); +} + +// [Travis] Fix timeout issues => ['travis'] +// [Travis Sonar] Support user token authentication -> ['travis', 'sonar'] +// [CRAN CPAN CTAN] Add test coverage => ['cran', 'cpan', 'ctan'] +function servicesForTitle (title) { + const matches = title.match(/\[([\w ]+)\]/); + if (matches === null) { + return []; + } + + const services = matches[1].toLowerCase().split(' '); + const blacklist = ['wip']; + return difference(services, blacklist); +} + +let server; +before('Start running the server', function () { + this.timeout(5000); + server = serverHelpers.start(); +}); +after('Shut down the server', function () { serverHelpers.stop(server); }); + +const runner = new Runner(); +runner.prepare(); +// The server's request cache causes side effects between tests. +runner.beforeEach = () => { serverHelpers.reset(server); }; + +const args = minimist(process.argv.slice(3)); +const prOption = args.pr; +const serviceOption = args.only; + +if (prOption !== undefined) { + const repoSlug = process.env.TRAVIS_REPO_SLUG; + const pullRequest = process.env.TRAVIS_PULL_REQUEST; + if (repoSlug === undefined || pullRequest === undefined) { + console.error('Please set TRAVIS_REPO_SLUG and TRAVIS_PULL_REQUEST.'); + process.exit(-1); + } + console.info(`PR: ${repoSlug}#${pullRequest}`); + + getTitle(repoSlug, pullRequest) + .then(title => { + console.info(`Title: ${title}`); + const services = servicesForTitle(title); + if (services.length === 0) { + console.info('No services found. Nothing to do.'); + } else { + console.info(`Services: (${services.length} found) ${services.join(', ')}\n`); + runner.only(services); + runner.toss(); + run(); + } + }).catch(err => { + console.error(err); + process.exit(1); + }); +} else { + if (serviceOption !== undefined) { + runner.only(serviceOption.split(',')); + } + + runner.toss(); + // Invoke run() asynchronously, beacuse Mocha will not start otherwise. + process.nextTick(run); +} diff --git a/service-tests/runner/runner.js b/service-tests/runner/runner.js new file mode 100644 index 0000000000000..75ccd990fbc4d --- /dev/null +++ b/service-tests/runner/runner.js @@ -0,0 +1,61 @@ +'use strict'; + +const glob = require('glob'); + +/** + * Load a collection of ServiceTester objects and register them with Mocha. + */ +class Runner { + /** + * Function to invoke before each test. This is a stub which can be + * overridden on instances. + */ + beforeEach () {} + + /** + * Prepare the runner by loading up all the ServiceTester objects. + */ + prepare () { + this.testers = glob.sync(`${__dirname}/../*.js`).map(name => require(name)); + this.testers.forEach(tester => { + tester.beforeEach = () => { this.beforeEach(); }; + }); + } + + _testersForService (service) { + return this.testers.filter(t => t.id.toLowerCase() === service); + } + + /** + * Limit the test run to the specified services. + * + * @param services An array of service ids to run + */ + only (services) { + const normalizedServices = new Set(services.map(v => v.toLowerCase())); + + const missingServices = []; + normalizedServices.forEach(service => { + const testers = this._testersForService(service); + + if (testers.length === 0) { + missingServices.push(service); + } + + testers.forEach(tester => { tester.only(); }); + }); + + // Throw at the end, to provide a better error message. + if (missingServices.length > 0) { + throw Error('Unknown services: ' + missingServices.join(', ')); + } + } + + /** + * Register the tests with Mocha. + */ + toss () { + this.testers.forEach(tester => { tester.toss(); }); + } +} +module.exports = Runner; diff --git a/service-tests/runner/service-tester.js b/service-tests/runner/service-tester.js new file mode 100644 index 0000000000000..4cff1ac798328 --- /dev/null +++ b/service-tests/runner/service-tester.js @@ -0,0 +1,73 @@ +'use strict'; + +const frisby = require('icedfrisby-nock')(require('icedfrisby')); +const config = require('../../test/config'); + +/** + * Encapsulate a suite of tests. Create new tests using create() and register + * them with Mocha using toss(). + */ +class ServiceTester { + /** + * @param attrs { id, title, pathPrefix } The `id` is used to specify which + * tests to run from the CLI or pull requests. The `title` prints in the + * Mocha output. The `path` is the path prefix which is automatically + * prepended to each tested URI. The default is `/${attrs.id}`. + */ + constructor (attrs) { + Object.assign(this, { + id: attrs.id, + title: attrs.title, + pathPrefix: attrs.pathPrefix === undefined + ? `/${attrs.id}` + : attrs.pathPrefix, + specs: [], + _only: false + }); + } + + /** + * Invoked before each test. This is a stub which can be overridden on + * instances. + */ + beforeEach () {} + + /** + * Create a new test. The hard work is delegated to IcedFrisby. + * https://github.com/MarkHerhold/IcedFrisby/#show-me-some-code + * + * Note: The caller should not invoke toss() on the Frisby chain, as it's + * invoked automatically by the tester. + * @param msg The name of the test + */ + create (msg) { + const spec = frisby.create(msg) + .baseUri(`http://localhost:${config.port}${this.pathPrefix}`) + .before(() => { this.beforeEach(); }); + + this.specs.push(spec); + + return spec; + } + + /** + * Run only this tester. This can be invoked using the --only argument to + * the CLI, or directly on the tester. + */ + only () { + this._only = true; + } + + /** + * Register the tests with Mocha. + */ + toss () { + const specs = this.specs; + + const fn = this._only ? describe.only : describe; + fn(this.title, function () { + specs.forEach(spec => { spec.toss(); }); + }); + } +} +module.exports = ServiceTester; diff --git a/test/config.js b/test/config.js new file mode 100644 index 0000000000000..bceb19bc1a8d7 --- /dev/null +++ b/test/config.js @@ -0,0 +1,3 @@ +module.exports = { + port: 1111 +}; diff --git a/test/in-process-server-helpers.js b/test/in-process-server-helpers.js new file mode 100644 index 0000000000000..b471d41d9fbaf --- /dev/null +++ b/test/in-process-server-helpers.js @@ -0,0 +1,66 @@ +/** + * Helpers to run a Shields server in process. + * + * Usage: + * let server; + * before('Start running the server', function () { + * this.timeout(5000); + * server = serverHelpers.start(); + * }); + * after('Shut down the server', function () { serverHelpers.stop(server); }); + */ + +'use strict'; + +const config = require('./config'); + +let startCalled = false; + +/** + * Start the server. + * + * @param {Number} port number (optional) + * @return {Object} The scoutcamp instance + */ +function start () { + if (startCalled) { + throw Error('Because of the way Shields works, you can only use this ' + + 'once per node process. Once you call stop(), the game is over.'); + } + startCalled = true; + + const originalArgv = process.argv; + // Modifying argv during import is a bit dirty, but it works, and avoids + // making bigger changes to server.js. + process.argv = ['', '', config.port, 'localhost']; + const server = require('../server'); + + process.argv = originalArgv; + return server; +} + +/** + * Reset the server, to avoid or reduce side effects between tests. + * + * @param {Object} server instance + */ +function reset (server) { + server.requestCache.clear(); +} + +/** + * Stop the server. + * + * @param {Object} server instance + */ +function stop (server) { + if (server) { + server.camp.close(); + } +} + +module.exports = { + start, + reset, + stop +}; diff --git a/test/test.spec.js b/test/test.spec.js index 8f09abf588feb..06e7b5bfd1cdf 100644 --- a/test/test.spec.js +++ b/test/test.spec.js @@ -7,6 +7,7 @@ var path = require('path'); var isPng = require('is-png'); var isSvg = require('is-svg'); var svg2img = require('../lib/svg-to-img'); +const serverHelpers = require('./in-process-server-helpers'); // Test parameters var port = '1111'; @@ -70,16 +71,12 @@ describe('The CLI', function () { }); describe('The server', function () { - var server; - before('Start running the server', function() { + let server; + before('Start running the server', function () { this.timeout(5000); - // This is a bit gross, but it works. - process.argv = ['', '', port, 'localhost']; - server = require('../server'); - }); - after('Shut down the server', function(done) { - server.camp.close(function () { done(); }); + server = serverHelpers.start(); }); + after('Shut down the server', function () { serverHelpers.stop(server); }); it('should produce colorscheme badges', function(done) { http.get(url + ':fruit-apple-green.svg',