Skip to content

Commit

Permalink
feat(npm): try auth recursive (#5698)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceice committed Mar 27, 2020
1 parent 6cbd4a7 commit 707d35d
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 44 deletions.
24 changes: 12 additions & 12 deletions lib/datasource/npm/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -214,15 +214,15 @@ 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\`
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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
128 changes: 128 additions & 0 deletions lib/datasource/npm/get.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
22 changes: 10 additions & 12 deletions lib/datasource/npm/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -207,7 +209,6 @@ export async function getDependency(
);
return null;
}
// istanbul ignore if
if (err.statusCode === 402) {
logger.debug(
{
Expand All @@ -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' ||
Expand All @@ -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;
}
}
6 changes: 4 additions & 2 deletions lib/datasource/npm/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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: {
Expand Down
14 changes: 2 additions & 12 deletions lib/datasource/npm/npmrc.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 (
Expand All @@ -52,20 +50,12 @@ 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)) {
npmrc[key] = envReplace(npmrc[key]);
}
for (const key of Object.keys(npmrc)) {
npmrc[key] = envReplace(npmrc[key]);
}
} else if (npmrc) {
logger.debug('Resetting npmrc');
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 707d35d

Please sign in to comment.