mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-11 14:36:25 +00:00
feat(npm): try auth recursive (#5698)
This commit is contained in:
parent
6cbd4a7743
commit
707d35db30
7 changed files with 156 additions and 44 deletions
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`api/npm should fetch package info from custom registry 1`] = `
|
||||
exports[`datasource/npm/index should fetch package info from custom registry 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -28,7 +28,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should fetch package info from npm 1`] = `
|
||||
exports[`datasource/npm/index should fetch package info from npm 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -56,7 +56,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should handle foobar 1`] = `
|
||||
exports[`datasource/npm/index should handle foobar 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -84,7 +84,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should handle no time 1`] = `
|
||||
exports[`datasource/npm/index should handle no time 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -110,7 +110,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should parse repo url (string) 1`] = `
|
||||
exports[`datasource/npm/index should parse repo url (string) 1`] = `
|
||||
Object {
|
||||
"homepage": undefined,
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -131,7 +131,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should parse repo url 1`] = `
|
||||
exports[`datasource/npm/index should parse repo url 1`] = `
|
||||
Object {
|
||||
"homepage": undefined,
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -152,7 +152,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should replace any environment variable in npmrc 1`] = `
|
||||
exports[`datasource/npm/index should replace any environment variable in npmrc 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -180,7 +180,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should return deprecated 1`] = `
|
||||
exports[`datasource/npm/index should return deprecated 1`] = `
|
||||
Object {
|
||||
"deprecationMessage": "On registry \`https://registry.npmjs.org/\`, the \\"latest\\" version (v0.0.2) of dependency \`foobar\` has the following deprecation notice:
|
||||
|
||||
|
@ -214,7 +214,7 @@ Marking the latest version of an npm package as deprecated results in the entire
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should return deprecated 2`] = `
|
||||
exports[`datasource/npm/index should return deprecated 2`] = `
|
||||
"On registry \`https://registry.npmjs.org/\`, the \\"latest\\" version (v0.0.2) of dependency \`foobar\` has the following deprecation notice:
|
||||
|
||||
\`This is deprecated\`
|
||||
|
@ -222,7 +222,7 @@ exports[`api/npm should return deprecated 2`] = `
|
|||
Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake."
|
||||
`;
|
||||
|
||||
exports[`api/npm should send an authorization header if provided 1`] = `
|
||||
exports[`datasource/npm/index should send an authorization header if provided 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -250,7 +250,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should use NPM_TOKEN if provided 1`] = `
|
||||
exports[`datasource/npm/index should use NPM_TOKEN if provided 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
@ -278,7 +278,7 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`api/npm should use default registry if missing from npmrc 1`] = `
|
||||
exports[`datasource/npm/index should use default registry if missing from npmrc 1`] = `
|
||||
Object {
|
||||
"homepage": "https://github.com/renovateapp/dummy",
|
||||
"latestVersion": "0.0.1",
|
||||
|
|
128
lib/datasource/npm/get.spec.ts
Normal file
128
lib/datasource/npm/get.spec.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
import got from 'got';
|
||||
import { getName, partial } from '../../../test/util';
|
||||
import { getDependency, resetMemCache } from './get';
|
||||
import { setNpmrc } from './npmrc';
|
||||
import * as _got from '../../util/got';
|
||||
import { DatasourceError } from '../common';
|
||||
|
||||
jest.mock('../../util/got');
|
||||
|
||||
const api: jest.Mock<got.GotPromise<object>> = _got.api as never;
|
||||
|
||||
describe(getName(__filename), () => {
|
||||
function mock(body: object): void {
|
||||
api.mockResolvedValueOnce(
|
||||
partial<got.Response<object>>({ body })
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
resetMemCache();
|
||||
mock({ body: { name: '@myco/test' } });
|
||||
});
|
||||
|
||||
describe('has bearer auth', () => {
|
||||
const configs = [
|
||||
`registry=https://test.org\n//test.org/:_authToken=XXX`,
|
||||
`registry=https://test.org/sub\n//test.org/:_authToken=XXX`,
|
||||
`registry=https://test.org/sub\n//test.org/sub/:_authToken=XXX`,
|
||||
`registry=https://test.org/sub\n_authToken=XXX`,
|
||||
`registry=https://test.org\n_authToken=XXX`,
|
||||
`registry=https://test.org\n_authToken=XXX`,
|
||||
`@myco:registry=https://test.org\n//test.org/:_authToken=XXX`,
|
||||
];
|
||||
|
||||
it.each(configs)('%p', async npmrc => {
|
||||
expect.assertions(1);
|
||||
setNpmrc(npmrc);
|
||||
await getDependency('@myco/test', 0);
|
||||
|
||||
expect(api.mock.calls[0][1].headers.authorization).toEqual('Bearer XXX');
|
||||
});
|
||||
});
|
||||
|
||||
describe('has basic auth', () => {
|
||||
const configs = [
|
||||
`registry=https://test.org\n//test.org/:_auth=dGVzdDp0ZXN0`,
|
||||
`registry=https://test.org\n//test.org/:username=test\n//test.org/:_password=dGVzdA==`,
|
||||
`registry=https://test.org/sub\n//test.org/:_auth=dGVzdDp0ZXN0`,
|
||||
`registry=https://test.org/sub\n//test.org/sub/:_auth=dGVzdDp0ZXN0`,
|
||||
`registry=https://test.org/sub\n_auth=dGVzdDp0ZXN0`,
|
||||
`registry=https://test.org\n_auth=dGVzdDp0ZXN0`,
|
||||
`registry=https://test.org\n_auth=dGVzdDp0ZXN0`,
|
||||
`@myco:registry=https://test.org\n//test.org/:_auth=dGVzdDp0ZXN0`,
|
||||
`@myco:registry=https://test.org\n_auth=dGVzdDp0ZXN0`,
|
||||
];
|
||||
|
||||
it.each(configs)('%p', async npmrc => {
|
||||
expect.assertions(1);
|
||||
setNpmrc(npmrc);
|
||||
await getDependency('@myco/test', 0);
|
||||
|
||||
expect(api.mock.calls[0][1].headers.authorization).toEqual(
|
||||
'Basic dGVzdDp0ZXN0'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no auth', () => {
|
||||
const configs = [
|
||||
`@myco:registry=https://test.org\n_authToken=XXX`,
|
||||
`@myco:registry=https://test.org\n//test.org/sub/:_authToken=XXX`,
|
||||
`@myco:registry=https://test.org\n//test.org/sub/:_auth=dGVzdDp0ZXN0`,
|
||||
`@myco:registry=https://test.org`,
|
||||
`registry=https://test.org`,
|
||||
];
|
||||
|
||||
it.each(configs)('%p', async npmrc => {
|
||||
expect.assertions(1);
|
||||
setNpmrc(npmrc);
|
||||
await getDependency('@myco/test', 0);
|
||||
|
||||
expect(api.mock.calls[0][1].headers.authorization).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('cover all paths', async () => {
|
||||
expect.assertions(9);
|
||||
|
||||
setNpmrc('registry=https://test.org\n_authToken=XXX');
|
||||
|
||||
expect(await getDependency('none', 0)).toBeNull();
|
||||
|
||||
mock({
|
||||
name: '@myco/test',
|
||||
repository: {},
|
||||
versions: { '1.0.0': {} },
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
});
|
||||
expect(await getDependency('@myco/test', 0)).toBeDefined();
|
||||
|
||||
mock({
|
||||
name: '@myco/test2',
|
||||
versions: { '1.0.0': {} },
|
||||
'dist-tags': { latest: '1.0.0' },
|
||||
});
|
||||
expect(await getDependency('@myco/test2', 0)).toBeDefined();
|
||||
|
||||
api.mockRejectedValueOnce({ statusCode: 401 });
|
||||
expect(await getDependency('error-401', 0)).toBeNull();
|
||||
api.mockRejectedValueOnce({ statusCode: 402 });
|
||||
expect(await getDependency('error-402', 0)).toBeNull();
|
||||
api.mockRejectedValueOnce({ statusCode: 404 });
|
||||
expect(await getDependency('error-404', 0)).toBeNull();
|
||||
|
||||
api.mockRejectedValueOnce({});
|
||||
expect(await getDependency('error4', 0)).toBeNull();
|
||||
|
||||
setNpmrc();
|
||||
api.mockRejectedValueOnce({ name: 'ParseError', body: 'parse-error' });
|
||||
await expect(getDependency('npm-parse-error', 0)).rejects.toThrow(
|
||||
DatasourceError
|
||||
);
|
||||
|
||||
api.mockRejectedValueOnce({ statusCode: 402 });
|
||||
expect(await getDependency('npm-error-402', 0)).toBeNull();
|
||||
});
|
||||
});
|
|
@ -3,7 +3,6 @@ import moment from 'moment';
|
|||
import url from 'url';
|
||||
import getRegistryUrl from 'registry-auth-token/registry-url';
|
||||
import registryAuthToken from 'registry-auth-token';
|
||||
import isBase64 from 'validator/lib/isBase64';
|
||||
import { OutgoingHttpHeaders } from 'http';
|
||||
import is from '@sindresorhus/is';
|
||||
import { logger } from '../../logger';
|
||||
|
@ -75,15 +74,19 @@ export async function getDependency(
|
|||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
}
|
||||
const authInfo = registryAuthToken(regUrl, { npmrc });
|
||||
const headers: OutgoingHttpHeaders = {};
|
||||
let authInfo = registryAuthToken(regUrl, { npmrc, recursive: true });
|
||||
|
||||
if (
|
||||
!authInfo &&
|
||||
npmrc &&
|
||||
npmrc._authToken &&
|
||||
regUrl.replace(/\/?$/, '/') === npmrc.registry?.replace(/\/?$/, '/')
|
||||
) {
|
||||
authInfo = { type: 'Bearer', token: npmrc._authToken };
|
||||
}
|
||||
|
||||
if (authInfo && authInfo.type && authInfo.token) {
|
||||
// istanbul ignore if
|
||||
if (npmrc && npmrc.massagedAuth && isBase64(authInfo.token)) {
|
||||
logger.debug('Massaging authorization type to Basic');
|
||||
authInfo.type = 'Basic';
|
||||
}
|
||||
headers.authorization = `${authInfo.type} ${authInfo.token}`;
|
||||
logger.trace(
|
||||
{ token: maskToken(authInfo.token), npmName: packageName },
|
||||
|
@ -115,7 +118,6 @@ export async function getDependency(
|
|||
useCache,
|
||||
};
|
||||
const raw = await got(pkgUrl, opts);
|
||||
// istanbul ignore if
|
||||
if (retries < 3) {
|
||||
logger.debug({ pkgUrl, retries }, 'Recovered from npm error');
|
||||
}
|
||||
|
@ -207,7 +209,6 @@ export async function getDependency(
|
|||
);
|
||||
return null;
|
||||
}
|
||||
// istanbul ignore if
|
||||
if (err.statusCode === 402) {
|
||||
logger.debug(
|
||||
{
|
||||
|
@ -231,7 +232,6 @@ export async function getDependency(
|
|||
return null;
|
||||
}
|
||||
if (uri.host === 'registry.npmjs.org') {
|
||||
// istanbul ignore if
|
||||
if (
|
||||
(err.name === 'ParseError' ||
|
||||
err.code === 'ECONNRESET' ||
|
||||
|
@ -242,13 +242,11 @@ export async function getDependency(
|
|||
await delay(5000);
|
||||
return getDependency(packageName, retries - 1);
|
||||
}
|
||||
// istanbul ignore if
|
||||
if (err.name === 'ParseError' && err.body) {
|
||||
err.body = 'err.body deleted by Renovate';
|
||||
}
|
||||
throw new DatasourceError(err);
|
||||
}
|
||||
// istanbul ignore next
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@ import nock from 'nock';
|
|||
import moment from 'moment';
|
||||
import * as npm from '.';
|
||||
import { DATASOURCE_FAILURE } from '../../constants/error-messages';
|
||||
import { getName } from '../../../test/util';
|
||||
|
||||
jest.mock('registry-auth-token');
|
||||
jest.mock('delay');
|
||||
|
||||
const registryAuthToken: any = _registryAuthToken;
|
||||
const registryAuthToken: jest.Mock<_registryAuthToken.NpmCredentials> = _registryAuthToken as never;
|
||||
let npmResponse: any;
|
||||
|
||||
function getRelease(
|
||||
|
@ -19,13 +20,14 @@ function getRelease(
|
|||
);
|
||||
}
|
||||
|
||||
describe('api/npm', () => {
|
||||
describe(getName(__filename), () => {
|
||||
delete process.env.NPM_TOKEN;
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
global.repoCache = {};
|
||||
global.trustLevel = 'low';
|
||||
npm.resetCache();
|
||||
npm.setNpmrc();
|
||||
npmResponse = {
|
||||
name: 'foobar',
|
||||
versions: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import ini from 'ini';
|
||||
import isBase64 from 'validator/lib/isBase64';
|
||||
import { logger } from '../../logger';
|
||||
|
||||
let npmrc: Record<string, any> | null = null;
|
||||
|
@ -36,7 +35,6 @@ export function setNpmrc(input?: string): void {
|
|||
npmrcRaw = input;
|
||||
logger.debug('Setting npmrc');
|
||||
npmrc = ini.parse(input.replace(/\\n/g, '\n'));
|
||||
// massage _auth to _authToken
|
||||
for (const [key, val] of Object.entries(npmrc)) {
|
||||
// istanbul ignore if
|
||||
if (
|
||||
|
@ -52,21 +50,13 @@ export function setNpmrc(input?: string): void {
|
|||
npmrc = existingNpmrc;
|
||||
return;
|
||||
}
|
||||
if (key !== '_auth' && key.endsWith('_auth') && isBase64(val)) {
|
||||
logger.debug('Massaging _auth to _authToken');
|
||||
npmrc[key + 'Token'] = val;
|
||||
npmrc.massagedAuth = true;
|
||||
delete npmrc[key];
|
||||
}
|
||||
}
|
||||
if (global.trustLevel !== 'high') {
|
||||
return;
|
||||
}
|
||||
for (const key in npmrc) {
|
||||
if (Object.prototype.hasOwnProperty.call(npmrc, key)) {
|
||||
for (const key of Object.keys(npmrc)) {
|
||||
npmrc[key] = envReplace(npmrc[key]);
|
||||
}
|
||||
}
|
||||
} else if (npmrc) {
|
||||
logger.debug('Resetting npmrc');
|
||||
npmrc = null;
|
||||
|
|
|
@ -165,7 +165,6 @@
|
|||
"traverse": "0.6.6",
|
||||
"upath": "1.2.0",
|
||||
"validate-npm-package-name": "3.0.0",
|
||||
"validator": "12.2.0",
|
||||
"www-authenticate": "0.6.2",
|
||||
"xmldoc": "1.1.2",
|
||||
"yarn": "1.22.4",
|
||||
|
|
|
@ -9867,11 +9867,6 @@ validate-npm-package-name@3.0.0, validate-npm-package-name@^3.0.0, validate-npm-
|
|||
dependencies:
|
||||
builtins "^1.0.3"
|
||||
|
||||
validator@12.2.0:
|
||||
version "12.2.0"
|
||||
resolved "https://registry.yarnpkg.com/validator/-/validator-12.2.0.tgz#660d47e96267033fd070096c3b1a6f2db4380a0a"
|
||||
integrity sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ==
|
||||
|
||||
verror@1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
||||
|
|
Loading…
Reference in a new issue