From b2ffabcf73af331aa4b190ecd341bace47e60e88 Mon Sep 17 00:00:00 2001
From: Sergio Zharinov <zharinov@users.noreply.github.com>
Date: Tue, 22 Sep 2020 08:10:11 +0400
Subject: [PATCH] fix(bitbucket): Return only PRs created by Renovate (#7244)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 .../__snapshots__/index.spec.ts.snap          | 52 ++++++++++++++++
 lib/platform/bitbucket/index.spec.ts          | 62 +++++++++++++++----
 lib/platform/bitbucket/index.ts               | 33 +++++++++-
 lib/platform/bitbucket/utils.ts               |  1 +
 4 files changed, 135 insertions(+), 13 deletions(-)

diff --git a/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap b/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap
index 17a5c002a2..17b05c9c28 100644
--- a/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap
+++ b/lib/platform/bitbucket/__snapshots__/index.spec.ts.snap
@@ -988,6 +988,58 @@ Array [
 
 exports[`platform/bitbucket getPrBody() returns diff files 1`] = `"**foo**bartext"`;
 
+exports[`platform/bitbucket getPrList() filters PR list by author 1`] = `
+Array [
+  Object {
+    "body": undefined,
+    "branchName": "branch-a",
+    "createdAt": undefined,
+    "number": 1,
+    "state": "open",
+    "targetBranch": "branch-b",
+    "title": undefined,
+  },
+]
+`;
+
+exports[`platform/bitbucket getPrList() filters PR list by author 2`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "Basic cmVub3ZhdGU6cGFzcw==",
+      "host": "api.bitbucket.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.bitbucket.org/2.0/user",
+  },
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "Basic YWJjOjEyMw==",
+      "host": "api.bitbucket.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.bitbucket.org/2.0/repositories/some/repo",
+  },
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "Basic YWJjOjEyMw==",
+      "host": "api.bitbucket.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://api.bitbucket.org/2.0/repositories/some/repo/pullrequests?state=OPEN&state=MERGED&state=DECLINED&state=SUPERSEDED&pagelen=50",
+  },
+]
+`;
+
 exports[`platform/bitbucket getRepos() returns repos 1`] = `
 Array [
   Object {
diff --git a/lib/platform/bitbucket/index.spec.ts b/lib/platform/bitbucket/index.spec.ts
index b98c7a1758..7ff3145052 100644
--- a/lib/platform/bitbucket/index.spec.ts
+++ b/lib/platform/bitbucket/index.spec.ts
@@ -66,18 +66,18 @@ describe('platform/bitbucket', () => {
 
   async function initRepoMock(
     config?: Partial<RepoParams>,
-    repoResp?: any
+    repoResp?: any,
+    existingScope?: nock.Scope
   ): Promise<nock.Scope> {
     const repository = config?.repository || 'some/repo';
 
-    const scope = httpMock
-      .scope(baseUrl)
-      .get(`/2.0/repositories/${repository}`)
-      .reply(200, {
-        owner: {},
-        mainbranch: { name: 'master' },
-        ...repoResp,
-      });
+    const scope = existingScope || httpMock.scope(baseUrl);
+
+    scope.get(`/2.0/repositories/${repository}`).reply(200, {
+      owner: {},
+      mainbranch: { name: 'master' },
+      ...repoResp,
+    });
 
     await bitbucket.initRepo({
       repository: 'some/repo',
@@ -90,9 +90,9 @@ describe('platform/bitbucket', () => {
   }
 
   describe('initPlatform()', () => {
-    it('should throw if no username/password', () => {
+    it('should throw if no username/password', async () => {
       expect.assertions(1);
-      expect(() => bitbucket.initPlatform({})).toThrow();
+      await expect(bitbucket.initPlatform({})).rejects.toThrow();
     });
     it('should show warning message if custom endpoint', async () => {
       await bitbucket.initPlatform({
@@ -112,6 +112,16 @@ describe('platform/bitbucket', () => {
         })
       ).toMatchSnapshot();
     });
+    it('should warn for missing "profile" scope', async () => {
+      const scope = httpMock.scope(baseUrl);
+      scope
+        .get('/2.0/user')
+        .reply(403, { error: { detail: { required: ['account'] } } });
+      await bitbucket.initPlatform({ username: 'renovate', password: 'pass' });
+      expect(logger.warn).toHaveBeenCalledWith(
+        `Bitbucket: missing 'account' scope for password`
+      );
+    });
   });
 
   describe('getRepos()', () => {
@@ -607,6 +617,36 @@ describe('platform/bitbucket', () => {
     it('exists', () => {
       expect(bitbucket.getPrList).toBeDefined();
     });
+    it('filters PR list by author', async () => {
+      const scope = httpMock.scope(baseUrl);
+      scope.get('/2.0/user').reply(200, { uuid: '12345' });
+      await bitbucket.initPlatform({ username: 'renovate', password: 'pass' });
+      await initRepoMock(null, null, scope);
+      scope
+        .get(
+          '/2.0/repositories/some/repo/pullrequests?state=OPEN&state=MERGED&state=DECLINED&state=SUPERSEDED&pagelen=50'
+        )
+        .reply(200, {
+          values: [
+            {
+              id: 2,
+              author: { uuid: 'abcde' },
+              source: { branch: { name: 'branch-a' } },
+              destination: { branch: { name: 'branch-a' } },
+              state: 'OPEN',
+            },
+            {
+              id: 1,
+              author: { uuid: '12345' },
+              source: { branch: { name: 'branch-a' } },
+              destination: { branch: { name: 'branch-b' } },
+              state: 'OPEN',
+            },
+          ],
+        });
+      expect(await bitbucket.getPrList()).toMatchSnapshot();
+      expect(httpMock.getTrace()).toMatchSnapshot();
+    });
   });
 
   describe('findPr()', () => {
diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts
index ef3f6dc126..1739bdc6fe 100644
--- a/lib/platform/bitbucket/index.ts
+++ b/lib/platform/bitbucket/index.ts
@@ -43,7 +43,9 @@ let config: utils.Config = {} as any;
 
 const defaults = { endpoint: BITBUCKET_PROD_ENDPOINT };
 
-export function initPlatform({
+let renovateUserUuid: string;
+
+export async function initPlatform({
   endpoint,
   username,
   password,
@@ -60,6 +62,26 @@ export function initPlatform({
     defaults.endpoint = endpoint;
   }
   setBaseUrl(defaults.endpoint);
+  renovateUserUuid = null;
+  try {
+    const { uuid } = (
+      await bitbucketHttp.getJson<{ uuid: string }>('/2.0/user', {
+        username,
+        password,
+        useCache: false,
+      })
+    ).body;
+    renovateUserUuid = uuid;
+  } catch (err) {
+    if (
+      err.statusCode === 403 &&
+      err.body?.error?.detail?.required?.includes('account')
+    ) {
+      logger.warn(`Bitbucket: missing 'account' scope for password`);
+    } else {
+      logger.debug({ err }, 'Unknown error fetching Bitbucket user identity');
+    }
+  }
   // TODO: Add a connection check that endpoint/username/password combination are valid
   const platformConfig: PlatformResult = {
     endpoint: endpoint || BITBUCKET_PROD_ENDPOINT,
@@ -197,7 +219,14 @@ export async function getPrList(): Promise<Pr[]> {
     let url = `/2.0/repositories/${config.repository}/pullrequests?`;
     url += utils.prStates.all.map((state) => 'state=' + state).join('&');
     const prs = await utils.accumulateValues(url, undefined, undefined, 50);
-    config.prList = prs.map(utils.prInfo);
+    config.prList = prs
+      .filter((pr) => {
+        const prAuthorId = pr?.author?.uuid;
+        return renovateUserUuid && prAuthorId
+          ? renovateUserUuid === prAuthorId
+          : true;
+      })
+      .map(utils.prInfo);
     logger.debug({ length: config.prList.length }, 'Retrieved Pull Requests');
   }
   return config.prList;
diff --git a/lib/platform/bitbucket/utils.ts b/lib/platform/bitbucket/utils.ts
index fa5bbb4589..9b0cb72850 100644
--- a/lib/platform/bitbucket/utils.ts
+++ b/lib/platform/bitbucket/utils.ts
@@ -14,6 +14,7 @@ export interface Config {
   prList: Pr[];
   repository: string;
   username: string;
+  userUuid: string;
 }
 
 export interface PagedResult<T = any> {
-- 
GitLab