import * as httpMock from '~test/http-mock.ts'; import { fakeSha, git, hostRules, logger, partial } from '~test/util.ts'; import { GlobalConfig } from '../../../config/global.ts'; import type { RepoGlobalConfig } from '../../../config/types.ts'; import { CONFIG_GIT_URL_UNAVAILABLE, REPOSITORY_ACCESS_FORBIDDEN, REPOSITORY_ARCHIVED, REPOSITORY_BLOCKED, REPOSITORY_CHANGED, REPOSITORY_EMPTY, REPOSITORY_MIRRORED, TEMPORARY_ERROR, } from '../../../constants/error-messages.ts'; import * as memCache from '../../../util/cache/memory/index.ts'; import * as repoCache from '../../../util/cache/repository/index.ts'; import { parseUrl } from '../../../util/url.ts'; import type { EnsureIssueConfig, RepoParams } from '../index.ts'; import * as helper from './gitea-helper.ts'; import * as gitea from './index.ts'; import type { Comment, CommitStatus, CommitStatusType, Issue, Label, PR, Repo, RepoPermission, User, } from './types.ts'; /** * latest tested gitea version. */ const GITEA_VERSION = '1.14.0+dev-754-g5d2b7ba63'; describe('modules/platform/gitea/index', () => { function mockedRepo(opts: Partial): Repo { return partial({ permissions: partial({ push: true, pull: true }), has_pull_requests: true, ...opts, }); } const mockCommitHash = fakeSha('mockCommitHash'); const mockUser: User = { id: 1, login: 'renovate', full_name: 'Renovate Bot', email: 'renovate@example.com', }; const mockRepo = mockedRepo({ allow_rebase: true, default_merge_style: 'rebase', clone_url: 'https://gitea.renovatebot.com/some/repo.git', ssh_url: 'git@gitea.renovatebot.com/some/repo.git', default_branch: 'master', full_name: 'some/repo', }); type MockPr = PR & Required>; const mockRepos: Repo[] = [ mockedRepo({ full_name: 'a/b' }), mockedRepo({ full_name: 'c/d' }), mockedRepo({ full_name: 'e/f', mirror: true }), ]; const mockTopicRepos: Repo[] = [mockedRepo({ full_name: 'a/b' })]; const mockNamespaceRepos: Repo[] = [ mockedRepo({ full_name: 'org1/repo' }), mockedRepo({ full_name: 'org2/repo' }), mockedRepo({ full_name: 'org2/repo2', archived: true }), ]; const mockPRs: MockPr[] = [ partial({ number: 1, title: 'Some PR', body: 'some random pull request', state: 'open', diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/1.diff', created_at: '2015-03-22T20:36:16Z', closed_at: '2015-03-22T21:36:16Z', updated_at: '2015-03-22T21:36:16Z', mergeable: true, base: { ref: 'some-base-branch' }, head: { label: 'some-head-branch', sha: fakeSha('some-head-sha'), repo: partial({ full_name: mockRepo.full_name }), }, }), partial({ number: 2, title: 'Other PR', body: 'other random pull request', state: 'closed', diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/2.diff', created_at: '2011-08-18T22:30:38Z', closed_at: '2016-01-09T10:03:21Z', updated_at: '2016-01-09T10:03:21Z', mergeable: true, base: { ref: 'other-base-branch' }, head: { label: 'other-head-branch', sha: fakeSha('other-head-sha'), repo: partial({ full_name: mockRepo.full_name }), }, labels: [ { id: 1, name: 'bug', }, ], }), partial({ number: 3, title: 'WIP: Draft PR', body: 'other random pull request', state: 'open', diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/3.diff', created_at: '2011-08-18T22:30:39Z', closed_at: '2016-01-09T10:03:22Z', updated_at: '2017-01-09T10:03:22Z', mergeable: false, base: { ref: 'draft-base-branch' }, head: { label: 'draft-head-branch', sha: fakeSha('draft-head-sha'), repo: partial({ full_name: mockRepo.full_name }), }, }), partial({ number: 4, title: 'Merged PR', body: 'other random merged pull request', state: 'closed', diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/4.diff', created_at: '2011-08-18T22:30:38Z', closed_at: '2016-01-09T10:03:21Z', updated_at: '2016-01-09T10:03:21Z', mergeable: true, merged: true, base: { ref: 'other-base-branch' }, head: { label: 'merged-head-branch', sha: fakeSha('merged-head-sha'), repo: partial({ full_name: mockRepo.full_name }), }, labels: [ { id: 1, name: 'bug', }, ], }), ]; const mockIssues: Issue[] = [ { number: 1, title: 'open-issue', state: 'open', body: 'some-content', assignees: [], labels: [], }, { number: 2, title: 'closed-issue', state: 'closed', body: 'other-content', assignees: [], labels: undefined as never, // coverage }, { number: 3, title: 'duplicate-issue', state: 'open', body: 'duplicate-content', assignees: [], labels: [], }, { number: 4, title: 'duplicate-issue', state: 'open', body: 'duplicate-content', assignees: [], labels: [], }, { number: 5, title: 'duplicate-issue', state: 'open', body: 'duplicate-content', assignees: [], labels: [], }, ]; const mockComments: Comment[] = [ { id: 11, body: 'some-body' }, { id: 12, body: 'other-body' }, { id: 13, body: '### some-topic\n\nsome-content' }, ]; const mockRepoLabels: Label[] = [ { id: 1, name: 'some-label', description: 'its a me', color: '#000000' }, { id: 2, name: 'other-label', description: 'labelario', color: '#ffffff' }, ]; const mockOrgLabels: Label[] = [ { id: 3, name: 'some-org-label', description: 'its a org me', color: '#0000aa', }, { id: 4, name: 'other-org-label', description: 'org labelario', color: '#ffffaa', }, ]; beforeEach(() => { GlobalConfig.reset(); gitea.resetPlatform(); memCache.init(); repoCache.resetCache(); git.isBranchBehindBase.mockResolvedValue(false); git.getBranchCommit.mockReturnValue(mockCommitHash); hostRules.clear(); }); async function initFakePlatform( scope: httpMock.Scope, version = GITEA_VERSION, ): Promise { scope .get('/user') .reply(200, mockUser) .get('/version') .reply(200, { version }); await gitea.initPlatform({ token: 'abc' }); } async function initFakeRepo( scope: httpMock.Scope, repo?: Partial, config?: RepoGlobalConfig, ): Promise { const repoResult = { ...mockRepo, ...repo }; const repository = repoResult.full_name; scope.get(`/repos/${repository}`).reply(200, repoResult); GlobalConfig.set({ ignorePrAuthor: true, ...config }); await gitea.initRepo({ repository }); } describe('initPlatform()', () => { it('should throw if no token', async () => { await expect(gitea.initPlatform({})).rejects.toThrow( 'Init: You must configure a Gitea personal access token', ); }); it('should throw if auth fails', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); scope.get('/user').reply(500); await expect(gitea.initPlatform({ token: 'some-token' })).rejects.toThrow( 'Init: Authentication failure', ); }); it('should support default endpoint', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); scope .get('/user') .reply(200, { ...mockUser, full_name: '' }) .get('/version') .reply(200, { version: GITEA_VERSION }); await expect( gitea.initPlatform({ token: 'some-token' }), ).resolves.toEqual({ endpoint: 'https://gitea.com/', gitAuthor: 'renovate ', }); }); it('should support custom endpoint', async () => { const scope = httpMock.scope('https://gitea.renovatebot.com/api/v1'); scope .get('/user') .reply(200, mockUser) .get('/version') .reply(200, { version: GITEA_VERSION }); await expect( gitea.initPlatform({ token: 'some-token', endpoint: 'https://gitea.renovatebot.com', }), ).resolves.toEqual({ endpoint: 'https://gitea.renovatebot.com/', gitAuthor: 'Renovate Bot ', }); }); it('should support custom endpoint including api path', async () => { const scope = httpMock.scope('https://gitea.renovatebot.com/api/v1'); scope .get('/user') .reply(200, mockUser) .get('/version') .reply(200, { version: GITEA_VERSION }); await expect( gitea.initPlatform({ token: 'some-token', endpoint: 'https://gitea.renovatebot.com', }), ).resolves.toEqual({ endpoint: 'https://gitea.renovatebot.com/', gitAuthor: 'Renovate Bot ', }); }); it('should use username as author name if full name is missing', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); scope .get('/user') .reply(200, { ...mockUser, full_name: undefined, }) .get('/version') .reply(200, { version: GITEA_VERSION }); await expect( gitea.initPlatform({ token: 'some-token' }), ).resolves.toEqual({ endpoint: 'https://gitea.com/', gitAuthor: 'renovate ', }); }); }); describe('getRepos', () => { it('should propagate any other errors', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/search') .query({ uid: 1, archived: false, }) .replyWithError(httpMock.error('searchRepos()')); await initFakePlatform(scope); await expect(gitea.getRepos()).rejects.toThrow('searchRepos()'); }); it('should return an array of repos', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/search') .query({ uid: 1, archived: false, }) .reply(200, { ok: true, data: mockRepos, }); await initFakePlatform(scope); const repos = await gitea.getRepos(); expect(repos).toEqual(['a/b', 'c/d']); }); it('should return an filtered array of repos', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); scope .get('/repos/search') .query({ uid: 1, archived: false, q: 'renovate', topic: true, }) .reply(200, { ok: true, data: mockTopicRepos, }); scope .get('/repos/search') .query({ uid: 1, archived: false, q: 'renovatebot', topic: true, }) .reply(200, { ok: true, data: mockTopicRepos, }); await initFakePlatform(scope); const repos = await gitea.getRepos({ topics: ['renovate', 'renovatebot'], }); expect(repos).toEqual(['a/b']); }); it('should query the organization endpoint for each namespace', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); scope.get('/orgs/org1/repos').reply(200, mockNamespaceRepos); scope.get('/orgs/org2/repos').reply(200, mockNamespaceRepos); await initFakePlatform(scope); const repos = await gitea.getRepos({ namespaces: ['org1', 'org2'], }); expect(repos).toEqual(['org1/repo', 'org2/repo']); }); it('Sorts repos', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/search') .query({ uid: 1, archived: false, sort: 'updated', order: 'desc', }) .reply(200, { ok: true, data: mockRepos, }); await initFakePlatform(scope); const repos = await gitea.getRepos({ sort: 'updated', order: 'desc', }); expect(repos).toEqual(['a/b', 'c/d']); }); }); describe('initRepo', () => { const initRepoCfg: RepoParams = { repository: mockRepo.full_name, }; it('should propagate API errors', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .replyWithError(httpMock.error('getRepo()')); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow('getRepo()'); }); it('should abort when repo is archived', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, archived: true, }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_ARCHIVED, ); }); it('should abort when repo is mirrored', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, mirror: true, }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_MIRRORED, ); }); it('should abort when repo is empty', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, empty: true, }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_EMPTY, ); }); it('should abort when repo has insufficient permissions', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, permissions: { pull: false, push: false, admin: false, }, }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_ACCESS_FORBIDDEN, ); }); it('should abort when repo has pulls disabled', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, has_pull_requests: false, }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_BLOCKED, ); }); it('should abort when repo has no available merge methods', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, allow_rebase: false, }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_BLOCKED, ); }); it('should select default merge method when it is allowed', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, allow_rebase: false, allow_fast_forward_only_merge: true, default_merge_style: 'fast-forward-only', }); await initFakePlatform(scope); await gitea.initRepo(initRepoCfg); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ mergeMethod: 'fast-forward-only', }), ); }); it('should fall back to merge method as per ordered list when default not allowed', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, allow_merge_commits: true, allow_rebase: false, allow_squash_merge: false, default_merge_style: 'squash', }); await initFakePlatform(scope); await gitea.initRepo(initRepoCfg); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ mergeMethod: 'merge', }), ); }); it('should throw if unknown default merge style is configured', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, default_merge_style: 'unknown', }); await initFakePlatform(scope); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow( REPOSITORY_BLOCKED, ); }); it('should use clone_url of repo if gitUrl is not specified', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, mockRepo); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, }; await gitea.initRepo(repoCfg); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ url: mockRepo.clone_url }), ); }); it('should use clone_url of repo if gitUrl has value default', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, mockRepo); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, gitUrl: 'default', }; await gitea.initRepo(repoCfg); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ url: mockRepo.clone_url }), ); }); it('should use ssh_url of repo if gitUrl has value ssh', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, mockRepo); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, gitUrl: 'ssh', }; await gitea.initRepo(repoCfg); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ url: mockRepo.ssh_url }), ); }); it('should abort when gitUrl has value ssh but ssh_url is empty', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, ssh_url: undefined }); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, gitUrl: 'ssh', }; await expect(gitea.initRepo(repoCfg)).rejects.toThrow( CONFIG_GIT_URL_UNAVAILABLE, ); }); it('should use generated url of repo if gitUrl has value endpoint', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, mockRepo); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, gitUrl: 'endpoint', }; await gitea.initRepo(repoCfg); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ url: `https://gitea.com/${mockRepo.full_name}.git`, }), ); }); it('should abort when clone_url is empty', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, clone_url: undefined, }); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, }; await expect(gitea.initRepo(repoCfg)).rejects.toThrow( CONFIG_GIT_URL_UNAVAILABLE, ); }); it('should use given access token if gitUrl has value endpoint', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, mockRepo); await initFakePlatform(scope); const token = 'abc'; hostRules.add({ hostType: 'gitea', matchHost: 'https://gitea.com/', token, }); const repoCfg: RepoParams = { repository: mockRepo.full_name, gitUrl: 'endpoint', }; await gitea.initRepo(repoCfg); const url = parseUrl(`${mockRepo.clone_url}`)!; url.username = token; expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ url: `https://${token}@gitea.com/${mockRepo.full_name}.git`, }), ); }); it('should use given access token if gitUrl is not specified', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, mockRepo); await initFakePlatform(scope); const token = 'abc'; hostRules.add({ hostType: 'gitea', matchHost: 'https://gitea.com/', token, }); const repoCfg: RepoParams = { repository: mockRepo.full_name, }; await gitea.initRepo(repoCfg); const url = parseUrl(`${mockRepo.clone_url}`)!; url.username = token; expect(git.initRepo).toHaveBeenCalledExactlyOnceWith( expect.objectContaining({ url: url.toString() }), ); }); it('should abort when clone_url is not valid', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get(`/repos/${initRepoCfg.repository}`) .reply(200, { ...mockRepo, clone_url: 'abc', }); await initFakePlatform(scope); const repoCfg: RepoParams = { repository: mockRepo.full_name, }; await expect(gitea.initRepo(repoCfg)).rejects.toThrow( CONFIG_GIT_URL_UNAVAILABLE, ); }); }); describe('setBranchStatus', () => { it('should create a new commit status', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post(`/repos/some/repo/statuses/${mockCommitHash}`, { state: 'success', context: 'some-context', description: 'some-description', }) .reply(200) .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, []); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.setBranchStatus({ branchName: 'some-branch', state: 'green', context: 'some-context', description: 'some-description', }), ).toResolve(); }); it('should default to pending state', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post(`/repos/some/repo/statuses/${mockCommitHash}`, { state: 'pending', context: 'some-context', description: 'some-description', }) .reply(200) .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, []); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.setBranchStatus({ branchName: 'some-branch', context: 'some-context', description: 'some-description', state: undefined as never, }), ).toResolve(); }); it('should include url if specified', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post(`/repos/some/repo/statuses/${mockCommitHash}`, { state: 'success', context: 'some-context', description: 'some-description', target_url: 'some-url', }) .reply(200) .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, []); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.setBranchStatus({ branchName: 'some-branch', state: 'green', context: 'some-context', description: 'some-description', url: 'some-url', }), ).toResolve(); }); it('should gracefully fail with warning', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post(`/repos/some/repo/statuses/${mockCommitHash}`) .replyWithError('unknown error'); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.setBranchStatus({ branchName: 'some-branch', state: 'green', context: 'some-context', description: 'some-description', }), ).toResolve(); expect(logger.logger.warn).toHaveBeenCalledWith( { err: expect.any(Error), }, 'Failed to set branch status', ); }); }); describe('getBranchStatus', () => { function commitStatus(status: CommitStatusType): CommitStatus { return { id: 1, status, context: '', description: '', target_url: '', created_at: '', }; } it('should return yellow for unknown result', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [commitStatus('unknown')]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatus('some-branch', true); expect(res).toBe('yellow'); }); it('should return pending state for pending result', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [commitStatus('pending')]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatus('some-branch', true); expect(res).toBe('yellow'); }); it('should return green state for success result', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [commitStatus('success')]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatus('some-branch', true); expect(res).toBe('green'); }); it('should return yellow for all other results', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [commitStatus('invalid' as never)]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatus('some-branch', true); expect(res).toBe('yellow'); }); it('should abort when branch status returns 404', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(404); await initFakePlatform(scope); await initFakeRepo(scope); await expect(gitea.getBranchStatus('some-branch', true)).rejects.toThrow( REPOSITORY_CHANGED, ); }); it('should propagate any other errors', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .replyWithError('unknown error'); await initFakePlatform(scope); await initFakeRepo(scope); await expect(gitea.getBranchStatus('some-branch', true)).rejects.toThrow( 'unknown error', ); }); it('should treat internal checks as success', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [ { id: 1, status: 'success', context: 'renovate/stability-days', description: 'internal check', target_url: '', created_at: '', }, ]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatus('some-branch', true); expect(res).toBe('green'); }); it('should not treat internal checks as success', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [ { id: 1, status: 'success', context: 'renovate/stability-days', description: 'internal check', target_url: '', created_at: '', }, ]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatus('some-branch', false); expect(res).toBe('yellow'); }); }); describe('getBranchStatusCheck', () => { it('should return null with no results', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, []); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.getBranchStatusCheck('some-branch', 'some-context'), ).resolves.toBeNull(); }); it('should return null with no matching results', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [ { id: 1, status: 'success', context: 'other-context', description: 'internal check', target_url: '', created_at: '', }, ]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatusCheck( 'some-branch', 'some-context', ); expect(res).toBeNull(); }); it('should return yellow with unknown status', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [ { id: 1, status: 'xyz', context: 'some-context', description: '', target_url: '', created_at: '', }, ]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatusCheck( 'some-branch', 'some-context', ); expect(res).toBe('yellow'); }); it('should return green of matching result', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/commits/some-branch/statuses') .reply(200, [ { id: 1, status: 'success', context: 'some-context', description: '', target_url: '', created_at: '', }, ]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getBranchStatusCheck( 'some-branch', 'some-context', ); expect(res).toBe('green'); }); }); describe('getPrList', () => { it('should return list of pull requests', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getPrList(); expect(res).toMatchObject([ { number: 4, title: 'Merged PR' }, { number: 3, title: 'Draft PR' }, { number: 2, title: 'Other PR' }, { number: 1, title: 'Some PR' }, ]); }); it('should filter list by creator', async () => { const thirdPartyPr = partial({ number: 42, title: 'Third-party PR', body: 'other random pull request', state: 'open', diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/3.diff', created_at: '2011-08-18T22:30:38Z', closed_at: '2016-01-09T10:03:21Z', mergeable: true, base: { ref: 'third-party-base-branch' }, head: { label: 'other-head-branch', sha: fakeSha('other-head-sha'), repo: partial({ full_name: mockRepo.full_name }), }, user: { login: 'not-renovate' }, }); const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, [ thirdPartyPr, ...mockPRs.map((pr) => ({ ...pr, user: { login: 'renovate' }, })), ]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getPrList(); expect(res).toMatchObject([ { number: 4, title: 'Merged PR' }, { number: 3, title: 'Draft PR' }, { number: 2, title: 'Other PR' }, { number: 1, title: 'Some PR' }, ]); }); it('should cache results after first query', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100, poster: mockUser.login, }) .reply(200, mockPRs.slice(0, 2), { // test correct pagination handling, domain should be ignored link: '; rel="next",; rel="last"', }) .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 50, page: 2 }) .reply(200, mockPRs.slice(2)); await initFakePlatform(scope); await initFakeRepo(scope, undefined, { ignorePrAuthor: false }); const res1 = await gitea.getPrList(); const res2 = await gitea.getPrList(); expect(res1).toEqual(res2); }); it('should update cache results', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs.slice(0, 2)) .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 20 }) .reply(200, mockPRs.slice(1)); await initFakePlatform(scope); await initFakeRepo(scope); const res1 = await gitea.getPrList(); expect(res1).toMatchObject([{ number: 2 }, { number: 1 }]); memCache.set('gitea-pr-cache-synced', false); const res2 = await gitea.getPrList(); expect(res2).toMatchObject([ { number: 4 }, { number: 3 }, { number: 2 }, { number: 1 }, ]); }); }); describe('getPr', () => { it('should return enriched pull request which exists if open', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getPr(1); expect(res).toMatchObject({ number: 1, title: 'Some PR' }); }); it('should fallback to direct fetching if cache fails', async () => { const pr = mockPRs.find((pr) => pr.number === 1); const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, []) .get('/repos/some/repo/pulls/1') .reply(200, pr); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getPr(1); expect(res).toMatchObject({ number: 1, title: 'Some PR' }); }); it('should return null for missing pull request', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, []) .get('/repos/some/repo/pulls/42') .reply(200); // TODO: 404 should be handled await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getPr(42); expect(res).toBeNull(); }); it('should throw temporary error for null pull request', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, [null]); // TODO: 404 should be handled await initFakePlatform(scope); await initFakeRepo(scope); await expect(gitea.getPr(42)).rejects.toThrow(TEMPORARY_ERROR); }); }); describe('findPr', () => { it('should find pull request without title or state', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'some-head-branch' }); expect(res).toMatchObject({ number: 1, sourceBranch: 'some-head-branch', }); }); it('should find pull request with title', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'some-head-branch', prTitle: 'Some PR', }); expect(res).toMatchObject({ number: 1, title: 'Some PR', }); }); it('should find pull request with state', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'some-head-branch', state: 'open', }); expect(res).toMatchObject({ number: 1, state: 'open', }); }); it('should not find pull request with inverted state', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'other-head-branch', state: `!open`, }); expect(res).toMatchObject({ number: 2, state: 'closed', title: 'Other PR', }); }); it('should find pull request with title and state', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'other-head-branch', prTitle: 'Other PR', state: 'closed', }); expect(res).toMatchObject({ number: 2, state: 'closed', title: 'Other PR', }); }); it('should find pull request with draft', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'draft-head-branch', prTitle: 'Draft PR', state: 'open', }); expect(res).toMatchObject({ number: 3, title: 'Draft PR', isDraft: true, }); }); it('should find merged pull request', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'merged-head-branch' }); expect(res).toMatchObject({ number: 4, sourceBranch: 'merged-head-branch', state: 'merged', }); }); it('should return null for missing pull request', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'missing' }); expect(res).toBeNull(); }); it('finds pr from other authors using base and head', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls/some-base-branch/some-head-branch') .reply(200, { number: 42, state: 'open', head: { label: 'some-head-branch', sha: mockCommitHash, repo: partial({ full_name: mockRepo.full_name }), }, base: { ref: 'some-base-branch', }, title: 'from other author', body: 'pr-body', mergeable: true, created_at: '2014-04-01T05:14:20Z', updated_at: '2017-12-28T12:17:48Z', }); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'some-head-branch', targetBranch: 'some-base-branch', includeOtherAuthors: true, }); expect(res).toMatchObject({ number: 42, sourceBranch: 'some-head-branch', targetBranch: 'some-base-branch', title: 'from other author', }); }); it('returns null if cannot find pr from other author', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls/some-base-branch/some-head-branch') .reply(404); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findPr({ branchName: 'some-head-branch', targetBranch: 'some-base-branch', includeOtherAuthors: true, }); expect(res).toBeNull(); }); }); describe('createPr', () => { beforeEach(() => { vi.restoreAllMocks(); memCache.set('gitea-pr-cache-synced', true); }); const mockNewPR: MockPr = { number: 42, state: 'open', head: { label: 'pr-branch', sha: mockCommitHash, repo: partial({ full_name: mockRepo.full_name }), }, base: { ref: mockRepo.default_branch, }, diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/42.diff', title: 'pr-title', body: 'pr-body', mergeable: true, created_at: '2014-04-01T05:14:20Z', closed_at: '2017-12-28T12:17:48Z', updated_at: '2017-12-28T12:17:48Z', }; it('should use base branch by default', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, { ...mockNewPR, base: { ref: 'devel' }, }); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'devel', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }); it('should use default branch if requested', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, draftPR: true, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }); it('should resolve and apply optional labels to pull request', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR) .get('/repos/some/repo/labels') .reply(200, mockRepoLabels) .get('/orgs/some/labels') .reply(200, mockOrgLabels); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, labels: [...mockRepoLabels, ...mockOrgLabels].map(({ name }) => name), }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }); it('should ensure new pull request gets added to cached pull requests', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR); await initFakePlatform(scope); await initFakeRepo(scope); await gitea.getPrList(); await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); const res = await gitea.getPr(mockNewPR.number); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }); it('should attempt to resolve 409 conflict error (w/o update)', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(409) .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, [mockNewPR]); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }); it('should attempt to resolve 409 conflict error (w/ update)', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(409) .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, [mockNewPR]) .patch('/repos/some/repo/pulls/42') .reply(200); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: 'new-title', prBody: 'new-body', }); expect(res).toMatchObject({ number: 42, title: 'new-title', }); }); it('should abort when response for created pull request is invalid', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, {}); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }), ).rejects.toThrow('Can not parse newly created Pull Request'); }); it('should use platform automerge', async () => { const mergePR = vi.spyOn(helper, 'mergePR'); const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR) .post('/repos/some/repo/pulls/42/merge') .reply(200); await initFakePlatform(scope, '1.24.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { usePlatformAutomerge: true }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); expect(mergePR).toHaveBeenCalled(); }); it('should not use platform automerge on forgejo v7', async () => { const mergePR = vi.spyOn(helper, 'mergePR'); const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR); await initFakePlatform(scope, '7.0.0-dev-2136-f075579c95+gitea-1.22.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { usePlatformAutomerge: true }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); expect(mergePR).not.toHaveBeenCalled(); }); it('should not use platform automerge on forgejo v7 LTS', async () => { const mergePR = vi.spyOn(helper, 'mergePR'); const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR); await initFakePlatform(scope, '7.0.0+LTS-gitea-1.22.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { usePlatformAutomerge: true }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); expect(mergePR).not.toHaveBeenCalled(); }); it('continues on platform automerge error', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR) .post('/repos/some/repo/pulls/42/merge') .replyWithError('unknown error'); await initFakePlatform(scope, '1.24.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { usePlatformAutomerge: true }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); expect(logger.logger.warn).toHaveBeenCalledWith( expect.objectContaining({ prNumber: 42 }), 'Gitea-native automerge: fail', ); }); it('continues if platform automerge is not supported', async () => { memCache.set('gitea-pr-cache-synced', true); const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR); await initFakePlatform(scope, '1.10.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { usePlatformAutomerge: true }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); expect(logger.logger.debug).toHaveBeenCalledWith( expect.objectContaining({ prNumber: 42 }), 'Gitea-native automerge: not supported on this version of Gitea. Use 1.24.0 or newer.', ); }); it('should create PR with repository merge method when automergeStrategy is auto', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR) .post('/repos/some/repo/pulls/42/merge') .reply(200); await initFakePlatform(scope, '10.0.0+gitea-1.22.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { automergeStrategy: 'auto', usePlatformAutomerge: true, }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }); it.each` automergeStrategy | prMergeStrategy ${'fast-forward'} | ${'rebase'} ${'merge-commit'} | ${'merge'} ${'rebase'} | ${'rebase-merge'} ${'squash'} | ${'squash'} `( 'should create PR with mergeStrategy $prMergeStrategy', async ({ automergeStrategy, prMergeStrategy }) => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls') .reply(200, mockNewPR) .post('/repos/some/repo/pulls/42/merge') .reply(200, { Do: prMergeStrategy, merge_when_checks_succeed: true, }); await initFakePlatform(scope, '1.24.0'); await initFakeRepo(scope); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, platformPrOptions: { automergeStrategy, usePlatformAutomerge: true, }, }); expect(res).toMatchObject({ number: 42, title: 'pr-title', }); }, ); }); describe('updatePr', () => { it('should update pull request with title', async () => { const pr = mockPRs.find((pr) => pr.number === 1); const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .patch('/repos/some/repo/pulls/1', { title: 'New Title' }) .reply(200, pr); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 1, prTitle: 'New Title' }), ).toResolve(); }); it('should update pull target branch', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .patch('/repos/some/repo/pulls/1', { title: 'New Title', base: 'New Base', }) .reply(200); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 1, prTitle: 'New Title', targetBranch: 'New Base', }), ).toResolve(); }); it('should update pull request with title and body', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .patch('/repos/some/repo/pulls/1', { title: 'New Title', body: 'New Body', }) .reply(200); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 1, prTitle: 'New Title', prBody: 'New Body', }), ).toResolve(); }); it('should update pull request with draft', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .patch('/repos/some/repo/pulls/3', { title: 'WIP: New Title', body: 'New Body', }) .reply(200); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 3, prTitle: 'New Title', prBody: 'New Body', }), ).toResolve(); }); it('should close pull request', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .patch('/repos/some/repo/pulls/1', { title: 'New Title', body: 'New Body', state: 'closed', }) .reply(200); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 1, prTitle: 'New Title', prBody: 'New Body', state: 'closed', }), ).toResolve(); }); it('should update labels', async () => { const updatedMockPR = partial({ ...mockPRs[0], number: 1, title: 'New Title', body: 'New Body', state: 'open', labels: [ { id: 1, name: 'some-label', }, ], }); const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .get('/repos/some/repo/labels') .reply(200, mockRepoLabels) .get('/orgs/some/labels') .reply(200, mockOrgLabels) .patch('/repos/some/repo/pulls/1') .reply(200, updatedMockPR); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 1, prTitle: 'New Title', prBody: 'New Body', state: 'open', labels: ['some-label'], }), ).toResolve(); }); it('should log a warning if labels could not be looked up', async () => { const updatedMockPR = partial({ ...mockPRs[0], number: 1, title: 'New Title', body: 'New Body', state: 'open', labels: [ { id: 1, name: 'some-label', }, ], }); const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/pulls') .query({ state: 'all', sort: 'recentupdate', limit: 100 }) .reply(200, mockPRs) .get('/repos/some/repo/labels') .reply(200, mockRepoLabels) .get('/orgs/some/labels') .reply(200, mockOrgLabels) .patch('/repos/some/repo/pulls/1') .reply(200, updatedMockPR); await initFakePlatform(scope); await initFakeRepo(scope); await expect( gitea.updatePr({ number: 1, prTitle: 'New Title', prBody: 'New Body', state: 'open', labels: ['some-label', 'unavailable-label'], }), ).toResolve(); expect(logger.logger.warn).toHaveBeenCalledWith( 'Some labels could not be looked up. Renovate may halt label updates assuming changes by others.', ); }); }); describe('mergePr', () => { it('should return true when merging succeeds', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls/1/merge', { Do: 'rebase', }) .reply(200); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.mergePr({ branchName: 'some-branch', id: 1, }); expect(res).toBe(true); }); it('should return false when merging fails', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .post('/repos/some/repo/pulls/1/merge', { Do: 'squash', }) .replyWithError('unknown'); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.mergePr({ branchName: 'some-branch', id: 1, strategy: 'squash', }); expect(res).toBe(false); }); }); describe('getIssueList', () => { it('should return empty for disabled issues', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); await initFakePlatform(scope); await initFakeRepo(scope, { has_issues: false }); const res = await gitea.getIssueList(); expect(res).toBeEmptyArray(); }); }); describe('getIssue', () => { it('should return the issue', async () => { const mockIssue = mockIssues.find((i) => i.number === 1)!; const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/issues/1') .reply(200, mockIssue); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.getIssue?.(mockIssue.number); expect(res).toEqual({ body: 'some-content', number: 1, }); }); it('should return null for disabled issues', async () => { const scope = httpMock.scope('https://gitea.com/api/v1'); await initFakePlatform(scope); await initFakeRepo(scope, { has_issues: false }); const res = await gitea.getIssue!(1); expect(res).toBeNull(); }); }); describe('findIssue', () => { it('should return existing open issue', async () => { const mockIssue = mockIssues.find(({ title }) => title === 'open-issue')!; const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/issues') .query({ state: 'all', type: 'issues' }) .reply(200, mockIssues) .get('/repos/some/repo/issues/1') .reply(200, mockIssue); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findIssue(mockIssue.title); expect(res).toMatchObject({ body: 'some-content', number: 1, }); }); it('should not return existing closed issue', async () => { const mockIssue = mockIssues.find( ({ title }) => title === 'closed-issue', )!; const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/issues') .query({ state: 'all', type: 'issues' }) .reply(200, mockIssues); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findIssue(mockIssue.title); expect(res).toBeNull(); }); it('should return null for missing issue', async () => { const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/issues') .query({ state: 'all', type: 'issues' }) .reply(200, mockIssues); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.findIssue('missing'); expect(res).toBeNull(); }); }); describe('ensureIssue', () => { it('should create issue if not found', async () => { const mockIssue = { title: 'new-title', body: 'new-body', shouldReOpen: false, once: false, }; const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/issues') .query({ state: 'all', type: 'issues' }) .reply(200, mockIssues) .post('/repos/some/repo/issues', { body: mockIssue.body, title: mockIssue.title, }) .reply(200, { number: 42 }); await initFakePlatform(scope); await initFakeRepo(scope); const res = await gitea.ensureIssue(mockIssue); expect(res).toBe('created'); }); it('should create issue with the correct labels', async () => { const mockIssue: EnsureIssueConfig = { title: 'new-title', body: 'new-body', shouldReOpen: false, once: false, labels: ['Renovate', 'Maintenance'], }; const scope = httpMock .scope('https://gitea.com/api/v1') .get('/repos/some/repo/issues') .query({ state: 'all', type: 'issues' }) .reply(200, mockIssues) .get('/repos/some/repo/labels') .reply(200, [ partial