0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

test: Increase coverage for unit test (#974)

* test(utils): add test for validate names

* test(utils): add unit test for dist-tags normalize utility

* refactor(notifications):  unit test for notifications

* test(cli): add unit test for address validation

* chore: add new constants

* chore: ignore debug from coverage

* test(bootstrap): test https is fails on start

* refactor: update code for rebase
This commit is contained in:
Juan Picado @jotadeveloper 2018-09-22 12:54:21 +02:00 committed by GitHub
parent 7e78209474
commit cf31982127
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 6633 additions and 6224 deletions

View file

@ -1,5 +1,5 @@
// flow-typed signature: 4cacceffd326bb118e4a3c1b4d629e98 // flow-typed signature: f5a484315a3dea13d273645306e4076a
// flow-typed version: e737b9832f/jest_v23.x.x/flow_>=v0.39.x // flow-typed version: 7c5d14b3d4/jest_v23.x.x/flow_>=v0.39.x
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = { type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
(...args: TArguments): TReturn, (...args: TArguments): TReturn,
@ -65,14 +65,14 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
*/ */
mockReturnThis(): void, mockReturnThis(): void,
/** /**
* Deprecated: use jest.fn(() => value) instead * Accepts a value that will be returned whenever the mock function is called.
*/ */
mockReturnValue(value: TReturn): JestMockFn<TArguments, TReturn>, mockReturnValue(value: TReturn): JestMockFn<TArguments, TReturn>,
/** /**
* Sugar for only returning a value once inside your mock * Sugar for only returning a value once inside your mock
*/ */
mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>, mockReturnValueOnce(value: TReturn): JestMockFn<TArguments, TReturn>,
/** /**
* Sugar for jest.fn().mockImplementation(() => Promise.resolve(value)) * Sugar for jest.fn().mockImplementation(() => Promise.resolve(value))
*/ */
mockResolvedValue(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>, mockResolvedValue(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>,
@ -140,6 +140,30 @@ type JestPromiseType = {
*/ */
type JestTestName = string | Function; type JestTestName = string | Function;
/**
* Plugin: jest-styled-components
*/
type JestStyledComponentsMatcherValue =
| string
| JestAsymmetricEqualityType
| RegExp
| typeof undefined;
type JestStyledComponentsMatcherOptions = {
media?: string;
modifier?: string;
supports?: string;
}
type JestStyledComponentsMatchersType = {
toHaveStyleRule(
property: string,
value: JestStyledComponentsMatcherValue,
options?: JestStyledComponentsMatcherOptions
): void,
};
/** /**
* Plugin: jest-enzyme * Plugin: jest-enzyme
*/ */
@ -500,7 +524,13 @@ type JestExtendedMatchersType = {
}; };
interface JestExpectType { interface JestExpectType {
not: JestExpectType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestExtendedMatchersType, not:
& JestExpectType
& EnzymeMatchersType
& DomTestingLibraryType
& JestJQueryMatchersType
& JestStyledComponentsMatchersType
& JestExtendedMatchersType,
/** /**
* If you have a mock function, you can use .lastCalledWith to test what * If you have a mock function, you can use .lastCalledWith to test what
* arguments it was last called with. * arguments it was last called with.
@ -664,6 +694,9 @@ interface JestExpectType {
* This ensures that an Object matches the most recent snapshot. * This ensures that an Object matches the most recent snapshot.
*/ */
toMatchSnapshot(name: string): void, toMatchSnapshot(name: string): void,
toMatchInlineSnapshot(snapshot?: string): void,
toMatchInlineSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, snapshot?: string): void,
/** /**
* Use .toThrow to test that a function throws when it is called. * 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 * If you want to test that a specific error gets thrown, you can provide an
@ -678,7 +711,8 @@ interface JestExpectType {
* Use .toThrowErrorMatchingSnapshot to test that a function throws a error * Use .toThrowErrorMatchingSnapshot to test that a function throws a error
* matching the most recent snapshot when it is called. * matching the most recent snapshot when it is called.
*/ */
toThrowErrorMatchingSnapshot(): void toThrowErrorMatchingSnapshot(): void,
toThrowErrorMatchingInlineSnapshot(snapshot?: string): void,
} }
type JestObjectType = { type JestObjectType = {
@ -897,6 +931,17 @@ declare var it: {
fn?: (done: () => void) => ?Promise<mixed>, fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void, ): void,
/**
* each runs this test against array of argument arrays per each run
*
* @param {table} table of Test
*/
each(
table: Array<Array<mixed>>
): (
name: JestTestName,
fn?: (...args: Array<any>) => ?Promise<mixed>
) => void,
/** /**
* Only run this test * Only run this test
* *
@ -908,7 +953,14 @@ declare var it: {
name: JestTestName, name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>, fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void, ): {
each(
table: Array<Array<mixed>>
): (
name: JestTestName,
fn?: (...args: Array<any>) => ?Promise<mixed>
) => void,
},
/** /**
* Skip running this test * Skip running this test
* *
@ -999,7 +1051,15 @@ type JestPrettyFormatPlugins = Array<JestPrettyFormatPlugin>;
/** The expect function is used every time you want to test a value */ /** The expect function is used every time you want to test a value */
declare var expect: { declare var expect: {
/** The object that you want to make assertions against */ /** The object that you want to make assertions against */
(value: any): JestExpectType & JestPromiseType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & JestExtendedMatchersType, (value: any):
& JestExpectType
& JestPromiseType
& EnzymeMatchersType
& DomTestingLibraryType
& JestJQueryMatchersType
& JestStyledComponentsMatchersType
& JestExtendedMatchersType,
/** Add additional Jasmine matchers to Jest's roster */ /** 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. */ /** Add a module that formats application-specific data structures. */

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,7 @@ module.exports = {
coveragePathIgnorePatterns: [ coveragePathIgnorePatterns: [
'node_modules', 'node_modules',
'fixtures', 'fixtures',
'<rootDir>/src/api/debug',
'<rootDir>/test', '<rootDir>/test',
], ],
moduleNameMapper: { moduleNameMapper: {

View file

@ -1,6 +1,7 @@
// @flow // @flow
import _ from 'lodash'; import _ from 'lodash';
import { import {
validateName as utilValidateName, validateName as utilValidateName,
validatePackage as utilValidatePackage, validatePackage as utilValidatePackage,
@ -115,7 +116,7 @@ export function allow(auth: IAuth) {
} else { } else {
// last plugin (that's our built-in one) returns either // last plugin (that's our built-in one) returns either
// cb(err) or cb(null, true), so this should never happen // cb(err) or cb(null, true), so this should never happen
throw ErrorCode.getInternalError('bug in the auth plugin system'); throw ErrorCode.getInternalError(API_ERROR.PLUGIN_ERROR);
} }
}); });
}; };
@ -123,15 +124,15 @@ export function allow(auth: IAuth) {
} }
export function final(body: any, req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) { export function final(body: any, req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader('WWW-Authenticate')) { if (res.statusCode === HTTP_STATUS.UNAUTHORIZED && !res.getHeader(HEADERS.WWW_AUTH)) {
// they say it's required for 401, so... // they say it's required for 401, so...
res.header('WWW-Authenticate', `${TOKEN_BASIC}, ${TOKEN_BEARER}`); res.header(HEADERS.WWW_AUTH, `${TOKEN_BASIC}, ${TOKEN_BEARER}`);
} }
try { try {
if (_.isString(body) || _.isObject(body)) { if (_.isString(body) || _.isObject(body)) {
if (!res.getHeader('Content-type')) { if (!res.getHeader(HEADERS.CONTENT_TYPE)) {
res.header('Content-type', HEADERS.JSON); res.header(HEADERS.CONTENT_TYPE, HEADERS.JSON);
} }
if (typeof(body) === 'object' && _.isNil(body) === false) { if (typeof(body) === 'object' && _.isNil(body) === false) {
@ -143,7 +144,7 @@ export function allow(auth: IAuth) {
// don't send etags with errors // don't send etags with errors
if (!res.statusCode || (res.statusCode >= 200 && res.statusCode < 300)) { if (!res.statusCode || (res.statusCode >= 200 && res.statusCode < 300)) {
res.header('ETag', '"' + stringToMD5(body) + '"'); res.header(HEADERS.ETAG, '"' + stringToMD5(body) + '"');
} }
} else { } else {
// send(null), send(204), etc. // send(null), send(204), etc.

68
src/lib/bootstrap.js vendored
View file

@ -1,7 +1,6 @@
// @flow // @flow
import {assign, isObject, isFunction} from 'lodash'; import {assign, isObject, isFunction} from 'lodash';
import Path from 'path';
import URL from 'url'; import URL from 'url';
import fs from 'fs'; import fs from 'fs';
import http from 'http'; import http from 'http';
@ -9,53 +8,14 @@ import https from 'https';
// $FlowFixMe // $FlowFixMe
import constants from 'constants'; import constants from 'constants';
import endPointAPI from '../api/index'; import endPointAPI from '../api/index';
import {parseAddress} from './utils'; import {getListListenAddresses, resolveConfigPath} from './cli/utils';
import {API_ERROR, certPem, csrPem, keyPem} from './constants';
import type {Callback} from '@verdaccio/types'; import type {Callback} from '@verdaccio/types';
import type {$Application} from 'express'; import type {$Application} from 'express';
import {DEFAULT_PORT} from './constants';
const logger = require('./logger'); const logger = require('./logger');
/**
* Retrieve all addresses defined in the config file.
* Verdaccio is able to listen multiple ports
* @param {String} argListen
* @param {String} configListen
* eg:
* listen:
- localhost:5555
- localhost:5557
@return {Array}
*/
export function getListListenAddresses(argListen: string, configListen: mixed) {
// command line || config file || default
let addresses;
if (argListen) {
addresses = [argListen];
} else if (Array.isArray(configListen)) {
addresses = configListen;
} else if (configListen) {
addresses = [configListen];
} else {
addresses = [DEFAULT_PORT];
}
addresses = addresses.map(function(addr) {
const parsedAddr = parseAddress(addr);
if (!parsedAddr) {
logger.logger.warn({addr: addr},
'invalid address - @{addr}, we expect a port (e.g. "4873"),'
+ ' host:port (e.g. "localhost:4873") or full url'
+ ' (e.g. "http://localhost:4873/")');
}
return parsedAddr;
}).filter(Boolean);
return addresses;
}
/** /**
* Trigger the server after configuration has been loaded. * Trigger the server after configuration has been loaded.
* @param {Object} config * @param {Object} config
@ -71,7 +31,7 @@ function startVerdaccio(config: any,
pkgName: string, pkgName: string,
callback: Callback) { callback: Callback) {
if (isObject(config) === false) { if (isObject(config) === false) {
throw new Error('config file must be an object'); throw new Error(API_ERROR.CONFIG_BAD_FORMAT);
} }
endPointAPI(config).then((app)=> { endPointAPI(config).then((app)=> {
@ -82,7 +42,7 @@ function startVerdaccio(config: any,
if (addr.proto === 'https') { if (addr.proto === 'https') {
// https must either have key cert and ca or a pfx and (optionally) a passphrase // https must either have key cert and ca or a pfx and (optionally) a passphrase
if (!config.https || !config.https.key || !config.https.cert || !config.https.ca) { if (!config.https || !config.https.key || !config.https.cert || !config.https.ca) {
displayHTTPSWarning(configPath); logHTTPSWarning(configPath);
} }
webServer = handleHTTPS(app, configPath, config); webServer = handleHTTPS(app, configPath, config);
@ -103,11 +63,7 @@ function unlinkAddressPath(addr) {
} }
} }
function displayHTTPSWarning(storageLocation) { function logHTTPSWarning(storageLocation) {
const resolveConfigPath = function(file) {
return Path.resolve(Path.dirname(storageLocation), file);
};
logger.logger.fatal([ logger.logger.fatal([
'You have enabled HTTPS and need to specify either ', 'You have enabled HTTPS and need to specify either ',
' "https.key", "https.cert" and "https.ca" or ', ' "https.key", "https.cert" and "https.ca" or ',
@ -116,16 +72,16 @@ function displayHTTPSWarning(storageLocation) {
'', '',
// commands are borrowed from node.js docs // commands are borrowed from node.js docs
'To quickly create self-signed certificate, use:', 'To quickly create self-signed certificate, use:',
' $ openssl genrsa -out ' + resolveConfigPath('verdaccio-key.pem') + ' 2048', ' $ openssl genrsa -out ' + resolveConfigPath(storageLocation, keyPem) + ' 2048',
' $ openssl req -new -sha256 -key ' + resolveConfigPath('verdaccio-key.pem') + ' -out ' + resolveConfigPath('verdaccio-csr.pem'), ' $ openssl req -new -sha256 -key ' + resolveConfigPath(storageLocation, keyPem) + ' -out ' + resolveConfigPath(storageLocation, csrPem),
' $ openssl x509 -req -in ' + resolveConfigPath('verdaccio-csr.pem') + ' $ openssl x509 -req -in ' + resolveConfigPath(storageLocation, csrPem) +
' -signkey ' + resolveConfigPath('verdaccio-key.pem') + ' -out ' + resolveConfigPath('verdaccio-cert.pem'), ' -signkey ' + resolveConfigPath(storageLocation, keyPem) + ' -out ' + resolveConfigPath(storageLocation, certPem),
'', '',
'And then add to config file (' + storageLocation + '):', 'And then add to config file (' + storageLocation + '):',
' https:', ' https:',
` key: ${resolveConfigPath('verdaccio-key.pem')}`, ` key: ${resolveConfigPath(storageLocation, keyPem)}`,
` cert: ${resolveConfigPath('verdaccio-cert.pem')}`, ` cert: ${resolveConfigPath(storageLocation, certPem)}`,
` ca: ${resolveConfigPath('verdaccio-csr.pem')}`, ` ca: ${resolveConfigPath(storageLocation, csrPem)}`,
].join('\n')); ].join('\n'));
process.exit(2); process.exit(2);
} }

51
src/lib/cli/utils.js Normal file
View file

@ -0,0 +1,51 @@
// @flow
import path from 'path';
import {parseAddress} from '../utils';
import {DEFAULT_PORT} from '../constants';
const logger = require('../logger');
export const resolveConfigPath = function(storageLocation: string, file: string) {
return path.resolve(path.dirname(storageLocation), file);
};
/**
* Retrieve all addresses defined in the config file.
* Verdaccio is able to listen multiple ports
* @param {String} argListen
* @param {String} configListen
* eg:
* listen:
- localhost:5555
- localhost:5557
@return {Array}
*/
export function getListListenAddresses(argListen: string, configListen: mixed) {
// command line || config file || default
let addresses;
if (argListen) {
addresses = [argListen];
} else if (Array.isArray(configListen)) {
addresses = configListen;
} else if (configListen) {
addresses = [configListen];
} else {
addresses = [DEFAULT_PORT];
}
addresses = addresses.map(function(addr) {
const parsedAddr = parseAddress(addr);
if (!parsedAddr) {
logger.logger.warn({addr: addr},
'invalid address - @{addr}, we expect a port (e.g. "4873"),'
+ ' host:port (e.g. "localhost:4873") or full url'
+ ' (e.g. "http://localhost:4873/")');
}
return parsedAddr;
}).filter(Boolean);
return addresses;
}

View file

@ -1,15 +1,24 @@
// @flow // @flow
export const DEFAULT_PORT: string = '4873'; export const DEFAULT_PORT: string = '4873';
export const DEFAULT_PROTOCOL: string = 'http';
export const DEFAULT_DOMAIN: string = 'localhost'; export const DEFAULT_DOMAIN: string = 'localhost';
export const TIME_EXPIRATION_24H: string ='24h'; export const TIME_EXPIRATION_24H: string ='24h';
export const TIME_EXPIRATION_7D: string = '7d'; export const TIME_EXPIRATION_7D: string = '7d';
export const DIST_TAGS = 'dist-tags';
export const keyPem = 'verdaccio-key.pem';
export const certPem = 'verdaccio-cert.pem';
export const csrPem = 'verdaccio-csr.pem';
export const HEADERS = { export const HEADERS = {
JSON: 'application/json', JSON: 'application/json',
CONTENT_TYPE: 'Content-type',
ETAG: 'ETag',
JSON_CHARSET: 'application/json; charset=utf-8', JSON_CHARSET: 'application/json; charset=utf-8',
OCTET_STREAM: 'application/octet-stream; charset=utf-8', OCTET_STREAM: 'application/octet-stream; charset=utf-8',
TEXT_CHARSET: 'text/plain; charset=utf-8', TEXT_CHARSET: 'text/plain; charset=utf-8',
WWW_AUTH: 'WWW-Authenticate',
GZIP: 'gzip', GZIP: 'gzip',
}; };
@ -73,8 +82,11 @@ export const API_MESSAGE = {
}; };
export const API_ERROR = { export const API_ERROR = {
PLUGIN_ERROR: 'bug in the auth plugin system',
CONFIG_BAD_FORMAT: 'config file must be an object',
BAD_USERNAME_PASSWORD: 'bad username/password, access denied', BAD_USERNAME_PASSWORD: 'bad username/password, access denied',
NO_PACKAGE: 'no such package available', NO_PACKAGE: 'no such package available',
BAD_DATA: 'bad data',
NOT_ALLOWED: 'not allowed to access package', NOT_ALLOWED: 'not allowed to access package',
INTERNAL_SERVER_ERROR: 'internal server error', INTERNAL_SERVER_ERROR: 'internal server error',
UNKNOWN_ERROR: 'unknown error', UNKNOWN_ERROR: 'unknown error',

View file

@ -1,14 +1,12 @@
import Handlebars from 'handlebars'; import Handlebars from 'handlebars';
import request from 'request';
import _ from 'lodash'; import _ from 'lodash';
import logger from './logger';
const handleNotify = function(metadata, notifyEntry, publisherInfo, publishedPackage) { import {notifyRequest} from './notify-request';
export function handleNotify(metadata, notifyEntry, publisherInfo, publishedPackage) {
let regex; let regex;
if (metadata.name && notifyEntry.packagePattern) { if (metadata.name && notifyEntry.packagePattern) {
// FUTURE: comment out due https://github.com/verdaccio/verdaccio/pull/108#issuecomment-312421052 regex = new RegExp(notifyEntry.packagePattern, notifyEntry.packagePatternFlags || '');
// regex = new RegExp(notifyEntry.packagePattern, notifyEntry.packagePatternFlags || '');
regex = new RegExp(notifyEntry.packagePattern);
if (!regex.test(metadata.name)) { if (!regex.test(metadata.name)) {
return; return;
} }
@ -49,33 +47,14 @@ const handleNotify = function(metadata, notifyEntry, publisherInfo, publishedPac
options.url = notifyEntry.endpoint; options.url = notifyEntry.endpoint;
} }
return new Promise((resolve, reject) => { return notifyRequest(options, content);
request(options, function(err, response, body) { }
if (err || response.statusCode >= 400) {
const errorMessage = _.isNil(err) ? response.body : err.message;
logger.logger.error({errorMessage}, 'notify service has thrown an error: @{errorMessage}');
reject(errorMessage); export function sendNotification(metadata, key, ...moreMedatata) {
} else {
logger.logger.info({content}, 'A notification has been shipped: @{content}');
if (_.isNil(body) === false) {
const bodyResolved = _.isNil(body) === false ? body : null;
logger.logger.debug({body}, ' body: @{body}');
return resolve(bodyResolved);
}
reject(Error('body is missing'));
}
});
});
};
function sendNotification(metadata, key, ...moreMedatata) {
return handleNotify(metadata, key, ...moreMedatata); return handleNotify(metadata, key, ...moreMedatata);
} }
const notify = function(metadata, config, ...moreMedatata) { export function notify(metadata, config, ...moreMedatata) {
if (config.notify) { if (config.notify) {
if (config.notify.content) { if (config.notify.content) {
return sendNotification(metadata, config.notify, ...moreMedatata); return sendNotification(metadata, config.notify, ...moreMedatata);
@ -86,6 +65,4 @@ const notify = function(metadata, config, ...moreMedatata) {
} }
return Promise.resolve(); return Promise.resolve();
}; }
export {notify};

View file

@ -0,0 +1,27 @@
import isNil from 'lodash/isNil';
import logger from '../logger';
import request from 'request';
import {HTTP_STATUS} from '../constants';
export function notifyRequest(options, content) {
return new Promise((resolve, reject) => {
request(options, function(err, response, body) {
if (err || response.statusCode >= HTTP_STATUS.BAD_REQUEST) {
const errorMessage = isNil(err) ? response.body : err.message;
logger.logger.error({errorMessage}, 'notify service has thrown an error: @{errorMessage}');
reject(errorMessage);
} else {
logger.logger.info({content}, 'A notification has been shipped: @{content}');
if (isNil(body) === false) {
const bodyResolved = isNil(body) === false ? body : null;
logger.logger.debug({body}, ' body: @{body}');
return resolve(bodyResolved);
}
reject(Error('body is missing'));
}
});
});
}

View file

View file

@ -14,6 +14,7 @@ import {
API_ERROR, API_ERROR,
DEFAULT_PORT, DEFAULT_PORT,
DEFAULT_DOMAIN, DEFAULT_DOMAIN,
DEFAULT_PROTOCOL,
CHARACTER_ENCODING CHARACTER_ENCODING
} from './constants'; } from './constants';
import {generateGravatarUrl} from '../utils/user'; import {generateGravatarUrl} from '../utils/user';
@ -67,17 +68,18 @@ export function validateName(name: string): boolean {
if (_.isString(name) === false) { if (_.isString(name) === false) {
return false; return false;
} }
name = name.toLowerCase();
const normalizedName: string = name.toLowerCase();
// all URL-safe characters and "@" for issue #75 // all URL-safe characters and "@" for issue #75
return !( return !(
!name.match(/^[-a-zA-Z0-9_.!~*'()@]+$/) || !normalizedName.match(/^[-a-zA-Z0-9_.!~*'()@]+$/) ||
name.charAt(0) === '.' || // ".bin", etc. normalizedName.charAt(0) === '.' || // ".bin", etc.
name.charAt(0) === '-' || // "-" is reserved by couchdb normalizedName.charAt(0) === '-' || // "-" is reserved by couchdb
name === 'node_modules' || normalizedName === 'node_modules' ||
name === '__proto__' || normalizedName === '__proto__' ||
name === 'package.json' || normalizedName === 'package.json' ||
name === 'favicon.ico' normalizedName === 'favicon.ico'
); );
} }
@ -221,7 +223,7 @@ export function tagVersion(data: Package, version: string, tag: StringValue): bo
*/ */
export function getVersion(pkg: Package, version: any) { export function getVersion(pkg: Package, version: any) {
// this condition must allow cast // this condition must allow cast
if (pkg.versions[version] != null) { if (_.isNil(pkg.versions[version]) === false) {
return pkg.versions[version]; return pkg.versions[version];
} }
@ -263,7 +265,7 @@ export function parseAddress(urlAddress: any) {
if (urlPattern) { if (urlPattern) {
return { return {
proto: urlPattern[2] || 'http', proto: urlPattern[2] || DEFAULT_PROTOCOL,
host: urlPattern[6] || urlPattern[7] || DEFAULT_DOMAIN, host: urlPattern[6] || urlPattern[7] || DEFAULT_DOMAIN,
port: urlPattern[8] || DEFAULT_PORT, port: urlPattern[8] || DEFAULT_PORT,
}; };
@ -273,7 +275,7 @@ export function parseAddress(urlAddress: any) {
if (urlPattern) { if (urlPattern) {
return { return {
proto: urlPattern[2] || 'http', proto: urlPattern[2] || DEFAULT_PROTOCOL,
path: urlPattern[4], path: urlPattern[4],
}; };
} }
@ -391,7 +393,7 @@ export const ErrorCode = {
return createError(HTTP_STATUS.CONFLICT, message); return createError(HTTP_STATUS.CONFLICT, message);
}, },
getBadData: (customMessage?: string) => { getBadData: (customMessage?: string) => {
return createError(HTTP_STATUS.BAD_DATA, customMessage || 'bad data'); return createError(HTTP_STATUS.BAD_DATA, customMessage || API_ERROR.BAD_DATA);
}, },
getBadRequest: (customMessage?: string) => { getBadRequest: (customMessage?: string) => {
return createError(HTTP_STATUS.BAD_REQUEST, customMessage); return createError(HTTP_STATUS.BAD_REQUEST, customMessage);

View file

@ -59,36 +59,40 @@ export default function(server) {
const testOnlyTest = 'test-only-test'; const testOnlyTest = 'test-only-test';
const testOnlyAuth = 'test-only-auth'; const testOnlyAuth = 'test-only-auth';
// all are allowed to access describe('all are allowed to access', () => {
checkAccess(validCredentials, testAccessOnly, true); checkAccess(validCredentials, testAccessOnly, true);
checkAccess(undefined, testAccessOnly, true); checkAccess(undefined, testAccessOnly, true);
checkAccess(badCredentials, testAccessOnly, true); checkAccess(badCredentials, testAccessOnly, true);
checkPublish(validCredentials, testAccessOnly, false); checkPublish(validCredentials, testAccessOnly, false);
checkPublish(undefined, testAccessOnly, false); checkPublish(undefined, testAccessOnly, false);
checkPublish(badCredentials, testAccessOnly, false); checkPublish(badCredentials, testAccessOnly, false);
});
// all are allowed to publish describe('all are allowed to publish', () => {
checkAccess(validCredentials, testPublishOnly, false); checkAccess(validCredentials, testPublishOnly, false);
checkAccess(undefined, testPublishOnly, false); checkAccess(undefined, testPublishOnly, false);
checkAccess(badCredentials, testPublishOnly, false); checkAccess(badCredentials, testPublishOnly, false);
checkPublish(validCredentials, testPublishOnly, true); checkPublish(validCredentials, testPublishOnly, true);
checkPublish(undefined, testPublishOnly, true); checkPublish(undefined, testPublishOnly, true);
checkPublish(badCredentials, testPublishOnly, true); checkPublish(badCredentials, testPublishOnly, true);
});
// only user "test" is allowed to publish and access describe('only user "test" is allowed to publish and access', () => {
checkAccess(validCredentials, testOnlyTest, true); checkAccess(validCredentials, testOnlyTest, true);
checkAccess(undefined, testOnlyTest, false); checkAccess(undefined, testOnlyTest, false);
checkAccess(badCredentials, testOnlyTest, false); checkAccess(badCredentials, testOnlyTest, false);
checkPublish(validCredentials, testOnlyTest, true); checkPublish(validCredentials, testOnlyTest, true);
checkPublish(undefined, testOnlyTest, false); checkPublish(undefined, testOnlyTest, false);
checkPublish(badCredentials, testOnlyTest, false); checkPublish(badCredentials, testOnlyTest, false);
});
// only authenticated users are allowed describe('only authenticated users are allowed', () => {
checkAccess(validCredentials, testOnlyAuth, true); checkAccess(validCredentials, testOnlyAuth, true);
checkAccess(undefined, testOnlyAuth, false); checkAccess(undefined, testOnlyAuth, false);
checkAccess(badCredentials, testOnlyAuth, false); checkAccess(badCredentials, testOnlyAuth, false);
checkPublish(validCredentials, testOnlyAuth, true); checkPublish(validCredentials, testOnlyAuth, true);
checkPublish(undefined, testOnlyAuth, false); checkPublish(undefined, testOnlyAuth, false);
checkPublish(badCredentials, testOnlyAuth, false); checkPublish(badCredentials, testOnlyAuth, false);
});
}); });
} }

View file

@ -2,27 +2,37 @@ import path from 'path';
import _ from 'lodash'; import _ from 'lodash';
import startServer from '../../../src/index'; import startServer from '../../../src/index';
import {getListListenAddresses} from '../../../src/lib/bootstrap';
import config from '../partials/config/index'; import config from '../partials/config/index';
import {DEFAULT_DOMAIN, DEFAULT_PORT} from '../../../src/lib/constants'; import {DEFAULT_DOMAIN, DEFAULT_PORT, DEFAULT_PROTOCOL} from '../../../src/lib/constants';
import {getListListenAddresses} from '../../../src/lib/cli/utils';
require('../../../src/lib/logger').setup([]); const logger = require('../../../src/lib/logger');
jest.mock('../../../src/lib/logger', () => ({
setup: jest.fn(),
logger: {
child: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
fatal: jest.fn()
}
}));
describe('startServer via API', () => { describe('startServer via API', () => {
describe('startServer launcher', () => { describe('startServer launcher', () => {
test('should provide all server data await/async', async (done) => { test('should provide all HTTP server data', async (done) => {
const store = path.join(__dirname, 'partials/store'); const store = path.join(__dirname, 'partials/store');
const serverName = 'verdaccio-test'; const serverName = 'verdaccio-test';
const version = '1.0.0'; const version = '1.0.0';
const port = '6000'; const port = '6000';
await startServer(config, port, store, version, serverName, await startServer(_.clone(config), port, store, version, serverName,
(webServer, addrs, pkgName, pkgVersion) => { (webServer, addrs, pkgName, pkgVersion) => {
expect(webServer).toBeDefined(); expect(webServer).toBeDefined();
expect(addrs).toBeDefined(); expect(addrs).toBeDefined();
expect(addrs.proto).toBe('http'); expect(addrs.proto).toBe(DEFAULT_PROTOCOL);
expect(addrs.host).toBe('localhost'); expect(addrs.host).toBe(DEFAULT_DOMAIN);
expect(addrs.port).toBe(port); expect(addrs.port).toBe(port);
expect(pkgName).toBeDefined(); expect(pkgName).toBeDefined();
expect(pkgVersion).toBeDefined(); expect(pkgVersion).toBeDefined();
@ -32,6 +42,28 @@ describe('startServer via API', () => {
}); });
}); });
test('should provide all HTTPS server fails', async (done) => {
const store = path.join(__dirname, 'partials/store');
const serverName = 'verdaccio-test';
const version = '1.0.0';
const address = 'https://www.domain.com:443';
const realProcess = process;
const conf = _.clone(config);
conf.https = {};
// save process to catch exist
const exitMock = jest.fn();
global.process = { ...realProcess, exit: exitMock };
await startServer(conf, address, store, version, serverName, () => {
expect(logger.logger.fatal).toBeCalled();
expect(logger.logger.fatal).toHaveBeenCalledTimes(2);
done();
});
expect(exitMock).toHaveBeenCalledWith(2);
// restore process
global.process = realProcess;
});
test('should fails if config is missing', async () => { test('should fails if config is missing', async () => {
try { try {
await startServer(); await startServer();
@ -43,27 +75,52 @@ describe('startServer via API', () => {
}); });
describe('getListListenAddresses test', () => { describe('getListListenAddresses test', () => {
test(`should return by default ${DEFAULT_PORT}`, () => {
const addrs = getListListenAddresses()[0];
expect(addrs.proto).toBe('http'); test('should return no address if a single address is wrong', () => {
const addrs = getListListenAddresses("wrong");
expect(_.isArray(addrs)).toBeTruthy();
expect(addrs).toHaveLength(0);
});
test('should return no address if a two address are wrong', () => {
const addrs = getListListenAddresses(["wrong", "same-wrong"]);
expect(_.isArray(addrs)).toBeTruthy();
expect(addrs).toHaveLength(0);
});
test('should return a list of 1 address provided', () => {
const addrs = getListListenAddresses(null, '1000');
expect(_.isArray(addrs)).toBeTruthy();
expect(addrs).toHaveLength(1);
});
test('should return a list of 2 address provided', () => {
const addrs = getListListenAddresses(null, ['1000', '2000']);
expect(_.isArray(addrs)).toBeTruthy();
expect(addrs).toHaveLength(2);
});
test(`should return by default ${DEFAULT_PORT}`, () => {
const [addrs] = getListListenAddresses();
expect(addrs.proto).toBe(DEFAULT_PROTOCOL);
expect(addrs.host).toBe(DEFAULT_DOMAIN); expect(addrs.host).toBe(DEFAULT_DOMAIN);
expect(addrs.port).toBe(DEFAULT_PORT); expect(addrs.port).toBe(DEFAULT_PORT);
}); });
test('should return a list of address and no cli argument provided', () => { test('should return default proto, host and custom port', () => {
const addrs = getListListenAddresses(null, ['1000', '2000']); const initPort = '1000';
const [addrs] = getListListenAddresses(null, initPort);
expect(_.isArray(addrs)).toBeTruthy(); expect(addrs.proto).toEqual(DEFAULT_PROTOCOL);
expect(addrs.host).toEqual(DEFAULT_DOMAIN);
expect(addrs.port).toEqual(initPort);
}); });
test('should return an address and no cli argument provided', () => {
const addrs = getListListenAddresses(null, '1000');
expect(_.isArray(addrs)).toBeTruthy();
});
}); });
}); });

View file

@ -0,0 +1,81 @@
// @flow
import {parseConfigurationFile} from '../__helper';
import {parseConfigFile} from '../../../src/lib/utils';
import {notify} from '../../../src/lib/notify';
import {notifyRequest} from '../../../src/lib/notify/notify-request';
jest.mock('./../../../src/lib/notify/notify-request', () => ({
notifyRequest: jest.fn((options, content) => Promise.resolve([options, content]))
}));
require('../../../src/lib/logger').setup([]);
const parseConfigurationNotifyFile = (name) => {
return parseConfigurationFile(`notify/${name}`);
};
const singleNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('single.notify'));
const singleHeaderNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('single.header.notify'));
const packagePatternNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('single.packagePattern.notify'));
const multiNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('multiple.notify'));
describe('Notify', () => {
beforeEach(() => {
jest.clearAllMocks();
});
//FUTURE: we should add some sort of health check of all props, (not implemented yet)
test("should not fails if config is not provided", async () => {
await notify({}, {});
expect(notifyRequest).toHaveBeenCalledTimes(0);
});
test("should send notification", async () => {
const name: string = 'package';
const response = await notify({name}, singleNotificationConfig);
const [options, content] = response;
expect(options.headers).toBeDefined();
expect(options.url).toBeDefined();
expect(options.body).toBeDefined();
expect(content).toMatch(name);
expect(response).toBeTruthy();
expect(notifyRequest).toHaveBeenCalledTimes(1);
});
test("should send single header notification", async () => {
await notify({}, singleHeaderNotificationConfig);
expect(notifyRequest).toHaveBeenCalledTimes(1);
});
test("should send multiple notification", async () => {
await notify({}, multiNotificationConfig);
expect(notifyRequest).toHaveBeenCalledTimes(3);
});
describe('packagePatternFlags', () => {
test("should send single notification with packagePatternFlags", async () => {
const name: string = 'package';
await notify({name}, packagePatternNotificationConfig);
expect(notifyRequest).toHaveBeenCalledTimes(1);
});
test("should not match on send single notification with packagePatternFlags", async () => {
const name: string = 'no-mach-name';
await notify({name}, packagePatternNotificationConfig);
expect(notifyRequest).toHaveBeenCalledTimes(0);
});
})
});

View file

@ -4,10 +4,10 @@ import { generateGravatarUrl, GRAVATAR_DEFAULT } from '../../../src/utils/user';
import { spliceURL } from '../../../src/utils/string'; import { spliceURL } from '../../../src/utils/string';
import Package from '../../../src/webui/components/Package/index'; import Package from '../../../src/webui/components/Package/index';
import { import {
validateName as validate, validateName,
convertDistRemoteToLocalTarballUrls, convertDistRemoteToLocalTarballUrls,
parseReadme, parseReadme,
addGravatarSupport addGravatarSupport, validatePackage, validateMetadata, DIST_TAGS, combineBaseUrl, getVersion, normalizeDistTags
} from '../../../src/lib/utils'; } from '../../../src/lib/utils';
import Logger, { setup } from '../../../src/lib/logger'; import Logger, { setup } from '../../../src/lib/logger';
import { readFile } from '../../functional/lib/test.utils'; import { readFile } from '../../functional/lib/test.utils';
@ -18,6 +18,218 @@ const readmeFile = (fileName: string = 'markdown.md') =>
setup([]); setup([]);
describe('Utilities', () => { describe('Utilities', () => {
const buildURI = (host, version) =>
`http://${host}/npm_test/-/npm_test-${version}.tgz`;
const fakeHost = 'fake.com';
const metadata: Package = {
name: 'npm_test',
versions: {
'1.0.0': {
dist: {
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.0.tgz'
}
},
'1.0.1': {
dist: {
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.1.tgz'
}
}
}
};
const cloneMetadata = (pkg = metadata) => Object.assign({}, pkg);
describe('API utilities', () => {
describe('convertDistRemoteToLocalTarballUrls', () => {
test('should build a URI for dist tarball based on new domain', () => {
const convertDist = convertDistRemoteToLocalTarballUrls(cloneMetadata(),
// $FlowFixMe
{
headers: {
host: fakeHost
},
get: () => 'http',
protocol: 'http'
});
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(buildURI(fakeHost, '1.0.0'));
expect(convertDist.versions['1.0.1'].dist.tarball).toEqual(buildURI(fakeHost, '1.0.1'));
});
test('should return same URI whether host is missing', () => {
const convertDist = convertDistRemoteToLocalTarballUrls(cloneMetadata(),
// $FlowFixMe
{
headers: {},
get: () => 'http',
protocol: 'http'
});
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(convertDist.versions['1.0.0'].dist.tarball);
});
});
describe('normalizeDistTags', () => {
test('should delete a invalid latest version', () => {
const pkg = cloneMetadata();
pkg[DIST_TAGS] = {
latest: '20000'
};
normalizeDistTags(pkg)
expect(Object.keys(pkg[DIST_TAGS])).toHaveLength(0);
});
test('should define last published version as latest', () => {
const pkg = cloneMetadata();
pkg[DIST_TAGS] = {};
normalizeDistTags(pkg)
expect(pkg[DIST_TAGS]).toEqual({latest: '1.0.1'});
});
test('should define last published version as latest with a custom dist-tag', () => {
const pkg = cloneMetadata();
pkg[DIST_TAGS] = {
beta: '1.0.1'
};
normalizeDistTags(pkg);
expect(pkg[DIST_TAGS]).toEqual({beta: '1.0.1', latest: '1.0.1'});
});
test('should convert any array of dist-tags to a plain string', () => {
const pkg = cloneMetadata();
pkg[DIST_TAGS] = {
latest: ['1.0.1']
};
normalizeDistTags(pkg);
expect(pkg[DIST_TAGS]).toEqual({latest: '1.0.1'});
});
});
describe('getVersion', () => {
test('should get the right version', () => {
expect(getVersion(cloneMetadata(), '1.0.0')).toEqual(metadata.versions['1.0.0']);
expect(getVersion(cloneMetadata(), 'v1.0.0')).toEqual(metadata.versions['1.0.0']);
});
test('should return nothing on get non existing version', () => {
expect(getVersion(cloneMetadata(), '0')).toBeUndefined();
expect(getVersion(cloneMetadata(), '2.0.0')).toBeUndefined();
expect(getVersion(cloneMetadata(), 'v2.0.0')).toBeUndefined();
expect(getVersion(cloneMetadata(), undefined)).toBeUndefined();
expect(getVersion(cloneMetadata(), null)).toBeUndefined();
expect(getVersion(cloneMetadata(), 2)).toBeUndefined();
})
});
describe('combineBaseUrl', () => {
test('should create a URI', () => {
expect(combineBaseUrl("http", 'domain')).toEqual('http://domain');
});
test('should create a base url for registry', () => {
expect(combineBaseUrl("http", 'domain', '/prefix/')).toEqual('http://domain/prefix');
expect(combineBaseUrl("http", 'domain', 'only-prefix')).toEqual('only-prefix');
});
});
describe('validatePackage', () => {
test('should validate package names', () => {
expect(validatePackage("package-name")).toBeTruthy();
expect(validatePackage("@scope/package-name")).toBeTruthy();
});
test('should fails on validate package names', () => {
expect(validatePackage("package-name/test/fake")).toBeFalsy();
expect(validatePackage("@/package-name")).toBeFalsy();
expect(validatePackage("$%$%#$%$#%#$%$#")).toBeFalsy();
expect(validatePackage("node_modules")).toBeFalsy();
expect(validatePackage("__proto__")).toBeFalsy();
expect(validatePackage("package.json")).toBeFalsy();
expect(validatePackage("favicon.ico")).toBeFalsy();
});
describe('validateName', () => {
test('should fails with no string', () => {
// intended to fail with flow, do not remove
// $FlowFixMe
expect(validateName(null)).toBeFalsy();
// $FlowFixMe
expect(validateName()).toBeFalsy();
});
test('good ones', () => {
expect(validateName('verdaccio')).toBeTruthy();
expect(validateName('some.weird.package-zzz')).toBeTruthy();
expect(validateName('old-package@0.1.2.tgz')).toBeTruthy();
});
test('should be valid using uppercase', () => {
expect(validateName('ETE')).toBeTruthy();
expect(validateName('JSONStream')).toBeTruthy();
});
test('should fails using package.json', () => {
expect(validateName('package.json')).toBeFalsy();
});
test('should fails with path seps', () => {
expect(validateName('some/thing')).toBeFalsy();
expect(validateName('some\\thing')).toBeFalsy();
});
test('should fail with no hidden files', () => {
expect(validateName('.bin')).toBeFalsy();
});
test('should fails with reserved words', () => {
expect(validateName('favicon.ico')).toBeFalsy();
expect(validateName('node_modules')).toBeFalsy();
expect(validateName('__proto__')).toBeFalsy();
});
test('should fails with other options', () => {
expect(validateName('pk g')).toBeFalsy();
expect(validateName('pk\tg')).toBeFalsy();
expect(validateName('pk%20g')).toBeFalsy();
expect(validateName('pk+g')).toBeFalsy();
expect(validateName('pk:g')).toBeFalsy();
});
});
});
describe('validateMetadata', () => {
test('should fills an empty metadata object', () => {
// intended to fail with flow, do not remove
// $FlowFixMe
expect(Object.keys(validateMetadata({}))).toContain(DIST_TAGS);
// $FlowFixMe
expect(Object.keys(validateMetadata({}))).toContain('versions');
// $FlowFixMe
expect(Object.keys(validateMetadata({}))).toContain('time');
});
test('should fails the assertions is not an object', () => {
expect(function ( ) {
// $FlowFixMe
validateMetadata('');
}).toThrow(assert.AssertionError);
});
test('should fails the assertions is name does not match', () => {
expect(function ( ) {
// $FlowFixMe
validateMetadata({}, "no-name");
}).toThrow(assert.AssertionError);
});
});
});
describe('String utilities', () => { describe('String utilities', () => {
test('should splice two strings and generate a url', () => { test('should splice two strings and generate a url', () => {
const url: string = spliceURL('http://domain.com', '/-/static/logo.png'); const url: string = spliceURL('http://domain.com', '/-/static/logo.png');
@ -47,90 +259,6 @@ describe('Utilities', () => {
}); });
}); });
describe('Validations', () => {
test('good ones', () => {
assert(validate('verdaccio'));
assert(validate('some.weird.package-zzz'));
assert(validate('old-package@0.1.2.tgz'));
});
test('uppercase', () => {
assert(validate('EVE'));
assert(validate('JSONStream'));
});
test('no package.json', () => {
assert(!validate('package.json'));
});
test('no path seps', () => {
assert(!validate('some/thing'));
assert(!validate('some\\thing'));
});
test('no hidden', () => {
assert(!validate('.bin'));
});
test('no reserved', () => {
assert(!validate('favicon.ico'));
assert(!validate('node_modules'));
assert(!validate('__proto__'));
});
test('other', () => {
assert(!validate('pk g'));
assert(!validate('pk\tg'));
assert(!validate('pk%20g'));
assert(!validate('pk+g'));
assert(!validate('pk:g'));
});
});
describe('Packages utilities', () => {
const metadata: Package = {
name: 'npm_test',
versions: {
'1.0.0': {
dist: {
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.0.tgz'
}
},
'1.0.1': {
dist: {
tarball: 'http://registry.org/npm_test/-/npm_test-1.0.1.tgz'
}
}
}
};
const buildURI = (host, version) =>
`http://${host}/npm_test/-/npm_test-${version}.tgz`;
const host = 'fake.com';
test('convertDistRemoteToLocalTarballUrls', () => {
const convertDist = convertDistRemoteToLocalTarballUrls(
Object.assign({}, metadata),
// $FlowFixMe
{
headers: {
host
},
get: () => 'http',
protocol: 'http'
},
''
);
expect(convertDist.versions['1.0.0'].dist.tarball).toEqual(
buildURI(host, '1.0.0')
);
expect(convertDist.versions['1.0.1'].dist.tarball).toEqual(
buildURI(host, '1.0.1')
);
});
});
describe('parseReadme', () => { describe('parseReadme', () => {
test('should parse makrdown text to html template', () => { test('should parse makrdown text to html template', () => {
const markdown = '# markdown'; const markdown = '# markdown';

View file

@ -0,0 +1,20 @@
notify:
'example-google-chat':
method: POST
headers: [{ 'Content-Type': 'application/json' }]
endpoint: https://chat.googleapis.com/v1/spaces/AAAAB_TcJYs/messages?key=myKey&token=myToken
content: '{"text":"New package published: `{{ name }}{{#each versions}} v{{version}}{{/each}}`"}'
'example-hipchat':
method: POST
headers: [{ 'Content-Type': 'application/json' }]
endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'
'example-stride':
method: POST
headers:
[
{ 'Content-Type': 'application/json' },
{ 'authorization': 'Bearer secretToken' },
]
endpoint: https://api.atlassian.com/site/{cloudId}/conversation/{conversationId}/message
content: '{"body": {"version": 1,"type": "doc","content": [{"type": "paragraph","content": [{"type": "text","text": "New package published: * {{ name }}* Publisher name: * {{ publisher.name }}"}]}]}}'

View file

@ -0,0 +1,5 @@
notify:
method: POST
headers: { 'Content-Type': 'application/json' }
endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'

View file

@ -0,0 +1,5 @@
notify:
method: POST
headers: [{ 'Content-Type': 'application/json' }]
endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'

View file

@ -0,0 +1,7 @@
notify:
method: POST
headers: { 'Content-Type': 'application/json' }
packagePattern: package
packagePatternFlags: g
endpoint: https://usagge.hipchat.com/v2/room/3729485/notification?auth_token=mySecretToken
content: '{"color":"green","message":"New package published: * {{ name }}*","notify":true,"message_format":"text"}'