feat(npm): try auth recursive (#5698)

This commit is contained in:
Michael Kriese 2020-03-27 11:28:20 +01:00 committed by GitHub
parent 6cbd4a7743
commit 707d35db30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 44 deletions

View file

@ -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",

View 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();
});
});

View file

@ -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;
}
}

View file

@ -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: {

View file

@ -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;

View file

@ -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",

View file

@ -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"