diff --git a/lib/util/http/__snapshots__/bitbucket-server.spec.ts.snap b/lib/util/http/__snapshots__/bitbucket-server.spec.ts.snap new file mode 100644 index 00000000000000..ec01132135153a --- /dev/null +++ b/lib/util/http/__snapshots__/bitbucket-server.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`util/http/bitbucket-server posts 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Bearer token", + "host": "git.example.com", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "POST", + "url": "https://git.example.com/some-url", + }, +] +`; diff --git a/lib/util/http/__snapshots__/index.spec.ts.snap b/lib/util/http/__snapshots__/index.spec.ts.snap index b8031b689a8b46..5d384aab52a5d4 100644 --- a/lib/util/http/__snapshots__/index.spec.ts.snap +++ b/lib/util/http/__snapshots__/index.spec.ts.snap @@ -60,3 +60,12 @@ Object { }, } `; + +exports[`util/http/index retries 1`] = ` +Object { + "body": "", + "headers": Object { + "x-some-header": "abc", + }, +} +`; diff --git a/lib/util/http/auth.spec.ts b/lib/util/http/auth.spec.ts index 42fc1473c7d2e8..a74d639f30c123 100644 --- a/lib/util/http/auth.spec.ts +++ b/lib/util/http/auth.spec.ts @@ -1,108 +1,151 @@ import { getName } from '../../../test/util'; -import { removeAuthorization } from './auth'; +import { PLATFORM_TYPE_GITEA } from '../../constants/platforms'; +import { applyAuthorization, removeAuthorization } from './auth'; describe(getName(__filename), () => { - it('removeAuthorization no authorization', () => { - const opts = { - hostname: 'amazon.com', - href: 'https://amazon.com', - search: 'something X-Amz-Algorithm something', - }; - - removeAuthorization(opts); - - expect(opts).toEqual({ - hostname: 'amazon.com', - href: 'https://amazon.com', - search: 'something X-Amz-Algorithm something', + describe('applyAuthorization', () => { + it('does nothing', () => { + const opts = { + hostname: 'amazon.com', + href: 'https://amazon.com', + auth: 'XXXX', + }; + + applyAuthorization(opts); + + expect(opts).toMatchInlineSnapshot(` + Object { + "auth": "XXXX", + "hostname": "amazon.com", + "href": "https://amazon.com", + } + `); }); - }); - it('removeAuthorization Amazon', () => { - const opts = { - auth: 'auth', - headers: { - authorization: 'auth', - }, - hostname: 'amazon.com', - href: 'https://amazon.com', - search: 'something X-Amz-Algorithm something', - }; - - removeAuthorization(opts); - - expect(opts).toEqual({ - headers: {}, - hostname: 'amazon.com', - href: 'https://amazon.com', - search: 'something X-Amz-Algorithm something', + it('gittea token', () => { + const opts = { + headers: {}, + token: 'XXXX', + hostType: PLATFORM_TYPE_GITEA, + }; + + applyAuthorization(opts); + + expect(opts).toMatchInlineSnapshot(` + Object { + "headers": Object { + "authorization": "token XXXX", + }, + "hostType": "gitea", + "token": "XXXX", + } + `); }); }); - it('removeAuthorization Amazon ports', () => { - const opts = { - auth: 'auth', - headers: { - authorization: 'auth', - }, - hostname: 'amazon.com', - href: 'https://amazon.com', - port: 3000, - search: 'something X-Amz-Algorithm something', - }; - - removeAuthorization(opts); - - expect(opts).toEqual({ - headers: {}, - hostname: 'amazon.com', - href: 'https://amazon.com', - search: 'something X-Amz-Algorithm something', + describe('removeAuthorization', () => { + it('no authorization', () => { + const opts = { + hostname: 'amazon.com', + href: 'https://amazon.com', + search: 'something X-Amz-Algorithm something', + }; + + removeAuthorization(opts); + + expect(opts).toEqual({ + hostname: 'amazon.com', + href: 'https://amazon.com', + search: 'something X-Amz-Algorithm something', + }); }); - }); - it('removeAuthorization Azure blob', () => { - const opts = { - auth: 'auth', - headers: { - authorization: 'auth', - }, - hostname: 'store123.blob.core.windows.net', - href: - 'https://.blob.core.windows.net///docker/registry/v2/blobs', - }; - - removeAuthorization(opts); - - expect(opts).toEqual({ - headers: {}, - hostname: 'store123.blob.core.windows.net', - href: - 'https://.blob.core.windows.net///docker/registry/v2/blobs', + it('Amazon', () => { + const opts = { + auth: 'auth', + headers: { + authorization: 'auth', + }, + hostname: 'amazon.com', + href: 'https://amazon.com', + search: 'something X-Amz-Algorithm something', + }; + + removeAuthorization(opts); + + expect(opts).toEqual({ + headers: {}, + hostname: 'amazon.com', + href: 'https://amazon.com', + search: 'something X-Amz-Algorithm something', + }); }); - }); - it('removeAuthorization keep auth', () => { - const opts = { - auth: 'auth', - headers: { - authorization: 'auth', - }, - hostname: 'renovate.com', - href: 'https://renovate.com', - search: 'something', - }; - - removeAuthorization(opts); - - expect(opts).toEqual({ - auth: 'auth', - headers: { - authorization: 'auth', - }, - hostname: 'renovate.com', - href: 'https://renovate.com', - search: 'something', + it('Amazon ports', () => { + const opts = { + auth: 'auth', + headers: { + authorization: 'auth', + }, + hostname: 'amazon.com', + href: 'https://amazon.com', + port: 3000, + search: 'something X-Amz-Algorithm something', + }; + + removeAuthorization(opts); + + expect(opts).toEqual({ + headers: {}, + hostname: 'amazon.com', + href: 'https://amazon.com', + search: 'something X-Amz-Algorithm something', + }); + }); + + it('Azure blob', () => { + const opts = { + auth: 'auth', + headers: { + authorization: 'auth', + }, + hostname: 'store123.blob.core.windows.net', + href: + 'https://.blob.core.windows.net///docker/registry/v2/blobs', + }; + + removeAuthorization(opts); + + expect(opts).toEqual({ + headers: {}, + hostname: 'store123.blob.core.windows.net', + href: + 'https://.blob.core.windows.net///docker/registry/v2/blobs', + }); + }); + + it('keep auth', () => { + const opts = { + auth: 'auth', + headers: { + authorization: 'auth', + }, + hostname: 'renovate.com', + href: 'https://renovate.com', + search: 'something', + }; + + removeAuthorization(opts); + + expect(opts).toEqual({ + auth: 'auth', + headers: { + authorization: 'auth', + }, + hostname: 'renovate.com', + href: 'https://renovate.com', + search: 'something', + }); }); }); }); diff --git a/lib/util/http/auth.ts b/lib/util/http/auth.ts index 31d3f523579fa2..80f22a45aaba58 100644 --- a/lib/util/http/auth.ts +++ b/lib/util/http/auth.ts @@ -49,6 +49,7 @@ export function removeAuthorization(options: any): void { // if there is no port in the redirect URL string, then delete it from the redirect options. // This can be evaluated for removal after upgrading to Got v10 const portInUrl = options.href.split('/')[2].split(':')[1]; + // istanbul ignore next if (!portInUrl) { // eslint-disable-next-line no-param-reassign delete options.port; // Redirect will instead use 80 or 443 for HTTP or HTTPS respectively diff --git a/lib/util/http/bitbucket-server.spec.ts b/lib/util/http/bitbucket-server.spec.ts new file mode 100644 index 00000000000000..9886b67ffbccac --- /dev/null +++ b/lib/util/http/bitbucket-server.spec.ts @@ -0,0 +1,37 @@ +import * as httpMock from '../../../test/httpMock'; +import { getName } from '../../../test/util'; +import { PLATFORM_TYPE_BITBUCKET_SERVER } from '../../constants/platforms'; +import * as hostRules from '../host-rules'; +import { BitbucketServerHttp, setBaseUrl } from './bitbucket-server'; + +const baseUrl = 'https://git.example.com'; + +describe(getName(__filename), () => { + let api: BitbucketServerHttp; + beforeEach(() => { + api = new BitbucketServerHttp(); + + // reset module + jest.resetAllMocks(); + + // clean up hostRules + hostRules.clear(); + hostRules.add({ + hostType: PLATFORM_TYPE_BITBUCKET_SERVER, + baseUrl, + token: 'token', + }); + + httpMock.reset(); + httpMock.setup(); + + setBaseUrl(baseUrl); + }); + it('posts', async () => { + const body = ['a', 'b']; + httpMock.scope(baseUrl).post('/some-url').reply(200, body); + const res = await api.postJson('some-url'); + expect(res.body).toEqual(body); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); +}); diff --git a/lib/util/http/github.spec.ts b/lib/util/http/github.spec.ts index 0e3cca49b431bc..835cc4b45df6f6 100644 --- a/lib/util/http/github.spec.ts +++ b/lib/util/http/github.spec.ts @@ -1,4 +1,4 @@ -import delay from 'delay'; +import nock from 'nock'; import * as httpMock from '../../../test/httpMock'; import { getName } from '../../../test/util'; import { @@ -8,14 +8,12 @@ import { PLATFORM_RATE_LIMIT_EXCEEDED, REPOSITORY_CHANGED, } from '../../constants/error-messages'; -import { GithubHttp, handleGotError, setBaseUrl } from './github'; +import { GithubHttp, setBaseUrl } from './github'; const githubApiHost = 'https://api.github.com'; -jest.mock('delay'); - describe(getName(__filename), () => { - let githubApi; + let githubApi: GithubHttp; beforeEach(() => { githubApi = new GithubHttp(); setBaseUrl(githubApiHost); @@ -29,19 +27,6 @@ describe(getName(__filename), () => { }); describe('HTTP', () => { - function getError(errOrig: any): Error { - try { - return handleGotError(errOrig, `${githubApiHost}/some-url`, {}); - } catch (err) { - return err; - } - return null; - } - - beforeEach(() => { - (delay as any).mockImplementation(() => Promise.resolve()); - }); - it('supports app mode', async () => { httpMock.scope(githubApiHost).get('/some-url').reply(200); global.appMode = true; @@ -100,124 +85,127 @@ describe(getName(__filename), () => { const trace = httpMock.getTrace(); expect(trace).toHaveLength(1); }); - it('should throw rate limit exceeded', () => { - const e = getError({ - statusCode: 403, - message: - 'Error updating branch: API rate limit exceeded for installation ID 48411. (403)', + describe('handleGotError', () => { + async function fail( + code: number, + body: any = undefined, + headers: nock.ReplyHeaders = undefined + ) { + const url = '/some-url'; + httpMock + .scope(githubApiHost) + .get(url) + .reply( + code, + function reply() { + // https://github.com/nock/nock/issues/1979 + if (typeof body === 'object' && 'message' in body) { + (this.req as any).response.statusMessage = body?.message; + } + return body; + }, + headers + ); + await githubApi.getJson(url); + } + async function failWithError(error: object) { + const url = '/some-url'; + httpMock.scope(githubApiHost).get(url).replyWithError(error); + await githubApi.getJson(url); + } + + it('should throw Not found', async () => { + await expect(fail(404)).rejects.toThrow( + 'Response code 404 (Not Found)' + ); }); - expect(e).toBeDefined(); - expect(e.message).toEqual(PLATFORM_RATE_LIMIT_EXCEEDED); - }); - it('should throw Bad credentials', () => { - const e = getError({ - statusCode: 401, - message: 'Bad credentials. (401)', + it('should throw rate limit exceeded', async () => { + await expect( + fail(403, { + message: + 'Error updating branch: API rate limit exceeded for installation ID 48411. (403)', + }) + ).rejects.toThrow(PLATFORM_RATE_LIMIT_EXCEEDED); }); - expect(e).toBeDefined(); - expect(e.message).toEqual(PLATFORM_BAD_CREDENTIALS); - }); - it('should throw platform failure', () => { - const e = getError({ - statusCode: 401, - message: 'Bad credentials. (401)', - headers: { - 'x-ratelimit-limit': '60', - }, + it('should throw Bad credentials', async () => { + await expect( + fail(401, { message: 'Bad credentials. (401)' }) + ).rejects.toThrow(PLATFORM_BAD_CREDENTIALS); }); - expect(e).toBeDefined(); - expect(e.message).toEqual(EXTERNAL_HOST_ERROR); - }); - it('should throw platform failure for ENOTFOUND, ETIMEDOUT or EAI_AGAIN', () => { - const codes = ['ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN']; - for (let idx = 0; idx < codes.length; idx += 1) { - const code = codes[idx]; - const e = getError({ - name: 'RequestError', - code, - }); - expect(e).toBeDefined(); - expect(e.message).toEqual(EXTERNAL_HOST_ERROR); - } - }); - it('should throw platform failure for 500', () => { - const e = getError({ - statusCode: 500, - message: 'Internal Server Error', + it('should throw platform failure', async () => { + await expect( + fail( + 401, + { message: 'Bad credentials. (401)' }, + { + 'x-ratelimit-limit': '60', + } + ) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); }); - expect(e).toBeDefined(); - expect(e.message).toEqual(EXTERNAL_HOST_ERROR); - }); - it('should throw platform failure ParseError', () => { - const e = getError({ - name: 'ParseError', + it('should throw platform failure for ENOTFOUND, ETIMEDOUT or EAI_AGAIN', async () => { + const codes = ['ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN']; + for (let idx = 0; idx < codes.length; idx += 1) { + const code = codes[idx]; + await expect(failWithError({ code })).rejects.toThrow( + EXTERNAL_HOST_ERROR + ); + } }); - expect(e).toBeDefined(); - expect(e.message).toEqual(EXTERNAL_HOST_ERROR); - }); - it('should throw for unauthorized integration', () => { - const e = getError({ - statusCode: 403, - message: 'Resource not accessible by integration (403)', + it('should throw platform failure for 500', async () => { + await expect(fail(500)).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + it('should throw platform failure ParseError', async () => { + await expect(fail(200, '{{')).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + it('should throw for unauthorized integration', async () => { + await expect( + fail(403, { message: 'Resource not accessible by integration (403)' }) + ).rejects.toThrow(PLATFORM_INTEGRATION_UNAUTHORIZED); + }); + it('should throw for unauthorized integration2', async () => { + await expect( + fail(403, { message: 'Upgrade to GitHub Pro' }) + ).rejects.toThrow('Upgrade to GitHub Pro'); + }); + it('should throw on abuse', async () => { + await expect( + fail(403, { + message: 'You have triggered an abuse detection mechanism', + }) + ).rejects.toThrow(PLATFORM_RATE_LIMIT_EXCEEDED); + }); + it('should throw on repository change', async () => { + await expect( + fail(422, { + message: 'foobar', + errors: [{ code: 'invalid' }], + }) + ).rejects.toThrow(REPOSITORY_CHANGED); + }); + it('should throw platform failure on 422 response', async () => { + await expect( + fail(422, { + message: 'foobar', + }) + ).rejects.toThrow(EXTERNAL_HOST_ERROR); + }); + it('should throw original error when failed to add reviewers', async () => { + await expect( + fail(422, { + message: 'Review cannot be requested from pull request author.', + }) + ).rejects.toThrow( + 'Review cannot be requested from pull request author.' + ); + }); + it('should throw original error of unknown type', async () => { + await expect( + fail(418, { + message: 'Sorry, this is a teapot', + }) + ).rejects.toThrow('Sorry, this is a teapot'); }); - expect(e).toBeDefined(); - expect(e.message).toEqual(PLATFORM_INTEGRATION_UNAUTHORIZED); - }); - it('should throw for unauthorized integration2', () => { - const gotErr = { - statusCode: 403, - body: { message: 'Upgrade to GitHub Pro' }, - }; - const e = getError(gotErr); - expect(e).toBeDefined(); - expect(e).toBe(gotErr); - }); - it('should throw on abuse', () => { - const gotErr = { - statusCode: 403, - message: 'You have triggered an abuse detection mechanism', - }; - const e = getError(gotErr); - expect(e).toBeDefined(); - expect(e.message).toEqual(PLATFORM_RATE_LIMIT_EXCEEDED); - }); - it('should throw on repository change', () => { - const gotErr = { - statusCode: 422, - body: { - message: 'foobar', - errors: [{ code: 'invalid' }], - }, - }; - const e = getError(gotErr); - expect(e).toBeDefined(); - expect(e.message).toEqual(REPOSITORY_CHANGED); - }); - it('should throw platform failure on 422 response', () => { - const gotErr = { - statusCode: 422, - message: 'foobar', - }; - const e = getError(gotErr); - expect(e).toBeDefined(); - expect(e.message).toEqual(EXTERNAL_HOST_ERROR); - }); - it('should throw original error when failed to add reviewers', () => { - const gotErr = { - statusCode: 422, - message: 'Review cannot be requested from pull request author.', - }; - const e = getError(gotErr); - expect(e).toBeDefined(); - expect(e).toStrictEqual(gotErr); - }); - it('should throw original error of unknown type', () => { - const gotErr = { - statusCode: 418, - message: 'Sorry, this is a teapot', - }; - const e = getError(gotErr); - expect(e).toBe(gotErr); }); }); @@ -238,7 +226,10 @@ describe(getName(__filename), () => { }`; it('supports app mode', async () => { - httpMock.scope(githubApiHost).post('/graphql').reply(200, {}); + httpMock + .scope(githubApiHost) + .post('/graphql') + .reply(200, { data: { repository: { testItem: 'XXX' } } }); global.appMode = true; await githubApi.queryRepoField(query, 'testItem', { paginate: false }); const [req] = httpMock.getTrace(); diff --git a/lib/util/http/github.ts b/lib/util/http/github.ts index fbabe57978964b..ae635429fb0014 100644 --- a/lib/util/http/github.ts +++ b/lib/util/http/github.ts @@ -44,7 +44,7 @@ interface GithubGraphqlResponse { errors?: { message: string; locations: unknown }[]; } -export function handleGotError( +function handleGotError( err: GotRequestError, url: string | URL, opts: GithubHttpOptions @@ -97,7 +97,7 @@ export function handleGotError( throw new Error(PLATFORM_INTEGRATION_UNAUTHORIZED); } if (err.statusCode === 401 && message.includes('Bad credentials')) { - const rateLimit = err.headers ? err.headers['x-ratelimit-limit'] : -1; + const rateLimit = err.headers?.['x-ratelimit-limit'] ?? -1; logger.debug( { token: maskToken(opts.token), @@ -191,6 +191,7 @@ export class GithubHttp extends Http { try { result = await super.request(url, opts); + // istanbul ignore else: Can result be null ??? if (result !== null) { if (opts.paginate) { // Check if result is paginated @@ -200,6 +201,7 @@ export class GithubHttp extends Http { parseLinkHeader(result.headers.link as string); if (linkHeader && linkHeader.next && linkHeader.last) { let lastPage = +linkHeader.last.page; + // istanbul ignore else: needs a test if (!process.env.RENOVATE_PAGINATE_ALL && opts.paginate !== 'all') { lastPage = Math.min(pageLimit, lastPage); } diff --git a/lib/util/http/gitlab.spec.ts b/lib/util/http/gitlab.spec.ts index 04fe86fc3f52c6..45ddb8acfd22aa 100644 --- a/lib/util/http/gitlab.spec.ts +++ b/lib/util/http/gitlab.spec.ts @@ -1,5 +1,6 @@ import * as httpMock from '../../../test/httpMock'; import { getName } from '../../../test/util'; +import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages'; import { PLATFORM_TYPE_GITLAB } from '../../constants/platforms'; import * as hostRules from '../host-rules'; import { GitlabHttp, setBaseUrl } from './gitlab'; @@ -70,4 +71,48 @@ describe(getName(__filename), () => { setBaseUrl('https://gitlab.renovatebot.com/api/v4/') ).not.toThrow(); }); + + describe('fails with', () => { + it('403', async () => { + httpMock.scope(gitlabApiHost).get('/api/v4/some-url').reply(403); + await expect( + gitlabApi.get('some-url') + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Response code 403 (Forbidden)"` + ); + }); + + it('404', async () => { + httpMock.scope(gitlabApiHost).get('/api/v4/some-url').reply(404); + await expect( + gitlabApi.get('some-url') + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Response code 404 (Not Found)"` + ); + }); + + it('500', async () => { + httpMock.scope(gitlabApiHost).get('/api/v4/some-url').reply(500); + await expect(gitlabApi.get('some-url')).rejects.toThrow( + EXTERNAL_HOST_ERROR + ); + }); + + it('EAI_AGAIN', async () => { + httpMock + .scope(gitlabApiHost) + .get('/api/v4/some-url') + .replyWithError({ code: 'EAI_AGAIN' }); + await expect(gitlabApi.get('some-url')).rejects.toThrow( + EXTERNAL_HOST_ERROR + ); + }); + + it('ParseError', async () => { + httpMock.scope(gitlabApiHost).get('/api/v4/some-url').reply(200, '{{'); + await expect(gitlabApi.getJson('some-url')).rejects.toThrow( + EXTERNAL_HOST_ERROR + ); + }); + }); }); diff --git a/lib/util/http/gitlab.ts b/lib/util/http/gitlab.ts index 34b8496285a944..5f666d5f3d8fb8 100644 --- a/lib/util/http/gitlab.ts +++ b/lib/util/http/gitlab.ts @@ -52,7 +52,7 @@ export class GitlabHttp extends Http { } } return result; - } catch (err) /* istanbul ignore next */ { + } catch (err) { if (err.statusCode === 404) { logger.trace({ err }, 'GitLab 404'); logger.debug({ url: err.url }, 'GitLab API 404'); diff --git a/lib/util/http/host-rules.spec.ts b/lib/util/http/host-rules.spec.ts new file mode 100644 index 00000000000000..a3c30315faa3b0 --- /dev/null +++ b/lib/util/http/host-rules.spec.ts @@ -0,0 +1,63 @@ +import * as httpMock from '../../../test/httpMock'; +import { getName } from '../../../test/util'; +import { + PLATFORM_TYPE_GITEA, + PLATFORM_TYPE_GITHUB, +} from '../../constants/platforms'; +import * as hostRules from '../host-rules'; +import { applyHostRules } from './host-rules'; + +const url = 'https://github.com'; + +describe(getName(__filename), () => { + const options = { + hostType: PLATFORM_TYPE_GITHUB, + }; + beforeEach(() => { + // reset module + jest.resetAllMocks(); + + // clean up hostRules + hostRules.clear(); + hostRules.add({ + hostType: PLATFORM_TYPE_GITHUB, + token: 'token', + }); + hostRules.add({ + hostType: PLATFORM_TYPE_GITEA, + password: 'password', + }); + + httpMock.reset(); + httpMock.setup(); + }); + + it('adds token', () => { + expect(applyHostRules(url, { ...options })).toMatchInlineSnapshot(` + Object { + "hostType": "github", + "token": "token", + } + `); + }); + + it('adds auth', () => { + expect(applyHostRules(url, { hostType: PLATFORM_TYPE_GITEA })) + .toMatchInlineSnapshot(` + Object { + "auth": ":password", + "hostType": "gitea", + } + `); + }); + + it('skips', () => { + expect(applyHostRules(url, { ...options, token: 'xxx' })) + .toMatchInlineSnapshot(` + Object { + "hostType": "github", + "token": "xxx", + } + `); + }); +}); diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts index 81cce5994f0453..c3a5e0993f4aba 100644 --- a/lib/util/http/host-rules.ts +++ b/lib/util/http/host-rules.ts @@ -9,7 +9,7 @@ export function applyHostRules(url: string, inOptions: any): any { hostRules.find({ hostType: options.hostType, url, - }) || {}; + }) || /* istanbul ignore next: can only happen in tests */ {}; const { username, password, token, enabled } = foundRules; if (options.headers?.authorization || options.auth || options.token) { logger.trace('Authorization already set for host: ' + options.hostname); diff --git a/lib/util/http/index.spec.ts b/lib/util/http/index.spec.ts index 0c9018b2a74026..eab8a5dabfbc29 100644 --- a/lib/util/http/index.spec.ts +++ b/lib/util/http/index.spec.ts @@ -115,4 +115,20 @@ describe(getName(__filename), () => { expect(data).toBe('{}'); expect(nock.isDone()).toBe(true); }); + + it('retries', async () => { + const NODE_ENV = process.env.NODE_ENV; + try { + delete process.env.NODE_ENV; + nock(baseUrl) + .head('/') + .reply(500) + .head('/') + .reply(200, undefined, { 'x-some-header': 'abc' }); + expect(await http.head('http://renovate.com')).toMatchSnapshot(); + expect(nock.isDone()).toBe(true); + } finally { + process.env.NODE_ENV = NODE_ENV; + } + }); });