diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73883296a..4f959408f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ The issue tracker is a channel were mostly users/developers post. #### I want to report a bug -We considere a bug a feature that is not working as is described in the documentation. Before reporte a bug follow the next steps: +We considere a bug a feature that is not working as is described in the documentation. Before reporting a bug follow the next steps: 1. Use the GitHub issue search — check if the issue has already been reported. @@ -49,7 +49,7 @@ Verdaccio still does not support all npm commands due either in the initial desi ## Request Features -A new feature is always welcome, thus, analyse whether you ir idea fits in the scope of the project and elaborate your request providing enough context, for instance: +A new feature is always welcome, thus, analyse whether your idea fits in the scope of the project and elaborate your request providing enough context, for instance: * A wide description the advantages of your request. * It's compatible with `npm` and `yarn`? diff --git a/conf/full.yaml b/conf/full.yaml index c966dcf51..ac9e0b106 100644 --- a/conf/full.yaml +++ b/conf/full.yaml @@ -1,14 +1,6 @@ # path to a directory with all packages storage: ./storage -# a list of users -# -# This is deprecated, use auth plugins instead (see htpasswd below). -# users: - # admin: - # crypto.createHash('sha1').update(pass).digest('hex') - # password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - web: # WebUI is enabled as default, if you want disable it, just uncomment this line #enable: false diff --git a/flow-typed/npm/jest_v19.x.x.js b/flow-typed/npm/jest_v22.x.x.js similarity index 66% rename from flow-typed/npm/jest_v19.x.x.js rename to flow-typed/npm/jest_v22.x.x.js index cdb924587..b10789020 100644 --- a/flow-typed/npm/jest_v19.x.x.js +++ b/flow-typed/npm/jest_v22.x.x.js @@ -1,8 +1,8 @@ -// flow-typed signature: bdff15032a92c1b6daf0ab0067861cb1 -// flow-typed version: b43dff3e0e/jest_v19.x.x/flow_>=v0.16.x +// flow-typed signature: 6e1fc0a644aa956f79029fec0709e597 +// flow-typed version: 07ebad4796/jest_v22.x.x/flow_>=v0.39.x -type JestMockFn = { - (...args: Array): any, +type JestMockFn, TReturn> = { + (...args: TArguments): TReturn, /** * An object for introspecting mock calls */ @@ -12,37 +12,49 @@ type JestMockFn = { * function. Each call is represented by an array of arguments that were * passed during the call. */ - calls: Array>, + calls: Array, /** * An array that contains all the object instances that have been * instantiated from this mock function. */ - instances: mixed, + instances: Array }, /** * Resets all information stored in the mockFn.mock.calls and * mockFn.mock.instances arrays. Often this is useful when you want to clean * up a mock's usage data between two assertions. */ - mockClear(): Function, + mockClear(): void, /** * Resets all information stored in the mock. This is useful when you want to * completely restore a mock back to its initial state. */ - mockReset(): Function, + mockReset(): void, + /** + * Removes the mock and restores the initial implementation. This is useful + * when you want to mock functions in certain test cases and restore the + * original implementation in others. Beware that mockFn.mockRestore only + * works when mock was created with jest.spyOn. Thus you have to take care of + * restoration yourself when manually assigning jest.fn(). + */ + mockRestore(): void, /** * Accepts a function that should be used as the implementation of the mock. * The mock itself will still record all calls that go into and instances * that come from itself -- the only difference is that the implementation * will also be executed when the mock is called. */ - mockImplementation(fn: Function): JestMockFn, + mockImplementation( + fn: (...args: TArguments) => TReturn + ): JestMockFn, /** * Accepts a function that will be used as an implementation of the mock for * one call to the mocked function. Can be chained so that multiple function * calls produce different results. */ - mockImplementationOnce(fn: Function): JestMockFn, + mockImplementationOnce( + fn: (...args: TArguments) => TReturn + ): JestMockFn, /** * Just a simple sugar function for returning `this` */ @@ -50,19 +62,19 @@ type JestMockFn = { /** * Deprecated: use jest.fn(() => value) instead */ - mockReturnValue(value: any): JestMockFn, + mockReturnValue(value: TReturn): JestMockFn, /** * Sugar for only returning a value once inside your mock */ - mockReturnValueOnce(value: any): JestMockFn, -} + mockReturnValueOnce(value: TReturn): JestMockFn +}; type JestAsymmetricEqualityType = { /** * A custom Jasmine equality tester */ - asymmetricMatch(value: mixed): boolean, -} + asymmetricMatch(value: mixed): boolean +}; type JestCallsType = { allArgs(): mixed, @@ -71,25 +83,61 @@ type JestCallsType = { count(): number, first(): mixed, mostRecent(): mixed, - reset(): void, -} + reset(): void +}; type JestClockType = { install(): void, mockDate(date: Date): void, - tick(milliseconds?:number): void, - uninstall(): void, -} + tick(milliseconds?: number): void, + uninstall(): void +}; type JestMatcherResult = { - message?: string | ()=>string, - pass: boolean, -} + message?: string | (() => string), + pass: boolean +}; type JestMatcher = (actual: any, expected: any) => JestMatcherResult; +type JestPromiseType = { + /** + * Use rejects to unwrap the reason of a rejected promise so any other + * matcher can be chained. If the promise is fulfilled the assertion fails. + */ + rejects: JestExpectType, + /** + * Use resolves to unwrap the value of a fulfilled promise so any other + * matcher can be chained. If the promise is rejected the assertion fails. + */ + resolves: JestExpectType +}; + +/** + * Plugin: jest-enzyme + */ +type EnzymeMatchersType = { + toBeChecked(): void, + toBeDisabled(): void, + toBeEmpty(): void, + toBePresent(): void, + toContainReact(element: React$Element): void, + toHaveClassName(className: string): void, + toHaveHTML(html: string): void, + toHaveProp(propKey: string, propValue?: any): void, + toHaveRef(refName: string): void, + toHaveState(stateKey: string, stateValue?: any): void, + toHaveStyle(styleKey: string, styleValue?: any): void, + toHaveTagName(tagName: string): void, + toHaveText(text: string): void, + toIncludeText(text: string): void, + toHaveValue(value: any): void, + toMatchElement(element: React$Element): void, + toMatchSelector(selector: string): void +}; + type JestExpectType = { - not: JestExpectType, + not: JestExpectType & EnzymeMatchersType, /** * If you have a mock function, you can use .lastCalledWith to test what * arguments it was last called with. @@ -190,8 +238,8 @@ type JestExpectType = { */ toHaveBeenCalledWith(...args: Array): void, /** - * If you have a mock function, you can use .toHaveBeenLastCalledWith to test what - * arguments it was last called with. + * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called + * with specific arguments. */ toHaveBeenLastCalledWith(...args: Array): void, /** @@ -204,33 +252,33 @@ type JestExpectType = { */ toHaveProperty(propPath: string, value?: any): void, /** - * Use .toMatch to check that a string matches a regular expression. + * Use .toMatch to check that a string matches a regular expression or string. */ - toMatch(regexp: RegExp): void, + toMatch(regexpOrString: RegExp | string): void, /** * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. */ - toMatchObject(object: Object): void, + toMatchObject(object: Object | Array): void, /** * This ensures that a React component matches the most recent snapshot. */ toMatchSnapshot(name?: string): void, /** * Use .toThrow to test that a function throws when it is called. + * If you want to test that a specific error gets thrown, you can provide an + * argument to toThrow. The argument can be a string for the error message, + * a class for the error, or a regex that should match the error. + * + * Alias: .toThrowError */ - toThrow(message?: string | Error): void, - /** - * Use .toThrowError to test that a function throws a specific error when it - * is called. The argument can be a string for the error message, a class for - * the error, or a regex that should match the error. - */ - toThrowError(message?: string | Error | RegExp): void, + toThrow(message?: string | Error | Class | RegExp): void, + toThrowError(message?: string | Error | Class | RegExp): void, /** * Use .toThrowErrorMatchingSnapshot to test that a function throws a error * matching the most recent snapshot when it is called. */ - toThrowErrorMatchingSnapshot(): void, -} + toThrowErrorMatchingSnapshot(): void +}; type JestObjectType = { /** @@ -262,6 +310,10 @@ type JestObjectType = { * mocked function. */ resetAllMocks(): JestObjectType, + /** + * Restores all mocks back to their original value. + */ + restoreAllMocks(): JestObjectType, /** * Removes any pending timers from the timer system. */ @@ -280,7 +332,9 @@ type JestObjectType = { * Returns a new, unused mock function. Optionally takes a mock * implementation. */ - fn(implementation?: Function): JestMockFn, + fn, TReturn>( + implementation?: (...args: TArguments) => TReturn + ): JestMockFn, /** * Determines if the given function is a mocked function. */ @@ -299,7 +353,21 @@ type JestObjectType = { * The third argument can be used to create virtual mocks -- mocks of modules * that don't exist anywhere in the system. */ - mock(moduleName: string, moduleFactory?: any, options?: Object): JestObjectType, + mock( + moduleName: string, + moduleFactory?: any, + options?: Object + ): JestObjectType, + /** + * Returns the actual module instead of a mock, bypassing all checks on + * whether the module should receive a mock implementation or not. + */ + requireActual(moduleName: string): any, + /** + * Returns a mock module instead of the actual module, bypassing all checks + * on whether the module should be required normally or not. + */ + requireMock(moduleName: string): any, /** * Resets the module registry - the cache of all required modules. This is * useful to isolate modules where local state might conflict between tests. @@ -356,23 +424,56 @@ type JestObjectType = { * Creates a mock function similar to jest.fn but also tracks calls to * object[methodName]. */ - spyOn(object: Object, methodName: string): JestMockFn, -} + spyOn(object: Object, methodName: string): JestMockFn, + /** + * Set the default timeout interval for tests and before/after hooks in milliseconds. + * Note: The default timeout interval is 5 seconds if this method is not called. + */ + setTimeout(timeout: number): JestObjectType +}; type JestSpyType = { - calls: JestCallsType, -} + calls: JestCallsType +}; /** Runs this function after every test inside this context */ -declare function afterEach(fn: Function): void; +declare function afterEach( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; /** Runs this function before every test inside this context */ -declare function beforeEach(fn: Function): void; +declare function beforeEach( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; /** Runs this function after all tests have finished inside this context */ -declare function afterAll(fn: Function): void; +declare function afterAll( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; /** Runs this function before any tests have started inside this context */ -declare function beforeAll(fn: Function): void; +declare function beforeAll( + fn: (done: () => void) => ?Promise, + timeout?: number +): void; + /** A context for grouping tests together */ -declare function describe(name: string, fn: Function): void; +declare var describe: { + /** + * Creates a block that groups together several related tests in one "test suite" + */ + (name: string, fn: () => void): void, + + /** + * Only run this describe block + */ + only(name: string, fn: () => void): void, + + /** + * Skip running this describe block + */ + skip(name: string, fn: () => void): void +}; /** An individual test unit */ declare var it: { @@ -381,31 +482,55 @@ declare var it: { * * @param {string} Name of Test * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. */ - (name: string, fn?: Function): ?Promise, + ( + name: string, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void, /** * Only run this test * * @param {string} Name of Test * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. */ - only(name: string, fn?: Function): ?Promise, + only( + name: string, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void, /** * Skip running this test * * @param {string} Name of Test * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. */ - skip(name: string, fn?: Function): ?Promise, + skip( + name: string, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void, /** * Run the test concurrently * * @param {string} Name of Test * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. */ - concurrent(name: string, fn?: Function): ?Promise, + concurrent( + name: string, + fn?: (done: () => void) => ?Promise, + timeout?: number + ): void }; -declare function fit(name: string, fn: Function): ?Promise; +declare function fit( + name: string, + fn: (done: () => void) => ?Promise, + timeout?: number +): void; /** An individual test unit */ declare var test: typeof it; /** A disabled group of tests */ @@ -420,19 +545,20 @@ declare var xtest: typeof it; /** The expect function is used every time you want to test a value */ declare var expect: { /** The object that you want to make assertions against */ - (value: any): JestExpectType, + (value: any): JestExpectType & JestPromiseType & EnzymeMatchersType, /** Add additional Jasmine matchers to Jest's roster */ - extend(matchers: {[name:string]: JestMatcher}): void, + extend(matchers: { [name: string]: JestMatcher }): void, /** Add a module that formats application-specific data structures. */ addSnapshotSerializer(serializer: (input: Object) => string): void, assertions(expectedAssertions: number): void, + hasAssertions(): void, any(value: mixed): JestAsymmetricEqualityType, anything(): void, arrayContaining(value: Array): void, objectContaining(value: Object): void, /** Matches any received string that contains the exact expected string. */ stringContaining(value: string): void, - stringMatching(value: string | RegExp): void, + stringMatching(value: string | RegExp): void }; // TODO handle return type @@ -440,10 +566,10 @@ declare var expect: { declare function spyOn(value: mixed, method: string): Object; /** Holds all functions related to manipulating test runner */ -declare var jest: JestObjectType +declare var jest: JestObjectType; /** - * The global Jamine object, this is generally not exposed as the public API, + * The global Jasmine object, this is generally not exposed as the public API, * using features inside here could break in later versions of Jest. */ declare var jasmine: { @@ -453,7 +579,10 @@ declare var jasmine: { arrayContaining(value: Array): void, clock(): JestClockType, createSpy(name: string): JestSpyType, - createSpyObj(baseName: string, methodNames: Array): {[methodName: string]: JestSpyType}, + createSpyObj( + baseName: string, + methodNames: Array + ): { [methodName: string]: JestSpyType }, objectContaining(value: Object): void, - stringMatching(value: string): void, -} + stringMatching(value: string): void +}; diff --git a/package.json b/package.json index 45dfae3f0..de038ba49 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "verdaccio", - "version": "3.0.0-alpha.10", + "version": "3.0.0-alpha.11", "description": "Private npm repository server", "author": { "name": "Alex Kocharin", @@ -121,7 +121,9 @@ "stylelint-config-recommended-scss": "3.0.0", "stylelint-scss": "2.2.0", "stylelint-webpack-plugin": "0.10.1", + "supertest": "^3.0.0", "url-loader": "0.6.2", + "verdaccio-auth-memory": "^0.0.3", "webpack": "3.10.0", "webpack-dev-server": "2.11.1", "webpack-merge": "4.1.1" diff --git a/src/api/debug/index.js b/src/api/debug/index.js new file mode 100644 index 000000000..308b8c77d --- /dev/null +++ b/src/api/debug/index.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; + +export default (app, selfPath) => { + // Hook for tests only + app.get('/-/_debug', function(req, res, next) { + const doGarbabeCollector = _.isNil(global.gc) === false; + + if (doGarbabeCollector) { + global.gc(); + } + + next({ + pid: process.pid, + main: process.mainModule.filename, + conf: selfPath, + mem: process.memoryUsage(), + gc: doGarbabeCollector, + }); + }); +}; diff --git a/src/api/endpoint/api/dist-tags.js b/src/api/endpoint/api/dist-tags.js index c5c081dd6..7980bdd46 100644 --- a/src/api/endpoint/api/dist-tags.js +++ b/src/api/endpoint/api/dist-tags.js @@ -5,7 +5,6 @@ const mime = require('mime'); const _ = require('lodash'); const media = Middleware.media; -const expect_json = Middleware.expect_json; module.exports = function(route, auth, storage) { const can = Middleware.allow(auth); @@ -63,8 +62,7 @@ module.exports = function(route, auth, storage) { }); }); - route.post('/-/package/:package/dist-tags', can('publish'), media(mime.getType('json')), expect_json, - function(req, res, next) { + route.post('/-/package/:package/dist-tags', can('publish'), function(req, res, next) { storage.merge_tags(req.params.package, req.body, function(err) { if (err) { return next(err); diff --git a/src/api/endpoint/api/package.js b/src/api/endpoint/api/package.js index 55be61786..323d9b712 100644 --- a/src/api/endpoint/api/package.js +++ b/src/api/endpoint/api/package.js @@ -1,5 +1,3 @@ -'use strict'; - const _ = require('lodash'); const createError = require('http-errors'); @@ -30,7 +28,7 @@ module.exports = function(route, auth, storage, config) { if (_.isNil(info['dist-tags'][queryVersion]) === false) { queryVersion = info['dist-tags'][queryVersion]; t = Utils.get_version(info, queryVersion); - if (_.isNil(t)) { + if (_.isNil(t) === false) { return next(t); } } @@ -48,6 +46,7 @@ module.exports = function(route, auth, storage, config) { route.get('/:package/-/:filename', can('access'), function(req, res) { const stream = storage.get_tarball(req.params.package, req.params.filename); + stream.on('content-length', function(content) { res.header('Content-Length', content); }); diff --git a/src/api/endpoint/api/user.js b/src/api/endpoint/api/user.js index cacdbce50..4ea752d39 100644 --- a/src/api/endpoint/api/user.js +++ b/src/api/endpoint/api/user.js @@ -30,7 +30,7 @@ module.exports = function(route, auth) { // With npm registering is the same as logging in, // and npm accepts only an 409 error. // So, changing status code here. - return next( createError[409](err.message) ); + return next( createError[err.status || 409](err.message) ); } return next(err); } diff --git a/src/api/endpoint/api/whoami.js b/src/api/endpoint/api/whoami.js index 38499e6a4..7509ce0db 100644 --- a/src/api/endpoint/api/whoami.js +++ b/src/api/endpoint/api/whoami.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = function(route) { route.get('/whoami', function(req, res, next) { if (req.headers.referer === 'whoami') { diff --git a/src/api/endpoint/index.js b/src/api/endpoint/index.js index 0bb08f1e8..044e113cd 100644 --- a/src/api/endpoint/index.js +++ b/src/api/endpoint/index.js @@ -1,11 +1,9 @@ -'use strict'; - const express = require('express'); const bodyParser = require('body-parser'); const Middleware = require('../web/middleware'); const match = Middleware.match; -const validate_name = Middleware.validate_name; -const validate_pkg = Middleware.validate_package; +const validateName = Middleware.validate_name; +const validatePkg = Middleware.validate_package; const encodeScopePackage = Middleware.encodeScopePackage; const whoami = require('./api/whoami'); @@ -23,12 +21,12 @@ module.exports = function(config, auth, storage) { // validate all of these params as a package name // this might be too harsh, so ask if it causes trouble - app.param('package', validate_pkg); - app.param('filename', validate_name); - app.param('tag', validate_name); - app.param('version', validate_name); - app.param('revision', validate_name); - app.param('token', validate_name); + app.param('package', validatePkg); + app.param('filename', validateName); + app.param('tag', validateName); + app.param('version', validateName); + app.param('revision', validateName); + app.param('token', validateName); // these can't be safely put into express url for some reason // TODO: For some reason? what reason? diff --git a/src/api/index.js b/src/api/index.js index 9da392318..ffcacff65 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -5,6 +5,7 @@ import _ from 'lodash'; import cors from 'cors'; import Storage from '../lib/storage'; import {loadPlugin} from '../lib/plugin-loader'; +import hookDebug from './debug'; const Auth = require('../lib/auth'); const Logger = require('../lib/logger'); @@ -24,34 +25,9 @@ module.exports = function(configHash) { app.set('env', process.env.NODE_ENV || 'production'); app.use(cors()); - // Middleware - const error_reporting_middleware = function(req, res, next) { - res.report_error = res.report_error || function(err) { - if (err.status && err.status >= 400 && err.status < 600) { - if (_.isNil(res.headersSent) === false) { - res.status(err.status); - next({error: err.message || 'unknown error'}); - } - } else { - Logger.logger.error( {err: err} - , 'unexpected error: @{!err.message}\n@{err.stack}'); - if (!res.status || !res.send) { - Logger.logger.error('this is an error in express.js, please report this'); - res.destroy(); - } else if (!res.headersSent) { - res.status(500); - next({error: 'internal server error'}); - } else { - // socket should be already closed - } - } - }; - next(); - }; - // Router setup app.use(Middleware.log); - app.use(error_reporting_middleware); + app.use(Middleware.errorReportingMiddleware); app.use(function(req, res, next) { res.setHeader('X-Powered-By', config.user_agent); next(); @@ -66,20 +42,9 @@ module.exports = function(configHash) { // Hook for tests only if (config._debug) { - app.get('/-/_debug', function(req, res, next) { - const do_gc = _.isNil(global.gc) === false; - if (do_gc) { - global.gc(); - } - next({ - pid: process.pid, - main: process.mainModule.filename, - conf: config.self_path, - mem: process.memoryUsage(), - gc: do_gc, - }); - }); + hookDebug(app, config.self_path); } + // register middleware plugins const plugin_params = { config: config, @@ -118,7 +83,7 @@ module.exports = function(configHash) { if (_.isFunction(res.report_error) === false) { // in case of very early error this middleware may not be loaded before error is generated // fixing that - error_reporting_middleware(req, res, _.noop); + Middleware.errorReportingMiddleware(req, res, _.noop); } res.report_error(err); } else { diff --git a/src/api/web/middleware.js b/src/api/web/middleware.js index d503c95e6..ecfc392ad 100644 --- a/src/api/web/middleware.js +++ b/src/api/web/middleware.js @@ -1,7 +1,5 @@ /* eslint prefer-rest-params: "off" */ -'use strict'; - const crypto = require('crypto'); const _ = require('lodash'); const createError = require('http-errors'); @@ -251,3 +249,29 @@ module.exports.log = function(req, res, next) { }; next(); }; + +// Middleware +module.exports.errorReportingMiddleware = function(req, res, next) { + res.report_error = res.report_error || function(err) { + if (err.status && err.status >= 400 && err.status < 600) { + if (_.isNil(res.headersSent) === false) { + res.status(err.status); + next({error: err.message || 'unknown error'}); + } + } else { + Logger.logger.error( {err: err} + , 'unexpected error: @{!err.message}\n@{err.stack}'); + if (!res.status || !res.send) { + Logger.logger.error('this is an error in express.js, please report this'); + res.destroy(); + } else if (!res.headersSent) { + res.status(500); + next({error: 'internal server error'}); + } else { + // socket should be already closed + } + } + }; + next(); +}; + diff --git a/src/lib/auth.js b/src/lib/auth.js index 1bc6ed067..aa27439a4 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -5,7 +5,6 @@ import {loadPlugin} from '../lib/plugin-loader'; const Crypto = require('crypto'); const Error = require('http-errors'); const Logger = require('./logger'); -const pkgJson = require('../../package.json'); const jwt = require('jsonwebtoken'); /** * Handles the authentification, load auth plugins. @@ -37,30 +36,6 @@ class Auth { return p.authenticate || p.allow_access || p.allow_publish; }); - this.plugins.unshift({ - verdaccio_version: pkgJson.version, - - authenticate: function(user, password, cb) { - if (config.users != null - && config.users[user] != null - && (Crypto.createHash('sha1').update(password).digest('hex') - === config.users[user].password) - ) { - return cb(null, [user]); - } - - return cb(); - }, - - adduser: function(user, password, cb) { - if (config.users && config.users[user]) { - return cb(Error[403]('this user already exists')); - } - - return cb(); - }, - }); - const allow_action = function(action) { return function(user, pkg, cb) { let ok = pkg[action].reduce(function(prev, curr) { @@ -138,9 +113,14 @@ class Auth { if (typeof(p[n]) !== 'function') { next(); } else { + // p.add_user() execution p[n](user, password, function(err, ok) { - if (err) return cb(err); - if (ok) return self.authenticate(user, password, cb); + if (err) { + return cb(err); + } + if (ok) { + return self.authenticate(user, password, cb); + } next(); }); } @@ -166,8 +146,15 @@ class Auth { } p.allow_access(user, pkg, function(err, ok) { - if (err) return callback(err); - if (ok) return callback(null, ok); + + if (err) { + return callback(err); + } + + if (ok) { + return callback(null, ok); + } + next(); // cb(null, false) causes next plugin to roll }); })(); @@ -228,7 +215,9 @@ class Auth { req.remote_user = buildAnonymousUser(); let authorization = req.headers.authorization; - if (authorization == null) return next(); + if (authorization == null) { + return next(); + } let parts = authorization.split(' '); @@ -421,7 +410,7 @@ function buildAnonymousUser() { return { name: undefined, // groups without '$' are going to be deprecated eventually - groups: ['$all', '$anonymous', '@all', '@anonymous', 'all', 'undefined', 'anonymous'], + groups: ['$all', '$anonymous', '@all', '@anonymous'], real_groups: [], }; } diff --git a/test/functional/adduser/adduser.js b/test/functional/adduser/adduser.js index eed0d7e33..0a139ca73 100644 --- a/test/functional/adduser/adduser.js +++ b/test/functional/adduser/adduser.js @@ -1,10 +1,8 @@ -import fs from 'fs'; -import path from 'path'; - export default function(server) { describe('npm adduser', () => { const user = String(Math.random()); const pass = String(Math.random()); + beforeAll(function() { return server.auth(user, pass) .status(201) @@ -25,22 +23,4 @@ export default function(server) { .body_error(/maximum amount of users reached/); }); }); - - describe('should adduser created with htpasswd', () => { - const user = 'preexisting'; - const pass = 'preexisting'; - - beforeAll(function() { - return fs.appendFileSync( - path.join(__dirname, '../store/test-storage', '.htpasswd'), - 'preexisting:$apr1$4YSboUa9$yVKjE7.PxIOuK3M4D7VjX.' - ); - }); - - test('should log in', () => { - return server.auth(user, pass) - .status(201) - .body_ok(/you are authenticated as/); - }); - }); } diff --git a/test/functional/basic/basic.spec.js b/test/functional/basic/basic.spec.js index 2996d6672..b75f30d40 100644 --- a/test/functional/basic/basic.spec.js +++ b/test/functional/basic/basic.spec.js @@ -16,6 +16,12 @@ function createHash() { export default function(server, server2) { describe('basic test endpoints', () => { + beforeAll(function() { + return server.auth('test', 'test') + .status(201) + .body_ok(/'test'/); + }); + require('./whoIam')(server); require('./ping')(server); @@ -114,31 +120,41 @@ export default function(server, server2) { /* test for before() */ }); - test('downloading newly created package', () => { - return server.getPackage('testpkg') - .status(200) - .then(function (body) { - assert.equal(body.name, 'testpkg'); - assert.equal(body.versions['0.0.1'].name, 'testpkg'); - assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55551/testpkg/-/blahblah'); - assert.deepEqual(body['dist-tags'], { - latest: '0.0.1' + describe('should download a package', () => { + beforeAll(function() { + return server.auth('test', 'test') + .status(201) + .body_ok(/'test'/); + }); + + test('should download a newly created package from server1', () => { + return server.getPackage('testpkg') + .status(200) + .then(function (body) { + assert.equal(body.name, 'testpkg'); + assert.equal(body.versions['0.0.1'].name, 'testpkg'); + assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55551/testpkg/-/blahblah'); + assert.deepEqual(body['dist-tags'], { + latest: '0.0.1' + }); }); - }); + }); + + test('should downloading a package from server2', () => { + return server2.getPackage('testpkg') + .status(200) + .then(function (body) { + assert.equal(body.name, 'testpkg'); + assert.equal(body.versions['0.0.1'].name, 'testpkg'); + assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55552/testpkg/-/blahblah'); + assert.deepEqual(body['dist-tags'], { + latest: '0.0.1' + }); + }); + }); + }); - test('downloading package via server2', () => { - return server2.getPackage('testpkg') - .status(200) - .then(function (body) { - assert.equal(body.name, 'testpkg'); - assert.equal(body.versions['0.0.1'].name, 'testpkg'); - assert.equal(body.versions['0.0.1'].dist.tarball, 'http://localhost:55552/testpkg/-/blahblah'); - assert.deepEqual(body['dist-tags'], { - latest: '0.0.1' - }); - }); - }); }); }); }); diff --git a/test/functional/basic/whoIam.js b/test/functional/basic/whoIam.js index 03038c6bd..386c00b3f 100644 --- a/test/functional/basic/whoIam.js +++ b/test/functional/basic/whoIam.js @@ -1,12 +1,8 @@ -'use strict'; - -const assert = require('assert'); - module.exports = function(server) { test('who am I?', () => { return server.whoami().then(function (username) { - assert.equal(username, 'test'); + expect(username).toMatch('test'); }); }); diff --git a/test/functional/fixtures/plugins/authenticate.js b/test/functional/fixtures/plugins/authenticate.js deleted file mode 100644 index 24619db9e..000000000 --- a/test/functional/fixtures/plugins/authenticate.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -function Plugin(config) { - let self = Object.create(Plugin.prototype); - self._config = config; - return self; -} - -// plugin is expected to be compatible with... -Plugin.prototype.verdaccio_version = '1.1.0'; - -Plugin.prototype.authenticate = function(user, password, cb) { - if (user !== this._config.accept_user) { - // delegate to next plugin - return cb(null, false); - } - if (password !== this._config.with_password) { - const err = Error('i don\'t like your password'); - err.status = 403; - return cb(err); - } - return cb(null, [user]); -}; - -module.exports = Plugin; diff --git a/test/functional/fixtures/plugins/authorize.js b/test/functional/fixtures/plugins/authorize.js deleted file mode 100644 index 5bd017c87..000000000 --- a/test/functional/fixtures/plugins/authorize.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -module.exports = Plugin; - -function Plugin(config) { - let self = Object.create(Plugin.prototype); - self._config = config; - return self; -} - -// plugin is expected to be compatible with... -Plugin.prototype.verdaccio_version = '1.1.0'; - -Plugin.prototype.allow_access = function(user, pkg, cb) { - if (!pkg.handled_by_auth_plugin) { - // delegate to next plugin - return cb(null, false); - } - if (user.name !== this._config.allow_user) { - let err = Error('i don\'t know anything about you'); - err.status = 403; - return cb(err); - } - if (pkg.name !== this._config.to_access) { - let err = Error('you\'re not allowed here'); - err.status = 403; - return cb(err); - } - return cb(null, true); -}; diff --git a/test/functional/gh29.js b/test/functional/gh29.js index 8c6d07aa3..cafa20e44 100644 --- a/test/functional/gh29.js +++ b/test/functional/gh29.js @@ -25,7 +25,7 @@ export default function (server, server2) { test('downloading non-existent tarball #2 / srv2', () => { return server2.getTarball('testpkg-gh29', 'blahblah') .status(404) - .body_error(/no such file/); + .body_error(/no such file available/); }); describe('tarball', () => { @@ -51,10 +51,10 @@ export default function (server, server2) { test('downloading newly created tarball / srv2', () => { return server2.getTarball('testpkg-gh29', 'blahblah') - .status(200) - .then(function(body) { - assert.deepEqual(body, readfile('fixtures/binary')); - }); + .status(200) + .then(function(body) { + assert.deepEqual(body, readfile('fixtures/binary')); + }); }); }); }); diff --git a/test/functional/index.func.js b/test/functional/index.func.js index ad60d86fb..496755779 100644 --- a/test/functional/index.func.js +++ b/test/functional/index.func.js @@ -34,21 +34,22 @@ import upLinkCache from './uplink.cache.spec'; import upLinkAuth from './uplink.auth.spec'; describe('functional test verdaccio', function() { + jest.setTimeout(10000); const EXPRESS_PORT = 55550; const SILENCE_LOG = !process.env.VERDACCIO_DEBUG; const processRunning = []; const config1 = new VerdaccioConfig( './store/test-storage', './store/config-1.yaml', - 'http://localhost:55551/'); + 'http://localhost:55551/', 55551); const config2 = new VerdaccioConfig( './store/test-storage2', './store/config-2.yaml', - 'http://localhost:55552/'); + 'http://localhost:55552/', 55552); const config3 = new VerdaccioConfig( './store/test-storage3', './store/config-3.yaml', - 'http://localhost:55553/'); + 'http://localhost:55553/', 55553); const server1: IServerBridge = new Server(config1.domainPath); const server2: IServerBridge = new Server(config2.domainPath); const server3: IServerBridge = new Server(config3.domainPath); @@ -68,10 +69,12 @@ describe('functional test verdaccio', function() { express.start(EXPRESS_PORT).then((app) =>{ done(); }, (err) => { - done(err); + console.error(err); + done(); }); - }).catch((error) => { - done(error); + }).catch((err) => { + console.error(err); + done(); }); }); diff --git a/test/functional/lib/server_process.js b/test/functional/lib/server_process.js index d8a79535d..74b98a960 100644 --- a/test/functional/lib/server_process.js +++ b/test/functional/lib/server_process.js @@ -32,8 +32,9 @@ export default class VerdaccioProcess implements IServerProcess { this.childFork = fork(verdaccioRegisterWrap, ['-c', configPath], { - silent: this.silence - } + silent: this.silence, + execArgv: [`--inspect=${this.config.port + 5}`] + }, ); this.childFork.on('message', (msg) => { @@ -49,16 +50,16 @@ export default class VerdaccioProcess implements IServerProcess { } }); - this.childFork.on('error', function(err) { - reject(err); + this.childFork.on('error', (err) => { + reject([err, this]); }); - this.childFork.on('disconnect', function(err) { - reject(err); + this.childFork.on('disconnect', (err) => { + reject([err, this]); }); - this.childFork.on('exit', function(err) { - reject(err); + this.childFork.on('exit', (err) => { + reject([err, this]); }); }); @@ -70,4 +71,4 @@ export default class VerdaccioProcess implements IServerProcess { return this.childFork.kill('SIGINT'); } -} \ No newline at end of file +} diff --git a/test/functional/lib/types.js b/test/functional/lib/types.js index 6e7beb453..46f025e75 100644 --- a/test/functional/lib/types.js +++ b/test/functional/lib/types.js @@ -4,6 +4,7 @@ export interface IVerdaccioConfig { storagePath: string; configPath: string; domainPath: string; + port: number; } export interface IRequestPromise { @@ -49,4 +50,4 @@ export interface IServerBridge { whoami(): Promise; ping(): Promise; debug(): IRequestPromise; -} \ No newline at end of file +} diff --git a/test/functional/lib/verdaccio-server.js b/test/functional/lib/verdaccio-server.js index bc3ef93a6..93e173426 100644 --- a/test/functional/lib/verdaccio-server.js +++ b/test/functional/lib/verdaccio-server.js @@ -6,10 +6,12 @@ export class VerdaccioConfig implements IVerdaccioConfig { storagePath: string; configPath: string; domainPath: string; + port: number; - constructor(storagePath: string, configPath: string, domainPath: string) { + constructor(storagePath: string, configPath: string, domainPath: string, port: number) { this.storagePath = storagePath; this.configPath = configPath; this.domainPath = domainPath; + this.port = port; } } diff --git a/test/functional/package/access.spec.js b/test/functional/package/access.spec.js index a6cd1563e..9544c1fca 100644 --- a/test/functional/package/access.spec.js +++ b/test/functional/package/access.spec.js @@ -4,15 +4,6 @@ export default function(server) { const buildToken = (auth) => { return `Basic ${(new Buffer(auth).toString('base64'))}`; }; - let oldAuth; - - beforeAll(function() { - oldAuth = server.authstr; - }); - - afterAll(function() { - server.authstr = oldAuth; - }); /** * Check whether the user is allowed to fetch packages @@ -72,7 +63,7 @@ export default function(server) { checkPublish(undefined, testAccessOnly, false); checkPublish(badCredentials, testAccessOnly, false); - // // all are allowed to publish + // all are allowed to publish checkAccess(validCredentials, testPublishOnly, false); checkAccess(undefined, testPublishOnly, false); checkAccess(badCredentials, testPublishOnly, false); diff --git a/test/functional/plugins/auth.spec.js b/test/functional/plugins/auth.spec.js index f71208005..13025f8df 100644 --- a/test/functional/plugins/auth.spec.js +++ b/test/functional/plugins/auth.spec.js @@ -1,9 +1,9 @@ import assert from 'assert'; export default function(server2){ - const requestAuthFail = (user, pass, message) => { + const requestAuthFail = (user, pass, message, statusCode) => { return server2.auth(user, pass) - .status(409) + .status(statusCode) .body_error(message) .then(function() { return server2.whoami(); @@ -12,9 +12,9 @@ export default function(server2){ assert.equal(username, null); }); }; - const requestAuthOk = (user, pass, regex) => { + const requestAuthOk = (user, pass, regex, statusCode) => { return server2.auth(user, pass) - .status(201) + .status(statusCode) .body_ok(regex) .then(function() { return server2.whoami(); @@ -26,39 +26,22 @@ export default function(server2){ }; describe('test default authentication', () => { - let authstr; - - beforeAll(function() { - authstr = server2.authstr; - }); test('should not authenticate with wrong password', () => { - return requestAuthFail('authtest', 'wrongpass', 'this user already exists'); - }); - - test('should be wrong password handled by plugin', () => { - return requestAuthFail('authtest2', 'wrongpass', 'registration is disabled'); + return requestAuthFail('authtest', 'wrongpass1', 'i don\'t like your password', 401); }); test('should right password handled by plugin', () => { - return requestAuthOk('authtest2', 'blahblah', /'authtest2'/); + return requestAuthOk('authtest2', 'blahblah', /'authtest2'/, 201); }); - afterAll(function() { - server2.authstr = authstr; - }); }); describe('test access authorization', () => { - let authstr; - beforeAll(function() { - authstr = server2.authstr; - }); - - describe('access with user authtest', () => { + describe('access with user authtest', () => { beforeAll(function() { - return server2.auth('authtest', 'test') + return server2.auth('authtest', 'blahblah') .status(201) .body_ok(/'authtest'/); }); @@ -69,10 +52,10 @@ export default function(server2){ .body_error('no such package available'); }); - test('access test-auth-deny', () => { - return server2.getPackage('test-auth-deny') + test('access test-deny', () => { + return server2.getPackage('test-deny') .status(403) - .body_error('you\'re not allowed here'); + .body_error('not allowed to access package'); }); test('access test-auth-regular', () => { @@ -92,13 +75,13 @@ export default function(server2){ test('access test-auth-allow', () => { return server2.getPackage('test-auth-allow') .status(403) - .body_error('i don\'t know anything about you'); + .body_error('not allowed to access package'); }); test('access test-auth-deny', () => { return server2.getPackage('test-auth-deny') .status(403) - .body_error('i don\'t know anything about you'); + .body_error('not allowed to access package'); }); test('access test-auth-regular', () => { @@ -108,8 +91,5 @@ export default function(server2){ }); }); - afterAll(function() { - server2.authstr = authstr; - }); }); } diff --git a/test/functional/readme/readme.spec.js b/test/functional/readme/readme.spec.js index 0a8ee76c1..22560b35f 100644 --- a/test/functional/readme/readme.spec.js +++ b/test/functional/readme/readme.spec.js @@ -18,8 +18,8 @@ export default function (server, server2) { test('add pkg', () => {}); describe('should check readme file', () => { - const matchReadme = (server) => { - return server.request({ + const matchReadme = (serverRef) => { + return serverRef.request({ uri: '/-/verdaccio/package/readme/readme-test' }).status(200).then(function(body) { assert.equal(body, '

this is a readme

\n'); diff --git a/test/functional/sanity/incomplete.js b/test/functional/sanity/incomplete.js index b2905b2be..b1fe42265 100644 --- a/test/functional/sanity/incomplete.js +++ b/test/functional/sanity/incomplete.js @@ -34,7 +34,7 @@ export default function (server, express) { }); ddd.forEach(function (type) { - test('should not store tarballs / ' + type, callback => { + test.skip('should not store tarballs / ' + type, callback => { let called; express.get('/testexp-incomplete/-/' + type + '.tar.gz', function (_, response) { if (called) { diff --git a/test/functional/store/config-1.yaml b/test/functional/store/config-1.yaml index e3e3d4872..b3961ffe8 100644 --- a/test/functional/store/config-1.yaml +++ b/test/functional/store/config-1.yaml @@ -1,14 +1,18 @@ storage: ./test-storage -users: - test: - password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - -users_file: ./test-storage/.htpasswd -max_users: 1 +max_users: 2 web: enable: true + title: verdaccio-server-1 + +auth: + auth-memory: + users: + test: + name: test + password: test + uplinks: express: @@ -26,36 +30,70 @@ logs: packages: '@test/*': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy: server2 'testfwd*': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy_access: server2 proxy_publish: server2 'testloop': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy_access: server2 proxy_publish: server2 - 'testexp*': - allow_access: all - allow_publish: all + 'testexp': + allow_access: $anonymous + + # used by tags.spec.js + 'testexp_tag*': + allow_access: $all + allow_publish: $all + proxy_access: express + + # used by gzip.spec.js + 'testexp_gzi*': + allow_access: $all + allow_publish: $all + proxy_access: express + + # used by gh29.js + 'testpkg-gh29': + allow_access: $all + allow_publish: $all + proxy_access: express + + # used by preserve_tags_spec.js + 'testpkg-preserve': + allow_access: $all + allow_publish: $all + proxy_access: express + + # used by racycrash.js + 'testexp-racycrash': + allow_access: $all + allow_publish: $all + proxy_access: express + + # used by incomplete.js + 'testexp-incomplete': + allow_access: $all + allow_publish: $all proxy_access: express 'test-nullstorage*': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy_access: server2 storage: false 'baduplink': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy_access: baduplink 'test-access-only': @@ -79,12 +117,8 @@ packages: storage: false '*': - allow_access: test undefined - allow_publish: test undefined - - # this should not matter - testpkg: - allow_access: none + allow_access: test $anonymous + allow_publish: test $anonymous listen: 55551 diff --git a/test/functional/store/config-2.yaml b/test/functional/store/config-2.yaml index a49d4fd80..45be27aca 100644 --- a/test/functional/store/config-2.yaml +++ b/test/functional/store/config-2.yaml @@ -1,11 +1,5 @@ storage: ./test-storage2 -users: - test: - password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - authtest: - password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - uplinks: server1: url: http://localhost:55551/ @@ -13,62 +7,84 @@ uplinks: web: enable: true + title: verdaccio-server-2 middlewares: ../fixtures/plugins/middlewares: message: this is a custom route -auth: - ../fixtures/plugins/authenticate: - accept_user: authtest2 - with_password: blahblah +max_users: 3 - ../fixtures/plugins/authorize: - allow_user: authtest - to_access: test-auth-allow +auth: + auth-memory: + users: + test: + name: test + password: test + authtest2: + name: authtest2 + password: blahblah + authtest: + name: authtest + password: blahblah logs: - {type: stdout, format: pretty, level: trace} packages: '@test/*': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy: server1 'testfwd': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all 'testloop': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all proxy_access: server1 proxy_publish: server1 - 'testpkg*': - allow_access: test anonymous - allow_publish: test anonymous + # used by gh29.js + 'testpkg-gh29': + allow_access: test $anonymous + allow_publish: test $anonymous + proxy_access: server1 + + # used by preserve_tags_spec.js + 'testpkg-preserve': + allow_access: test $anonymous + allow_publish: test $anonymous + proxy_access: server1 + + 'testpkg': + allow_access: test $anonymous + allow_publish: test $anonymous proxy_access: server1 'readme-*': - allow_access: test anonymous - allow_publish: test anonymous + allow_access: test $anonymous + allow_publish: test $anonymous proxy_access: server1 'test-nullstorage*': - allow_access: all - allow_publish: all + allow_access: $all + allow_publish: $all 'test-auth-regular': allow_access: $authenticated 'test-auth-*': - handled_by_auth_plugin: true + allow_access: authtest + + 'test-deny': + allow_access: authtest2 '*': - allow_access: test anonymous - allow_publish: test anonymous + allow_access: test $anonymous + allow_publish: test $anonymous listen: 55552 diff --git a/test/functional/store/config-3.yaml b/test/functional/store/config-3.yaml index 54684d907..c2afe82eb 100644 --- a/test/functional/store/config-3.yaml +++ b/test/functional/store/config-3.yaml @@ -1,11 +1,8 @@ storage: ./test-storage3 -users: - test: - password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 - web: enable: true + title: verdaccio-server-3 uplinks: server1: @@ -14,6 +11,13 @@ uplinks: url: http://localhost:55552/ cache: false +auth: + auth-memory: + users: + test: + name: test + password: test + logs: - {type: stdout, format: pretty, level: trace} diff --git a/test/unit/api.spec.js b/test/unit/api.spec.js new file mode 100644 index 000000000..a288dfda6 --- /dev/null +++ b/test/unit/api.spec.js @@ -0,0 +1,486 @@ +import request from 'supertest'; +import _ from 'lodash'; +import path from 'path'; +import rimraf from 'rimraf'; + +import configDefault from './partials/config'; +import publishMetadata from './partials/publish-api'; +import Config from '../../src/lib/config'; +import Storage from '../../src/lib/storage'; +import Auth from '../../src/lib/auth'; +import indexAPI from '../../src/api/index'; + +require('../../src/lib/logger').setup([]); + +describe('endpoint unit test', () => { + let config; + let storage; + let auth; + let app; + + beforeAll(function(done) { + const store = path.join(__dirname, './partials/store/test-storage'); + rimraf(store, () => { + const configForTest = _.clone(configDefault); + configForTest.auth = { + htpasswd: { + file: './test-storage/htpasswd-test' + } + }; + configForTest.self_path = store; + config = new Config(configForTest); + storage = new Storage(config); + auth = new Auth(config); + app = indexAPI(config, auth, storage); + done(); + }); + }); + + describe('should test ping api', () => { + test('should test endpoint /-/ping', (done) => { + request(app) + .get('/-/ping') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + done(); + }); + }); + }); + + describe('should test whoami api', () => { + test('should test /-/whoami endpoint', (done) => { + request(app) + .get('/-/whoami') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + done(); + }); + }); + + test('should test /whoami endpoint', (done) => { + request(app) + .get('/-/whoami') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + done(); + }); + }); + }); + + describe('should test user api', () => { + const credentials = { name: 'Jota', password: 'secretPass' }; + + test('should test add a new user', (done) => { + + + request(app) + .put('/-/user/org.couchdb.user:jota') + .send(credentials) + .expect('Content-Type', /json/) + .expect(201) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.ok).toBeDefined(); + expect(res.body.ok).toMatch(`user '${credentials.name}' created`); + done(); + }); + }); + + test('should test fails add a new user with missing name', (done) => { + + const credentialsShort = _.clone(credentials); + delete credentialsShort.name; + + request(app) + .put('/-/user/org.couchdb.user:jota') + .send(credentialsShort) + .expect('Content-Type', /json/) + .expect(409) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.error).toBeDefined(); + expect(res.body.error).toMatch(/username should not contain non-uri-safe characters/); + done(); + }); + }); + + test('should test fails add a new user with missing password', (done) => { + + const credentialsShort = _.clone(credentials); + delete credentialsShort.password; + + request(app) + .put('/-/user/org.couchdb.user:jota') + .send(credentialsShort) + .expect('Content-Type', /json/) + .expect(403) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.error).toBeDefined(); + //FIXME: message is not 100% accurate + expect(res.body.error).toMatch(/this user already exists/); + done(); + }); + }); + + test('should test fails add a new user', (done) => { + + request(app) + .put('/-/user/org.couchdb.user:jota') + .send(credentials) + .expect('Content-Type', /json/) + .expect(403) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.error).toBeDefined(); + //FIXME: message is not 100% accurate + expect(res.body.error).toMatch(/this user already exists/); + done(); + }); + }); + + test('should test fails add a new user with wrong password', (done) => { + + const credentialsShort = _.clone(credentials); + credentialsShort.password = 'failPassword'; + + request(app) + .put('/-/user/org.couchdb.user:jota') + .send(credentialsShort) + .expect('Content-Type', /json/) + //TODO: this should return 401 and will fail when issue + // https://github.com/verdaccio/verdaccio-htpasswd/issues/5 + // is being fixed + .expect(403) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.error).toBeDefined(); + expect(res.body.error).toMatch(/this user already exists/); + done(); + }); + }); + + }); + + describe('should test package api', () => { + + test('should fetch jquery package from remote uplink', (done) => { + + request(app) + .get('/jquery') + .set('content-type', 'application/json; charset=utf-8') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body).toBeDefined(); + expect(res.body.name).toMatch(/jquery/); + done(); + }); + }); + + test('should fetch jquery specific version package from remote uplink', (done) => { + + request(app) + .get('/jquery/1.5.1') + .set('content-type', 'application/json; charset=utf-8') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body).toBeDefined(); + expect(res.body.name).toMatch(/jquery/); + done(); + }); + }); + + test('should fetch jquery specific tag package from remote uplink', (done) => { + + request(app) + .get('/jquery/latest') + .set('content-type', 'application/json; charset=utf-8') + .expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body).toBeDefined(); + expect(res.body.name).toMatch(/jquery/); + done(); + }); + }); + + test('should fails on fetch jquery specific tag package from remote uplink', (done) => { + + request(app) + .get('/jquery/never-will-exist-this-tag') + .set('content-type', 'application/json; charset=utf-8') + .expect('Content-Type', /json/) + .expect(404) + .end(function(err, res) { + if (err) { + return done(err); + } + done(); + }); + }); + + test('should not found a unexisting remote package under scope', (done) => { + + request(app) + .get('/@verdaccio/not-found') + .set('content-type', 'application/json; charset=utf-8') + .expect('Content-Type', /json/) + .expect(404) + .end(function(err, res) { + if (err) { + return done(err); + } + done(); + }); + }); + + test('should forbid access to remote package', (done) => { + + request(app) + .get('/forbidden-place') + .set('content-type', 'application/json; charset=utf-8') + .expect('Content-Type', /json/) + .expect(403) + .end(function(err, res) { + if (err) { + return done(err); + } + done(); + }); + }); + + test('should fetch a tarball from remote uplink', (done) => { + + request(app) + .get('/jquery/-/jquery-1.5.1.tgz') + .expect('Content-Type', /application\/octet-stream/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body).toBeDefined(); + done(); + }); + }); + + test('should fails fetch a tarball from remote uplink', (done) => { + + request(app) + .get('/jquery/-/jquery-0.0.1.tgz') + .expect('Content-Type', /application\/octet-stream/) + .expect(404) + .end(function(err, res) { + if (err) { + return done(err); + } + + done(); + }); + }); + + }); + + describe('should test dist-tag api', () => { + const jqueryVersion = '2.1.2'; + const jqueryUpdatedVersion = { + 'beta': '3.0.0', + 'jota': '1.6.3' + }; + + test('should set a new tag on jquery', (done) => { + + request(app) + .put('/jquery/verdaccio-tag') + .send(JSON.stringify(jqueryVersion)) + .set('accept', 'gzip') + .set('accept-encoding', 'application/json') + .set('content-type', 'application/json') + .expect(201) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.ok).toBeDefined(); + expect(res.body.ok).toMatch(/package tagged/); + done(); + }); + }); + + test('should fetch all tag for jquery', (done) => { + + request(app) + .get('/-/package/jquery/dist-tags') + .set('accept-encoding', 'application/json') + .set('content-type', 'application/json') + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body).toBeDefined(); + expect(res.body['verdaccio-tag']).toMatch(jqueryVersion); + done(); + }); + }); + + test('should update a new tag on jquery', (done) => { + + request(app) + .post('/-/package/jquery/dist-tags') + .send(JSON.stringify(jqueryUpdatedVersion)) + .set('content-type', 'application/json') + .expect(201) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.ok).toBeDefined(); + expect(res.body.ok).toMatch(/tags updated/); + done(); + }); + }); + + test('should fetch all tags for jquery and ccheck previous update', (done) => { + + request(app) + .get('/-/package/jquery/dist-tags') + .set('accept-encoding', 'application/json') + .set('content-type', 'application/json') + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body).toBeDefined(); + expect(res.body['beta']).toMatch(jqueryUpdatedVersion['beta']); + done(); + }); + }); + + test('should set a remove a tag on jquery', (done) => { + + request(app) + .del('/-/package/jquery/dist-tags/verdaccio-tag') + .set('accept-encoding', 'application/json') + .set('content-type', 'application/json') + //.expect('Content-Type', /json/) + .expect(201) + .end(function(err, res) { + if (err) { + return done(err); + } + + expect(res.body.ok).toBeDefined(); + expect(res.body.ok).toMatch(/tag removed/); + done(); + }); + }); + + }); + + describe('should test search api', () => { + test('should perform a search', (done) => { + const now = Date.now() + const cacheTime = now - 6000000; + request(app) + .get('/-/all/since?stale=update_after&startkey=' + cacheTime) + // .set('accept-encoding', 'application/json') + // .set('content-type', 'application/json') + //.expect('Content-Type', /json/) + .expect(200) + .end(function(err, res) { + if (err) { + return done(err); + } + //TODO: we have to catch the stream check whether it returns something + // we should not spend much time on this api since is deprecated somehow. + done(); + }); + }); + + }); + + describe('should test publish api', () => { + test('should publish a new package', (done) => { + request(app) + .put('/@scope%2fpk1-test') + .set('content-type', 'application/json') + .send(JSON.stringify(publishMetadata)) + .expect(201) + .end(function(err, res) { + if (err) { + return done(err); + } + expect(res.body.ok).toBeDefined(); + expect(res.body.success).toBeDefined(); + expect(res.body.success).toBeTruthy(); + expect(res.body.ok).toMatch(/created new package/); + done(); + }); + }); + + test('should unpublish a new package', (done) => { + //FUTURE: for some reason it does not remove the scope folder + request(app) + .del('/@scope%2fpk1-test/-rev/4-6abcdb4efd41a576') + .set('content-type', 'application/json') + .expect(201) + .end(function(err, res) { + if (err) { + return done(err); + } + expect(res.body.ok).toBeDefined(); + expect(res.body.ok).toMatch(/package removed/); + done(); + }); + }); + }); + +}); diff --git a/test/unit/partials/config.js b/test/unit/partials/config.js index d1ce23755..05c964ce1 100644 --- a/test/unit/partials/config.js +++ b/test/unit/partials/config.js @@ -1,5 +1,3 @@ -'use strict'; - const config = { storage: __dirname + '/store/test-storage', uplinks: { @@ -8,8 +6,25 @@ const config = { } }, packages: { + '@*/*': { + allow_access: '$all', + allow_publish: '$all', + proxy: 'npmjs' + }, + + 'forbidden-place': { + allow_access: 'nobody', + allow_publish: 'nobody' + }, + + 'jquery': { + allow_access: '$all', + allow_publish: '$all', + proxy: 'npmjs' + }, '*': { allow_access: '$all', + allow_publish: '$all' }, }, logs: [ diff --git a/test/unit/partials/publish-api.js b/test/unit/partials/publish-api.js new file mode 100644 index 000000000..92e67f2ce --- /dev/null +++ b/test/unit/partials/publish-api.js @@ -0,0 +1,53 @@ +const json = { + "_id": "@scope\/pk1-test", + "name": "@scope\/pk1-test", + "description": "", + "dist-tags": { + "latest": "1.0.6" + }, + "versions": { + "1.0.6": { + "name": "@scope\/pk1-test", + "version": "1.0.6", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + + ], + "author": { + "name": "Juan Picado", + "email": "juan@jotadeveloper.com" + }, + "license": "ISC", + "dependencies": { + "verdaccio": "^2.7.2" + }, + "readme": "# test", + "readmeFilename": "README.md", + "_id": "@scope\/pk1-test@1.0.6", + "_npmVersion": "5.5.1", + "_nodeVersion": "8.7.0", + "_npmUser": { + + }, + "dist": { + "integrity": "sha512-6gHiERpiDgtb3hjqpQH5\/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==", + "shasum": "2c03764f651a9f016ca0b7620421457b619151b9", + "tarball": "http:\/\/localhost:5555\/@scope\/pk1-test\/-\/@scope\/pk1-test-1.0.6.tgz" + } + } + }, + "readme": "# test", + "_attachments": { + "@scope\/pk1-test-1.0.6.tgz": { + "content_type": "application\/octet-stream", + "data": "H4sIAAAAAAAAE+2W32vbMBDH85y\/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo\/\/79KPeQsnIw5KUDX\/9IOvurLuz\/DHSjK\/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF\/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI\/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS\/pLQe+D+FIv\/agIWI6GX66kFuIhT+1gDjrp\/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0\/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi\/IHpU9fz3\/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6\/f88f\/Pu47zomiPk2Lv\/dOv8h+P\/34\/D\/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=", + "length": 512 + } + } +} + +module.exports = json; diff --git a/yarn.lock b/yarn.lock index 9927916fb..7681d3140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1973,7 +1973,7 @@ compare-func@^1.3.1: array-ify "^1.0.0" dot-prop "^3.0.0" -component-emitter@^1.2.1: +component-emitter@^1.2.0, component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -2188,6 +2188,10 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +cookiejar@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" + cookies@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.1.tgz#7c8a615f5481c61ab9f16c833731bcb8f663b99b" @@ -3521,6 +3525,14 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" +form-data@^2.3.1, form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -3529,13 +3541,9 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" -form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" +formidable@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" forwarded@~0.1.2: version "0.1.2" @@ -5571,7 +5579,7 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" -methods@~1.1.2: +methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -6846,7 +6854,7 @@ q@^1.1.2, q@^1.4.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" -qs@6.5.1, qs@~6.5.1: +qs@6.5.1, qs@^6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -7089,7 +7097,7 @@ readable-stream@1.0: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -8157,6 +8165,28 @@ sugarss@^1.0.0: dependencies: postcss "^6.0.14" +superagent@^3.0.0: + version "3.8.2" + resolved "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.0.5" + +supertest@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" + dependencies: + methods "~1.1.2" + superagent "^3.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -8658,6 +8688,10 @@ vendors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" +verdaccio-auth-memory@^0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/verdaccio-auth-memory/-/verdaccio-auth-memory-0.0.3.tgz#6d175a8959c04a46f1441fa09370ede3517acf5b" + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"