Skip to content

Commit

Permalink
fix(bitbucket): Use schema for repo result validation (#27855)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Mar 18, 2024
1 parent 814d2ec commit b0ea915
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 90 deletions.
12 changes: 10 additions & 2 deletions lib/modules/datasource/bitbucket-tags/index.spec.ts
Expand Up @@ -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')
Expand All @@ -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')
Expand Down
9 changes: 4 additions & 5 deletions lib/modules/datasource/bitbucket-tags/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -102,10 +103,8 @@ export class BitbucketTagsDatasource extends Datasource {
})
async getMainBranch(repo: string): Promise<string> {
return (
await this.bitbucketHttp.getJson<RepoInfoBody>(
`/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
Expand Down
Expand Up @@ -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**
Expand Down
93 changes: 74 additions & 19 deletions lib/modules/platform/bitbucket/index.spec.ts
Expand Up @@ -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,
});

Expand Down Expand Up @@ -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']);
Expand All @@ -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'] });
Expand All @@ -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'] });
Expand All @@ -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 () => {
Expand All @@ -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),
});
});
});
Expand All @@ -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',
Expand All @@ -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' } },
Expand All @@ -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' },
Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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([]);
});

Expand Down
34 changes: 15 additions & 19 deletions lib/modules/platform/bitbucket/index.ts
Expand Up @@ -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,
Expand All @@ -43,8 +44,6 @@ import type {
PagedResult,
PrResponse,
RepoBranchingModel,
RepoInfo,
RepoInfoBody,
} from './types';
import * as utils from './utils';
import { mergeBodyTransformer } from './utils';
Expand Down Expand Up @@ -117,14 +116,11 @@ export async function initPlatform({
export async function getRepos(config: AutodiscoverConfig): Promise<string[]> {
logger.debug('Autodiscovering Bitbucket Cloud repositories');
try {
let repos = (
await bitbucketHttp.getJson<PagedResult<RepoInfoBody>>(
`/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
Expand All @@ -134,12 +130,14 @@ export async function getRepos(config: AutodiscoverConfig): Promise<string[]> {
{ 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;
Expand Down Expand Up @@ -198,13 +196,11 @@ export async function initRepo({
let info: RepoInfo;
let mainBranch: string;
try {
info = utils.repoInfoTransformer(
(
await bitbucketHttp.getJson<RepoInfoBody>(
`/2.0/repositories/${repository}`,
)
).body,
const { body: repoInfo } = await bitbucketHttp.getJson(
`/2.0/repositories/${repository}`,
RepoInfo,
);
info = repoInfo;

mainBranch = info.mainbranch;

Expand Down
54 changes: 54 additions & 0 deletions 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']);

Expand All @@ -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<typeof RepoInfo>;

export const Repositories = z
.object({
values: LooseArray(RepoInfo),
})
.transform((body) => body.values);

0 comments on commit b0ea915

Please sign in to comment.