diff --git a/packages/config/src/config-path.ts b/packages/config/src/config-path.ts index 96d39796b..d084f77ed 100644 --- a/packages/config/src/config-path.ts +++ b/packages/config/src/config-path.ts @@ -1,6 +1,5 @@ import fs from 'fs'; import path from 'path'; -import Path from 'path'; import _ from 'lodash'; import buildDebug from 'debug'; @@ -27,28 +26,34 @@ const debug = buildDebug('verdaccio:config'); * Find and get the first config file that match. * @return {String} the config file path */ -function findConfigFile(configPath: string | undefined): string { +function findConfigFile(configPath?: string): string { + // console.log(process.env); if (typeof configPath !== 'undefined') { - return Path.resolve(configPath); + return path.resolve(configPath); } const configPaths: SetupDirectory[] = getConfigPaths(); - + debug('%o posible locations found', configPaths.length); if (_.isEmpty(configPaths)) { + // this should never happens throw new Error('no configuration files can be processed'); } - const primaryConf: any = _.find(configPaths, (configLocation: any) => + // find the first location that already exist + const primaryConf: SetupDirectory | void = _.find(configPaths, (configLocation: SetupDirectory) => fileExists(configLocation.path) ); - if (_.isNil(primaryConf) === false) { + + if (typeof primaryConf !== 'undefined') { + debug('previous location exist already %s', primaryConf?.path); return primaryConf.path; } + // @ts-ignore return createConfigFile(_.head(configPaths)).path; } -function createConfigFile(configLocation: any): SetupDirectory { +function createConfigFile(configLocation: SetupDirectory): SetupDirectory { createConfigFolder(configLocation); const defaultConfig = updateStorageLinks(configLocation, readDefaultConfig()); @@ -60,13 +65,18 @@ function createConfigFile(configLocation: any): SetupDirectory { export function readDefaultConfig(): Buffer { const pathDefaultConf: string = path.resolve(__dirname, 'conf/default.yaml'); - + try { + debug('default configuration file %s', pathDefaultConf); + fs.accessSync(pathDefaultConf, fs.constants.R_OK); + } catch { + throw new TypeError('configuration file does not have enough permissions for reading'); + } // @ts-ignore return fs.readFileSync(pathDefaultConf, CHARACTER_ENCODING.UTF8); } function createConfigFolder(configLocation): void { - fs.mkdirSync(Path.dirname(configLocation.path), { recursive: true }); + fs.mkdirSync(path.dirname(configLocation.path), { recursive: true }); debug(`Creating default config file in %o`, configLocation?.path); } @@ -78,64 +88,89 @@ function updateStorageLinks(configLocation, defaultConfig): string { // $XDG_DATA_HOME defines the base directory relative to which user specific data // files should be stored, If $XDG_DATA_HOME is either not set or empty, a default // equal to $HOME/.local/share should be used. - // $FlowFixMe let dataDir = - process.env.XDG_DATA_HOME || Path.join(process.env.HOME as string, '.local', 'share'); + process.env.XDG_DATA_HOME || path.join(process.env.HOME as string, '.local', 'share'); if (folderExists(dataDir)) { - dataDir = Path.resolve(Path.join(dataDir, pkgJSON.name, 'storage')); + debug(`previous storage located`); + debug(`update storage links to %s`, dataDir); + dataDir = path.resolve(path.join(dataDir, pkgJSON.name, 'storage')); return defaultConfig.replace(/^storage: .\/storage$/m, `storage: ${dataDir}`); } + debug(`could not find a previous storage location, skip override`); return defaultConfig; } +/** + * Return a list of configuration locations by platform. + * @returns + */ function getConfigPaths(): SetupDirectory[] { - const listPaths: SetupDirectory[] = [ + const listPaths: (SetupDirectory | void)[] = [ getXDGDirectory(), getWindowsDirectory(), getRelativeDefaultDirectory(), getOldDirectory(), - ].reduce(function (acc, currentValue: any): SetupDirectory[] { - if (_.isUndefined(currentValue) === false) { + ]; + + return listPaths.reduce(function (acc, currentValue: SetupDirectory | void): SetupDirectory[] { + if (typeof currentValue !== 'undefined') { + debug('directory detected path %s for type %s', currentValue?.path, currentValue.type); acc.push(currentValue); } return acc; }, [] as SetupDirectory[]); - - return listPaths; } +/** + * Get XDG_CONFIG_HOME or HOME location (usually unix) + * @returns + */ const getXDGDirectory = (): SetupDirectory | void => { - const XDGConfig = getXDGHome() || (process.env.HOME && Path.join(process.env.HOME, '.config')); - - if (XDGConfig && folderExists(XDGConfig)) { + const xDGConfigPath = + process.env.XDG_CONFIG_HOME || (process.env.HOME && path.join(process.env.HOME, '.config')); + if (xDGConfigPath && folderExists(xDGConfigPath)) { + debug('XDGConfig folder path %s', xDGConfigPath); return { - path: Path.join(XDGConfig, pkgJSON.name, CONFIG_FILE), + path: path.join(xDGConfigPath, pkgJSON.name, CONFIG_FILE), type: XDG, }; } }; -const getXDGHome = (): string | void => process.env.XDG_CONFIG_HOME; - +/** + * Detect windows location, APPDATA + * usually something like C:\User\\AppData\Local + * @returns + */ const getWindowsDirectory = (): SetupDirectory | void => { if (process.platform === WIN32 && process.env.APPDATA && folderExists(process.env.APPDATA)) { + debug('is windows appdata: %s', process.env.APPDATA); return { - path: Path.resolve(Path.join(process.env.APPDATA, pkgJSON.name, CONFIG_FILE)), + path: path.resolve(path.join(process.env.APPDATA, pkgJSON.name, CONFIG_FILE)), type: WIN, }; } }; +/** + * Return relative directory, this is the default. + * It will cretate config in your {currentLocation/verdaccio/config.yaml} + * @returns + */ const getRelativeDefaultDirectory = (): SetupDirectory => { return { - path: Path.resolve(Path.join('.', pkgJSON.name, CONFIG_FILE)), + path: path.resolve(path.join('.', pkgJSON.name, CONFIG_FILE)), type: 'def', }; }; +/** + * This should never happens, consider it DEPRECATED + * @returns + */ const getOldDirectory = (): SetupDirectory => { return { - path: Path.resolve(Path.join('.', CONFIG_FILE)), + path: path.resolve(path.join('.', CONFIG_FILE)), type: 'old', }; }; diff --git a/packages/config/src/config.ts b/packages/config/src/config.ts index a6ff40225..1d7762d6e 100644 --- a/packages/config/src/config.ts +++ b/packages/config/src/config.ts @@ -106,7 +106,7 @@ class Config implements AppConfig { /** * Store or create whether receive a secret key */ - public checkSecretKey(secret: string): string { + public checkSecretKey(secret?: string): string { debug('check secret key'); if (_.isString(secret) && _.isEmpty(secret) === false) { this.secret = secret; diff --git a/packages/config/test/config.path.spec.ts b/packages/config/test/config.path.spec.ts new file mode 100644 index 000000000..3f1545331 --- /dev/null +++ b/packages/config/test/config.path.spec.ts @@ -0,0 +1,105 @@ +import os from 'os'; +import { findConfigFile } from '../src/config-path'; + +const mockmkDir = jest.fn(); +const mockaccessSync = jest.fn(); +const mockwriteFile = jest.fn(); + +jest.mock('fs', () => { + const fsOri = jest.requireActual('fs'); + return { + ...fsOri, + statSync: (path) => ({ + isDirectory: () => { + if (path.match(/fail/)) { + throw Error('file does not exist'); + } + return true; + }, + }), + accessSync: (a) => mockaccessSync(a), + mkdirSync: (a) => mockmkDir(a), + writeFileSync: (a) => mockwriteFile(a), + }; +}); + +jest.mock('fs'); + +describe('config-path', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + describe('findConfigFile', () => { + if (os.platform() !== 'win32') { + describe('using defiled location from arguments', () => { + test('with custom location', () => { + expect(findConfigFile('/home/user/custom/location/config.yaml')).toEqual( + '/home/user/custom/location/config.yaml' + ); + expect(mockwriteFile).not.toHaveBeenCalled(); + expect(mockmkDir).not.toHaveBeenCalled(); + }); + }); + + describe('whith env variables', () => { + test('with XDG_CONFIG_HOME if directory exist but config file is missing', () => { + process.env.XDG_CONFIG_HOME = '/home/user'; + expect(findConfigFile()).toEqual('/home/user/verdaccio/config.yaml'); + expect(mockwriteFile).toHaveBeenCalledWith('/home/user/verdaccio/config.yaml'); + expect(mockmkDir).toHaveBeenCalledWith('/home/user/verdaccio'); + }); + + test('with HOME if directory exist but config file is missing', () => { + delete process.env.XDG_CONFIG_HOME; + process.env.HOME = '/home/user'; + expect(findConfigFile()).toEqual('/home/user/.config/verdaccio/config.yaml'); + expect(mockwriteFile).toHaveBeenCalledWith('/home/user/.config/verdaccio/config.yaml'); + expect(mockmkDir).toHaveBeenCalledWith('/home/user/.config/verdaccio'); + }); + + describe('error handling', () => { + test('XDG_CONFIG_HOME is not directory fallback to default', () => { + process.env.XDG_CONFIG_HOME = '/home/user/fail'; + mockaccessSync.mockImplementation(() => {}); + mockwriteFile.mockImplementation(() => {}); + expect(findConfigFile()).toMatch('packages/config/verdaccio/config.yaml'); + }); + + test('no permissions on read default config file', () => { + process.env.XDG_CONFIG_HOME = '/home/user'; + mockaccessSync.mockImplementation(() => { + throw new Error('error on write file'); + }); + + expect(function () { + findConfigFile(); + }).toThrow(/configuration file does not have enough permissions for reading/); + }); + }); + }); + + describe('with no env variables', () => { + test('with relative location', () => { + mockaccessSync.mockImplementation(() => {}); + delete process.env.XDG_CONFIG_HOME; + delete process.env.HOME; + process.env.APPDATA = '/app/data/'; + expect(findConfigFile()).toMatch('packages/config/verdaccio/config.yaml'); + expect(mockwriteFile).toHaveBeenCalled(); + expect(mockmkDir).toHaveBeenCalled(); + }); + }); + } else { + test('with windows as directory exist but config file is missing', () => { + delete process.env.XDG_CONFIG_HOME; + delete process.env.HOME; + process.env.APPDATA = '/app/data/'; + expect(findConfigFile()).toEqual('D:\\app\\data\\verdaccio\\config.yaml'); + expect(mockwriteFile).toHaveBeenCalledWith('D:\\app\\data\\verdaccio\\config.yaml'); + expect(mockmkDir).toHaveBeenCalledWith('D:\\app\\data\\verdaccio'); + }); + } + }); +}); diff --git a/packages/config/test/config.spec.ts b/packages/config/test/config.spec.ts index 09b50e3e9..eb0f15122 100644 --- a/packages/config/test/config.spec.ts +++ b/packages/config/test/config.spec.ts @@ -9,6 +9,7 @@ import { parseConfigFile, ROLES, WEB_TITLE, + getMatchedPackagesSpec, } from '../src'; import { parseConfigurationFile } from './utils'; @@ -23,56 +24,56 @@ const checkDefaultUplink = (config) => { expect(config.uplinks[DEFAULT_UPLINK].url).toMatch(DEFAULT_REGISTRY); }; -const checkDefaultConfPackages = (config) => { - // auth - expect(_.isObject(config.auth)).toBeTruthy(); - expect(_.isObject(config.auth.htpasswd)).toBeTruthy(); - expect(config.auth.htpasswd.file).toMatch(/htpasswd/); - - // web - expect(_.isObject(config.web)).toBeTruthy(); - expect(config.web.title).toBe(WEB_TITLE); - expect(config.web.enable).toBeUndefined(); - - // packages - expect(_.isObject(config.packages)).toBeTruthy(); - expect(Object.keys(config.packages).join('|')).toBe('@*/*|**'); - expect(config.packages['@*/*'].access).toBeDefined(); - expect(config.packages['@*/*'].access).toContainEqual(ROLES.$ALL); - expect(config.packages['@*/*'].publish).toBeDefined(); - expect(config.packages['@*/*'].publish).toContainEqual(ROLES.$AUTH); - expect(config.packages['@*/*'].proxy).toBeDefined(); - expect(config.packages['@*/*'].proxy).toContainEqual(DEFAULT_UPLINK); - expect(config.packages['**'].access).toBeDefined(); - expect(config.packages['**'].access).toContainEqual(ROLES.$ALL); - expect(config.packages['**'].publish).toBeDefined(); - expect(config.packages['**'].publish).toContainEqual(ROLES.$AUTH); - expect(config.packages['**'].proxy).toBeDefined(); - expect(config.packages['**'].proxy).toContainEqual(DEFAULT_UPLINK); - // uplinks - expect(config.uplinks[DEFAULT_UPLINK]).toBeDefined(); - expect(config.uplinks[DEFAULT_UPLINK].url).toEqual(DEFAULT_REGISTRY); - // audit - expect(config.middlewares).toBeDefined(); - expect(config.middlewares.audit).toBeDefined(); - expect(config.middlewares.audit.enabled).toBeTruthy(); - // logs - expect(config.logs).toBeDefined(); - expect(config.logs.type).toEqual('stdout'); - expect(config.logs.format).toEqual('pretty'); - expect(config.logs.level).toEqual('http'); - // must not be enabled by default - expect(config.notify).toBeUndefined(); - expect(config.store).toBeUndefined(); - expect(config.publish).toBeUndefined(); - expect(config.url_prefix).toBeUndefined(); - expect(config.url_prefix).toBeUndefined(); - - expect(config.experiments).toBeUndefined(); - expect(config.security).toEqual(defaultSecurity); -}; - describe('check basic content parsed file', () => { + const checkDefaultConfPackages = (config) => { + // auth + expect(_.isObject(config.auth)).toBeTruthy(); + expect(_.isObject(config.auth.htpasswd)).toBeTruthy(); + expect(config.auth.htpasswd.file).toMatch(/htpasswd/); + + // web + expect(_.isObject(config.web)).toBeTruthy(); + expect(config.web.title).toBe(WEB_TITLE); + expect(config.web.enable).toBeUndefined(); + + // packages + expect(_.isObject(config.packages)).toBeTruthy(); + expect(Object.keys(config.packages).join('|')).toBe('@*/*|**'); + expect(config.packages['@*/*'].access).toBeDefined(); + expect(config.packages['@*/*'].access).toContainEqual(ROLES.$ALL); + expect(config.packages['@*/*'].publish).toBeDefined(); + expect(config.packages['@*/*'].publish).toContainEqual(ROLES.$AUTH); + expect(config.packages['@*/*'].proxy).toBeDefined(); + expect(config.packages['@*/*'].proxy).toContainEqual(DEFAULT_UPLINK); + expect(config.packages['**'].access).toBeDefined(); + expect(config.packages['**'].access).toContainEqual(ROLES.$ALL); + expect(config.packages['**'].publish).toBeDefined(); + expect(config.packages['**'].publish).toContainEqual(ROLES.$AUTH); + expect(config.packages['**'].proxy).toBeDefined(); + expect(config.packages['**'].proxy).toContainEqual(DEFAULT_UPLINK); + // uplinks + expect(config.uplinks[DEFAULT_UPLINK]).toBeDefined(); + expect(config.uplinks[DEFAULT_UPLINK].url).toEqual(DEFAULT_REGISTRY); + // audit + expect(config.middlewares).toBeDefined(); + expect(config.middlewares.audit).toBeDefined(); + expect(config.middlewares.audit.enabled).toBeTruthy(); + // logs + expect(config.logs).toBeDefined(); + expect(config.logs.type).toEqual('stdout'); + expect(config.logs.format).toEqual('pretty'); + expect(config.logs.level).toEqual('http'); + // must not be enabled by default + expect(config.notify).toBeUndefined(); + expect(config.store).toBeUndefined(); + expect(config.publish).toBeUndefined(); + expect(config.url_prefix).toBeUndefined(); + expect(config.url_prefix).toBeUndefined(); + + expect(config.experiments).toBeUndefined(); + expect(config.security).toEqual(defaultSecurity); + }; + test('parse default.yaml', () => { const config = new Config(parseConfigFile(resolveConf('default'))); checkDefaultUplink(config); @@ -81,6 +82,57 @@ describe('check basic content parsed file', () => { checkDefaultConfPackages(config); }); + test('parse docker.yaml', () => { + const config = new Config(parseConfigFile(resolveConf('docker'))); + checkDefaultUplink(config); + expect(config.storage).toBe('/verdaccio/storage/data'); + expect(config.auth.htpasswd.file).toBe('/verdaccio/storage/htpasswd'); + checkDefaultConfPackages(config); + }); +}); + +describe('checkSecretKey', () => { + test('with default.yaml and pre selected secret', () => { + const config = new Config(parseConfigFile(resolveConf('default'))); + expect(config.checkSecretKey('12345')).toEqual('12345'); + }); + + test('with default.yaml and void secret', () => { + const config = new Config(parseConfigFile(resolveConf('default'))); + expect(typeof config.checkSecretKey() === 'string').toBeTruthy(); + }); + + test('with default.yaml and emtpy string secret', () => { + const config = new Config(parseConfigFile(resolveConf('default'))); + expect(typeof config.checkSecretKey('') === 'string').toBeTruthy(); + }); +}); + +describe('getMatchedPackagesSpec', () => { + test('should match with react as defined in config file', () => { + const configParsed = parseConfigFile(parseConfigurationFile('config-getMatchedPackagesSpec')); + const config = new Config(configParsed); + expect(config.getMatchedPackagesSpec('react')).toEqual({ + access: ['admin'], + proxy: ['facebook'], + publish: ['admin'], + unpublish: false, + }); + }); + + test('should not match with react as defined in config file', () => { + const configParsed = parseConfigFile(parseConfigurationFile('config-getMatchedPackagesSpec')); + const config = new Config(configParsed); + expect(config.getMatchedPackagesSpec('somePackage')).toEqual({ + access: [ROLES.$ALL], + proxy: ['npmjs'], + publish: [ROLES.$AUTH], + unpublish: false, + }); + }); +}); + +describe('VERDACCIO_STORAGE_PATH', () => { test('should set storage to value set in VERDACCIO_STORAGE_PATH environment variable', () => { const storageLocation = '/tmp/verdaccio'; process.env.VERDACCIO_STORAGE_PATH = storageLocation; @@ -106,12 +158,4 @@ describe('check basic content parsed file', () => { expect(config.storage).toBe(storageLocation); delete process.env.VERDACCIO_STORAGE_PATH; }); - - test('parse docker.yaml', () => { - const config = new Config(parseConfigFile(resolveConf('docker'))); - checkDefaultUplink(config); - expect(config.storage).toBe('/verdaccio/storage/data'); - expect(config.auth.htpasswd.file).toBe('/verdaccio/storage/htpasswd'); - checkDefaultConfPackages(config); - }); }); diff --git a/packages/config/test/package-access.spec.ts b/packages/config/test/package-access.spec.ts index a78bbb613..228c367a8 100644 --- a/packages/config/test/package-access.spec.ts +++ b/packages/config/test/package-access.spec.ts @@ -88,26 +88,17 @@ describe('Package access utilities', () => { () => { const { packages } = parseConfigFile(parseConfigurationFile('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(); - - // Intended checks, Typescript should catch this, we test the runtime part - // @ts-ignore expect(react.access).toEqual([]); - // @ts-ignore expect(react.publish[0]).toBe('admin'); expect(react.proxy).toBeDefined(); - // @ts-ignore expect(react.proxy).toEqual([]); expect(react.storage).toBeDefined(); - expect(react.storage).toBe('react-storage'); expect(scoped).toBeDefined(); expect(scoped.storage).not.toBeDefined(); @@ -126,7 +117,6 @@ describe('Package access utilities', () => { const scoped = access[`${PACKAGE_ACCESS.SCOPE}`]; expect(scoped).toBeUndefined(); - // ** should be added by default ** const all = access[`${PACKAGE_ACCESS.ALL}`]; expect(all).toBeDefined(); @@ -141,23 +131,23 @@ describe('Package access utilities', () => { describe('getMatchedPackagesSpec', () => { test('should test basic config', () => { const { packages } = parseConfigFile(parseConfigurationFile('pkgs-custom')); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook'); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google'); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('vue', packages).proxy).toMatch('npmjs'); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('@scope/vue', packages).proxy).toMatch('npmjs'); }); test('should test no ** wildcard on config', () => { const { packages } = parseConfigFile(parseConfigurationFile('pkgs-nosuper-wildcard-custom')); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('react', packages).proxy).toMatch('facebook'); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('angular', packages).proxy).toMatch('google'); - // @ts-ignore + // @ts-expect-error expect(getMatchedPackagesSpec('@fake/angular', packages).proxy).toMatch('npmjs'); expect(getMatchedPackagesSpec('vue', packages)).toBeUndefined(); expect(getMatchedPackagesSpec('@scope/vue', packages)).toBeUndefined(); diff --git a/packages/config/test/partials/config/yaml/config-getMatchedPackagesSpec.yaml b/packages/config/test/partials/config/yaml/config-getMatchedPackagesSpec.yaml new file mode 100644 index 000000000..4ee938acf --- /dev/null +++ b/packages/config/test/partials/config/yaml/config-getMatchedPackagesSpec.yaml @@ -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