diff --git a/lib/platform/bitbucket/bb-got-wrapper.ts b/lib/platform/bitbucket/bb-got-wrapper.ts deleted file mode 100644 index c8c702a7a5a3f1f1a9bce51f601920ac6df80d7c..0000000000000000000000000000000000000000 --- a/lib/platform/bitbucket/bb-got-wrapper.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { GotJSONOptions } from 'got'; -import { PLATFORM_TYPE_BITBUCKET } from '../../constants/platforms'; -import got from '../../util/got'; -import { GotApi, GotApiOptions, GotResponse } from '../common'; - -let baseUrl = 'https://api.bitbucket.org/'; -async function get( - path: string, - options: GotApiOptions & GotJSONOptions -): Promise<GotResponse> { - const opts: GotApiOptions & GotJSONOptions = { - json: true, - hostType: PLATFORM_TYPE_BITBUCKET, - baseUrl, - ...options, - }; - const res = await got(path, opts); - return res; -} - -const helpers = ['get', 'post', 'put', 'patch', 'head', 'delete']; - -export const api: GotApi = {} as any; - -for (const x of helpers) { - (api as any)[x] = (url: string, opts: any): Promise<GotResponse> => - get(url, { ...opts, method: x.toUpperCase() }); -} - -// eslint-disable-next-line @typescript-eslint/unbound-method -api.setBaseUrl = (newBaseUrl: string): void => { - baseUrl = newBaseUrl; -}; - -export default api; diff --git a/lib/platform/bitbucket/comments.spec.ts b/lib/platform/bitbucket/comments.spec.ts index be923fe3b0c2f92c259253f6bce964f5720b0997..7299f9eb58ba3aae91bfdb559e634412e8c8d890 100644 --- a/lib/platform/bitbucket/comments.spec.ts +++ b/lib/platform/bitbucket/comments.spec.ts @@ -1,5 +1,5 @@ import * as httpMock from '../../../test/httpMock'; -import { api } from './bb-got-wrapper'; +import { setBaseUrl } from '../../util/http/bitbucket'; import * as comments from './comments'; const baseUrl = 'https://api.bitbucket.org'; @@ -13,7 +13,7 @@ describe('platform/comments', () => { httpMock.reset(); httpMock.setup(); - api.setBaseUrl(baseUrl); + setBaseUrl(baseUrl); }); describe('ensureComment()', () => { diff --git a/lib/platform/bitbucket/comments.ts b/lib/platform/bitbucket/comments.ts index 86a1c40086d6effa52796a84b7830e8fb433b603..8f2bc5d455bcf02a17580789b0d537c66223b74d 100644 --- a/lib/platform/bitbucket/comments.ts +++ b/lib/platform/bitbucket/comments.ts @@ -1,8 +1,10 @@ import { logger } from '../../logger'; +import { BitbucketHttp } from '../../util/http/bitbucket'; import { EnsureCommentConfig } from '../common'; -import { api } from './bb-got-wrapper'; import { Config, accumulateValues } from './utils'; +const bitbucketHttp = new BitbucketHttp(); + interface Comment { content: { raw: string }; id: number; @@ -31,7 +33,7 @@ async function addComment( prNo: number, raw: string ): Promise<void> { - await api.post( + await bitbucketHttp.postJson( `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments`, { body: { content: { raw } }, @@ -45,7 +47,7 @@ async function editComment( commentId: number, raw: string ): Promise<void> { - await api.put( + await bitbucketHttp.putJson( `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments/${commentId}`, { body: { content: { raw } }, @@ -58,7 +60,7 @@ async function deleteComment( prNo: number, commentId: number ): Promise<void> { - await api.delete( + await bitbucketHttp.deleteJson( `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments/${commentId}` ); } diff --git a/lib/platform/bitbucket/index.spec.ts b/lib/platform/bitbucket/index.spec.ts index 41e5f1ef9500c7d8902413e62cc52634c70fa23f..55effefbe7e5110e34c2a7c89f42f7eb3f18aa6d 100644 --- a/lib/platform/bitbucket/index.spec.ts +++ b/lib/platform/bitbucket/index.spec.ts @@ -3,6 +3,7 @@ import * as httpMock from '../../../test/httpMock'; import { REPOSITORY_DISABLED } from '../../constants/error-messages'; import { logger as _logger } from '../../logger'; import { BranchStatus } from '../../types'; +import { setBaseUrl } from '../../util/http/bitbucket'; import { Platform, RepoParams } from '../common'; const baseUrl = 'https://api.bitbucket.org'; @@ -83,6 +84,8 @@ describe('platform/bitbucket', () => { username: 'abc', password: '123', }); + + setBaseUrl(baseUrl); }); afterEach(async () => { diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts index d774134d78bd229a6fcd0e9f39056aaaad2f61a8..bd85cf7916ff59f90df9947a247115ee0fd7416e 100644 --- a/lib/platform/bitbucket/index.ts +++ b/lib/platform/bitbucket/index.ts @@ -11,6 +11,7 @@ import { PR_STATE_ALL, PR_STATE_OPEN } from '../../constants/pull-requests'; import { logger } from '../../logger'; import { BranchStatus } from '../../types'; import * as hostRules from '../../util/host-rules'; +import { BitbucketHttp, setBaseUrl } from '../../util/http/bitbucket'; import { sanitize } from '../../util/sanitize'; import { BranchStatusConfig, @@ -31,10 +32,11 @@ import { import GitStorage, { StatusResult } from '../git/storage'; import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; -import { api } from './bb-got-wrapper'; import * as comments from './comments'; import * as utils from './utils'; +const bitbucketHttp = new BitbucketHttp(); + const BITBUCKET_PROD_ENDPOINT = 'https://api.bitbucket.org/'; let config: utils.Config = {} as any; @@ -88,7 +90,7 @@ export async function initRepo({ hostType: PLATFORM_TYPE_BITBUCKET, url: endpoint, }); - api.setBaseUrl(endpoint); + setBaseUrl(endpoint); config = { repository, username: opts.username, @@ -97,7 +99,7 @@ export async function initRepo({ let info: utils.RepoInfo; try { info = utils.repoInfoTransformer( - (await api.get(`/2.0/repositories/${repository}`)).body + (await bitbucketHttp.getJson(`/2.0/repositories/${repository}`)).body ); if (optimizeForDisabled) { @@ -108,7 +110,7 @@ export async function initRepo({ let renovateConfig: RenovateConfig; try { renovateConfig = ( - await api.get<RenovateConfig>( + await bitbucketHttp.getJson<RenovateConfig>( `/2.0/repositories/${repository}/src/${info.mainbranch}/renovate.json` ) ).body; @@ -272,7 +274,7 @@ export async function deleteBranch( if (closePr) { const pr = await findPr({ branchName, state: PR_STATE_OPEN }); if (pr) { - await api.post( + await bitbucketHttp.postJson( `/2.0/repositories/${config.repository}/pullrequests/${pr.number}/decline` ); } @@ -311,7 +313,7 @@ export function getCommitMessages(): Promise<string[]> { async function isPrConflicted(prNo: number): Promise<boolean> { const diff = ( - await api.get( + await bitbucketHttp.get( `/2.0/repositories/${config.repository}/pullrequests/${prNo}/diff`, { json: false } as any ) @@ -320,10 +322,27 @@ async function isPrConflicted(prNo: number): Promise<boolean> { return utils.isConflicted(parseDiff(diff)); } +interface PrResponse { + id: string; + state: string; + links: { + commits: { + href: string; + }; + }; + source: { + branch: { + name: string; + }; + }; +} + // Gets details for a PR export async function getPr(prNo: number): Promise<Pr | null> { const pr = ( - await api.get(`/2.0/repositories/${config.repository}/pullrequests/${prNo}`) + await bitbucketHttp.getJson<PrResponse>( + `/2.0/repositories/${config.repository}/pullrequests/${prNo}` + ) ).body; // istanbul ignore if @@ -345,7 +364,9 @@ export async function getPr(prNo: number): Promise<Pr | null> { // we only want the first two commits, because size tells us the overall number const url = pr.links.commits.href + '?pagelen=2'; - const { body } = await api.get<utils.PagedResult<Commit>>(url); + const { body } = await bitbucketHttp.getJson<utils.PagedResult<Commit>>( + url + ); const size = body.size || body.values.length; // istanbul ignore if @@ -379,11 +400,17 @@ export async function getPr(prNo: number): Promise<Pr | null> { const escapeHash = (input: string): string => input ? input.replace(/#/g, '%23') : input; +interface BranchResponse { + target: { + hash: string; + }; +} + // Return the commit SHA for a branch async function getBranchCommit(branchName: string): Promise<string | null> { try { const branch = ( - await api.get( + await bitbucketHttp.getJson<BranchResponse>( `/2.0/repositories/${config.repository}/refs/branches/${escapeHash( branchName )}` @@ -491,7 +518,7 @@ export async function setBranchStatus({ url, }; - await api.post( + await bitbucketHttp.postJson( `/2.0/repositories/${config.repository}/commit/${sha}/statuses/build`, { body } ); @@ -512,7 +539,7 @@ async function findOpenIssues(title: string): Promise<BbIssue[]> { ); return ( ( - await api.get( + await bitbucketHttp.getJson<{ values: BbIssue[] }>( `/2.0/repositories/${config.repository}/issues?q=${filter}` ) ).body.values || /* istanbul ignore next */ [] @@ -543,7 +570,7 @@ export async function findIssue(title: string): Promise<Issue> { } async function closeIssue(issueNumber: number): Promise<void> { - await api.put( + await bitbucketHttp.putJson( `/2.0/repositories/${config.repository}/issues/${issueNumber}`, { body: { state: 'closed' }, @@ -587,7 +614,7 @@ export async function ensureIssue({ const [issue] = issues; if (String(issue.content.raw).trim() !== description.trim()) { logger.debug('Issue updated'); - await api.put( + await bitbucketHttp.putJson( `/2.0/repositories/${config.repository}/issues/${issue.id}`, { body: { @@ -602,12 +629,18 @@ export async function ensureIssue({ } } else { logger.info('Issue created'); - await api.post(`/2.0/repositories/${config.repository}/issues`, { - body: { - title, - content: { raw: readOnlyIssueBody(description), markup: 'markdown' }, - }, - }); + await bitbucketHttp.postJson( + `/2.0/repositories/${config.repository}/issues`, + { + body: { + title, + content: { + raw: readOnlyIssueBody(description), + markup: 'markdown', + }, + }, + } + ); return 'created'; } } catch (err) /* istanbul ignore next */ { @@ -641,7 +674,7 @@ export /* istanbul ignore next */ async function getIssueList(): Promise< ); return ( ( - await api.get( + await bitbucketHttp.getJson<{ values: Issue[] }>( `/2.0/repositories/${config.repository}/issues?q=${filter}` ) ).body.values || /* istanbul ignore next */ [] @@ -687,9 +720,12 @@ export async function addReviewers( reviewers: reviewers.map((username: string) => ({ username })), }; - await api.put(`/2.0/repositories/${config.repository}/pullrequests/${prId}`, { - body, - }); + await bitbucketHttp.putJson( + `/2.0/repositories/${config.repository}/pullrequests/${prId}`, + { + body, + } + ); } export /* istanbul ignore next */ function deleteLabel(): never { @@ -737,7 +773,7 @@ export async function createPr({ if (config.bbUseDefaultReviewers) { const reviewersResponse = ( - await api.get<utils.PagedResult<Reviewer>>( + await bitbucketHttp.getJson<utils.PagedResult<Reviewer>>( `/2.0/repositories/${config.repository}/default-reviewers` ) ).body; @@ -765,9 +801,12 @@ export async function createPr({ try { const prInfo = ( - await api.post(`/2.0/repositories/${config.repository}/pullrequests`, { - body, - }) + await bitbucketHttp.postJson<PrResponse>( + `/2.0/repositories/${config.repository}/pullrequests`, + { + body, + } + ) ).body; // TODO: fix types const pr: Pr = { @@ -800,9 +839,12 @@ export async function updatePr( description: string ): Promise<void> { logger.debug(`updatePr(${prNo}, ${title}, body)`); - await api.put(`/2.0/repositories/${config.repository}/pullrequests/${prNo}`, { - body: { title, description: sanitize(description) }, - }); + await bitbucketHttp.putJson( + `/2.0/repositories/${config.repository}/pullrequests/${prNo}`, + { + body: { title, description: sanitize(description) }, + } + ); } export async function mergePr( @@ -812,7 +854,7 @@ export async function mergePr( logger.debug(`mergePr(${prNo}, ${branchName})`); try { - await api.post( + await bitbucketHttp.postJson( `/2.0/repositories/${config.repository}/pullrequests/${prNo}/merge`, { body: { diff --git a/lib/platform/bitbucket/utils.spec.ts b/lib/platform/bitbucket/utils.spec.ts index de9374737f060569bd299c95f26a6266359a46fe..7900bcca9ea00a4f33ca16d36ed27b4d36cc37ce 100644 --- a/lib/platform/bitbucket/utils.spec.ts +++ b/lib/platform/bitbucket/utils.spec.ts @@ -1,15 +1,19 @@ import * as httpMock from '../../../test/httpMock'; +import { setBaseUrl } from '../../util/http/bitbucket'; import * as utils from './utils'; const range = (count: number) => [...Array(count).keys()]; +const baseUrl = 'https://api.bitbucket.org'; + describe('accumulateValues()', () => { it('paginates', async () => { httpMock.reset(); httpMock.setup(); + setBaseUrl(baseUrl); httpMock - .scope('https://api.bitbucket.org') + .scope(baseUrl) .get('/some-url?pagelen=10') .reply(200, { values: range(10), diff --git a/lib/platform/bitbucket/utils.ts b/lib/platform/bitbucket/utils.ts index c4721cbbeeedfbc04c282ff9fed7af7776868fdb..8e3185d90a06d6e6a56570f8a818806b81f5c5ba 100644 --- a/lib/platform/bitbucket/utils.ts +++ b/lib/platform/bitbucket/utils.ts @@ -1,9 +1,12 @@ import url from 'url'; import { PR_STATE_CLOSED } from '../../constants/pull-requests'; import { BranchStatus } from '../../types'; -import { GotResponse, Pr } from '../common'; +import { HttpResponse } from '../../util/http'; +import { BitbucketHttp } from '../../util/http/bitbucket'; +import { Pr } from '../common'; import { Storage } from '../git/storage'; -import { api } from './bb-got-wrapper'; + +const bitbucketHttp = new BitbucketHttp(); export interface Config { baseBranch: string; @@ -74,6 +77,29 @@ const addMaxLength = (inputUrl: string, pagelen = 100): string => { return maxedUrl; }; +async function callApi<T>( + apiUrl: string, + method: string, + options?: any +): Promise<HttpResponse<T>> { + /* istanbul ignore next */ + switch (method.toLowerCase()) { + case 'post': + return bitbucketHttp.postJson<T>(apiUrl, options); + case 'put': + return bitbucketHttp.putJson<T>(apiUrl, options); + case 'patch': + return bitbucketHttp.patchJson<T>(apiUrl, options); + case 'head': + return bitbucketHttp.headJson<T>(apiUrl, options); + case 'delete': + return bitbucketHttp.deleteJson<T>(apiUrl, options); + case 'get': + default: + return bitbucketHttp.getJson<T>(apiUrl, options); + } +} + export async function accumulateValues<T = any>( reqUrl: string, method = 'get', @@ -82,13 +108,13 @@ export async function accumulateValues<T = any>( ): Promise<T[]> { let accumulator: T[] = []; let nextUrl = addMaxLength(reqUrl, pagelen); - const lowerCaseMethod = method.toLocaleLowerCase(); while (typeof nextUrl !== 'undefined') { - const { body } = (await api[lowerCaseMethod]( + const { body } = await callApi<{ values: T[]; next: string }>( nextUrl, + method, options - )) as GotResponse<PagedResult<T>>; + ); accumulator = [...accumulator, ...body.values]; nextUrl = body.next; } diff --git a/lib/platform/bitbucket/__snapshots__/bb-got-wrapper.spec.ts.snap b/lib/util/http/__snapshots__/bitbucket.spec.ts.snap similarity index 85% rename from lib/platform/bitbucket/__snapshots__/bb-got-wrapper.spec.ts.snap rename to lib/util/http/__snapshots__/bitbucket.spec.ts.snap index 895899be6e74009964ee3ce9178ee0ed5a00c838..8a145e14ea1b6ec5bc9d7cb1e51685e374bd1ebe 100644 --- a/lib/platform/bitbucket/__snapshots__/bb-got-wrapper.spec.ts.snap +++ b/lib/util/http/__snapshots__/bitbucket.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`platform/gl-got-wrapper accepts custom baseUrl 1`] = ` +exports[`util/http/bitbucket accepts custom baseUrl 1`] = ` Array [ Object { "headers": Object { @@ -26,7 +26,7 @@ Array [ ] `; -exports[`platform/gl-got-wrapper posts 1`] = ` +exports[`util/http/bitbucket posts 1`] = ` Array [ Object { "headers": Object { @@ -42,9 +42,9 @@ Array [ ] `; -exports[`platform/gl-got-wrapper returns cached 1`] = `Object {}`; +exports[`util/http/bitbucket returns cached 1`] = `Object {}`; -exports[`platform/gl-got-wrapper returns cached 2`] = ` +exports[`util/http/bitbucket returns cached 2`] = ` Array [ Object { "headers": Object { diff --git a/lib/platform/bitbucket/bb-got-wrapper.spec.ts b/lib/util/http/bitbucket.spec.ts similarity index 70% rename from lib/platform/bitbucket/bb-got-wrapper.spec.ts rename to lib/util/http/bitbucket.spec.ts index 89bb7a9acc55ac6a52173af6c1ff2a1b9e6e1b87..d99bd8dbe44270505538f20d8c7817ae39867463 100644 --- a/lib/platform/bitbucket/bb-got-wrapper.spec.ts +++ b/lib/util/http/bitbucket.spec.ts @@ -1,12 +1,16 @@ import * as httpMock from '../../../test/httpMock'; +import { getName } from '../../../test/util'; import { PLATFORM_TYPE_BITBUCKET } from '../../constants/platforms'; -import * as hostRules from '../../util/host-rules'; -import { api } from './bb-got-wrapper'; +import * as hostRules from '../host-rules'; +import { BitbucketHttp, setBaseUrl } from './bitbucket'; const baseUrl = 'https://api.bitbucket.org'; -describe('platform/gl-got-wrapper', () => { +describe(getName(__filename), () => { + let api: BitbucketHttp; beforeEach(() => { + api = new BitbucketHttp(); + // reset module jest.resetAllMocks(); @@ -21,12 +25,12 @@ describe('platform/gl-got-wrapper', () => { httpMock.reset(); httpMock.setup(); - api.setBaseUrl(baseUrl); + setBaseUrl(baseUrl); }); it('posts', async () => { const body = ['a', 'b']; httpMock.scope(baseUrl).post('/some-url').reply(200, body); - const res = await api.post('some-url'); + const res = await api.postJson('some-url'); expect(res.body).toEqual(body); expect(httpMock.getTrace()).toMatchSnapshot(); }); @@ -35,16 +39,16 @@ describe('platform/gl-got-wrapper', () => { httpMock.scope(baseUrl).post('/some-url').reply(200, {}); httpMock.scope(customBaseUrl).post('/some-url').reply(200, {}); - await api.post('some-url'); + await api.postJson('some-url'); - api.setBaseUrl(customBaseUrl); - await api.post('some-url'); + setBaseUrl(customBaseUrl); + await api.postJson('some-url'); expect(httpMock.getTrace()).toMatchSnapshot(); }); it('returns cached', async () => { httpMock.scope(baseUrl).get('/projects/foo').reply(200, {}); - const { body } = await api.get('projects/foo'); + const { body } = await api.getJson('projects/foo'); expect(body).toMatchSnapshot(); expect(httpMock.getTrace()).toMatchSnapshot(); }); diff --git a/lib/util/http/bitbucket.ts b/lib/util/http/bitbucket.ts new file mode 100644 index 0000000000000000000000000000000000000000..428c541a421a1ba3026735015fd99799e5f09693 --- /dev/null +++ b/lib/util/http/bitbucket.ts @@ -0,0 +1,25 @@ +import { URL } from 'url'; +import { PLATFORM_TYPE_BITBUCKET } from '../../constants/platforms'; +import { Http, HttpOptions, HttpResponse, InternalHttpOptions } from '.'; + +let baseUrl = 'https://api.bitbucket.org/'; +export const setBaseUrl = (url: string): void => { + baseUrl = url; +}; + +export class BitbucketHttp extends Http { + constructor(options?: HttpOptions) { + super(PLATFORM_TYPE_BITBUCKET, options); + } + + protected async request<T>( + url: string | URL, + options?: InternalHttpOptions + ): Promise<HttpResponse<T> | null> { + const opts = { + baseUrl, + ...options, + }; + return super.request<T>(url, opts); + } +}