├── publish.js ├── test ├── run.test.js ├── extensibility.test.js └── custom.test.js ├── team.js ├── user ├── logout.js └── add.js ├── access.js ├── unpublish.js ├── .gitignore ├── ping.js ├── pkg ├── fetch.js ├── update.js ├── dist-tag.js └── show.js ├── whoami.js ├── LICENSE ├── package.json ├── views ├── query.js └── all.js ├── lib ├── schedule.js ├── nit.js └── requests.js ├── index.js └── README.md /publish.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('./lib/nit')('POST /:pkg'); 4 | 5 | /** 6 | * Test coverage for publishing npm packages 7 | * See: "npm help publish" 8 | */ 9 | module.exports.valid = nit.skip(':api with a valid payload', function () { 10 | return function () { 11 | throw new Error('Not implemented.'); 12 | }; 13 | }); 14 | 15 | module.exports.invalid = nit.skip(':api with an invalid payload', function () { 16 | return function () { 17 | throw new Error('Not implemented.'); 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /test/run.test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const abstractRegistry = require('../'); 4 | 5 | abstractRegistry({ 6 | suites: [ 7 | 'pkg/show', 8 | 'pkg/fetch', 9 | 'publish', 10 | 'unpublish', 11 | 'pkg/dist-tag', 12 | 'user/add', 13 | 'user/logout', 14 | 'pkg/update', 15 | 'ping', 16 | // 17 | // TODO: The test is functional but we need to use private 18 | // credentials in Travis to unskip this. 19 | // 20 | // 'whoami', 21 | 'team', 22 | 'access', 23 | 'views/all', 24 | 'views/query' 25 | ] 26 | }, function () { 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /team.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('./lib/nit')('/-/org/:scope'); 4 | 5 | /** 6 | * Test coverage for access control to public and private teams 7 | * See: "npm help team" 8 | */ 9 | module.exports.list = nit.skip('GET :api?format=cli', function () { 10 | return function () { 11 | throw new Error('Not implemented.'); 12 | }; 13 | }); 14 | 15 | // 16 | // TODO: Write stubs for these functions 17 | // npm team create 18 | // npm team destroy 19 | // npm team add 20 | // npm team rm 21 | // npm team edit 22 | // 23 | -------------------------------------------------------------------------------- /user/logout.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('DELETE /-/user/token/:token'); 4 | 5 | /** 6 | * Test coverage for logging out 7 | * See: "npm help logout", npm-registry-client 8 | * https://github.com/npm/npm-registry-client/blob/master/lib/logout.js#L15-L19 9 | */ 10 | module.exports.found = nit.skip(':api with a token that exists', function () { 11 | return function () { 12 | throw new Error('Not implemented.'); 13 | }; 14 | }); 15 | 16 | module.exports.notFound = nit.skip(':api with a token that does not exist', function () { 17 | return function () { 18 | throw new Error('Not implemented.'); 19 | }; 20 | }); 21 | -------------------------------------------------------------------------------- /access.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('./lib/nit')('/-/package/:scope/:pkg/access'); 4 | 5 | /** 6 | * Test coverage for access control to public packages 7 | * See: "npm help access" 8 | */ 9 | module.exports.public = nit.skip('GET :api', function () { 10 | return function () { 11 | throw new Error('Not implemented.'); 12 | }; 13 | }); 14 | 15 | // 16 | // TODO: Write stubs for these methods 17 | // npm access restricted [] 18 | // npm access grant [] 19 | // npm access revoke [] 20 | // npm access ls-packages [||] 21 | // npm access ls-collaborators [ []] 22 | // npm access edit [] 23 | // 24 | -------------------------------------------------------------------------------- /unpublish.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('./lib/nit')('DELETE /:pkg/-rev/:rev'); 4 | 5 | /** 6 | * Test coverage for unpublishing npm packages 7 | * See: "npm help unpublish" 8 | */ 9 | module.exports.singleVersion = nit.skip(':api for a single version', function () { 10 | return function () { 11 | throw new Error('Not implemented.'); 12 | }; 13 | }); 14 | 15 | module.exports.lastVersion = nit.skip(':api for the last version', function () { 16 | return function () { 17 | throw new Error('Not implemented.'); 18 | }; 19 | }); 20 | 21 | module.exports.allForce = nit.skip(':api for all versions (i.e. force)', function () { 22 | return function () { 23 | throw new Error('Not implemented.'); 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /user/add.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('PUT /-/user/org.couchdb.user:[:user]'); 4 | 5 | /** 6 | * Test coverage for adding new npm users 7 | * See: "npm help adduser", npm-registry-client, npm-registry-couchapp 8 | * https://github.com/npm/npm-registry-client/blob/master/lib/adduser.js#L35-L62 9 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L48-L53 10 | */ 11 | module.exports.isNew = nit.skip(':api when the user is new.', function () { 12 | return function () { 13 | throw new Error('Not implemented.'); 14 | }; 15 | }); 16 | 17 | module.exports.existing = nit.skip(':api when the user exists', function () { 18 | return function () { 19 | throw new Error('Not implemented.'); 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # Used for local testing of new API routes 36 | test/experimental.test.js 37 | -------------------------------------------------------------------------------- /ping.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('./lib/nit')('GET /-/ping'); 4 | const requests = require('./lib/requests'); 5 | 6 | /** 7 | * Test coverage for the simple ping route 8 | * See: `curl http://{AUTH}@registry.npmjs.org/-/ping?write=true` 9 | */ 10 | module.exports.standard = nit(':api', function (opts) { 11 | return function (done) { 12 | requests.json({ 13 | host: opts.registry, 14 | path: '/-/ping', 15 | method: 'GET', 16 | status: 200, 17 | body: {} 18 | }, done); 19 | }; 20 | }); 21 | 22 | module.exports.write = nit(':api?write=true', function (opts) { 23 | return function (done) { 24 | requests.json({ 25 | host: opts.registry, 26 | path: '/-/ping?write=true', 27 | method: 'GET', 28 | status: 200, 29 | body: {} 30 | }, done); 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /pkg/fetch.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('GET /:pkg/-/:att'); 4 | 5 | /** 6 | * Test coverage for downloading packages 7 | * See: "npm help install" 8 | * "npm help pack -g" 9 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L88-L92 10 | */ 11 | module.exports.found = nit.skip(':api for a valid version', function () { 12 | return function () { 13 | throw new Error('Not implemented.'); 14 | }; 15 | }); 16 | 17 | module.exports.noVersion = nit.skip(':api for an invalid or missing version', function () { 18 | return function () { 19 | throw new Error('Not implemented.'); 20 | }; 21 | }); 22 | 23 | module.exports.noPackage = nit.skip(':api for missing package', function () { 24 | return function () { 25 | throw new Error('Not implemented.'); 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /pkg/update.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('PUT /:pkg'); 4 | 5 | /** 6 | * Test coverage for updating JSON of npm packages 7 | * See: npm-registry-couchapp 8 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L89-L90 9 | */ 10 | module.exports.correctRev = nit.skip(':api', function () { 11 | return function () { 12 | throw new Error('Not implemented.'); 13 | }; 14 | }); 15 | 16 | module.exports.conflict409 = nit.skip(':api (409 Update Conflict)', function () { 17 | return function () { 18 | throw new Error('Not implemented.'); 19 | }; 20 | }); 21 | 22 | /** 23 | * Test coverage for working with npm stars 24 | * See: "npm help star" 25 | * "npm help stars" 26 | */ 27 | module.exports.star = nit.skip(':api?write=true (add star)', function () { 28 | return function () { 29 | throw new Error('Not implemented.'); 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /pkg/dist-tag.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('/-/package/:pkg/dist-tags'); 4 | const requests = require('../lib/requests'); 5 | 6 | /** 7 | * Test coverage for working with dist-tags on packages 8 | * See: "npm help dist-tag" 9 | */ 10 | module.exports.add = nit.skip('PUT :api/:tag', function () { 11 | return function () { 12 | throw new Error('Not implemented.'); 13 | }; 14 | }); 15 | 16 | module.exports.list = nit('GET :api', function (opts) { 17 | const pkg = opts.pkg || 'smart-private-npm'; 18 | return function (done) { 19 | requests.json({ 20 | host: opts.registry, 21 | method: 'GET', 22 | path: `/-/package/${pkg}/dist-tags`, 23 | status: 200, 24 | body: opts.body || { latest: '2.3.0' } 25 | }, done); 26 | }; 27 | }); 28 | 29 | module.exports.remove = nit.skip('DELETE :api/:tag', function () { 30 | return function () { 31 | throw new Error('Not implemented.'); 32 | }; 33 | }); 34 | -------------------------------------------------------------------------------- /whoami.js: -------------------------------------------------------------------------------- 1 | 2 | const nit = require('./lib/nit')('GET /-/whoami'); 3 | const requests = require('./lib/requests'); 4 | 5 | /** 6 | * Test coverage for the whoami route 7 | * See: curl https://{AUTH}@registry.npmjs.org/-/whoami 8 | */ 9 | module.exports.auth = nit(':api with basic auth', function (opts) { 10 | return function (done) { 11 | requests.authed({ 12 | host: opts.registry, 13 | username: opts.username, 14 | password: opts.password, 15 | path: '/-/whoami', 16 | method: 'GET', 17 | status: 200, 18 | body: { username: opts.username } 19 | }, done); 20 | }; 21 | }); 22 | 23 | module.exports.bearerToken = nit.skip(':api with bearer token', function () { 24 | return function () { 25 | throw new Error('Not implemented.'); 26 | }; 27 | }); 28 | 29 | module.exports.noAuth = nit(':api with no auth', function (opts) { 30 | return function (done) { 31 | requests.json({ 32 | host: opts.registry, 33 | path: '/-/whoami', 34 | method: 'GET', 35 | status: 401, 36 | body: {} 37 | }, done); 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 GoDaddy Operating Company, LLC. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abstract-npm-registry", 3 | "version": "0.0.1", 4 | "description": "An open and extendible test suite for you can use to test various functional areas of an npm registry.", 5 | "main": "index.js", 6 | "scripts": { 7 | "eslint": "eslint-godaddy ./*.js ./*/*.js test/*.js", 8 | "pretest": "npm run eslint", 9 | "test": "DEBUG=abstract* node test/run.test.js && mocha test/{custom,extensibility}.test.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/warehouseai/abstract-npm-registry.git" 14 | }, 15 | "keywords": [ 16 | "npm", 17 | "npm-registry", 18 | "npm-client", 19 | "abstract", 20 | "mocha", 21 | "tests" 22 | ], 23 | "author": "Charlie Robbins ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/warehouseai/abstract-npm-registry/issues" 27 | }, 28 | "homepage": "https://github.com/warehouseai/abstract-npm-registry#readme", 29 | "devDependencies": { 30 | "assume": "^1.4.1", 31 | "eslint": "^4.4.1", 32 | "eslint-config-godaddy": "^2.0.0", 33 | "eslint-plugin-json": "^1.2.0", 34 | "eslint-plugin-mocha": "^4.11.0", 35 | "mocha": "^2.4.5", 36 | "supertest": "^1.2.0" 37 | }, 38 | "dependencies": { 39 | "diagnostics": "^1.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /views/query.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('GET /-'); 4 | 5 | /** 6 | * Test coverage for querying packages. Generally 7 | * older CouchDB views only available on "skimdb". 8 | * See: npm-registry-couchapp 9 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L35-L40 10 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L61-L64 11 | */ 12 | module.exports.scripts = nit.skip(':api/scripts', function () { 13 | return function () { 14 | throw new Error('Not implemented.'); 15 | }; 16 | }); 17 | 18 | module.exports.byField = nit.skip(':api/by-field', function () { 19 | return function () { 20 | throw new Error('Not implemented.'); 21 | }; 22 | }); 23 | 24 | module.exports.fields = nit.skip(':api/fields', function () { 25 | return function () { 26 | throw new Error('Not implemented.'); 27 | }; 28 | }); 29 | 30 | module.exports.needBuild = nit.skip(':api/needbuild', function () { 31 | return function () { 32 | throw new Error('Not implemented.'); 33 | }; 34 | }); 35 | 36 | module.exports.top = nit.skip(':api/top', function () { 37 | return function () { 38 | throw new Error('Not implemented.'); 39 | }; 40 | }); 41 | 42 | module.exports.starredByUser = nit.skip(':api/starred-by-user/:user', function () { 43 | return function () { 44 | throw new Error('Not implemented.'); 45 | }; 46 | }); 47 | 48 | module.exports.starredByPackage = nit.skip(':api/starred-by-package/:pkg', function () { 49 | return function () { 50 | throw new Error('Not implemented.'); 51 | }; 52 | }); 53 | -------------------------------------------------------------------------------- /test/extensibility.test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const abstractNpmRegistry = require('../')({ 4 | registry: 'https://registry.npmjs.org', 5 | headers: { 'X-ANY-HEADER-YOU-WANT': true } 6 | }); 7 | 8 | console.log('\n\n> Starting my custom test suite with overrides using mocha...'); 9 | 10 | describe('My extended custom test suite', function () { 11 | this.timeout(5000); 12 | 13 | // abstractNpmRegistry.it('pkg/dist-tag.add'); 14 | abstractNpmRegistry.it('pkg/dist-tag.list', { 15 | pkg: 'winston', 16 | body: { latest: '2.2.0' } 17 | }); 18 | // abstractNpmRegistry.it('pkg/dist-tag.remove'); 19 | 20 | // abstractNpmRegistry.it('pkg/fetch.found'); 21 | // abstractNpmRegistry.it('pkg/fetch.noVersion'); 22 | // abstractNpmRegistry.it('pkg/fetch.noPackage'); 23 | 24 | // abstractNpmRegistry.it('pkg/update.correctRev'); 25 | // abstractNpmRegistry.it('pkg/update.conflict409'); 26 | // abstractNpmRegistry.it('pkg/update.star'); 27 | 28 | abstractNpmRegistry.it('pkg/show.found', { 29 | pkg: 'winston', 30 | expect: ((res, assume) => { 31 | const doc = res.body; 32 | assume(doc.name).equal('winston'); 33 | }) 34 | }); 35 | 36 | abstractNpmRegistry.it('pkg/show.version', { 37 | pkg: 'winston', 38 | version: '2.0.0' 39 | }); 40 | 41 | abstractNpmRegistry.it('pkg/show.noPackage', { 42 | pkg: 'this-no-exist-for-reals-' + Date.now() 43 | }); 44 | 45 | abstractNpmRegistry.it('pkg/show.noVersion', { 46 | pkg: 'this-no-exist-for-reals-' + Date.now(), 47 | version: '0.0.1' 48 | }); 49 | 50 | // abstractNpmRegistry.it('user/add.isNew'); 51 | // abstractNpmRegistry.it('user/add.existing'); 52 | 53 | // abstractNpmRegistry.it('user/logout.found'); 54 | // abstractNpmRegistry.it('user/logout.notFound'); 55 | }); 56 | -------------------------------------------------------------------------------- /views/all.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const assume = require('assume'); 4 | const nit = require('../lib/nit')('GET /-/all'); 5 | const requests = require('../lib/requests'); 6 | 7 | /** 8 | * Test coverage for listing all packages 9 | * See: npm-registry-couchapp 10 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L24 11 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#L32-L33 12 | */ 13 | module.exports.index = nit(':api', function (opts) { 14 | return function (done) { 15 | requests.json({ 16 | host: opts.registry, 17 | path: '/-/all/', 18 | method: 'GET', 19 | status: 404 20 | }, done); 21 | }; 22 | }); 23 | 24 | module.exports.since = nit(':api/since', function (opts) { 25 | return function (done) { 26 | requests.go({ 27 | host: opts.registry, 28 | path: '/-/all/since', 29 | method: 'GET', 30 | status: 302, 31 | expect: function (res) { 32 | assume(res.headers.location).equals( 33 | `${opts.registry}/-/all/static/all.json` 34 | ); 35 | } 36 | }, done); 37 | }; 38 | }); 39 | 40 | module.exports.static = nit(':api/static/all.json', function (opts) { 41 | return function (done) { 42 | var superquest = requests.json({ 43 | host: opts.registry, 44 | path: '/-/all/static/all.json', 45 | method: 'GET', 46 | status: 200, 47 | expect: function (res) { 48 | assume(res.headers['content-length']).is.a('number'); 49 | } 50 | }, done); 51 | 52 | // 53 | // This returns more than 100MB of JSON so we want to end it ASAP 54 | // since we are only asserting headers and response code. 55 | // 56 | superquest.req.on('response', function (res) { 57 | res.end(); 58 | }); 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /test/custom.test.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const abstractNpmRegistry = require('../')({ 4 | registry: 'https://registry.npmjs.org', 5 | headers: { 'X-ANY-HEADER-YOU-WANT': true } 6 | }); 7 | 8 | console.log('\n\n> Starting my custom test suite using mocha...'); 9 | 10 | describe('My custom test suite of defaults', function () { 11 | this.timeout(5000); 12 | 13 | abstractNpmRegistry.it('ping.standard'); 14 | abstractNpmRegistry.it('ping.write'); 15 | 16 | // 17 | // TODO: The test is functional but we need to use private 18 | // credentials in Travis to unskip this. 19 | // 20 | // abstractNpmRegistry.it('whoami.auth', { 21 | // username: process.env.NPM_USERNAME, 22 | // password: process.env.NPM_PASSWORD 23 | // }); 24 | abstractNpmRegistry.it('whoami.bearerToken'); 25 | abstractNpmRegistry.it('whoami.noAuth'); 26 | 27 | abstractNpmRegistry.it('pkg/show.found'); 28 | abstractNpmRegistry.it('pkg/show.version'); 29 | abstractNpmRegistry.it('pkg/show.noPackage'); 30 | abstractNpmRegistry.it('pkg/show.noVersion'); 31 | 32 | abstractNpmRegistry.it('pkg/dist-tag.list'); 33 | abstractNpmRegistry.it('pkg/dist-tag.add'); 34 | abstractNpmRegistry.it('pkg/dist-tag.remove'); 35 | 36 | abstractNpmRegistry.it('pkg/fetch.found'); 37 | abstractNpmRegistry.it('pkg/fetch.noVersion'); 38 | abstractNpmRegistry.it('pkg/fetch.noPackage'); 39 | 40 | abstractNpmRegistry.it('pkg/update.correctRev'); 41 | abstractNpmRegistry.it('pkg/update.conflict409'); 42 | abstractNpmRegistry.it('pkg/update.star'); 43 | 44 | abstractNpmRegistry.it('user/add.isNew'); 45 | abstractNpmRegistry.it('user/add.existing'); 46 | 47 | abstractNpmRegistry.it('user/logout.found'); 48 | abstractNpmRegistry.it('user/logout.notFound'); 49 | 50 | // 51 | // TODO: Write stubs for usage of 52 | // access.js 53 | // publish.js 54 | // team.js 55 | // unpublish.js 56 | // 57 | }); 58 | -------------------------------------------------------------------------------- /lib/schedule.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const path = require('path'); 4 | const debug = require('diagnostics')('abstract-npm-registry:mocha'); 5 | const Mocha = require('mocha'); 6 | 7 | /** 8 | * Simple implementation of a mocha test runner to decouple 9 | * `abstract-npm-registry` from the `mocha` and `_mocha` binaries. 10 | * This is necessary because of how we separate the concerns of 11 | * DEFINING A TEST as a stand-alone export and SCHEDULING A TEST 12 | * with `mocha`. 13 | * @param {[type]} opts [description] 14 | * @param {Function} callback [description] 15 | * @returns {[type]} [description] 16 | */ 17 | module.exports = function (opts) { 18 | var mocha = new Mocha(); 19 | var rootd = path.resolve(__dirname, '..') + path.sep; 20 | 21 | // 22 | // Do the bear minimum to the required mocha 23 | // instance to ensure that it is runnable. 24 | // 25 | mocha.reporter('spec'); 26 | mocha.ui('bdd'); 27 | mocha.timeout(opts.timeout || 5000); 28 | mocha.files = []; 29 | 30 | (opts.suites || [ 31 | 'pkg/show', 32 | 'pkg/fetch', 33 | 'pkg/version', 34 | 'publish', 35 | 'unpublish', 36 | 'pkg/dist-tag', 37 | 'user/add', 38 | 'user/logout', 39 | 'pkg/update', 40 | 'ping', 41 | 'whoami', 42 | 'team', 43 | 'access', 44 | 'views/all', 45 | 'views/query' 46 | ]).forEach(function (file) { 47 | // 48 | // We slightly modify the core mocha file loading logic 49 | // to expect each file to export a set of functions each of 50 | // which is the BODY OF AN IT STATEMENT. 51 | // 52 | file = path.resolve(rootd, file); 53 | var suite = mocha.suite; 54 | const target = require(file); 55 | var basefile = file.replace(rootd, ''); 56 | 57 | debug('pre-require', basefile); 58 | suite.emit('pre-require', global, file, mocha); 59 | 60 | global.describe(basefile, function () { 61 | Object.keys(target).forEach(function (exp) { 62 | var cmd = target[exp]; 63 | if (typeof cmd.it !== 'function') { 64 | return; 65 | } 66 | 67 | cmd.prefix = exp; 68 | debug('schedule.it \n %s \n %j', cmd.displayName, { 69 | skip: cmd['it.skip'], 70 | require: `${basefile}.js` 71 | }); 72 | 73 | cmd.it(opts); 74 | }); 75 | }); 76 | }); 77 | 78 | return mocha; 79 | }; 80 | -------------------------------------------------------------------------------- /pkg/show.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const nit = require('../lib/nit')('GET /:pkg'); 4 | const requests = require('../lib/requests'); 5 | const assume = require('assume'); 6 | 7 | /** 8 | * Test coverage for getting JSON for npm packages 9 | * See: npm-registry-couchapp 10 | * https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js#70-L74 11 | */ 12 | module.exports.found = nit(':api', function (opts) { 13 | const pkg = opts.pkg || 'smart-private-npm'; 14 | return function (done) { 15 | requests.json({ 16 | host: opts.registry, 17 | method: 'GET', 18 | path: `/${pkg}`, 19 | status: 200, 20 | expect: function (res) { 21 | const doc = res.body; 22 | assume(doc).is.an('object'); 23 | assume(doc.name).equals(pkg); 24 | assume(doc._id).equals(pkg); 25 | assume(doc.versions).is.an('object'); 26 | assume(doc.time).is.an('object'); 27 | 28 | if (opts.expect) { 29 | opts.expect(res, assume); 30 | } 31 | } 32 | }, done); 33 | }; 34 | }); 35 | 36 | module.exports.version = nit(':api/:version', function (opts) { 37 | const pkg = opts.pkg || 'smart-private-npm'; 38 | const version = opts.version || '2.3.0'; 39 | 40 | return function (done) { 41 | requests.json({ 42 | host: opts.registry, 43 | method: 'GET', 44 | path: `/${pkg}/${version}`, 45 | status: 200, 46 | expect: function (res) { 47 | const doc = res.body; 48 | assume(doc).is.an('object'); 49 | assume(doc.version).equals(version); 50 | 51 | if (opts.expect) { 52 | opts.expect(doc, assume); 53 | } 54 | } 55 | }, done); 56 | }; 57 | }); 58 | 59 | module.exports.noPackage = nit(':api for an unknown package', function (opts) { 60 | const pkg = opts.pkg || 'i-am-no-exist-' + Date.now(); 61 | 62 | return function (done) { 63 | requests.json({ 64 | host: opts.registry, 65 | method: 'GET', 66 | path: `/${pkg}`, 67 | status: 404, 68 | // 69 | // Remark: Since this is a CouchDB / implementation specific error, we may 70 | // want to consider only enabling this assertion in a "strict mode". 71 | // 72 | body: {} 73 | }, done); 74 | }; 75 | }); 76 | 77 | module.exports.noVersion = nit(':api/:version for an unknown version', function (opts) { 78 | const pkg = opts.pkg || 'i-am-no-exist-' + Date.now(); 79 | const version = opts.version || '0.0.1'; 80 | 81 | return function (done) { 82 | requests.json({ 83 | host: opts.registry, 84 | method: 'GET', 85 | path: `/${pkg}/${version}`, 86 | status: 404, 87 | // 88 | // Remark: Since this is a CouchDB specific error, we may 89 | // want to consider only enabling this assertion in a "strict mode". 90 | // 91 | body: { 92 | error: 'not_found', 93 | reason: 'document not found' 94 | } 95 | }, done); 96 | }; 97 | }); 98 | -------------------------------------------------------------------------------- /lib/nit.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const diagnostics = require('diagnostics'); 4 | var debug = { 5 | define: diagnostics('abstract-npm-registry:define'), 6 | mocha: diagnostics('abstract-npm-registry:mocha') 7 | }; 8 | 9 | /** 10 | * Returns function that sets the name of the `test` 11 | * function provided to be a one-level hierarchy of 12 | * `${route} ${name}`. This is useful for setting the 13 | * value passed to mocha `it`. 14 | * @param {String} route [description] 15 | * @returns {[type]} [description] 16 | * @public 17 | */ 18 | module.exports = function (route) { 19 | /** 20 | * [nit description] 21 | * @param {[type]} name [description] 22 | * @param {[type]} testFn [description] 23 | * @returns {NitTest} [description] 24 | */ 25 | function nit(name, testFn) { 26 | // 27 | // We implement an encapsulated command pattern 28 | // here so that these are more easily consumable 29 | // both in our own "framework" or for our users. 30 | // 31 | var cmd = new NitTest(route, name, testFn); 32 | debug.define('it', cmd['it.name']); 33 | return cmd; 34 | } 35 | 36 | nit.skip = function (name, testFn) { 37 | var cmd = nit(name, testFn); 38 | cmd['it.skip'] = true; 39 | debug.define('skip', cmd['it.name']); 40 | return cmd; 41 | }; 42 | 43 | return nit; 44 | }; 45 | 46 | /** 47 | * Constructor function for the NitTest object that represents 48 | * a single "command" for self-invoking mocha tests. 49 | * @param {[type]} route [description] 50 | * @param {[type]} name [description] 51 | * @param {[type]} testFn [description] 52 | */ 53 | function NitTest(route, name, testFn) { 54 | this.route = route; 55 | this.name = name; 56 | this.fn = testFn; 57 | this['it.name'] = name.replace(':api', route); 58 | testFn['it.name'] = this['it.name']; 59 | } 60 | 61 | /** 62 | * The full display name that this instance should use 63 | * when presenting itself to a mocha `it` or `xit` function. 64 | */ 65 | Object.defineProperty(NitTest.prototype, 'displayName', { 66 | configurable: false, 67 | enumerable: true, 68 | get: function () { 69 | // 70 | // Setup names to be flexible about things 71 | // 72 | var testName = this['it.name'] || this.name; 73 | return this.prefix 74 | ? `(${this.prefix}) ${testName}` 75 | : testName; 76 | } 77 | }); 78 | 79 | /** 80 | * Executes this "command" instance by scheduling the 81 | * stored test `fn` using the current global `mocha` context. 82 | * @param {[type]} opts [description] 83 | */ 84 | NitTest.prototype.it = function (opts) { 85 | var skip = this['it.skip']; 86 | var itFn = skip && global.xit || global.it; 87 | var displayName = this.displayName; 88 | 89 | // 90 | // Replace any keys in the displayName with values 91 | // passed in through the options. 92 | // 93 | Object.keys(opts).forEach((key) => { 94 | displayName = displayName.replace(':' + key, opts[key]); 95 | }); 96 | 97 | itFn(displayName, this.fn(opts)); 98 | }; 99 | -------------------------------------------------------------------------------- /lib/requests.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undefined */ 2 | const assume = require('assume'); 3 | const request = require('supertest'); 4 | 5 | /** 6 | * Simple boilerplate for supertest which accepts the most common 7 | * options required to make a request to the npm registry. 8 | * 9 | * @param {Object} opts Options for making the request. All are required except one of 10 | * `opts.body` or `opts.expect` may be omitted. 11 | * - opts.host {string} Full host of the npm registry (e.g. https://registry.npmjs.org) 12 | * - opts.method {string} HTTP method for the request 13 | * - opts.path {string} Relative path for the request (e.g. /winston/2.0.0) 14 | * - opts.expect {function} Assertion function for the response 15 | * - opts.body {Object} 16 | * - opts.status {number} Expected status code 17 | * @param {function} setup **Optional** Extends the supertest instance before execution. 18 | * @param {function} done Continuation to pass control to when the supertest completes. 19 | */ 20 | module.exports.go = function (opts, setup, done) { 21 | // 22 | // Allow for an optional setup function for more helpers 23 | // to be based on this. 24 | // 25 | if (arguments.length === 2) { 26 | done = setup; 27 | setup = undefined; 28 | } 29 | 30 | // 31 | // Override opts.expect if it is not provided 32 | // 33 | // eslint-disable-next-line no-unused-vars 34 | let expect = opts.expect || function (res) {}; 35 | if (opts.body && !opts.expect) { 36 | expect = function (res) { 37 | assume(res.body).deep.equals(opts.body); 38 | }; 39 | } 40 | 41 | const method = opts.method.toLowerCase(); 42 | let superquest = request(opts.host)[method](opts.path); 43 | if (setup) { 44 | superquest = setup(superquest); 45 | } 46 | 47 | superquest 48 | .expect(expect) 49 | .expect(opts.status, done); 50 | }; 51 | 52 | /** 53 | * Sets expected JSON headers on the request and expects 54 | * them on the response. 55 | * @param {[type]} opts [description] 56 | * @param {[type]} setup [description] 57 | * @param {Function} done [description] 58 | */ 59 | module.exports.json = function (opts, setup, done) { 60 | // 61 | // Allow for an optional setup function for more helpers 62 | // to be based on this. 63 | // 64 | if (arguments.length === 2) { 65 | done = setup; 66 | setup = undefined; 67 | } 68 | 69 | module.exports.go(opts, (superquest) => { 70 | if (setup) { 71 | superquest = setup(superquest); 72 | } 73 | 74 | return superquest 75 | .set('Accept', 'application/json') 76 | .expect('Content-Type', /json/); 77 | }, done); 78 | }; 79 | 80 | /** 81 | * Simple wrapper to our JSON expectation which also accepts auth. 82 | * @param {[type]} opts [description] 83 | * @param {Function} done [description] 84 | */ 85 | module.exports.authed = function (opts, done) { 86 | assume(opts.username).is.a('string', '**Set this with NPM_USERNAME environment variable**'); 87 | assume(opts.password).is.a('string', '**Set this with NPM_PASSWORD environment variable**'); 88 | 89 | module.exports.json(opts, (superquest) => { 90 | return superquest.auth(opts.username, opts.password); 91 | }, done); 92 | }; 93 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const path = require('path'); 4 | const debug = require('diagnostics')('abstract-npm-registry'); 5 | 6 | /** 7 | * Creates a new instance of AbstractNpmRegistry and 8 | * "immediately" runs it. 9 | * @param {Object} opts Configuration options 10 | * @param {String} opts.registry Location of registry 11 | * @param {[type]} opts.run [description] 12 | * @param {Function} callback Function to execute when all other logic completes 13 | * @returns {Object} object that holds the state of a 14 | * run against a registry endpoint 15 | */ 16 | module.exports = function (opts, callback) { 17 | opts = opts || {}; 18 | opts.registry = opts.registry || 'https://registry.npmjs.org'; 19 | 20 | var suite = new AbstractNpmRegistry(opts); 21 | if (callback || opts.run) { 22 | setImmediate(function () { suite.run(callback); }); 23 | } 24 | 25 | return suite; 26 | }; 27 | 28 | // 29 | // Expose our AbstractNpmRegistry in case we need it 30 | // and/or for testability. 31 | // 32 | module.exports.AbstractNpmRegistry = AbstractNpmRegistry; 33 | 34 | /** 35 | * Constructor function for the AbstractNpmRegistry object 36 | * which is responsible for holding all of the state of a 37 | * run against a registry endpoint. 38 | * @param {Object} opts Options to associate with 39 | * this instance of AbstractNpmRegistry 40 | */ 41 | function AbstractNpmRegistry(opts) { 42 | this.opts = opts || {}; 43 | this.rootd = path.resolve(__dirname) + '/'; 44 | this.parse = /^([^]+)\.(.*)$/; 45 | } 46 | 47 | /** 48 | * Returns the resulting function of evaluating and invoking the 49 | * `${module}.${export}` expression with the options associated with 50 | * this instance. 51 | * @param {[type]} expr Test expression 52 | * @param {Object} extend Additional options 53 | * @returns {Object} the instance 54 | */ 55 | AbstractNpmRegistry.prototype.it = function (expr, extend) { 56 | var opts = this.opts; 57 | var match; 58 | 59 | if ((match === this.parse.exec(expr))) { 60 | const basefile = match[1]; 61 | const method = match[2]; 62 | const fullpath = path.resolve(this.rootd, basefile); 63 | 64 | if (extend) { 65 | // 66 | // Remark: we DO NOT WANT to remember `extend` here 67 | // since the calls to `it` may be mutually exclusive. 68 | // 69 | opts = Object.assign({}, this.opts, extend); 70 | } 71 | 72 | debug('require %s', fullpath); 73 | const suite = require(fullpath); 74 | debug('invoke %s.%s(opts)', basefile, method); 75 | suite[method].it(opts); 76 | } 77 | 78 | return this; 79 | }; 80 | 81 | /** 82 | * Schedules all of the specified suites specified in the 83 | * shallow merge of `extend` and `this.opts` invoking the 84 | * optional callback or exiting the process. 85 | * @param {Object} extend Additional options 86 | * @param {Function} callback Function to execute after process completes 87 | * @returns {Object} the instance 88 | */ 89 | AbstractNpmRegistry.prototype.run = function (extend, callback) { 90 | if (!callback && typeof extend === 'function') { 91 | callback = extend; 92 | extend = null; 93 | } 94 | 95 | if (extend) { 96 | this.opts = Object.assign(this.opts, extend); 97 | } 98 | 99 | debug('run suite %s', JSON.stringify(this.opts, null, 2)); 100 | require('./lib/schedule')(this.opts) 101 | .run(callback || function (code) { 102 | process.on('exit', function () { process.exit(code); }); 103 | }); 104 | 105 | return this; 106 | }; 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abstract-npm-registry 2 | 3 | An open and extendible test suite for you can use to test various functional areas of an npm registry. 4 | 5 | ## Motivation 6 | 7 | Understanding the wire protocol expected by the npm CLI is incredibly important. Without a thorough, accurate, and open representation of this HTTP-based API a number of important scenarios to the success of the Node.js ecosystem are largely impossible and definitely improbable: 8 | 9 | - Interoperability between registries (e.g. [migrating between two private registries][registry-migrate]). 10 | - Evaluation of local developer solutions (e.g. _"Should I use [sinopia] or [local-npm]?"_). 11 | - More sophisticated developer tooling built on top of `npm` (e.g. a remote `npm` **post-publish** hook similar to a `git` post-commit hook). 12 | 13 | This project is an attempt to document the public `npm` wire protocol for these reasons and more by creating an open and extendible test suite for anyone to use and contribute to. It pulls data from multiple sources: 14 | 15 | 1. [npm/npm-registry-couchapp]: technically "deprecated", but largely the most accurate representation of the the public npm API. 16 | 2. [npm/npm-registry-client]: all references to `url.resolve` represent one or more routes that `Client` instances consume when used by the `npm` CLI. 17 | 3. [npm/newwww] and [npm/public-api]: a loosely coupled set of internal APIs that have added to the original API exposed in [npm/npm-registry-couchapp]. 18 | 19 | ### Status & Completeness 20 | 21 | The goal of this project is to have 100% coverage over all routes and important usage scenarios (e.g. attempting to publish a package that is not yours). We cannot do this without **YOUR HELP!** 22 | 23 | - [x] pkg/show 24 | - [x] ping 25 | - [ ] whoami _(partial)_ 26 | - [ ] pkg/dist-tag _(partial)_ 27 | - [ ] pkg/fetch 28 | - [ ] publish 29 | - [ ] unpublish 30 | - [ ] user/add 31 | - [ ] user/logout 32 | - [ ] pkg/update 33 | - [ ] team 34 | - [ ] access 35 | - [ ] views/all 36 | - [ ] views/query 37 | 38 | ## Usage 39 | 40 | `abstract-npm-registry` uses `mocha` and `assume` for test execution and assertion. Most common configurations can be accomplished by using the micro-runner provided by `abstract-npm-registry`. 41 | 42 | ``` js 43 | const abstractNpmRegistry = require('abstract-npm-registry'); 44 | 45 | // 46 | // Runs the entire suite of tests 47 | // 48 | abstractNpmRegistry({ 49 | registry: 'https://registry.npmjs.org', 50 | headers: { 51 | 'X-ANY-HEADER-YOU-WANT': true 52 | }, 53 | // 54 | // By default all of these suites are 55 | // included. 56 | // 57 | suites: [ 58 | 'publish', 59 | 'unpublish' 60 | ] 61 | }); 62 | ``` 63 | 64 | _**n.b. By default all test suites are included**_ 65 | 66 | ``` js 67 | suites: [ 68 | 'pkg/show', 69 | 'pkg/fetch', 70 | 'publish', 71 | 'unpublish', 72 | 'pkg/dist-tag', 73 | 'user/add', 74 | 'user/logout', 75 | 'pkg/update', 76 | 'ping', 77 | 'whoami', 78 | 'team', 79 | 'access', 80 | 'views/all', 81 | 'views/query' 82 | ] 83 | ``` 84 | 85 | Want more options or more granular options? Use `abstract-npm-registry` with `mocha` directly (see below) or [open an issue!](https://github.com/warehouseai/abstract-npm-registry). 86 | 87 | ### Using with `mocha` directly 88 | 89 | Each named export on any `require`able "suite" exposed by `abstract-npm-registry` is simply **a function that returns an `it` function.** The returned function can be passed to `it` in any `mocha` suite. e.g. 90 | 91 | **my.custom.test.js** 92 | ``` js 93 | const abstractNpmRegistry = require('../')({ 94 | registry: 'https://registry.npmjs.org', 95 | headers: { 'X-ANY-HEADER-YOU-WANT': true } 96 | }); 97 | 98 | console.log('\n\n> Starting my custom test suite using mocha...'); 99 | 100 | describe('My super custom test suite', function () { 101 | abstractNpmRegistry.it('pkg/dist-tag.add'); 102 | abstractNpmRegistry.it('pkg/dist-tag.list'); 103 | abstractNpmRegistry.it('pkg/dist-tag.remove'); 104 | 105 | abstractNpmRegistry.it('pkg/fetch.found'); 106 | abstractNpmRegistry.it('pkg/fetch.noVersion'); 107 | abstractNpmRegistry.it('pkg/fetch.noPackage'); 108 | }); 109 | ``` 110 | 111 | ##### LICENSE: MIT 112 | ##### AUTHOR: [Charlie Robbins](https://github.com/indexzero) 113 | 114 | [npm/npm-registry-couchapp]: https://github.com/npm/npm-registry-couchapp/blob/master/registry/rewrites.js 115 | [npm/npm-registry-client]: https://github.com/npm/npm-registry-client/search?utf8=%E2%9C%93&q=url.resolve%28 116 | [npm/newwww]: https://github.com/npm/newww/tree/master/agents 117 | [npm/public-api]: https://github.com/npm/public-api 118 | [local-npm]: https://github.com/nolanlawson/local-npm#readme 119 | [sinopia]: https://github.com/rlidwka/sinopia#readme 120 | [registry-migrate]: https://github.com/jcrugzz/registry-migrate#readme 121 | --------------------------------------------------------------------------------