1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2025-02-21 07:29:37 +01:00

Merge pull request #800 from verdaccio/refactor-config

refactor: config and utils
This commit is contained in:
Juan Picado @jotadeveloper 2018-07-16 11:48:55 +02:00 committed by GitHub
commit 45b21fa87d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1182 additions and 325 deletions

@ -31,6 +31,8 @@
}, },
"rules": { "rules": {
"no-useless-escape": 2, "no-useless-escape": 2,
"react/no-deprecated": 1,
"react/jsx-no-target-blank": 1,
"handle-callback-err": 2, "handle-callback-err": 2,
"no-fallthrough": 2, "no-fallthrough": 2,
"no-new-require": 2, "no-new-require": 2,

@ -21,10 +21,9 @@ ENV NODE_ENV=production
RUN npm config set registry http://registry.npmjs.org/ && \ RUN npm config set registry http://registry.npmjs.org/ && \
yarn global add -s flow-bin@0.69.0 && \ yarn global add -s flow-bin@0.69.0 && \
yarn install --production=false && \ yarn install --production=false && \
yarn run lint && \ yarn lint && \
yarn run code:docker-build && \ yarn code:docker-build && \
yarn run build:webui && \ yarn build:webui && \
yarn run test:unit -- --silent true --coverage false --bail && \
yarn cache clean && \ yarn cache clean && \
yarn install --production=true --pure-lockfile yarn install --production=true --pure-lockfile

@ -1,5 +1,5 @@
// flow-typed signature: 6e1fc0a644aa956f79029fec0709e597 // flow-typed signature: 4cacceffd326bb118e4a3c1b4d629e98
// flow-typed version: 07ebad4796/jest_v22.x.x/flow_>=v0.39.x // flow-typed version: e737b9832f/jest_v23.x.x/flow_>=v0.39.x
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = { type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
(...args: TArguments): TReturn, (...args: TArguments): TReturn,
@ -55,6 +55,11 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
mockImplementationOnce( mockImplementationOnce(
fn: (...args: TArguments) => TReturn fn: (...args: TArguments) => TReturn
): JestMockFn<TArguments, TReturn>, ): JestMockFn<TArguments, TReturn>,
/**
* Accepts a string to use in test result output in place of "jest.fn()" to
* indicate which mock function is being referenced.
*/
mockName(name: string): JestMockFn<TArguments, TReturn>,
/** /**
* Just a simple sugar function for returning `this` * Just a simple sugar function for returning `this`
*/ */
@ -66,7 +71,23 @@ type JestMockFn<TArguments: $ReadOnlyArray<*>, 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))
*/
mockResolvedValue(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>,
/**
* Sugar for jest.fn().mockImplementationOnce(() => Promise.resolve(value))
*/
mockResolvedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<TReturn>>,
/**
* Sugar for jest.fn().mockImplementation(() => Promise.reject(value))
*/
mockRejectedValue(value: TReturn): JestMockFn<TArguments, Promise<any>>,
/**
* Sugar for jest.fn().mockImplementationOnce(() => Promise.reject(value))
*/
mockRejectedValueOnce(value: TReturn): JestMockFn<TArguments, Promise<any>>
}; };
type JestAsymmetricEqualityType = { type JestAsymmetricEqualityType = {
@ -113,6 +134,12 @@ type JestPromiseType = {
resolves: JestExpectType resolves: JestExpectType
}; };
/**
* Jest allows functions and classes to be used as test names in test() and
* describe()
*/
type JestTestName = string | Function;
/** /**
* Plugin: jest-enzyme * Plugin: jest-enzyme
*/ */
@ -120,14 +147,16 @@ type EnzymeMatchersType = {
toBeChecked(): void, toBeChecked(): void,
toBeDisabled(): void, toBeDisabled(): void,
toBeEmpty(): void, toBeEmpty(): void,
toBeEmptyRender(): void,
toBePresent(): void, toBePresent(): void,
toContainReact(element: React$Element<any>): void, toContainReact(element: React$Element<any>): void,
toExist(): void,
toHaveClassName(className: string): void, toHaveClassName(className: string): void,
toHaveHTML(html: string): void, toHaveHTML(html: string): void,
toHaveProp(propKey: string, propValue?: any): void, toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void),
toHaveRef(refName: string): void, toHaveRef(refName: string): void,
toHaveState(stateKey: string, stateValue?: any): void, toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void),
toHaveStyle(styleKey: string, styleValue?: any): void, toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void),
toHaveTagName(tagName: string): void, toHaveTagName(tagName: string): void,
toHaveText(text: string): void, toHaveText(text: string): void,
toIncludeText(text: string): void, toIncludeText(text: string): void,
@ -136,8 +165,342 @@ type EnzymeMatchersType = {
toMatchSelector(selector: string): void toMatchSelector(selector: string): void
}; };
type JestExpectType = { // DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers
not: JestExpectType & EnzymeMatchersType, type DomTestingLibraryType = {
toBeInTheDOM(): void,
toHaveTextContent(content: string): void,
toHaveAttribute(name: string, expectedValue?: string): void
};
// Jest JQuery Matchers: https://github.com/unindented/custom-jquery-matchers
type JestJQueryMatchersType = {
toExist(): void,
toHaveLength(len: number): void,
toHaveId(id: string): void,
toHaveClass(className: string): void,
toHaveTag(tag: string): void,
toHaveAttr(key: string, val?: any): void,
toHaveProp(key: string, val?: any): void,
toHaveText(text: string | RegExp): void,
toHaveData(key: string, val?: any): void,
toHaveValue(val: any): void,
toHaveCss(css: {[key: string]: any}): void,
toBeChecked(): void,
toBeDisabled(): void,
toBeEmpty(): void,
toBeHidden(): void,
toBeSelected(): void,
toBeVisible(): void,
toBeFocused(): void,
toBeInDom(): void,
toBeMatchedBy(sel: string): void,
toHaveDescendant(sel: string): void,
toHaveDescendantWithText(sel: string, text: string | RegExp): void
};
// Jest Extended Matchers: https://github.com/jest-community/jest-extended
type JestExtendedMatchersType = {
/**
* Note: Currently unimplemented
* Passing assertion
*
* @param {String} message
*/
// pass(message: string): void;
/**
* Note: Currently unimplemented
* Failing assertion
*
* @param {String} message
*/
// fail(message: string): void;
/**
* Use .toBeEmpty when checking if a String '', Array [] or Object {} is empty.
*/
toBeEmpty(): void;
/**
* Use .toBeOneOf when checking if a value is a member of a given Array.
* @param {Array.<*>} members
*/
toBeOneOf(members: any[]): void;
/**
* Use `.toBeNil` when checking a value is `null` or `undefined`.
*/
toBeNil(): void;
/**
* Use `.toSatisfy` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean`.
* @param {Function} predicate
*/
toSatisfy(predicate: (n: any) => boolean): void;
/**
* Use `.toBeArray` when checking if a value is an `Array`.
*/
toBeArray(): void;
/**
* Use `.toBeArrayOfSize` when checking if a value is an `Array` of size x.
* @param {Number} x
*/
toBeArrayOfSize(x: number): void;
/**
* Use `.toIncludeAllMembers` when checking if an `Array` contains all of the same members of a given set.
* @param {Array.<*>} members
*/
toIncludeAllMembers(members: any[]): void;
/**
* Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the members of a given set.
* @param {Array.<*>} members
*/
toIncludeAnyMembers(members: any[]): void;
/**
* Use `.toSatisfyAll` when you want to use a custom matcher by supplying a predicate function that returns a `Boolean` for all values in an array.
* @param {Function} predicate
*/
toSatisfyAll(predicate: (n: any) => boolean): void;
/**
* Use `.toBeBoolean` when checking if a value is a `Boolean`.
*/
toBeBoolean(): void;
/**
* Use `.toBeTrue` when checking a value is equal (===) to `true`.
*/
toBeTrue(): void;
/**
* Use `.toBeFalse` when checking a value is equal (===) to `false`.
*/
toBeFalse(): void;
/**
* Use .toBeDate when checking if a value is a Date.
*/
toBeDate(): void;
/**
* Use `.toBeFunction` when checking if a value is a `Function`.
*/
toBeFunction(): void;
/**
* Use `.toHaveBeenCalledBefore` when checking if a `Mock` was called before another `Mock`.
*
* Note: Required Jest version >22
* Note: Your mock functions will have to be asynchronous to cause the timestamps inside of Jest to occur in a differentJS event loop, otherwise the mock timestamps will all be the same
*
* @param {Mock} mock
*/
toHaveBeenCalledBefore(mock: JestMockFn<any, any>): void;
/**
* Use `.toBeNumber` when checking if a value is a `Number`.
*/
toBeNumber(): void;
/**
* Use `.toBeNaN` when checking a value is `NaN`.
*/
toBeNaN(): void;
/**
* Use `.toBeFinite` when checking if a value is a `Number`, not `NaN` or `Infinity`.
*/
toBeFinite(): void;
/**
* Use `.toBePositive` when checking if a value is a positive `Number`.
*/
toBePositive(): void;
/**
* Use `.toBeNegative` when checking if a value is a negative `Number`.
*/
toBeNegative(): void;
/**
* Use `.toBeEven` when checking if a value is an even `Number`.
*/
toBeEven(): void;
/**
* Use `.toBeOdd` when checking if a value is an odd `Number`.
*/
toBeOdd(): void;
/**
* Use `.toBeWithin` when checking if a number is in between the given bounds of: start (inclusive) and end (exclusive).
*
* @param {Number} start
* @param {Number} end
*/
toBeWithin(start: number, end: number): void;
/**
* Use `.toBeObject` when checking if a value is an `Object`.
*/
toBeObject(): void;
/**
* Use `.toContainKey` when checking if an object contains the provided key.
*
* @param {String} key
*/
toContainKey(key: string): void;
/**
* Use `.toContainKeys` when checking if an object has all of the provided keys.
*
* @param {Array.<String>} keys
*/
toContainKeys(keys: string[]): void;
/**
* Use `.toContainAllKeys` when checking if an object only contains all of the provided keys.
*
* @param {Array.<String>} keys
*/
toContainAllKeys(keys: string[]): void;
/**
* Use `.toContainAnyKeys` when checking if an object contains at least one of the provided keys.
*
* @param {Array.<String>} keys
*/
toContainAnyKeys(keys: string[]): void;
/**
* Use `.toContainValue` when checking if an object contains the provided value.
*
* @param {*} value
*/
toContainValue(value: any): void;
/**
* Use `.toContainValues` when checking if an object contains all of the provided values.
*
* @param {Array.<*>} values
*/
toContainValues(values: any[]): void;
/**
* Use `.toContainAllValues` when checking if an object only contains all of the provided values.
*
* @param {Array.<*>} values
*/
toContainAllValues(values: any[]): void;
/**
* Use `.toContainAnyValues` when checking if an object contains at least one of the provided values.
*
* @param {Array.<*>} values
*/
toContainAnyValues(values: any[]): void;
/**
* Use `.toContainEntry` when checking if an object contains the provided entry.
*
* @param {Array.<String, String>} entry
*/
toContainEntry(entry: [string, string]): void;
/**
* Use `.toContainEntries` when checking if an object contains all of the provided entries.
*
* @param {Array.<Array.<String, String>>} entries
*/
toContainEntries(entries: [string, string][]): void;
/**
* Use `.toContainAllEntries` when checking if an object only contains all of the provided entries.
*
* @param {Array.<Array.<String, String>>} entries
*/
toContainAllEntries(entries: [string, string][]): void;
/**
* Use `.toContainAnyEntries` when checking if an object contains at least one of the provided entries.
*
* @param {Array.<Array.<String, String>>} entries
*/
toContainAnyEntries(entries: [string, string][]): void;
/**
* Use `.toBeExtensible` when checking if an object is extensible.
*/
toBeExtensible(): void;
/**
* Use `.toBeFrozen` when checking if an object is frozen.
*/
toBeFrozen(): void;
/**
* Use `.toBeSealed` when checking if an object is sealed.
*/
toBeSealed(): void;
/**
* Use `.toBeString` when checking if a value is a `String`.
*/
toBeString(): void;
/**
* Use `.toEqualCaseInsensitive` when checking if a string is equal (===) to another ignoring the casing of both strings.
*
* @param {String} string
*/
toEqualCaseInsensitive(string: string): void;
/**
* Use `.toStartWith` when checking if a `String` starts with a given `String` prefix.
*
* @param {String} prefix
*/
toStartWith(prefix: string): void;
/**
* Use `.toEndWith` when checking if a `String` ends with a given `String` suffix.
*
* @param {String} suffix
*/
toEndWith(suffix: string): void;
/**
* Use `.toInclude` when checking if a `String` includes the given `String` substring.
*
* @param {String} substring
*/
toInclude(substring: string): void;
/**
* Use `.toIncludeRepeated` when checking if a `String` includes the given `String` substring the correct number of times.
*
* @param {String} substring
* @param {Number} times
*/
toIncludeRepeated(substring: string, times: number): void;
/**
* Use `.toIncludeMultiple` when checking if a `String` includes all of the given substrings.
*
* @param {Array.<String>} substring
*/
toIncludeMultiple(substring: string[]): void;
};
interface JestExpectType {
not: JestExpectType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & 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.
@ -148,10 +511,6 @@ type JestExpectType = {
* strict equality. * strict equality.
*/ */
toBe(value: any): void, toBe(value: any): void,
/**
* Use .toHaveBeenCalled to ensure that a mock function got called.
*/
toBeCalled(): void,
/** /**
* Use .toBeCalledWith to ensure that a mock function was called with * Use .toBeCalledWith to ensure that a mock function was called with
* specific arguments. * specific arguments.
@ -227,21 +586,55 @@ type JestExpectType = {
* Use .toHaveBeenCalled to ensure that a mock function got called. * Use .toHaveBeenCalled to ensure that a mock function got called.
*/ */
toHaveBeenCalled(): void, toHaveBeenCalled(): void,
toBeCalled(): void;
/** /**
* Use .toHaveBeenCalledTimes to ensure that a mock function got called exact * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact
* number of times. * number of times.
*/ */
toHaveBeenCalledTimes(number: number): void, toHaveBeenCalledTimes(number: number): void,
toBeCalledTimes(number: number): void;
/**
*
*/
toHaveBeenNthCalledWith(nthCall: number, ...args: Array<any>): void;
nthCalledWith(nthCall: number, ...args: Array<any>): void;
/**
*
*/
toHaveReturned(): void;
toReturn(): void;
/**
*
*/
toHaveReturnedTimes(number: number): void;
toReturnTimes(number: number): void;
/**
*
*/
toHaveReturnedWith(value: any): void;
toReturnWith(value: any): void;
/**
*
*/
toHaveLastReturnedWith(value: any): void;
lastReturnedWith(value: any): void;
/**
*
*/
toHaveNthReturnedWith(nthCall: number, value: any): void;
nthReturnedWith(nthCall: number, value: any): void;
/** /**
* Use .toHaveBeenCalledWith to ensure that a mock function was called with * Use .toHaveBeenCalledWith to ensure that a mock function was called with
* specific arguments. * specific arguments.
*/ */
toHaveBeenCalledWith(...args: Array<any>): void, toHaveBeenCalledWith(...args: Array<any>): void,
toBeCalledWith(...args: Array<any>): void,
/** /**
* Use .toHaveBeenLastCalledWith to ensure that a mock function was last called * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called
* with specific arguments. * with specific arguments.
*/ */
toHaveBeenLastCalledWith(...args: Array<any>): void, toHaveBeenLastCalledWith(...args: Array<any>): void,
lastCalledWith(...args: Array<any>): void,
/** /**
* Check that an object has a .length property and it is set to a certain * Check that an object has a .length property and it is set to a certain
* numeric value. * numeric value.
@ -260,9 +653,17 @@ type JestExpectType = {
*/ */
toMatchObject(object: Object | Array<Object>): void, toMatchObject(object: Object | Array<Object>): void,
/** /**
* This ensures that a React component matches the most recent snapshot. * Use .toStrictEqual to check that a javascript object matches a subset of the properties of an object.
*/ */
toMatchSnapshot(name?: string): void, toStrictEqual(value: any): void,
/**
* This ensures that an Object matches the most recent snapshot.
*/
toMatchSnapshot(propertyMatchers?: {[key: string]: JestAsymmetricEqualityType}, name?: string): void,
/**
* This ensures that an Object matches the most recent snapshot.
*/
toMatchSnapshot(name: 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
@ -278,7 +679,7 @@ type JestExpectType = {
* matching the most recent snapshot when it is called. * matching the most recent snapshot when it is called.
*/ */
toThrowErrorMatchingSnapshot(): void toThrowErrorMatchingSnapshot(): void
}; }
type JestObjectType = { type JestObjectType = {
/** /**
@ -391,6 +792,13 @@ type JestObjectType = {
* Executes only the macro task queue (i.e. all tasks queued by setTimeout() * Executes only the macro task queue (i.e. all tasks queued by setTimeout()
* or setInterval() and setImmediate()). * or setInterval() and setImmediate()).
*/ */
advanceTimersByTime(msToRun: number): void,
/**
* Executes only the macro task queue (i.e. all tasks queued by setTimeout()
* or setInterval() and setImmediate()).
*
* Renamed to `advanceTimersByTime`.
*/
runTimersToTime(msToRun: number): void, runTimersToTime(msToRun: number): void,
/** /**
* Executes only the macro-tasks that are currently pending (i.e., only the * Executes only the macro-tasks that are currently pending (i.e., only the
@ -424,7 +832,7 @@ type JestObjectType = {
* Creates a mock function similar to jest.fn but also tracks calls to * Creates a mock function similar to jest.fn but also tracks calls to
* object[methodName]. * object[methodName].
*/ */
spyOn(object: Object, methodName: string): JestMockFn<any, any>, spyOn(object: Object, methodName: string, accessType?: "get" | "set"): JestMockFn<any, any>,
/** /**
* Set the default timeout interval for tests and before/after hooks in milliseconds. * 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. * Note: The default timeout interval is 5 seconds if this method is not called.
@ -462,17 +870,17 @@ declare var describe: {
/** /**
* Creates a block that groups together several related tests in one "test suite" * Creates a block that groups together several related tests in one "test suite"
*/ */
(name: string, fn: () => void): void, (name: JestTestName, fn: () => void): void,
/** /**
* Only run this describe block * Only run this describe block
*/ */
only(name: string, fn: () => void): void, only(name: JestTestName, fn: () => void): void,
/** /**
* Skip running this describe block * Skip running this describe block
*/ */
skip(name: string, fn: () => void): void skip(name: JestTestName, fn: () => void): void
}; };
/** An individual test unit */ /** An individual test unit */
@ -480,54 +888,54 @@ declare var it: {
/** /**
* An individual test unit * An individual test unit
* *
* @param {string} Name of Test * @param {JestTestName} Name of Test
* @param {Function} Test * @param {Function} Test
* @param {number} Timeout for the test, in milliseconds. * @param {number} Timeout for the test, in milliseconds.
*/ */
( (
name: string, name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>, fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void, ): void,
/** /**
* Only run this test * Only run this test
* *
* @param {string} Name of Test * @param {JestTestName} Name of Test
* @param {Function} Test * @param {Function} Test
* @param {number} Timeout for the test, in milliseconds. * @param {number} Timeout for the test, in milliseconds.
*/ */
only( only(
name: string, name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>, fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void, ): void,
/** /**
* Skip running this test * Skip running this test
* *
* @param {string} Name of Test * @param {JestTestName} Name of Test
* @param {Function} Test * @param {Function} Test
* @param {number} Timeout for the test, in milliseconds. * @param {number} Timeout for the test, in milliseconds.
*/ */
skip( skip(
name: string, name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>, fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void, ): void,
/** /**
* Run the test concurrently * Run the test concurrently
* *
* @param {string} Name of Test * @param {JestTestName} Name of Test
* @param {Function} Test * @param {Function} Test
* @param {number} Timeout for the test, in milliseconds. * @param {number} Timeout for the test, in milliseconds.
*/ */
concurrent( concurrent(
name: string, name: JestTestName,
fn?: (done: () => void) => ?Promise<mixed>, fn?: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void ): void
}; };
declare function fit( declare function fit(
name: string, name: JestTestName,
fn: (done: () => void) => ?Promise<mixed>, fn: (done: () => void) => ?Promise<mixed>,
timeout?: number timeout?: number
): void; ): void;
@ -542,23 +950,75 @@ declare var xit: typeof it;
/** A disabled individual test */ /** A disabled individual test */
declare var xtest: typeof it; declare var xtest: typeof it;
type JestPrettyFormatColors = {
comment: { close: string, open: string },
content: { close: string, open: string },
prop: { close: string, open: string },
tag: { close: string, open: string },
value: { close: string, open: string },
};
type JestPrettyFormatIndent = string => string;
type JestPrettyFormatRefs = Array<any>;
type JestPrettyFormatPrint = any => string;
type JestPrettyFormatStringOrNull = string | null;
type JestPrettyFormatOptions = {|
callToJSON: boolean,
edgeSpacing: string,
escapeRegex: boolean,
highlight: boolean,
indent: number,
maxDepth: number,
min: boolean,
plugins: JestPrettyFormatPlugins,
printFunctionName: boolean,
spacing: string,
theme: {|
comment: string,
content: string,
prop: string,
tag: string,
value: string,
|},
|};
type JestPrettyFormatPlugin = {
print: (
val: any,
serialize: JestPrettyFormatPrint,
indent: JestPrettyFormatIndent,
opts: JestPrettyFormatOptions,
colors: JestPrettyFormatColors,
) => string,
test: any => boolean,
};
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, (value: any): JestExpectType & JestPromiseType & EnzymeMatchersType & DomTestingLibraryType & JestJQueryMatchersType & 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. */
addSnapshotSerializer(serializer: (input: Object) => string): void, addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void,
assertions(expectedAssertions: number): void, assertions(expectedAssertions: number): void,
hasAssertions(): void, hasAssertions(): void,
any(value: mixed): JestAsymmetricEqualityType, any(value: mixed): JestAsymmetricEqualityType,
anything(): void, anything(): any,
arrayContaining(value: Array<mixed>): void, arrayContaining(value: Array<mixed>): Array<mixed>,
objectContaining(value: Object): void, objectContaining(value: Object): Object,
/** Matches any received string that contains the exact expected string. */ /** Matches any received string that contains the exact expected string. */
stringContaining(value: string): void, stringContaining(value: string): string,
stringMatching(value: string | RegExp): void stringMatching(value: string | RegExp): string,
not: {
arrayContaining: (value: $ReadOnlyArray<mixed>) => Array<mixed>,
objectContaining: (value: {}) => Object,
stringContaining: (value: string) => string,
stringMatching: (value: string | RegExp) => string,
},
}; };
// TODO handle return type // TODO handle return type
@ -575,14 +1035,14 @@ declare var jest: JestObjectType;
declare var jasmine: { declare var jasmine: {
DEFAULT_TIMEOUT_INTERVAL: number, DEFAULT_TIMEOUT_INTERVAL: number,
any(value: mixed): JestAsymmetricEqualityType, any(value: mixed): JestAsymmetricEqualityType,
anything(): void, anything(): any,
arrayContaining(value: Array<mixed>): void, arrayContaining(value: Array<mixed>): Array<mixed>,
clock(): JestClockType, clock(): JestClockType,
createSpy(name: string): JestSpyType, createSpy(name: string): JestSpyType,
createSpyObj( createSpyObj(
baseName: string, baseName: string,
methodNames: Array<string> methodNames: Array<string>
): { [methodName: string]: JestSpyType }, ): { [methodName: string]: JestSpyType },
objectContaining(value: Object): void, objectContaining(value: Object): Object,
stringMatching(value: string): void stringMatching(value: string): string
}; };

@ -52,11 +52,11 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "6.1.3", "@commitlint/cli": "6.1.3",
"@commitlint/config-conventional": "6.1.3", "@commitlint/config-conventional": "6.1.3",
"@verdaccio/types": "3.0.0", "@verdaccio/types": "3.0.1",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
"babel-core": "6.26.0", "babel-core": "6.26.0",
"babel-eslint": "8.2.2", "babel-eslint": "8.2.2",
"babel-jest": "22.4.3", "babel-jest": "23.2.0",
"babel-loader": "7.1.4", "babel-loader": "7.1.4",
"babel-plugin-flow-runtime": "0.17.0", "babel-plugin-flow-runtime": "0.17.0",
"babel-plugin-syntax-dynamic-import": "6.18.0", "babel-plugin-syntax-dynamic-import": "6.18.0",
@ -82,14 +82,14 @@
"element-theme-default": "1.4.13", "element-theme-default": "1.4.13",
"enzyme": "3.3.0", "enzyme": "3.3.0",
"enzyme-adapter-react-16": "1.1.1", "enzyme-adapter-react-16": "1.1.1",
"eslint": "4.18.2", "eslint": "5.0.1",
"eslint-config-google": "0.9.1", "eslint-config-google": "0.9.1",
"eslint-loader": "2.0.0", "eslint-loader": "2.0.0",
"eslint-plugin-babel": "4.1.2", "eslint-plugin-babel": "4.1.2",
"eslint-plugin-flowtype": "2.46.1", "eslint-plugin-flowtype": "2.49.3",
"eslint-plugin-import": "2.9.0", "eslint-plugin-import": "2.13.0",
"eslint-plugin-jest": "21.14.0", "eslint-plugin-jest": "21.17.0",
"eslint-plugin-react": "7.7.0", "eslint-plugin-react": "7.10.0",
"file-loader": "1.1.11", "file-loader": "1.1.11",
"flow-bin": "0.69.0", "flow-bin": "0.69.0",
"flow-runtime": "0.17.0", "flow-runtime": "0.17.0",
@ -99,10 +99,10 @@
"husky": "0.15.0-rc.8", "husky": "0.15.0-rc.8",
"identity-obj-proxy": "3.0.0", "identity-obj-proxy": "3.0.0",
"in-publish": "2.0.0", "in-publish": "2.0.0",
"jest": "22.4.3", "jest": "23.2.0",
"jest-environment-jsdom": "22.4.3", "jest-environment-jsdom": "23.2.0",
"jest-environment-jsdom-global": "1.0.3", "jest-environment-jsdom-global": "1.1.0",
"jest-environment-node": "22.4.3", "jest-environment-node": "23.2.0",
"localstorage-memory": "1.0.2", "localstorage-memory": "1.0.2",
"mini-css-extract-plugin": "0.4.0", "mini-css-extract-plugin": "0.4.0",
"node-mocks-http": "1.6.7", "node-mocks-http": "1.6.7",
@ -154,10 +154,10 @@
"flow": "flow", "flow": "flow",
"pretest": "npm run code:build", "pretest": "npm run code:build",
"test": "npm run test:unit", "test": "npm run test:unit",
"test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest.config.unit.js --maxWorkers 2", "test:unit": "cross-env NODE_ENV=test BABEL_ENV=test TZ=UTC jest --config ./jest.config.js --maxWorkers 2",
"test:functional": "cross-env NODE_ENV=testOldEnv jest --config ./test/jest.config.functional.js --testPathPattern ./test/functional/index*", "test:functional": "cross-env NODE_ENV=testOldEnv jest --config ./test/jest.config.functional.js --testPathPattern ./test/functional/index*",
"test:e2e": "cross-env BABEL_ENV=testOldEnv jest --config ./test/jest.config.e2e.js", "test:e2e": "cross-env BABEL_ENV=testOldEnv jest --config ./test/jest.config.e2e.js",
"test:all": "npm run test && npm run test:functional && npm run test:e2e", "test:all": "npm run build:webui && npm run test && npm run test:functional && npm run test:e2e",
"pre:ci": "npm run lint && npm run build:webui", "pre:ci": "npm run lint && npm run build:webui",
"commitmsg": "commitlint -e $GIT_PARAMS", "commitmsg": "commitlint -e $GIT_PARAMS",
"coverage:publish": "codecov", "coverage:publish": "codecov",

@ -15,13 +15,13 @@ import type {$ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler,
import type {Config as IConfig} from '@verdaccio/types'; import type {Config as IConfig} from '@verdaccio/types';
import {ErrorCode} from '../lib/utils'; import {ErrorCode} from '../lib/utils';
import {API_ERROR, HTTP_STATUS} from '../lib/constants'; import {API_ERROR, HTTP_STATUS} from '../lib/constants';
import AppConfig from '../lib/config';
const LoggerApp = require('../lib/logger'); const LoggerApp = require('../lib/logger');
const Config = require('../lib/config');
const Middleware = require('./middleware'); const Middleware = require('./middleware');
const Cats = require('../lib/status-cats'); const Cats = require('../lib/status-cats');
const defineAPI = function(config: Config, storage: IStorageHandler) { const defineAPI = function(config: IConfig, storage: IStorageHandler) {
const auth: IAuth = new Auth(config); const auth: IAuth = new Auth(config);
const app: $Application = express(); const app: $Application = express();
// run in production mode by default, just in case // run in production mode by default, just in case
@ -103,7 +103,7 @@ const defineAPI = function(config: Config, storage: IStorageHandler) {
export default async function(configHash: any) { export default async function(configHash: any) {
LoggerApp.setup(configHash.logs); LoggerApp.setup(configHash.logs);
const config: IConfig = new Config(configHash); const config: IConfig = new AppConfig(configHash);
const storage: IStorageHandler = new Storage(config); const storage: IStorageHandler = new Storage(config);
// waits until init calls have been intialized // waits until init calls have been intialized
await storage.init(config); await storage.init(config);

34
src/lib/auth-utils.js Normal file

@ -0,0 +1,34 @@
import {ErrorCode} from './utils';
import {API_ERROR} from './constants';
export function allow_action(action) {
return function(user, pkg, callback) {
const {name, groups} = user;
const hasPermission = pkg[action].some((group) => name === group || groups.includes(group));
if (hasPermission) {
return callback(null, true);
}
if (name) {
callback(ErrorCode.getForbidden(`user ${name} is not allowed to ${action} package ${pkg.name}`));
} else {
callback(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`));
}
};
}
export function getDefaultPlugins() {
return {
authenticate(user, password, cb) {
cb(ErrorCode.getForbidden(API_ERROR.BAD_USERNAME_PASSWORD));
},
add_user(user, password, cb) {
return cb(ErrorCode.getConflict(API_ERROR.BAD_USERNAME_PASSWORD));
},
allow_access: allow_action('access'),
allow_publish: allow_action('publish'),
};
}

@ -2,20 +2,20 @@
import _ from 'lodash'; import _ from 'lodash';
import {loadPlugin} from '../lib/plugin-loader'; import {loadPlugin} from '../lib/plugin-loader';
import {ErrorCode} from './utils'; import {buildBase64Buffer, ErrorCode} from './utils';
import {aesDecrypt, aesEncrypt, signPayload, verifyPayload} from './crypto-utils'; import {aesDecrypt, aesEncrypt, signPayload, verifyPayload} from './crypto-utils';
import type {Config, Logger, Callback} from '@verdaccio/types'; import type {Config, Logger, Callback} from '@verdaccio/types';
import type {$Response, NextFunction} from 'express'; import type {$Response, NextFunction} from 'express';
import type {$RequestExtend, JWTPayload} from '../../types'; import type {$RequestExtend, JWTPayload} from '../../types';
import {ROLES} from './constants'; import {API_ERROR, HTTP_STATUS, ROLES, TOKEN_BASIC, TOKEN_BEARER} from './constants';
import {getMatchedPackagesSpec} from './config-utils';
import type {IAuth} from '../../types';
import {getDefaultPlugins} from './auth-utils';
const LoggerApi = require('./logger'); const LoggerApi = require('./logger');
/**
* Handles the authentification, load auth plugins. class Auth implements IAuth {
*/
class Auth {
config: Config; config: Config;
logger: Logger; logger: Logger;
secret: string; secret: string;
@ -31,48 +31,20 @@ class Auth {
} }
_loadPlugin(config: Config) { _loadPlugin(config: Config) {
const plugin_params = { const pluginOptions = {
config, config,
logger: this.logger, logger: this.logger,
}; };
return loadPlugin(config, config.auth, plugin_params, function(p) { return loadPlugin(config, config.auth, pluginOptions, (plugin) => {
return p.authenticate || p.allow_access || p.allow_publish; const {authenticate, allow_access, allow_publish} = plugin;
return authenticate || allow_access || allow_publish;
}); });
} }
_applyDefaultPlugins() { _applyDefaultPlugins() {
const allow_action = function(action) { this.plugins.push(getDefaultPlugins());
return function(user, pkg, cb) {
const ok = pkg[action].reduce(function(prev, curr) {
if (user.name === curr || user.groups.indexOf(curr) !== -1) return true;
return prev;
}, false);
if (ok) {
return cb(null, true);
}
if (user.name) {
cb(ErrorCode.getForbidden(`user ${user.name} is not allowed to ${action} package ${pkg.name}`));
} else {
cb(ErrorCode.getForbidden(`unregistered users are not allowed to ${action} package ${pkg.name}`));
}
};
};
this.plugins.push({
authenticate: function(user, password, cb) {
cb(ErrorCode.getForbidden('bad username/password, access denied'));
},
add_user: function(user, password, cb) {
return cb(ErrorCode.getConflict('bad username/password, access denied'));
},
allow_access: allow_action('access'),
allow_publish: allow_action('publish'),
});
} }
authenticate(user: string, password: string, cb: Callback) { authenticate(user: string, password: string, cb: Callback) {
@ -80,7 +52,7 @@ class Auth {
(function next() { (function next() {
const plugin = plugins.shift(); const plugin = plugins.shift();
if (typeof(plugin.authenticate) !== 'function') { if (_.isFunction(plugin.authenticate) === false) {
return next(); return next();
} }
@ -98,12 +70,12 @@ class Auth {
// Info: Cannot use `== false to check falsey values` // Info: Cannot use `== false to check falsey values`
if (!!groups && groups.length !== 0) { if (!!groups && groups.length !== 0) {
// TODO: create a better understanding of expectations // TODO: create a better understanding of expectations
if (typeof groups === 'string') { if (_.isString(groups)) {
throw new TypeError('invalid type for function'); throw new TypeError('invalid type for function');
} }
const isGroupValid: boolean = _.isArray(groups); const isGroupValid: boolean = _.isArray(groups);
if (!isGroupValid) { if (!isGroupValid) {
throw new TypeError('user groups is different than an array'); throw new TypeError(API_ERROR.BAD_FORMAT_USER_GROUP);
} }
return cb(err, authenticatedUser(user, groups)); return cb(err, authenticatedUser(user, groups));
@ -115,19 +87,19 @@ class Auth {
add_user(user: string, password: string, cb: Callback) { add_user(user: string, password: string, cb: Callback) {
let self = this; let self = this;
let plugins = this.plugins.slice(0) let plugins = this.plugins.slice(0);
;(function next() { (function next() {
let p = plugins.shift(); let plugin = plugins.shift();
let n = 'adduser'; let method = 'adduser';
if (typeof(p[n]) !== 'function') { if (_.isFunction(plugin[method]) === false) {
n = 'add_user'; method = 'add_user';
} }
if (typeof(p[n]) !== 'function') { if (_.isFunction[method] === false) {
next(); next();
} else { } else {
// p.add_user() execution // p.add_user() execution
p[n](user, password, function(err, ok) { plugin[method](user, password, function(err, ok) {
if (err) { if (err) {
return cb(err); return cb(err);
} }
@ -146,7 +118,7 @@ class Auth {
allow_access(packageName: string, user: string, callback: Callback) { allow_access(packageName: string, user: string, callback: Callback) {
let plugins = this.plugins.slice(0); let plugins = this.plugins.slice(0);
// $FlowFixMe // $FlowFixMe
let pkg = Object.assign({name: packageName}, this.config.getMatchedPackagesSpec(packageName)); let pkg = Object.assign({name: packageName}, getMatchedPackagesSpec(packageName, this.config.packages));
(function next() { (function next() {
const plugin = plugins.shift(); const plugin = plugins.shift();
@ -175,7 +147,7 @@ class Auth {
allow_publish(packageName: string, user: string, callback: Callback) { allow_publish(packageName: string, user: string, callback: Callback) {
let plugins = this.plugins.slice(0); let plugins = this.plugins.slice(0);
// $FlowFixMe // $FlowFixMe
let pkg = Object.assign({name: packageName}, this.config.getMatchedPackagesSpec(packageName)); let pkg = Object.assign({name: packageName}, getMatchedPackagesSpec(packageName, this.config.packages));
(function next() { (function next() {
const plugin = plugins.shift(); const plugin = plugins.shift();
@ -184,7 +156,7 @@ class Auth {
return next(); return next();
} }
plugin.allow_publish(user, pkg, function(err, ok) { plugin.allow_publish(user, pkg, (err, ok) => {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -219,13 +191,13 @@ class Auth {
req.remote_user = buildAnonymousUser(); req.remote_user = buildAnonymousUser();
const authorization = req.headers.authorization; const authorization = req.headers.authorization;
if (authorization == null) { if (_.isNil(authorization)) {
return next(); return next();
} }
const parts = authorization.split(' '); const parts = authorization.split(' ');
if (parts.length !== 2) { if (parts.length !== 2) {
return next( ErrorCode.getBadRequest('bad authorization header') ); return next( ErrorCode.getBadRequest(API_ERROR.BAD_AUTH_HEADER) );
} }
const credentials = this._parseCredentials(parts); const credentials = this._parseCredentials(parts);
@ -257,12 +229,12 @@ class Auth {
_parseCredentials(parts: Array<string>) { _parseCredentials(parts: Array<string>) {
let credentials; let credentials;
const scheme = parts[0]; const scheme = parts[0];
if (scheme.toUpperCase() === 'BASIC') { if (scheme.toUpperCase() === TOKEN_BASIC.toUpperCase()) {
credentials = new Buffer(parts[1], 'base64').toString(); credentials = buildBase64Buffer(parts[1]).toString();
this.logger.info('basic authentication is deprecated, please use JWT instead'); this.logger.info(API_ERROR.DEPRECATED_BASIC_HEADER);
return credentials; return credentials;
} else if (scheme.toUpperCase() === 'BEARER') { } else if (scheme.toUpperCase() === TOKEN_BEARER.toUpperCase()) {
const token = new Buffer(parts[1], 'base64'); const token = buildBase64Buffer(parts[1]);
credentials = aesDecrypt(token, this.secret).toString('utf8'); credentials = aesDecrypt(token, this.secret).toString('utf8');
return credentials; return credentials;
@ -276,17 +248,17 @@ class Auth {
*/ */
webUIJWTmiddleware() { webUIJWTmiddleware() {
return (req: $RequestExtend, res: $Response, _next: NextFunction) => { return (req: $RequestExtend, res: $Response, _next: NextFunction) => {
if (req.remote_user !== null && req.remote_user.name !== undefined) { if (_.isNull(req.remote_user) === false && _.isNil(req.remote_user.name) === false) {
return _next(); return _next();
} }
req.pause(); req.pause();
const next = function(_err) { const next = () => {
req.resume(); req.resume();
return _next(); return _next();
}; };
const token = (req.headers.authorization || '').replace('Bearer ', ''); const token = (req.headers.authorization || '').replace(`${TOKEN_BEARER} `, '');
if (!token) { if (!token) {
return next(); return next();
} }
@ -328,7 +300,7 @@ class Auth {
try { try {
decoded = verifyPayload(token, this.secret); decoded = verifyPayload(token, this.secret);
} catch (err) { } catch (err) {
throw ErrorCode.getCode(401, err.message); throw ErrorCode.getCode(HTTP_STATUS.UNAUTHORIZED, err.message);
} }
return decoded; return decoded;
@ -350,7 +322,7 @@ function buildAnonymousUser() {
return { return {
name: undefined, name: undefined,
// groups without '$' are going to be deprecated eventually // groups without '$' are going to be deprecated eventually
groups: ['$all', '$anonymous', '@all', '@anonymous'], groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONUMOUS],
real_groups: [], real_groups: [],
}; };
} }
@ -361,7 +333,12 @@ function buildAnonymousUser() {
*/ */
function authenticatedUser(name: string, pluginGroups: Array<any>) { function authenticatedUser(name: string, pluginGroups: Array<any>) {
const isGroupValid: boolean = _.isArray(pluginGroups); const isGroupValid: boolean = _.isArray(pluginGroups);
const groups = (isGroupValid ? pluginGroups : []).concat([ROLES.$ALL, ROLES.$AUTH, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_AUTH, ROLES.ALL]); const groups = (isGroupValid ? pluginGroups : []).concat([
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL]);
return { return {
name, name,

@ -64,9 +64,12 @@ export function getListListenAddresses(argListen: string, configListen: mixed) {
* @param {String} pkgVersion * @param {String} pkgVersion
* @param {String} pkgName * @param {String} pkgName
*/ */
function startVerdaccio(config: any, cliListen: string, function startVerdaccio(config: any,
configPath: string, pkgVersion: string, cliListen: string,
pkgName: string, callback: Callback) { configPath: string,
pkgVersion: string,
pkgName: string,
callback: Callback) {
if (isObject(config) === false) { if (isObject(config) === false) {
throw new Error('config file must be an object'); throw new Error('config file must be an object');
} }

128
src/lib/config-utils.js Normal file

@ -0,0 +1,128 @@
// @flow
import _ from 'lodash';
import assert from 'assert';
import minimatch from 'minimatch';
import {ErrorCode} from './utils';
import type {PackageList, UpLinksConfList} from '@verdaccio/types';
import type {MatchedPackage} from '../../types';
const BLACKLIST = {
all: true,
anonymous: true,
undefined: true,
owner: true,
none: true,
};
/**
* Normalise user list.
* @return {Array}
*/
export function normalizeUserlist(oldFormat: any, newFormat: any) {
const result = [];
/* eslint prefer-rest-params: "off" */
for (let i=0; i < arguments.length; i++) {
if (arguments[i] == null) {
continue;
}
// if it's a string, split it to array
if (_.isString(arguments[i])) {
result.push(arguments[i].split(/\s+/));
} else if (Array.isArray(arguments[i])) {
result.push(arguments[i]);
} else {
throw ErrorCode.getInternalError('CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i]));
}
}
return _.flatten(result);
}
export function uplinkSanityCheck(uplinks: UpLinksConfList, users: any = BLACKLIST) {
const newUplinks = _.clone(uplinks);
let newUsers = _.clone(users);
for (let uplink in newUplinks) {
if (Object.prototype.hasOwnProperty.call(newUplinks, uplink)) {
if (_.isNil(newUplinks[uplink].cache)) {
newUplinks[uplink].cache = true;
}
newUsers = sanityCheckNames(uplink, newUsers);
}
}
return newUplinks;
}
export function sanityCheckNames(item: string, users: any) {
assert(item !== 'all' && item !== 'owner' && item !== 'anonymous' && item !== 'undefined' && item !== 'none', 'CONFIG: reserved uplink name: ' + item);
assert(!item.match(/\s/), 'CONFIG: invalid uplink name: ' + item);
assert(_.isNil(users[item]), 'CONFIG: duplicate uplink name: ' + item);
users[item] = true;
return users;
}
export function sanityCheckUplinksProps(configUpLinks: any) {
const uplinks = _.clone(configUpLinks);
for (let uplink in uplinks) {
if (Object.prototype.hasOwnProperty.call(uplinks, uplink)) {
assert(uplinks[uplink].url, 'CONFIG: no url for uplink: ' + uplink);
assert( _.isString(uplinks[uplink].url), 'CONFIG: wrong url format for uplink: ' + uplink);
uplinks[uplink].url = uplinks[uplink].url.replace(/\/$/, '');
}
}
return uplinks;
}
/**
* Check whether an uplink can proxy
*/
export function hasProxyTo(pkg: string, upLink: string, packages: PackageList): boolean {
const matchedPkg: MatchedPackage = (getMatchedPackagesSpec(pkg, packages): MatchedPackage);
const proxyList = typeof matchedPkg !== 'undefined' ? matchedPkg.proxy : [];
if (proxyList) {
return proxyList.some((curr) => upLink === curr);
}
return false;
}
export function getMatchedPackagesSpec(pkg: string, packages: PackageList): MatchedPackage {
for (let i in packages) {
// $FlowFixMe
if (minimatch.makeRe(i).exec(pkg)) {
return packages[i];
}
}
return;
}
export function normalisePackageAccess(packages: PackageList): PackageList {
const normalizedPkgs: PackageList = {...packages};
// add a default rule for all packages to make writing plugins easier
if (_.isNil(normalizedPkgs['**'])) {
normalizedPkgs['**'] = {};
}
for (let pkg in packages) {
if (Object.prototype.hasOwnProperty.call(packages, pkg)) {
assert(_.isObject(packages[pkg]) && _.isArray(packages[pkg]) === false,
`CONFIG: bad "'${pkg}'" package description (object expected)`);
normalizedPkgs[pkg].access = normalizeUserlist(packages[pkg].allow_access, packages[pkg].access);
delete normalizedPkgs[pkg].allow_access;
normalizedPkgs[pkg].publish = normalizeUserlist(packages[pkg].allow_publish, packages[pkg].publish);
delete normalizedPkgs[pkg].allow_publish;
normalizedPkgs[pkg].proxy = normalizeUserlist(packages[pkg].proxy_access, packages[pkg].proxy);
delete normalizedPkgs[pkg].proxy_access;
}
}
return normalizedPkgs;
}

@ -1,210 +1,107 @@
// @flow
import _ from 'lodash';
import assert from 'assert';
import {generateRandomHexString} from './crypto-utils'; import {generateRandomHexString} from './crypto-utils';
import {
getMatchedPackagesSpec,
normalisePackageAccess,
sanityCheckUplinksProps,
uplinkSanityCheck} from './config-utils';
import {getUserAgent, isObject} from './utils';
import {APP_ERROR} from './constants';
const assert = require('assert'); import type {
const _ = require('lodash'); PackageList,
const Error = require('http-errors'); Config as AppConfig,
const minimatch = require('minimatch'); Logger,
} from '@verdaccio/types';
const Utils = require('./utils'); import type {MatchedPackage, StartUpConfig} from '../../types';
const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars
const pkgVersion = module.exports.version; const LoggerApi = require('./logger');
const pkgName = module.exports.name; const strategicConfigProps = ['uplinks', 'packages'];
const strategicConfigProps = ['users', 'uplinks', 'packages'];
const allowedEnvConfig = ['http_proxy', 'https_proxy', 'no_proxy']; const allowedEnvConfig = ['http_proxy', 'https_proxy', 'no_proxy'];
/**
* [[a, [b, c]], d] -> [a, b, c, d]
* @param {*} array
* @return {Array}
*/
function flatten(array) {
let result = [];
for (let i=0; i < array.length; i++) {
if (Array.isArray(array[i])) {
/* eslint prefer-spread: "off" */
result.push.apply(result, flatten(array[i]));
} else {
result.push(array[i]);
}
}
return result;
}
function checkUserOrUplink(item, users) {
assert(item !== 'all' && item !== 'owner'
&& item !== 'anonymous' && item !== 'undefined' && item !== 'none', 'CONFIG: reserved user/uplink name: ' + item);
assert(!item.match(/\s/), 'CONFIG: invalid user name: ' + item);
assert(users[item] == null, 'CONFIG: duplicate user/uplink name: ' + item);
users[item] = true;
}
/**
* Normalise user list.
* @return {Array}
*/
function normalizeUserlist() {
let result = [];
/* eslint prefer-rest-params: "off" */
for (let i=0; i < arguments.length; i++) {
if (arguments[i] == null) {
continue;
}
// if it's a string, split it to array
if (typeof(arguments[i]) === 'string') {
result.push(arguments[i].split(/\s+/));
} else if (Array.isArray(arguments[i])) {
result.push(arguments[i]);
} else {
throw Error('CONFIG: bad package acl (array or string expected): ' + JSON.stringify(arguments[i]));
}
}
return flatten(result);
}
/** /**
* Coordinates the application configuration * Coordinates the application configuration
*/ */
class Config { class Config implements AppConfig {
/** logger: Logger;
* @param {*} config config the content user_agent: string;
*/ secret: string;
constructor(config) { uplinks: any;
packages: PackageList;
users: any;
server_id: string;
self_path: string;
storage: string | void;
$key: any;
$value: any;
constructor(config: StartUpConfig) {
const self = this; const self = this;
const users = { this.logger = LoggerApi.logger;
all: true, this.self_path = config.self_path;
anonymous: true, this.storage = config.storage;
undefined: true,
owner: true,
none: true,
};
for (let configProp in config) { for (let configProp in config) {
if (self[configProp] == null) { if (self[configProp] == null) {
self[configProp] = config[configProp]; self[configProp] = config[configProp];
} }
} }
if (!self.user_agent) {
self.user_agent = `${pkgName}/${pkgVersion}`; if (_.isNil(this.user_agent)) {
this.user_agent = getUserAgent();
} }
// some weird shell scripts are valid yaml files parsed as string // some weird shell scripts are valid yaml files parsed as string
assert.equal(typeof(config), 'object', 'CONFIG: it doesn\'t look like a valid config file'); assert(_.isObject(config), APP_ERROR.CONFIG_NOT_VALID);
// sanity check for strategic config properties // sanity check for strategic config properties
strategicConfigProps.forEach(function(x) { strategicConfigProps.forEach(function(x) {
if (self[x] == null) self[x] = {}; if (self[x] == null) {
assert(Utils.isObject(self[x]), `CONFIG: bad "${x}" value (object expected)`); self[x] = {};
}
assert(isObject(self[x]), `CONFIG: bad "${x}" value (object expected)`);
}); });
// sanity check for users this.uplinks = sanityCheckUplinksProps(uplinkSanityCheck(this.uplinks));
for (let i in self.users) {
if (Object.prototype.hasOwnProperty.call(self.users, i)) { if (_.isNil(this.users) === false) {
checkUserOrUplink(i, users); this.logger.warn(`[users]: property on configuration file
} is not longer supported, property being ignored`);
}
// sanity check for uplinks
/* eslint guard-for-in: 0 */
for (let i in self.uplinks) {
if (self.uplinks[i].cache == null) {
self.uplinks[i].cache = true;
}
if (Object.prototype.hasOwnProperty.call(self.uplinks, i)) {
checkUserOrUplink(i, users);
}
} }
for (let user in self.users) { this.packages = normalisePackageAccess(self.packages);
if (Object.prototype.hasOwnProperty.call(self.users, user)) {
assert(self.users[user].password, 'CONFIG: no password for user: ' + user);
assert(typeof(self.users[user].password) === 'string' &&
self.users[user].password.match(/^[a-f0-9]{40}$/)
, 'CONFIG: wrong password format for user: ' + user + ', sha1 expected');
}
}
for (let uplink in self.uplinks) {
if (Object.prototype.hasOwnProperty.call(self.uplinks, uplink)) {
assert(self.uplinks[uplink].url, 'CONFIG: no url for uplink: ' + uplink);
assert( typeof(self.uplinks[uplink].url) === 'string'
, 'CONFIG: wrong url format for uplink: ' + uplink);
self.uplinks[uplink].url = self.uplinks[uplink].url.replace(/\/$/, '');
}
}
// add a default rule for all packages to make writing plugins easier
if (self.packages['**'] == null) {
self.packages['**'] = {};
}
for (let pkg in self.packages) {
if (Object.prototype.hasOwnProperty.call(self.packages, pkg)) {
assert(
typeof(self.packages[pkg]) === 'object' &&
!Array.isArray(self.packages[pkg])
, 'CONFIG: bad "'+pkg+'" package description (object expected)');
self.packages[pkg].access = normalizeUserlist(self.packages[pkg].allow_access, self.packages[pkg].access);
delete self.packages[pkg].allow_access;
self.packages[pkg].publish = normalizeUserlist(self.packages[pkg].allow_publish, self.packages[pkg].publish);
delete self.packages[pkg].allow_publish;
self.packages[pkg].proxy = normalizeUserlist(self.packages[pkg].proxy_access, self.packages[pkg].proxy);
delete self.packages[pkg].proxy_access;
}
}
// loading these from ENV if aren't in config // loading these from ENV if aren't in config
allowedEnvConfig.forEach((function(v) { allowedEnvConfig.forEach((envConf) => {
if (!(v in self)) { if (!(envConf in self)) {
self[v] = process.env[v] || process.env[v.toUpperCase()]; self[envConf] = process.env[envConf] || process.env[envConf.toUpperCase()];
} }
})); });
// unique identifier of self server (or a cluster), used to avoid loops // unique identifier of self server (or a cluster), used to avoid loops
if (!self.server_id) { if (!this.server_id) {
self.server_id = generateRandomHexString(6); this.server_id = generateRandomHexString(6);
} }
} }
/**
* Check whether an uplink can proxy
* @param {String} pkg package anem
* @param {*} upLink
* @return {Boolean}
*/
hasProxyTo(pkg, upLink) {
return (this.getMatchedPackagesSpec(pkg).proxy || []).reduce(function(prev, curr) {
if (upLink === curr) {
return true;
}
return prev;
}, false);
}
/** /**
* Check for package spec * Check for package spec
* @param {String} pkg package name
* @return {Object}
*/ */
getMatchedPackagesSpec(pkg) { getMatchedPackagesSpec(pkg: string): MatchedPackage {
for (let i in this.packages) { return getMatchedPackagesSpec(pkg, this.packages);
if (minimatch.makeRe(i).exec(pkg)) {
return this.packages[i];
}
}
return {};
} }
/** /**
* Store or create whether recieve a secret key * Store or create whether recieve a secret key
* @param {String} secret
* @return {String}
*/ */
checkSecretKey(secret) { checkSecretKey(secret: string): string {
if (_.isString(secret) && secret !== '') { if (_.isString(secret) && _.isEmpty(secret) === false) {
this.secret = secret; this.secret = secret;
return secret; return secret;
} }
@ -215,4 +112,4 @@ class Config {
} }
} }
module.exports = Config; export default Config;

@ -29,10 +29,12 @@ export const DEFAULT_UPLINK = 'npmjs';
export const ROLES = { export const ROLES = {
$ALL: '$all', $ALL: '$all',
ALL: 'all',
$AUTH: '$authenticated', $AUTH: '$authenticated',
$ANONYMOUS: '$anonymous',
DEPRECATED_ALL: '@all', DEPRECATED_ALL: '@all',
DEPRECATED_AUTH: '@authenticated', DEPRECATED_AUTH: '@authenticated',
ALL: 'all', DEPRECATED_ANONUMOUS: '@anonymous',
}; };
export const HTTP_STATUS = { export const HTTP_STATUS = {
@ -65,21 +67,37 @@ export const API_MESSAGE = {
}; };
export const API_ERROR = { export const API_ERROR = {
BAD_USERNAME_PASSWORD: 'bad username/password, access denied {APP}',
NO_PACKAGE: 'no such package available', NO_PACKAGE: 'no such package available',
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',
NOT_PACKAGE_UPLINK: 'package does not exist on uplink', NOT_PACKAGE_UPLINK: 'package does not exist on uplink',
UPLINK_OFFLINE_PUBLISH: 'one of the uplinks is down, refuse to publish',
UPLINK_OFFLINE: 'uplink is offline',
CONTENT_MISMATCH: 'content length mismatch', CONTENT_MISMATCH: 'content length mismatch',
NOT_FILE_UPLINK: 'file doesn\'t exist on uplink', NOT_FILE_UPLINK: 'file doesn\'t exist on uplink',
MAX_USERS_REACHED: 'maximum amount of users reached', MAX_USERS_REACHED: 'maximum amount of users reached',
VERSION_NOT_EXIST: 'this version doesn\'t exist', VERSION_NOT_EXIST: 'this version doesn\'t exist',
FILE_NOT_FOUND: 'File not found', FILE_NOT_FOUND: 'File not found',
BAD_STATUS_CODE: 'bad status code', BAD_STATUS_CODE: 'bad status code',
PACKAGE_EXIST: 'this package is already present',
BAD_AUTH_HEADER: 'bad authorization header',
WEB_DISABLED: 'Web interface is disabled in the config file', WEB_DISABLED: 'Web interface is disabled in the config file',
DEPRECATED_BASIC_HEADER: 'basic authentication is deprecated, please use JWT instead',
BAD_FORMAT_USER_GROUP: 'user groups is different than an array',
};
export const APP_ERROR = {
CONFIG_NOT_VALID: 'CONFIG: it does not look like a valid config file',
}; };
export const DEFAULT_NO_README = 'ERROR: No README data found!'; export const DEFAULT_NO_README = 'ERROR: No README data found!';
export const WEB_TITLE = 'Verdaccio'; export const WEB_TITLE = 'Verdaccio';
export const PACKAGE_ACCESS = {
SCOPE: '@*/*',
ALL: '**',
};

@ -7,6 +7,7 @@ import {generateRandomHexString} from '../lib/crypto-utils';
import type {Package, Version} from '@verdaccio/types'; import type {Package, Version} from '@verdaccio/types';
import type {IStorage} from '../../types'; import type {IStorage} from '../../types';
import {API_ERROR, HTTP_STATUS} from './constants';
const pkgFileName = 'package.json'; const pkgFileName = 'package.json';
const fileExist: string = 'EEXISTS'; const fileExist: string = 'EEXISTS';
@ -123,11 +124,11 @@ export function cleanUpLinksRef(keepUpLinkData: boolean, result: Package): Packa
export function checkPackageLocal(name: string, localStorage: IStorage): Promise<any> { export function checkPackageLocal(name: string, localStorage: IStorage): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
localStorage.getPackageMetadata(name, (err, results) => { localStorage.getPackageMetadata(name, (err, results) => {
if (!_.isNil(err) && err.status !== 404) { if (!_.isNil(err) && err.status !== HTTP_STATUS.NOT_FOUND) {
return reject(err); return reject(err);
} }
if (results) { if (results) {
return reject(ErrorCode.getConflict('this package is already present')); return reject(ErrorCode.getConflict(API_ERROR.PACKAGE_EXIST));
} }
return resolve(); return resolve();
}); });
@ -152,25 +153,25 @@ export function checkPackageRemote(name: string, isAllowPublishOffline: boolean,
// $FlowFixMe // $FlowFixMe
syncMetadata(name, null, {}, (err, packageJsonLocal, upLinksErrors) => { syncMetadata(name, null, {}, (err, packageJsonLocal, upLinksErrors) => {
// something weird // something weird
if (err && err.status !== 404) { if (err && err.status !== HTTP_STATUS.NOT_FOUND) {
return reject(err); return reject(err);
} }
// checking package exist already // checking package exist already
if (_.isNil(packageJsonLocal) === false) { if (_.isNil(packageJsonLocal) === false) {
return reject(ErrorCode.getConflict('this package is already present')); return reject(ErrorCode.getConflict(API_ERROR.PACKAGE_EXIST));
} }
for (let errorItem = 0; errorItem < upLinksErrors.length; errorItem++) { for (let errorItem = 0; errorItem < upLinksErrors.length; errorItem++) {
// checking error // checking error
// if uplink fails with a status other than 404, we report failure // if uplink fails with a status other than 404, we report failure
if (_.isNil(upLinksErrors[errorItem][0]) === false) { if (_.isNil(upLinksErrors[errorItem][0]) === false) {
if (upLinksErrors[errorItem][0].status !== 404) { if (upLinksErrors[errorItem][0].status !== HTTP_STATUS.NOT_FOUND) {
if (isAllowPublishOffline) { if (isAllowPublishOffline) {
return resolve(); return resolve();
} }
return reject(ErrorCode.getServiceUnavailable('one of the uplinks is down, refuse to publish')); return reject(ErrorCode.getServiceUnavailable(API_ERROR.UPLINK_OFFLINE_PUBLISH));
} }
} }
} }

@ -26,6 +26,7 @@ Callback,
Logger, Logger,
} from '@verdaccio/types'; } from '@verdaccio/types';
import type {IReadTarball, IUploadTarball} from '@verdaccio/streams'; import type {IReadTarball, IUploadTarball} from '@verdaccio/streams';
import {hasProxyTo} from './config-utils';
const LoggerApi = require('../lib/logger'); const LoggerApi = require('../lib/logger');
@ -412,9 +413,9 @@ class Storage implements IStorageHandler {
packageInfo = generatePackageTemplate(name); packageInfo = generatePackageTemplate(name);
} }
for (let up in this.uplinks) { for (let uplink in this.uplinks) {
if (this.config.hasProxyTo(name, up)) { if (hasProxyTo(name, uplink, this.config.packages)) {
upLinks.push(this.uplinks[up]); upLinks.push(this.uplinks[uplink]);
} }
} }

@ -105,10 +105,10 @@ class ProxyStorage implements IProxy {
process.nextTick(function() { process.nextTick(function() {
if (cb) { if (cb) {
cb(ErrorCode.getInternalError('uplink is offline')); cb(ErrorCode.getInternalError(API_ERROR.UPLINK_OFFLINE));
} }
// $FlowFixMe // $FlowFixMe
streamRead.emit('error', ErrorCode.getInternalError('uplink is offline')); streamRead.emit('error', ErrorCode.getInternalError(API_ERROR.UPLINK_OFFLINE));
}); });
// $FlowFixMe // $FlowFixMe
streamRead._read = function() {}; streamRead._read = function() {};

@ -17,9 +17,22 @@ import type {$Request} from 'express';
import type {StringValue} from '../../types'; import type {StringValue} from '../../types';
const Logger = require('./logger'); const Logger = require('./logger');
const pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars
const pkgVersion = module.exports.version;
const pkgName = module.exports.name;
export const DIST_TAGS = 'dist-tags'; export const DIST_TAGS = 'dist-tags';
export function getUserAgent(): string {
assert(_.isString(pkgName));
assert(_.isString(pkgVersion));
return `${pkgName}/${pkgVersion}`;
}
export function buildBase64Buffer(payload: string): Buffer {
return new Buffer(payload, 'base64');
}
/** /**
* Validate a package. * Validate a package.
* @return {Boolean} whether the package is valid or not * @return {Boolean} whether the package is valid or not

@ -1,9 +1,5 @@
storage: ./.verdaccio_test_env/test-storage storage: ./.verdaccio_test_env/test-storage
users:
test:
password: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3
uplinks: uplinks:
npmjs: npmjs:
url: https://registry.npmjs.org/ url: https://registry.npmjs.org/

@ -4,17 +4,21 @@ import path from 'path';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import {HEADERS} from '../../../src/lib/constants'; import {HEADERS} from '../../../src/lib/constants';
import configDefault from '../partials/config/access'; import configDefault from '../partials/config/config_access';
import Config from '../../../src/lib/config'; import Config from '../../../src/lib/config';
import endPointAPI from '../../../src/api/index'; import endPointAPI from '../../../src/api/index';
import {mockServer} from './mock';
import {DOMAIN_SERVERS} from '../../functional/config.functional';
require('../../../src/lib/logger').setup([]); require('../../../src/lib/logger').setup([]);
describe('api with no limited access configuration', () => { describe('api with no limited access configuration', () => {
let config; let config;
let app; let app;
let mockRegistry;
beforeAll(function(done) { beforeAll(function(done) {
const mockServerPort = 55530;
const store = path.join(__dirname, './partials/store/access-storage'); const store = path.join(__dirname, './partials/store/access-storage');
rimraf(store, async () => { rimraf(store, async () => {
const configForTest = _.clone(configDefault); const configForTest = _.clone(configDefault);
@ -24,8 +28,14 @@ describe('api with no limited access configuration', () => {
} }
}; };
configForTest.self_path = store; configForTest.self_path = store;
configForTest.uplinks = {
npmjs: {
url: `http://${DOMAIN_SERVERS}:${mockServerPort}`
}
};
config = new Config(configForTest); config = new Config(configForTest);
app = await endPointAPI(config); app = await endPointAPI(config);
mockRegistry = await mockServer(mockServerPort).init();
done(); done();
}); });
}); });
@ -34,9 +44,11 @@ describe('api with no limited access configuration', () => {
const store = path.join(__dirname, './partials/store/access-storage'); const store = path.join(__dirname, './partials/store/access-storage');
rimraf(store, (err) => { rimraf(store, (err) => {
if (err) { if (err) {
mockRegistry[0].stop();
return done(err); return done(err);
} }
mockRegistry[0].stop();
return done(); return done();
}); });
}); });

@ -29,7 +29,7 @@ describe('endpoint unit test', () => {
const configForTest = _.clone(configDefault); const configForTest = _.clone(configDefault);
configForTest.auth = { configForTest.auth = {
htpasswd: { htpasswd: {
file: './test-storage/htpasswd-test' file: './test-storage/.htpasswd'
} }
}; };
configForTest.uplinks = { configForTest.uplinks = {

@ -0,0 +1,226 @@
// @flow
import path from 'path';
import {spliceURL} from '../../../src/utils/string';
import {parseConfigFile} from '../../../src/lib/utils';
import {
getMatchedPackagesSpec,
hasProxyTo,
normalisePackageAccess, sanityCheckUplinksProps,
uplinkSanityCheck
} from '../../../src/lib/config-utils';
import {PACKAGE_ACCESS, ROLES} from '../../../src/lib/constants';
describe('Config Utilities', () => {
const parsePartial = (name) => {
return path.join(__dirname, `../partials/config/yaml/${name}.yaml`);
};
describe('uplinkSanityCheck', () => {
test('should test basic conversion', ()=> {
const uplinks = uplinkSanityCheck(parseConfigFile(parsePartial('uplink-basic')).uplinks);
expect(Object.keys(uplinks)).toContain('server1');
expect(Object.keys(uplinks)).toContain('server2');
});
test('should throw error on blacklisted uplink name', ()=> {
const {uplinks} = parseConfigFile(parsePartial('uplink-wrong'));
expect(() => {
uplinkSanityCheck(uplinks)
}).toThrow('CONFIG: reserved uplink name: anonymous');
});
});
describe('sanityCheckUplinksProps', () => {
test('should fails if url prop is missing', ()=> {
const {uplinks} = parseConfigFile(parsePartial('uplink-wrong'));
expect(() => {
sanityCheckUplinksProps(uplinks)
}).toThrow('CONFIG: no url for uplink: none-url');
});
test('should bypass an empty uplink list', ()=> {
expect(sanityCheckUplinksProps([])).toHaveLength(0);
});
});
describe('normalisePackageAccess', () => {
test('should test basic conversion', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-basic'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
const all = access[`${PACKAGE_ACCESS.ALL}`];
expect(scoped).toBeDefined();
expect(all).toBeDefined();
});
test('should test multi group', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-multi-group'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
const all = access[`${PACKAGE_ACCESS.ALL}`];
expect(scoped).toBeDefined();
expect(scoped.access).toContain('$all');
expect(scoped.publish).toHaveLength(2);
expect(scoped.publish).toContain('admin');
expect(scoped.publish).toContain('superadmin');
expect(all).toBeDefined();
expect(all.access).toHaveLength(3);
expect(all.access).toContain('$all');
expect(all.publish).toHaveLength(1);
expect(all.publish).toContain('admin');
});
test('should deprecated packages props', ()=> {
const {packages} = parseConfigFile(parsePartial('deprecated-pkgs-basic'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
const all = access[`${PACKAGE_ACCESS.ALL}`];
const react = access['react-*'];
expect(react).toBeDefined();
expect(react.access).toBeDefined();
// $FlowFixMe
expect(react.access[0]).toBe(ROLES.$ALL);
expect(react.publish).toBeDefined();
// $FlowFixMe);
expect(react.publish[0]).toBe('admin');
expect(react.proxy).toBeDefined();
// $FlowFixMe
expect(react.proxy[0]).toBe('uplink2');
expect(react.storage).toBeDefined();
expect(react.storage).toBe('react-storage');
expect(scoped).toBeDefined();
expect(scoped.storage).not.toBeDefined();
expect(all).toBeDefined();
expect(all.access).toBeDefined();
expect(all.storage).not.toBeDefined();
expect(all.publish).toBeDefined();
expect(all.proxy).toBeDefined();
expect(all.allow_access).toBeUndefined();
expect(all.allow_publish).toBeUndefined();
expect(all.proxy_access).toBeUndefined();
});
test('should check not default packages access', ()=> {
const {packages} = parseConfigFile(parsePartial('pkgs-empty'));
const access = normalisePackageAccess(packages);
expect(access).toBeDefined();
const scoped = access[`${PACKAGE_ACCESS.SCOPE}`];
expect(scoped).toBeUndefined();
// ** should be added by default
const all = access[`${PACKAGE_ACCESS.ALL}`];
expect(all).toBeDefined();
expect(all.access).toBeUndefined();
expect(all.publish).toBeUndefined();
});
});
describe('getMatchedPackagesSpec', () => {
test('should test basic config', () => {
const {packages} = parseConfigFile(parsePartial('pkgs-custom'));
// $FlowFixMe
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
// $FlowFixMe
expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google');
// $FlowFixMe
expect(getMatchedPackagesSpec('vue', packages).proxy).toMatch('npmjs');
// $FlowFixMe
expect(getMatchedPackagesSpec('@scope/vue', packages).proxy).toMatch('npmjs');
});
test('should test no ** wildcard on config', () => {
const {packages} = parseConfigFile(parsePartial('pkgs-nosuper-wildcard-custom'));
// $FlowFixMe
expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook');
// $FlowFixMe
expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google');
// $FlowFixMe
expect(getMatchedPackagesSpec('@fake/angular', packages).proxy).toMatch('npmjs');
expect(getMatchedPackagesSpec('vue', packages)).toBeUndefined();
expect(getMatchedPackagesSpec('@scope/vue', packages)).toBeUndefined();
});
});
describe('hasProxyTo', () => {
test('should test basic config', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-basic')).packages);
// react
expect(hasProxyTo('react', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('react', 'google', packages)).toBeFalsy();
// vue
expect(hasProxyTo('vue', 'google', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'fake', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeTruthy();
// angular
expect(hasProxyTo('angular', 'google', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeTruthy();
});
test('should test resolve based on custom package access', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-custom')).packages);
// react
expect(hasProxyTo('react', 'facebook', packages)).toBeTruthy();
expect(hasProxyTo('react', 'google', packages)).toBeFalsy();
// vue
expect(hasProxyTo('vue', 'google', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'fake', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeTruthy();
// angular
expect(hasProxyTo('angular', 'google', packages)).toBeTruthy();
expect(hasProxyTo('angular', 'facebook', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
});
test('should not resolve any proxy', () => {
const packages = normalisePackageAccess(parseConfigFile(parsePartial('pkgs-empty')).packages);
// react
expect(hasProxyTo('react', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('react', 'npmjs', packages)).toBeFalsy();
// vue
expect(hasProxyTo('vue', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('vue', 'npmjs', packages)).toBeFalsy();
// angular
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
expect(hasProxyTo('angular', 'npmjs', packages)).toBeFalsy();
// private
expect(hasProxyTo('private', 'fake', packages)).toBeFalsy();
});
});
describe('spliceURL', () => {
test('should splice two strings and generate a url', () => {
const url: string = spliceURL('http://domain.com', '/-/static/logo.png');
expect(url).toMatch('http://domain.com/-/static/logo.png');
});
test('should splice a empty strings and generate a url', () => {
const url: string = spliceURL('', '/-/static/logo.png');
expect(url).toMatch('/-/static/logo.png');
});
});
});

@ -6,6 +6,7 @@ import {parseConfigFile} from '../../../src/lib/utils';
import {DEFAULT_REGISTRY, DEFAULT_UPLINK, ROLES, WEB_TITLE} from '../../../src/lib/constants'; import {DEFAULT_REGISTRY, DEFAULT_UPLINK, ROLES, WEB_TITLE} from '../../../src/lib/constants';
const resolveConf = (conf) => path.join(__dirname, `../../../conf/${conf}.yaml`); const resolveConf = (conf) => path.join(__dirname, `../../../conf/${conf}.yaml`);
require('../../../src/lib/logger').setup([]);
const checkDefaultUplink = (config) => { const checkDefaultUplink = (config) => {
expect(_.isObject(config.uplinks[DEFAULT_UPLINK])).toBeTruthy(); expect(_.isObject(config.uplinks[DEFAULT_UPLINK])).toBeTruthy();

@ -1,10 +1,10 @@
import assert from 'assert'; import assert from 'assert';
import Search from '../../../src/lib/search'; import Search from '../../../src/lib/search';
import Config from '../../../src/lib/config';
import Storage from '../../../src/lib/storage'; import Storage from '../../../src/lib/storage';
let config_hash = require('../partials/config/index'); let config_hash = require('../partials/config/index');
let Config = require('../../../src/lib/config');
require('../../../src/lib/logger').setup([]); require('../../../src/lib/logger').setup([]);

@ -8,7 +8,7 @@ import {setup} from '../../../src/lib/logger';
import type {Config, UpLinkConf} from '@verdaccio/types'; import type {Config, UpLinkConf} from '@verdaccio/types';
import type {IProxy} from '../../../types/index'; import type {IProxy} from '../../../types/index';
import {API_ERROR} from "../../../src/lib/constants"; import {API_ERROR, HTTP_STATUS} from "../../../src/lib/constants";
import {mockServer} from './mock'; import {mockServer} from './mock';
import {DOMAIN_SERVERS} from '../../functional/config.functional'; import {DOMAIN_SERVERS} from '../../functional/config.functional';
@ -102,8 +102,8 @@ describe('UpStorge', () => {
stream.on('error', function(err) { stream.on('error', function(err) {
expect(err).not.toBeNull(); expect(err).not.toBeNull();
expect(err.statusCode).toBe(404); expect(err.statusCode).toBe(HTTP_STATUS.NOT_FOUND);
expect(err.message).toMatch(/file doesn't exist on uplink/); expect(err.message).toMatch(API_ERROR.NOT_FILE_UPLINK);
done(); done();
}); });
@ -141,9 +141,9 @@ describe('UpStorge', () => {
const streamThirdTry = proxy.fetchTarball(tarball); const streamThirdTry = proxy.fetchTarball(tarball);
streamThirdTry.on('error', function(err) { streamThirdTry.on('error', function(err) {
expect(err).not.toBeNull(); expect(err).not.toBeNull();
expect(err.statusCode).toBe(500); expect(err.statusCode).toBe(HTTP_STATUS.INTERNAL_ERROR);
expect(proxy.failed_requests).toBe(2); expect(proxy.failed_requests).toBe(2);
expect(err.message).toMatch(/uplink is offline/); expect(err.message).toMatch(API_ERROR.UPLINK_OFFLINE);
done(); done();
}); });
}); });

@ -1,11 +1,10 @@
import path from 'path'; import path from 'path';
import {DEFAULT_REGISTRY} from '../../../../src/lib/constants';
const config = { const config = {
storage: path.join(__dirname, '../store/access-storage'), storage: path.join(__dirname, '../store/access-storage'),
uplinks: { uplinks: {
'npmjs': { 'npmjs': {
'url': DEFAULT_REGISTRY 'url': 'http://never_use:0000/'
} }
}, },
packages: { packages: {

@ -0,0 +1,14 @@
packages:
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'react-*':
allow_access: $all
publish: admin
proxy_access: uplink2
storage: 'react-storage'
'**':
allow_access: $all
allow_publish: $authenticated
proxy_access: npmjs

@ -0,0 +1,9 @@
packages:
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs

@ -0,0 +1,17 @@
packages:
'react':
access: admin
publish: admin
proxy: facebook
'angular':
access: admin
publish: admin
proxy: google
'@*/*':
access: $all
publish: $authenticated
proxy: npmjs
'**':
access: $all
publish: $authenticated
proxy: npmjs

@ -0,0 +1,4 @@
packages:
'private':
access: admin
publish: admin

@ -0,0 +1,9 @@
packages:
'@*/*':
access: $all
publish: admin superadmin
proxy: npmjs
'**':
access: $all user1 user2
publish: admin
proxy: npmjs

@ -0,0 +1,13 @@
packages:
'react':
access: admin
publish: admin
proxy: facebook
'angular':
access: admin
publish: admin
proxy: google
'@fake/*':
access: $all
publish: $authenticated
proxy: npmjs

@ -0,0 +1,7 @@
uplinks:
server1:
url: http://localhost:55551/
maxage: 0
server2:
url: http://localhost:55551/
maxage: 0

@ -0,0 +1,9 @@
uplinks:
facebook:
url: http://localhost:55551/
maxage: 0
anonymous:
url: http://localhost:55551/
maxage: 0
none-url:
maxage: 0

@ -8,6 +8,7 @@ import type {
MergeTags, MergeTags,
Config, Config,
Logger, Logger,
PackageAccess,
Package} from '@verdaccio/types'; Package} from '@verdaccio/types';
import type { import type {
IUploadTarball, IUploadTarball,
@ -95,6 +96,13 @@ export interface IStorageHandler {
_updateVersionsHiddenUpLink(versions: Versions, upLink: IProxy): void; _updateVersionsHiddenUpLink(versions: Versions, upLink: IProxy): void;
} }
export type StartUpConfig = {
storage: string;
self_path: string;
}
export type MatchedPackage = PackageAccess | void;
export interface IStorage { export interface IStorage {
config: Config; config: Config;
localData: ILocalData; localData: ILocalData;

BIN
yarn.lock

Binary file not shown.