From b0ea9156da923f1b73df4bb386c9fdb227882de9 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 18 Mar 2024 18:50:22 -0300 Subject: [PATCH] fix(bitbucket): Use schema for repo result validation (#27855) --- .../datasource/bitbucket-tags/index.spec.ts | 12 ++- .../datasource/bitbucket-tags/index.ts | 9 +- .../__snapshots__/index.spec.ts.snap | 8 -- lib/modules/platform/bitbucket/index.spec.ts | 93 +++++++++++++++---- lib/modules/platform/bitbucket/index.ts | 34 +++---- lib/modules/platform/bitbucket/schema.ts | 54 +++++++++++ lib/modules/platform/bitbucket/types.ts | 23 ----- lib/modules/platform/bitbucket/utils.ts | 14 --- 8 files changed, 157 insertions(+), 90 deletions(-) diff --git a/lib/modules/datasource/bitbucket-tags/index.spec.ts b/lib/modules/datasource/bitbucket-tags/index.spec.ts index 4a8780d689e67f..08f8e8d9a682f0 100644 --- a/lib/modules/datasource/bitbucket-tags/index.spec.ts +++ b/lib/modules/datasource/bitbucket-tags/index.spec.ts @@ -62,7 +62,11 @@ describe('modules/datasource/bitbucket-tags/index', () => { httpMock .scope('https://api.bitbucket.org') .get('/2.0/repositories/some/dep2') - .reply(200, { mainbranch: { name: 'master' } }); + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }); httpMock .scope('https://api.bitbucket.org') .get('/2.0/repositories/some/dep2/commits/master') @@ -87,7 +91,11 @@ describe('modules/datasource/bitbucket-tags/index', () => { httpMock .scope('https://api.bitbucket.org') .get('/2.0/repositories/some/dep2') - .reply(200, { mainbranch: { name: 'master' } }); + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }); httpMock .scope('https://api.bitbucket.org') .get('/2.0/repositories/some/dep2/commits/master') diff --git a/lib/modules/datasource/bitbucket-tags/index.ts b/lib/modules/datasource/bitbucket-tags/index.ts index 7a2457c0c3a324..57149f7e74e77e 100644 --- a/lib/modules/datasource/bitbucket-tags/index.ts +++ b/lib/modules/datasource/bitbucket-tags/index.ts @@ -2,7 +2,8 @@ import { cache } from '../../../util/cache/package/decorator'; import type { PackageCacheNamespace } from '../../../util/cache/package/types'; import { BitbucketHttp } from '../../../util/http/bitbucket'; import { ensureTrailingSlash } from '../../../util/url'; -import type { PagedResult, RepoInfoBody } from '../../platform/bitbucket/types'; +import { RepoInfo } from '../../platform/bitbucket/schema'; +import type { PagedResult } from '../../platform/bitbucket/types'; import { Datasource } from '../datasource'; import type { DigestConfig, GetReleasesConfig, ReleaseResult } from '../types'; import type { BitbucketCommit, BitbucketTag } from './types'; @@ -102,10 +103,8 @@ export class BitbucketTagsDatasource extends Datasource { }) async getMainBranch(repo: string): Promise { return ( - await this.bitbucketHttp.getJson( - `/2.0/repositories/${repo}`, - ) - ).body.mainbranch.name; + await this.bitbucketHttp.getJson(`/2.0/repositories/${repo}`, RepoInfo) + ).body.mainbranch; } // getDigest fetched the latest commit for repository main branch diff --git a/lib/modules/platform/bitbucket/__snapshots__/index.spec.ts.snap b/lib/modules/platform/bitbucket/__snapshots__/index.spec.ts.snap index 1818c5b6361ede..b009eb1bde00d7 100644 --- a/lib/modules/platform/bitbucket/__snapshots__/index.spec.ts.snap +++ b/lib/modules/platform/bitbucket/__snapshots__/index.spec.ts.snap @@ -132,14 +132,6 @@ exports[`modules/platform/bitbucket/index getPrList() filters PR list by author ] `; -exports[`modules/platform/bitbucket/index initRepo() works with username and password 1`] = ` -{ - "defaultBranch": "master", - "isFork": false, - "repoFingerprint": "56653db0e9341ef4957c92bb78ee668b0a3f03c75b77db94d520230557385fca344cc1f593191e3594183b5b050909d29996c040045e8852f21774617b240642", -} -`; - exports[`modules/platform/bitbucket/index massageMarkdown() returns diff files 1`] = ` "**foo** diff --git a/lib/modules/platform/bitbucket/index.spec.ts b/lib/modules/platform/bitbucket/index.spec.ts index 9d66ff1757b21b..66fd23ac131a2e 100644 --- a/lib/modules/platform/bitbucket/index.spec.ts +++ b/lib/modules/platform/bitbucket/index.spec.ts @@ -56,8 +56,9 @@ describe('modules/platform/bitbucket/index', () => { const scope = existingScope ?? httpMock.scope(baseUrl); scope.get(`/2.0/repositories/${repository}`).reply(200, { - owner: {}, mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', ...repoResp, }); @@ -131,7 +132,18 @@ describe('modules/platform/bitbucket/index', () => { .scope(baseUrl) .get('/2.0/repositories?role=contributor&pagelen=100') .reply(200, { - values: [{ full_name: 'foo/bar' }, { full_name: 'some/repo' }], + values: [ + { + mainbranch: { name: 'master' }, + uuid: '111', + full_name: 'foo/bar', + }, + { + mainbranch: { name: 'master' }, + uuid: '222', + full_name: 'some/repo', + }, + ], }); const res = await bitbucket.getRepos({}); expect(res).toEqual(['foo/bar', 'some/repo']); @@ -143,8 +155,18 @@ describe('modules/platform/bitbucket/index', () => { .get('/2.0/repositories?role=contributor&pagelen=100') .reply(200, { values: [ - { full_name: 'foo/bar', project: { name: 'ignore' } }, - { full_name: 'some/repo', project: { name: 'allow' } }, + { + mainbranch: { name: 'master' }, + uuid: '111', + full_name: 'foo/bar', + project: { name: 'ignore' }, + }, + { + mainbranch: { name: 'master' }, + uuid: '222', + full_name: 'some/repo', + project: { name: 'allow' }, + }, ], }); const res = await bitbucket.getRepos({ projects: ['allow'] }); @@ -157,8 +179,18 @@ describe('modules/platform/bitbucket/index', () => { .get('/2.0/repositories?role=contributor&pagelen=100') .reply(200, { values: [ - { full_name: 'foo/bar', project: { name: 'ignore' } }, - { full_name: 'some/repo', project: { name: 'allow' } }, + { + mainbranch: { name: 'master' }, + uuid: '111', + full_name: 'foo/bar', + project: { name: 'ignore' }, + }, + { + mainbranch: { name: 'master' }, + uuid: '222', + full_name: 'some/repo', + project: { name: 'allow' }, + }, ], }); const res = await bitbucket.getRepos({ projects: ['!ignore'] }); @@ -171,12 +203,20 @@ describe('modules/platform/bitbucket/index', () => { httpMock .scope(baseUrl) .get('/2.0/repositories/some/repo') - .reply(200, { owner: {}, mainbranch: { name: 'master' } }); + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }); expect( await bitbucket.initRepo({ repository: 'some/repo', }), - ).toMatchSnapshot(); + ).toMatchObject({ + defaultBranch: 'master', + isFork: false, + repoFingerprint: expect.any(String), + }); }); it('works with only token', async () => { @@ -187,16 +227,19 @@ describe('modules/platform/bitbucket/index', () => { httpMock .scope(baseUrl) .get('/2.0/repositories/some/repo') - .reply(200, { owner: {}, mainbranch: { name: 'master' } }); + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }); expect( await bitbucket.initRepo({ repository: 'some/repo', }), - ).toEqual({ + ).toMatchObject({ defaultBranch: 'master', isFork: false, - repoFingerprint: - '56653db0e9341ef4957c92bb78ee668b0a3f03c75b77db94d520230557385fca344cc1f593191e3594183b5b050909d29996c040045e8852f21774617b240642', + repoFingerprint: expect.any(String), }); }); }); @@ -206,7 +249,11 @@ describe('modules/platform/bitbucket/index', () => { httpMock .scope(baseUrl) .get('/2.0/repositories/some/repo') - .reply(200, { owner: {}, mainbranch: { name: 'master' } }); + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }); const res = await bitbucket.initRepo({ repository: 'some/repo', @@ -220,7 +267,11 @@ describe('modules/platform/bitbucket/index', () => { httpMock .scope(baseUrl) .get('/2.0/repositories/some/repo') - .reply(200, { owner: {}, mainbranch: { name: 'master' } }) + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }) .get('/2.0/repositories/some/repo/branching-model') .reply(200, { development: { name: 'develop', branch: { name: 'develop' } }, @@ -238,7 +289,11 @@ describe('modules/platform/bitbucket/index', () => { httpMock .scope(baseUrl) .get('/2.0/repositories/some/repo') - .reply(200, { owner: {}, mainbranch: { name: 'master' } }) + .reply(200, { + mainbranch: { name: 'master' }, + uuid: '123', + full_name: 'some/repo', + }) .get('/2.0/repositories/some/repo/branching-model') .reply(200, { development: { name: 'develop' }, @@ -607,8 +662,8 @@ describe('modules/platform/bitbucket/index', () => { }); describe('ensureIssueClosing()', () => { - it('does not throw', async () => { - await initRepoMock(); + it('does not throw for disabled issues', async () => { + await initRepoMock({ repository: 'some/repo' }, { has_issues: false }); await expect(bitbucket.ensureIssueClosing('title')).toResolve(); }); @@ -641,8 +696,8 @@ describe('modules/platform/bitbucket/index', () => { }); describe('getIssueList()', () => { - it('has no issues', async () => { - await initRepoMock(); + it('returns empty array for disabled issues', async () => { + await initRepoMock({ repository: 'some/repo' }, { has_issues: false }); expect(await bitbucket.getIssueList()).toEqual([]); }); diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts index a2eef445a4d067..79d022410043b5 100644 --- a/lib/modules/platform/bitbucket/index.ts +++ b/lib/modules/platform/bitbucket/index.ts @@ -34,6 +34,7 @@ import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; import * as comments from './comments'; import { BitbucketPrCache } from './pr-cache'; +import { RepoInfo, Repositories } from './schema'; import type { Account, BitbucketStatus, @@ -43,8 +44,6 @@ import type { PagedResult, PrResponse, RepoBranchingModel, - RepoInfo, - RepoInfoBody, } from './types'; import * as utils from './utils'; import { mergeBodyTransformer } from './utils'; @@ -117,14 +116,11 @@ export async function initPlatform({ export async function getRepos(config: AutodiscoverConfig): Promise { logger.debug('Autodiscovering Bitbucket Cloud repositories'); try { - let repos = ( - await bitbucketHttp.getJson>( - `/2.0/repositories/?role=contributor`, - { - paginate: true, - }, - ) - ).body.values; + let { body: repos } = await bitbucketHttp.getJson( + `/2.0/repositories/?role=contributor`, + { paginate: true }, + Repositories, + ); // if autodiscoverProjects is configured // filter the repos list @@ -134,12 +130,14 @@ export async function getRepos(config: AutodiscoverConfig): Promise { { autodiscoverProjects: config.projects }, 'Applying autodiscoverProjects filter', ); - repos = repos.filter((repo) => - matchRegexOrGlobList(repo.project.name, autodiscoverProjects), + repos = repos.filter( + (repo) => + repo.projectName && + matchRegexOrGlobList(repo.projectName, autodiscoverProjects), ); } - return repos.map((repo) => repo.full_name); + return repos.map(({ owner, name }) => `${owner}/${name}`); } catch (err) /* istanbul ignore next */ { logger.error({ err }, `bitbucket getRepos error`); throw err; @@ -198,13 +196,11 @@ export async function initRepo({ let info: RepoInfo; let mainBranch: string; try { - info = utils.repoInfoTransformer( - ( - await bitbucketHttp.getJson( - `/2.0/repositories/${repository}`, - ) - ).body, + const { body: repoInfo } = await bitbucketHttp.getJson( + `/2.0/repositories/${repository}`, + RepoInfo, ); + info = repoInfo; mainBranch = info.mainbranch; diff --git a/lib/modules/platform/bitbucket/schema.ts b/lib/modules/platform/bitbucket/schema.ts index 48104f2dc55e90..679073591b6f25 100644 --- a/lib/modules/platform/bitbucket/schema.ts +++ b/lib/modules/platform/bitbucket/schema.ts @@ -1,4 +1,6 @@ import { z } from 'zod'; +import { logger } from '../../../logger'; +import { LooseArray } from '../../../util/schema-utils'; const BitbucketSourceTypeSchema = z.enum(['commit_directory', 'commit_file']); @@ -20,3 +22,55 @@ const PagedSchema = z.object({ export const PagedSourceResultsSchema = PagedSchema.extend({ values: z.array(SourceResultsSchema), }); + +export const RepoInfo = z + .object({ + parent: z.unknown().optional().catch(undefined), + mainbranch: z.object({ + name: z.string(), + }), + has_issues: z.boolean().catch(() => { + logger.once.warn('Bitbucket: "has_issues" field missing from repo info'); + return false; + }), + uuid: z.string(), + full_name: z + .string() + .regex( + /^[^/]+\/[^/]+$/, + 'Expected repository full_name to be in the format "owner/repo"', + ), + is_private: z.boolean().catch(() => { + logger.once.warn('Bitbucket: "is_private" field missing from repo info'); + return true; + }), + project: z + .object({ + name: z.string(), + }) + .nullable() + .catch(null), + }) + .transform((repoInfoBody) => { + const isFork = !!repoInfoBody.parent; + const [owner, name] = repoInfoBody.full_name.split('/'); + + return { + isFork, + owner, + name, + mainbranch: repoInfoBody.mainbranch.name, + mergeMethod: 'merge', + has_issues: repoInfoBody.has_issues, + uuid: repoInfoBody.uuid, + is_private: repoInfoBody.is_private, + projectName: repoInfoBody.project?.name, + }; + }); +export type RepoInfo = z.infer; + +export const Repositories = z + .object({ + values: LooseArray(RepoInfo), + }) + .transform((body) => body.values); diff --git a/lib/modules/platform/bitbucket/types.ts b/lib/modules/platform/bitbucket/types.ts index fb4638179e0106..26ef43e3125b53 100644 --- a/lib/modules/platform/bitbucket/types.ts +++ b/lib/modules/platform/bitbucket/types.ts @@ -26,16 +26,6 @@ export interface PagedResult { values: T[]; } -export interface RepoInfo { - isFork: boolean; - owner: string; - mainbranch: string; - mergeMethod: string; - has_issues: boolean; - uuid: string; - is_private: boolean; -} - export interface RepoBranchingModel { development: { name: string; @@ -58,19 +48,6 @@ export interface BitbucketStatus { state: BitbucketBranchState; } -export interface RepoInfoBody { - parent?: any; - owner: { username: string }; - mainbranch: { name: string }; - has_issues: boolean; - uuid: string; - full_name: string; - is_private: boolean; - project: { - name: string; - }; -} - export interface PrResponse { id: number; title: string; diff --git a/lib/modules/platform/bitbucket/utils.ts b/lib/modules/platform/bitbucket/utils.ts index 852f61611164f5..f0d2fa7f9a9676 100644 --- a/lib/modules/platform/bitbucket/utils.ts +++ b/lib/modules/platform/bitbucket/utils.ts @@ -7,22 +7,8 @@ import type { BitbucketMergeStrategy, MergeRequestBody, PrResponse, - RepoInfo, - RepoInfoBody, } from './types'; -export function repoInfoTransformer(repoInfoBody: RepoInfoBody): RepoInfo { - return { - isFork: !!repoInfoBody.parent, - owner: repoInfoBody.owner.username, - mainbranch: repoInfoBody.mainbranch.name, - mergeMethod: 'merge', - has_issues: repoInfoBody.has_issues, - uuid: repoInfoBody.uuid, - is_private: repoInfoBody.is_private, - }; -} - const bitbucketMergeStrategies: Map = new Map([ ['squash', 'squash'],