import type { BranchStatusConfig, EnsureIssueConfig, Platform, RepoParams, RepoResult, } from '..'; import { partial } from '../../../../test/util'; import { REPOSITORY_ACCESS_FORBIDDEN, REPOSITORY_ARCHIVED, REPOSITORY_BLOCKED, REPOSITORY_CHANGED, REPOSITORY_EMPTY, REPOSITORY_MIRRORED, } from '../../../constants/error-messages'; import type { logger as _logger } from '../../../logger'; import { BranchStatus, PrState } from '../../../types'; import type * as _git from '../../../util/git'; import { setBaseUrl } from '../../../util/http/gitea'; import type { PlatformResult } from '../types'; import type * as ght from './gitea-helper'; /** * latest tested gitea version. */ const GITEA_VERSION = '1.14.0+dev-754-g5d2b7ba63'; describe('modules/platform/gitea/index', () => { let gitea: Platform; let helper: jest.Mocked<typeof import('./gitea-helper')>; let logger: jest.Mocked<typeof _logger>; let gitvcs: jest.Mocked<typeof _git>; const mockCommitHash = '0d9c7726c3d628b7e28af234595cfd20febdbf8e'; const mockUser: ght.User = { id: 1, username: 'renovate', full_name: 'Renovate Bot', email: 'renovate@example.com', }; const mockRepo = partial<ght.Repo>({ allow_rebase: true, clone_url: 'https://gitea.renovatebot.com/some/repo.git', default_branch: 'master', full_name: 'some/repo', permissions: { pull: true, push: true, admin: false, }, }); const mockRepos: ght.Repo[] = [ partial<ght.Repo>({ full_name: 'a/b' }), partial<ght.Repo>({ full_name: 'c/d' }), ]; const mockPRs: ght.PR[] = [ partial<ght.PR>({ number: 1, title: 'Some PR', body: 'some random pull request', state: PrState.Open, diff_url: 'https://gitea.renovatebot.com/some/repo/pulls/1.diff', created_at: '2015-03-22T20:36:16Z', closed_at: null, mergeable: true, base: { ref: 'some-base-branch' }, head: { label: 'some-head-branch', sha: 'some-head-sha', repo: partial<ght.Repo>({ full_name: mockRepo.full_name }), }, }), partial<ght.PR>({ number: 2, title: 'Other PR', body: 'other random pull request', state: PrState.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', mergeable: true, base: { ref: 'other-base-branch' }, head: { label: 'other-head-branch', sha: 'other-head-sha', repo: partial<ght.Repo>({ full_name: mockRepo.full_name }), }, }), ]; const mockIssues: ght.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: [], }, { 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: ght.Comment[] = [ { id: 11, body: 'some-body' }, { id: 12, body: 'other-body' }, { id: 13, body: '### some-topic\n\nsome-content' }, ]; const mockRepoLabels: ght.Label[] = [ { id: 1, name: 'some-label', description: 'its a me', color: '#000000' }, { id: 2, name: 'other-label', description: 'labelario', color: '#ffffff' }, ]; const mockOrgLabels: ght.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(async () => { jest.resetModules(); jest.clearAllMocks(); jest.mock('./gitea-helper'); jest.mock('../../../util/git'); jest.mock('../../../logger'); gitea = await import('.'); helper = (await import('./gitea-helper')) as any; logger = (await import('../../../logger')).logger as any; gitvcs = require('../../../util/git'); gitvcs.isBranchStale.mockResolvedValue(false); gitvcs.getBranchCommit.mockReturnValue(mockCommitHash); setBaseUrl('https://gitea.renovatebot.com/api/v1'); }); function initFakePlatform(version = GITEA_VERSION): Promise<PlatformResult> { helper.getCurrentUser.mockResolvedValueOnce(mockUser); helper.getVersion.mockResolvedValueOnce(version); return gitea.initPlatform({ token: 'abc' }); } function initFakeRepo( repo?: Partial<ght.Repo>, config?: Partial<RepoParams> ): Promise<RepoResult> { helper.getRepo.mockResolvedValueOnce({ ...mockRepo, ...repo }); return gitea.initRepo({ repository: mockRepo.full_name, ...config, }); } describe('initPlatform()', () => { it('should throw if no token', async () => { await expect(gitea.initPlatform({})).rejects.toThrow(); }); it('should throw if auth fails', async () => { helper.getCurrentUser.mockRejectedValueOnce(new Error()); await expect( gitea.initPlatform({ token: 'some-token' }) ).rejects.toThrow(); }); it('should support default endpoint', async () => { helper.getCurrentUser.mockResolvedValueOnce(mockUser); expect( await gitea.initPlatform({ token: 'some-token' }) ).toMatchSnapshot(); }); it('should support custom endpoint', async () => { helper.getCurrentUser.mockResolvedValueOnce(mockUser); expect( await gitea.initPlatform({ token: 'some-token', endpoint: 'https://gitea.renovatebot.com', }) ).toMatchSnapshot(); }); it('should use username as author name if full name is missing', async () => { helper.getCurrentUser.mockResolvedValueOnce({ ...mockUser, full_name: undefined, }); expect( await gitea.initPlatform({ token: 'some-token' }) ).toMatchSnapshot(); }); }); describe('getRepos', () => { it('should propagate any other errors', async () => { helper.searchRepos.mockRejectedValueOnce(new Error('searchRepos()')); await expect(gitea.getRepos()).rejects.toThrow('searchRepos()'); }); it('should return an array of repos', async () => { helper.searchRepos.mockResolvedValueOnce(mockRepos); const repos = await gitea.getRepos(); expect(repos).toMatchSnapshot(); }); }); describe('initRepo', () => { const initRepoCfg: RepoParams = { repository: mockRepo.full_name, }; it('should propagate API errors', async () => { helper.getRepo.mockRejectedValueOnce(new Error('getRepo()')); await expect(gitea.initRepo(initRepoCfg)).rejects.toThrow('getRepo()'); }); it('should abort when repo is archived', async () => { await expect(initFakeRepo({ archived: true })).rejects.toThrow( REPOSITORY_ARCHIVED ); }); it('should abort when repo is mirrored', async () => { await expect(initFakeRepo({ mirror: true })).rejects.toThrow( REPOSITORY_MIRRORED ); }); it('should abort when repo is empty', async () => { await expect(initFakeRepo({ empty: true })).rejects.toThrow( REPOSITORY_EMPTY ); }); it('should abort when repo has insufficient permissions', async () => { await expect( initFakeRepo({ permissions: { pull: false, push: false, admin: false, }, }) ).rejects.toThrow(REPOSITORY_ACCESS_FORBIDDEN); }); it('should abort when repo has no available merge methods', async () => { await expect(initFakeRepo({ allow_rebase: false })).rejects.toThrow( REPOSITORY_BLOCKED ); }); it('should fall back to merge method "rebase-merge"', async () => { expect( await initFakeRepo({ allow_rebase: false, allow_rebase_explicit: true }) ).toMatchSnapshot(); }); it('should fall back to merge method "squash"', async () => { expect( await initFakeRepo({ allow_rebase: false, allow_squash_merge: true }) ).toMatchSnapshot(); }); it('should fall back to merge method "merge"', async () => { expect( await initFakeRepo({ allow_rebase: false, allow_merge_commits: true, }) ).toMatchSnapshot(); }); }); describe('setBranchStatus', () => { const setBranchStatus = async (bsc?: Partial<BranchStatusConfig>) => { await initFakeRepo(); await gitea.setBranchStatus({ branchName: 'some-branch', state: BranchStatus.green, context: 'some-context', description: 'some-description', ...bsc, }); }; it('should create a new commit status', async () => { await setBranchStatus(); expect(helper.createCommitStatus).toHaveBeenCalledTimes(1); expect(helper.createCommitStatus).toHaveBeenCalledWith( mockRepo.full_name, mockCommitHash, { state: 'success', context: 'some-context', description: 'some-description', } ); }); it('should default to pending state', async () => { await setBranchStatus({ state: undefined }); expect(helper.createCommitStatus).toHaveBeenCalledTimes(1); expect(helper.createCommitStatus).toHaveBeenCalledWith( mockRepo.full_name, mockCommitHash, { state: 'pending', context: 'some-context', description: 'some-description', } ); }); it('should include url if specified', async () => { await setBranchStatus({ url: 'some-url' }); expect(helper.createCommitStatus).toHaveBeenCalledTimes(1); expect(helper.createCommitStatus).toHaveBeenCalledWith( mockRepo.full_name, mockCommitHash, { state: 'success', context: 'some-context', description: 'some-description', target_url: 'some-url', } ); }); it('should gracefully fail with warning', async () => { helper.createCommitStatus.mockRejectedValueOnce(new Error()); await setBranchStatus(); expect(logger.warn).toHaveBeenCalledTimes(1); }); }); describe('getBranchStatus', () => { const getBranchStatus = async (state: string): Promise<BranchStatus> => { await initFakeRepo(); helper.getCombinedCommitStatus.mockResolvedValueOnce( partial<ght.CombinedCommitStatus>({ worstStatus: state as ght.CommitStatusType, }) ); return gitea.getBranchStatus('some-branch'); }; it('should return yellow for unknown result', async () => { expect(await getBranchStatus('unknown')).toEqual(BranchStatus.yellow); }); it('should return pending state for pending result', async () => { expect(await getBranchStatus('pending')).toEqual(BranchStatus.yellow); }); it('should return success state for success result', async () => { expect(await getBranchStatus('success')).toEqual(BranchStatus.green); }); it('should return null for all other results', async () => { expect(await getBranchStatus('invalid')).toEqual(BranchStatus.yellow); }); it('should abort when branch status returns 404', async () => { helper.getCombinedCommitStatus.mockRejectedValueOnce({ statusCode: 404 }); await expect(gitea.getBranchStatus('some-branch')).rejects.toThrow( REPOSITORY_CHANGED ); }); it('should propagate any other errors', async () => { helper.getCombinedCommitStatus.mockRejectedValueOnce( new Error('getCombinedCommitStatus()') ); await expect(gitea.getBranchStatus('some-branch')).rejects.toThrow( 'getCombinedCommitStatus()' ); }); }); describe('getBranchStatusCheck', () => { it('should return null with no results', async () => { helper.getCombinedCommitStatus.mockResolvedValueOnce( partial<ght.CombinedCommitStatus>({ statuses: [], }) ); expect( await gitea.getBranchStatusCheck('some-branch', 'some-context') ).toBeNull(); }); it('should return null with no matching results', async () => { helper.getCombinedCommitStatus.mockResolvedValueOnce( partial<ght.CombinedCommitStatus>({ statuses: [partial<ght.CommitStatus>({ context: 'other-context' })], }) ); expect( await gitea.getBranchStatusCheck('some-branch', 'some-context') ).toBeNull(); }); it('should return yellow with unknown status', async () => { helper.getCombinedCommitStatus.mockResolvedValueOnce( partial<ght.CombinedCommitStatus>({ statuses: [ partial<ght.CommitStatus>({ context: 'some-context', }), ], }) ); expect( await gitea.getBranchStatusCheck('some-branch', 'some-context') ).toEqual(BranchStatus.yellow); }); it('should return green of matching result', async () => { helper.getCombinedCommitStatus.mockResolvedValueOnce( partial<ght.CombinedCommitStatus>({ statuses: [ partial<ght.CommitStatus>({ status: 'success', context: 'some-context', }), ], }) ); expect( await gitea.getBranchStatusCheck('some-branch', 'some-context') ).toEqual(BranchStatus.green); }); }); describe('getPrList', () => { it('should return list of pull requests', async () => { helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res = await gitea.getPrList(); expect(res).toHaveLength(mockPRs.length); expect(res).toMatchSnapshot(); }); it('should filter list by creator', async () => { helper.getCurrentUser.mockResolvedValueOnce(mockUser); expect( await gitea.initPlatform({ token: 'some-token' }) ).toMatchSnapshot(); await initFakeRepo(); helper.searchPRs.mockResolvedValueOnce([ partial<ght.PR>({ number: 3, title: 'Third-party PR', body: 'other random pull request', state: PrState.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: 'other-head-sha', repo: partial<ght.Repo>({ full_name: mockRepo.full_name }), }, user: { username: 'not-renovate' }, }), ...mockPRs.map((pr) => ({ ...pr, user: { username: 'renovate' } })), ]); const res = await gitea.getPrList(); expect(res).toHaveLength(mockPRs.length); expect(res).toMatchSnapshot(); }); it('should cache results after first query', async () => { helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res1 = await gitea.getPrList(); const res2 = await gitea.getPrList(); expect(res1).toEqual(res2); expect(helper.searchPRs).toHaveBeenCalledTimes(1); }); }); describe('getPr', () => { it('should return enriched pull request which exists if open', async () => { const mockPR = mockPRs[0]; helper.searchPRs.mockResolvedValueOnce(mockPRs); helper.getBranch.mockResolvedValueOnce( partial<ght.Branch>({ commit: { id: mockCommitHash, author: partial<ght.CommitUser>({ email: 'renovate@whitesourcesoftware.com', }), }, }) ); await initFakeRepo(); const res = await gitea.getPr(mockPR.number); expect(res).toHaveProperty('number', mockPR.number); expect(res).toMatchSnapshot(); }); it('should fallback to direct fetching if cache fails', async () => { const mockPR = mockPRs[0]; helper.searchPRs.mockResolvedValueOnce([]); helper.getPR.mockResolvedValueOnce({ ...mockPR, mergeable: false }); await initFakeRepo(); const res = await gitea.getPr(mockPR.number); expect(res).toHaveProperty('number', mockPR.number); expect(res).toMatchSnapshot(); expect(helper.getPR).toHaveBeenCalledTimes(1); }); it('should return null for missing pull request', async () => { helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); expect(await gitea.getPr(42)).toBeNull(); }); it('should block modified pull request for rebasing', async () => { const mockPR = mockPRs[0]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res = await gitea.getPr(mockPR.number); expect(res).toHaveProperty('number', mockPR.number); }); }); describe('findPr', () => { it('should find pull request without title or state', async () => { const mockPR = mockPRs[0]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res = await gitea.findPr({ branchName: mockPR.head.label }); expect(res).toHaveProperty('sourceBranch', mockPR.head.label); }); it('should find pull request with title', async () => { const mockPR = mockPRs[0]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res = await gitea.findPr({ branchName: mockPR.head.label, prTitle: mockPR.title, }); expect(res).toHaveProperty('sourceBranch', mockPR.head.label); expect(res).toHaveProperty('title', mockPR.title); }); it('should find pull request with state', async () => { const mockPR = mockPRs[1]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res = await gitea.findPr({ branchName: mockPR.head.label, state: mockPR.state, }); expect(res).toHaveProperty('sourceBranch', mockPR.head.label); expect(res).toHaveProperty('state', mockPR.state); }); it('should not find pull request with inverted state', async () => { const mockPR = mockPRs[1]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); expect( await gitea.findPr({ branchName: mockPR.head.label, state: `!${mockPR.state}` as ght.PRState, }) ).toBeNull(); }); it('should find pull request with title and state', async () => { const mockPR = mockPRs[1]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); const res = await gitea.findPr({ branchName: mockPR.head.label, prTitle: mockPR.title, state: mockPR.state, }); expect(res).toHaveProperty('sourceBranch', mockPR.head.label); expect(res).toHaveProperty('title', mockPR.title); expect(res).toHaveProperty('state', mockPR.state); }); it('should return null for missing pull request', async () => { helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); expect(await gitea.findPr({ branchName: 'missing' })).toBeNull(); }); }); describe('createPr', () => { const mockNewPR: ght.PR = { number: 42, state: PrState.Open, head: { label: 'pr-branch', sha: mockCommitHash, repo: partial<ght.Repo>({ 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', }; it('should use base branch by default', async () => { helper.createPR.mockResolvedValueOnce({ ...mockNewPR, base: { ref: 'devel' }, }); await initFakeRepo(); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'devel', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); expect(res).toHaveProperty('number', mockNewPR.number); expect(res).toHaveProperty('targetBranch', 'devel'); expect(res).toMatchSnapshot(); expect(helper.createPR).toHaveBeenCalledTimes(1); expect(helper.createPR).toHaveBeenCalledWith(mockRepo.full_name, { base: 'devel', head: mockNewPR.head.label, title: mockNewPR.title, body: mockNewPR.body, labels: [], }); }); it('should use default branch if requested', async () => { helper.createPR.mockResolvedValueOnce(mockNewPR); await initFakeRepo(); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); expect(res).toHaveProperty('number', mockNewPR.number); expect(res).toHaveProperty('targetBranch', mockNewPR.base.ref); expect(res).toMatchSnapshot(); expect(helper.createPR).toHaveBeenCalledTimes(1); expect(helper.createPR).toHaveBeenCalledWith(mockRepo.full_name, { base: mockNewPR.base.ref, head: mockNewPR.head.label, title: mockNewPR.title, body: mockNewPR.body, labels: [], }); }); it('should resolve and apply optional labels to pull request', async () => { helper.createPR.mockResolvedValueOnce(mockNewPR); helper.getRepoLabels.mockResolvedValueOnce(mockRepoLabels); helper.getOrgLabels.mockResolvedValueOnce(mockOrgLabels); const mockLabels = mockRepoLabels.concat(mockOrgLabels); await initFakeRepo(); await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, labels: mockLabels.map((l) => l.name), }); expect(helper.createPR).toHaveBeenCalledTimes(1); expect(helper.createPR).toHaveBeenCalledWith(mockRepo.full_name, { base: mockNewPR.base.ref, head: mockNewPR.head.label, title: mockNewPR.title, body: mockNewPR.body, labels: mockLabels.map((l) => l.id), }); }); it('should ensure new pull request gets added to cached pull requests', async () => { helper.searchPRs.mockResolvedValueOnce(mockPRs); helper.createPR.mockResolvedValueOnce(mockNewPR); await initFakeRepo(); await gitea.getPrList(); await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); const res = gitea.getPr(mockNewPR.number); expect(res).not.toBeNull(); expect(helper.searchPRs).toHaveBeenCalledTimes(1); }); it('should attempt to resolve 409 conflict error (w/o update)', async () => { helper.createPR.mockRejectedValueOnce({ statusCode: 409 }); helper.searchPRs.mockResolvedValueOnce([mockNewPR]); await initFakeRepo(); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }); expect(res).toHaveProperty('number', mockNewPR.number); }); it('should attempt to resolve 409 conflict error (w/ update)', async () => { helper.createPR.mockRejectedValueOnce({ statusCode: 409 }); helper.searchPRs.mockResolvedValueOnce([mockNewPR]); await initFakeRepo(); const res = await gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: 'new-title', prBody: 'new-body', }); expect(res).toHaveProperty('number', mockNewPR.number); expect(helper.updatePR).toHaveBeenCalledTimes(1); expect(helper.updatePR).toHaveBeenCalledWith( mockRepo.full_name, mockNewPR.number, { title: 'new-title', body: 'new-body' } ); }); it('should abort when response for created pull request is invalid', async () => { helper.createPR.mockResolvedValueOnce(partial<ght.PR>({})); await initFakeRepo(); await expect( gitea.createPr({ sourceBranch: mockNewPR.head.label, targetBranch: 'master', prTitle: mockNewPR.title, prBody: mockNewPR.body, }) ).rejects.toThrow(); }); }); describe('updatePr', () => { it('should update pull request with title', async () => { await initFakeRepo(); await gitea.updatePr({ number: 1, prTitle: 'New Title' }); expect(helper.updatePR).toHaveBeenCalledTimes(1); expect(helper.updatePR).toHaveBeenCalledWith(mockRepo.full_name, 1, { title: 'New Title', }); }); it('should update pull request with title and body', async () => { await initFakeRepo(); await gitea.updatePr({ number: 1, prTitle: 'New Title', prBody: 'New Body', }); expect(helper.updatePR).toHaveBeenCalledTimes(1); expect(helper.updatePR).toHaveBeenCalledWith(mockRepo.full_name, 1, { title: 'New Title', body: 'New Body', }); }); it('should close pull request', async () => { await initFakeRepo(); await gitea.updatePr({ number: 1, prTitle: 'New Title', prBody: 'New Body', state: PrState.Closed, }); expect(helper.updatePR).toHaveBeenCalledWith(mockRepo.full_name, 1, { title: 'New Title', body: 'New Body', state: PrState.Closed, }); }); }); describe('mergePr', () => { it('should return true when merging succeeds', async () => { await initFakeRepo(); expect( await gitea.mergePr({ branchName: 'some-branch', id: 1, }) ).toBe(true); expect(helper.mergePR).toHaveBeenCalledTimes(1); expect(helper.mergePR).toHaveBeenCalledWith( mockRepo.full_name, 1, 'rebase' ); }); it('should return false when merging fails', async () => { helper.mergePR.mockRejectedValueOnce(new Error()); await initFakeRepo(); expect( await gitea.mergePr({ branchName: 'some-branch', id: 1, }) ).toBe(false); }); }); describe('getIssue', () => { it('should return the issue', async () => { const mockIssue = mockIssues.find((i) => i.number === 1); helper.getIssue.mockResolvedValueOnce(mockIssue); await initFakeRepo(); expect(await gitea.getIssue(mockIssue.number)).toHaveProperty( 'number', mockIssue.number ); }); }); describe('findIssue', () => { it('should return existing open issue', async () => { const mockIssue = mockIssues.find((i) => i.title === 'open-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.getIssue.mockResolvedValueOnce(mockIssue); await initFakeRepo(); expect(await gitea.findIssue(mockIssue.title)).toHaveProperty( 'number', mockIssue.number ); }); it('should not return existing closed issue', async () => { const mockIssue = mockIssues.find((i) => i.title === 'closed-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); await initFakeRepo(); expect(await gitea.findIssue(mockIssue.title)).toBeNull(); }); it('should return null for missing issue', async () => { helper.searchIssues.mockResolvedValueOnce(mockIssues); await initFakeRepo(); expect(await gitea.findIssue('missing')).toBeNull(); }); }); describe('ensureIssue', () => { it('should create issue if not found', async () => { const mockIssue = { title: 'new-title', body: 'new-body', shouldReOpen: false, once: false, }; helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.createIssue.mockResolvedValueOnce( partial<ght.Issue>({ number: 42 }) ); await initFakeRepo(); const res = await gitea.ensureIssue(mockIssue); expect(res).toBe('created'); expect(helper.createIssue).toHaveBeenCalledTimes(1); expect(helper.createIssue).toHaveBeenCalledWith(mockRepo.full_name, { body: mockIssue.body, title: mockIssue.title, }); }); 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 mockLabels: ght.Label[] = [ partial<ght.Label>({ id: 1, name: 'Renovate' }), partial<ght.Label>({ id: 3, name: 'Maintenance' }), ]; helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); helper.getOrgLabels.mockResolvedValueOnce([]); helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.createIssue.mockResolvedValueOnce( partial<ght.Issue>({ number: 42 }) ); await initFakeRepo(); const res = await gitea.ensureIssue(mockIssue); expect(res).toBe('created'); expect(helper.createIssue).toHaveBeenCalledTimes(1); expect(helper.createIssue).toHaveBeenCalledWith(mockRepo.full_name, { body: mockIssue.body, title: mockIssue.title, labels: [1, 3], }); }); it('should not reopen closed issue by default', async () => { const closedIssue = mockIssues.find((i) => i.title === 'closed-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.updateIssue.mockResolvedValueOnce(closedIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ title: closedIssue.title, body: closedIssue.body, shouldReOpen: false, once: false, }); expect(res).toBe('updated'); expect(helper.updateIssue).toHaveBeenCalledTimes(1); expect(helper.updateIssue).toHaveBeenCalledWith( mockRepo.full_name, closedIssue.number, { body: closedIssue.body, state: closedIssue.state, title: 'closed-issue', } ); }); it('should not update labels when not necessary', async () => { const mockLabels: ght.Label[] = [ partial<ght.Label>({ id: 1, name: 'Renovate' }), partial<ght.Label>({ id: 3, name: 'Maintenance' }), ]; const mockIssue: ght.Issue = { number: 10, title: 'label-issue', body: 'label-body', assignees: [], labels: mockLabels, state: 'open', }; helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); helper.getOrgLabels.mockResolvedValueOnce([]); helper.searchIssues.mockResolvedValueOnce([mockIssue]); helper.updateIssue.mockResolvedValueOnce(mockIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ title: mockIssue.title, body: 'new-body', labels: ['Renovate', 'Maintenance'], }); expect(res).toBe('updated'); expect(helper.updateIssue).toHaveBeenCalledTimes(1); expect(helper.updateIssueLabels).toHaveBeenCalledTimes(0); }); it('should update labels when missing', async () => { const mockLabels: ght.Label[] = [ partial<ght.Label>({ id: 1, name: 'Renovate' }), partial<ght.Label>({ id: 3, name: 'Maintenance' }), ]; const mockIssue: ght.Issue = { number: 10, title: 'label-issue', body: 'label-body', assignees: [], labels: [mockLabels[0]], state: 'open', }; helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); helper.getOrgLabels.mockResolvedValueOnce([]); helper.searchIssues.mockResolvedValueOnce([mockIssue]); helper.updateIssue.mockResolvedValueOnce(mockIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ title: mockIssue.title, body: 'new-body', labels: ['Renovate', 'Maintenance'], }); expect(res).toBe('updated'); expect(helper.updateIssue).toHaveBeenCalledTimes(1); expect(helper.updateIssueLabels).toHaveBeenCalledTimes(1); expect(helper.updateIssueLabels).toHaveBeenCalledWith( mockRepo.full_name, mockIssue.number, { labels: [1, 3], } ); }); it('should reset labels when others have been set', async () => { const mockLabels: ght.Label[] = [ partial<ght.Label>({ id: 1, name: 'Renovate' }), partial<ght.Label>({ id: 2, name: 'Other label' }), partial<ght.Label>({ id: 3, name: 'Maintenance' }), ]; const mockIssue: ght.Issue = { number: 10, title: 'label-issue', body: 'label-body', assignees: [], labels: mockLabels, state: 'open', }; helper.getRepoLabels.mockResolvedValueOnce(partial(mockLabels)); helper.getOrgLabels.mockResolvedValueOnce([]); helper.searchIssues.mockResolvedValueOnce([mockIssue]); helper.updateIssue.mockResolvedValueOnce(mockIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ title: mockIssue.title, body: 'new-body', labels: ['Renovate', 'Maintenance'], }); expect(res).toBe('updated'); expect(helper.updateIssue).toHaveBeenCalledTimes(1); expect(helper.updateIssueLabels).toHaveBeenCalledTimes(1); expect(helper.updateIssueLabels).toHaveBeenCalledWith( mockRepo.full_name, mockIssue.number, { labels: [1, 3], } ); }); it('should reopen closed issue if desired', async () => { const closedIssue = mockIssues.find((i) => i.title === 'closed-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.updateIssue.mockResolvedValueOnce(closedIssue); await initFakeRepo(); const res = await gitea.ensureIssue({ title: closedIssue.title, body: closedIssue.body, shouldReOpen: true, once: false, }); expect(res).toBe('updated'); expect(helper.updateIssue).toHaveBeenCalledTimes(1); expect(helper.updateIssue).toHaveBeenCalledWith( mockRepo.full_name, closedIssue.number, { body: closedIssue.body, state: 'open', title: 'closed-issue', } ); }); it('should not update existing closed issue if desired', async () => { const closedIssue = mockIssues.find((i) => i.title === 'closed-issue'); helper.searchIssues.mockResolvedValueOnce(mockIssues); await initFakeRepo(); const res = await gitea.ensureIssue({ title: closedIssue.title, body: closedIssue.body, shouldReOpen: false, once: true, }); expect(res).toBeNull(); expect(helper.updateIssue).not.toHaveBeenCalled(); }); it('should close all open duplicate issues except first one when updating', async () => { const duplicates = mockIssues.filter( (i) => i.title === 'duplicate-issue' ); const firstDuplicate = duplicates[0]; helper.searchIssues.mockResolvedValueOnce(duplicates); await initFakeRepo(); const res = await gitea.ensureIssue({ title: firstDuplicate.title, body: firstDuplicate.body, shouldReOpen: false, once: false, }); expect(res).toBeNull(); expect(helper.closeIssue).toHaveBeenCalledTimes(duplicates.length - 1); for (const issue of duplicates) { if (issue.number !== firstDuplicate.number) { // eslint-disable-next-line jest/no-conditional-expect expect(helper.closeIssue).toHaveBeenCalledWith( mockRepo.full_name, issue.number ); } } expect(helper.updateIssue).not.toHaveBeenCalled(); }); it('should reset issue cache when creating an issue', async () => { helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.searchIssues.mockResolvedValueOnce(mockIssues); helper.createIssue.mockResolvedValueOnce( partial<ght.Issue>({ number: 42 }) ); await initFakeRepo(); await gitea.ensureIssue({ title: 'new-title', body: 'new-body', shouldReOpen: false, once: false, }); await gitea.getIssueList(); expect(helper.searchIssues).toHaveBeenCalledTimes(2); }); it('should gracefully fail with warning', async () => { helper.searchIssues.mockRejectedValueOnce(new Error()); await initFakeRepo(); await gitea.ensureIssue({ title: 'new-title', body: 'new-body', shouldReOpen: false, once: false, }); expect(logger.warn).toHaveBeenCalledTimes(1); }); }); describe('ensureIssueClosing', () => { it('should close issues with matching title', async () => { const mockIssue = mockIssues[0]; helper.searchIssues.mockResolvedValueOnce(mockIssues); await initFakeRepo(); await gitea.ensureIssueClosing(mockIssue.title); expect(helper.closeIssue).toHaveBeenCalledTimes(1); expect(helper.closeIssue).toHaveBeenCalledWith( mockRepo.full_name, mockIssue.number ); }); }); describe('deleteLabel', () => { it('should delete a label which exists', async () => { const mockLabel = mockRepoLabels[0]; helper.getRepoLabels.mockResolvedValueOnce(mockRepoLabels); helper.getOrgLabels.mockRejectedValueOnce(new Error()); await initFakeRepo(); await gitea.deleteLabel(42, mockLabel.name); expect(helper.unassignLabel).toHaveBeenCalledTimes(1); expect(helper.unassignLabel).toHaveBeenCalledWith( mockRepo.full_name, 42, mockLabel.id ); }); it('should gracefully fail with warning if label is missing', async () => { helper.getRepoLabels.mockResolvedValueOnce(mockRepoLabels); helper.getOrgLabels.mockResolvedValueOnce([]); await initFakeRepo(); await gitea.deleteLabel(42, 'missing'); expect(helper.unassignLabel).not.toHaveBeenCalled(); expect(logger.warn).toHaveBeenCalledTimes(1); }); }); describe('getRepoForceRebase', () => { it('should return false - unsupported by platform', async () => { expect(await gitea.getRepoForceRebase()).toBe(false); }); }); describe('ensureComment', () => { it('should add comment with topic if not found', async () => { helper.getComments.mockResolvedValueOnce(mockComments); helper.createComment.mockResolvedValueOnce( partial<ght.Comment>({ id: 42 }) ); await initFakeRepo(); const res = await gitea.ensureComment({ number: 1, topic: 'other-topic', content: 'other-content', }); const body = '### other-topic\n\nother-content'; expect(res).toBe(true); expect(helper.updateComment).not.toHaveBeenCalled(); expect(helper.createComment).toHaveBeenCalledTimes(1); expect(helper.createComment).toHaveBeenCalledWith( mockRepo.full_name, 1, body ); }); it('should add comment without topic if not found', async () => { helper.getComments.mockResolvedValueOnce(mockComments); helper.createComment.mockResolvedValueOnce( partial<ght.Comment>({ id: 42 }) ); await initFakeRepo(); const res = await gitea.ensureComment({ number: 1, content: 'other-content', topic: undefined, }); expect(res).toBe(true); expect(helper.updateComment).not.toHaveBeenCalled(); expect(helper.createComment).toHaveBeenCalledTimes(1); expect(helper.createComment).toHaveBeenCalledWith( mockRepo.full_name, 1, 'other-content' ); }); it('should update comment with topic if found', async () => { helper.getComments.mockResolvedValueOnce(mockComments); helper.updateComment.mockResolvedValueOnce( partial<ght.Comment>({ id: 13 }) ); await initFakeRepo(); const res = await gitea.ensureComment({ number: 1, topic: 'some-topic', content: 'some-new-content', }); const body = '### some-topic\n\nsome-new-content'; expect(res).toBe(true); expect(helper.createComment).not.toHaveBeenCalled(); expect(helper.updateComment).toHaveBeenCalledTimes(1); expect(helper.updateComment).toHaveBeenCalledWith( mockRepo.full_name, 13, body ); }); it('should skip if comment is up-to-date', async () => { helper.getComments.mockResolvedValueOnce(mockComments); await initFakeRepo(); const res = await gitea.ensureComment({ number: 1, topic: 'some-topic', content: 'some-content', }); expect(res).toBe(true); expect(helper.createComment).not.toHaveBeenCalled(); expect(helper.updateComment).not.toHaveBeenCalled(); }); it('should gracefully fail with warning', async () => { helper.getComments.mockRejectedValueOnce(new Error()); await initFakeRepo(); const res = await gitea.ensureComment({ number: 1, topic: 'some-topic', content: 'some-content', }); expect(res).toBe(false); expect(logger.warn).toHaveBeenCalledTimes(1); }); }); describe('ensureCommentRemoval', () => { it('should remove existing comment by topic', async () => { helper.getComments.mockResolvedValueOnce(mockComments); await initFakeRepo(); await gitea.ensureCommentRemoval({ type: 'by-topic', number: 1, topic: 'some-topic', }); expect(helper.deleteComment).toHaveBeenCalledTimes(1); expect(helper.deleteComment).toHaveBeenCalledWith(mockRepo.full_name, 13); }); it('should remove existing comment by content', async () => { helper.getComments.mockResolvedValueOnce(mockComments); await initFakeRepo(); await gitea.ensureCommentRemoval({ type: 'by-content', number: 1, content: 'some-body', }); expect(helper.deleteComment).toHaveBeenCalledTimes(1); expect(helper.deleteComment).toHaveBeenCalledWith(mockRepo.full_name, 11); }); it('should gracefully fail with warning', async () => { helper.getComments.mockResolvedValueOnce(mockComments); helper.deleteComment.mockRejectedValueOnce(new Error()); await initFakeRepo(); await gitea.ensureCommentRemoval({ type: 'by-topic', number: 1, topic: 'some-topic', }); expect(logger.warn).toHaveBeenCalledTimes(1); }); it('should abort silently if comment is missing', async () => { helper.getComments.mockResolvedValueOnce(mockComments); await initFakeRepo(); await gitea.ensureCommentRemoval({ type: 'by-topic', number: 1, topic: 'missing', }); expect(helper.deleteComment).not.toHaveBeenCalled(); }); }); describe('getBranchPr', () => { it('should return existing pull request for branch', async () => { const mockPR = mockPRs[0]; helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); expect(await gitea.getBranchPr(mockPR.head.label)).toHaveProperty( 'number', mockPR.number ); }); it('should return null if no pull request exists', async () => { helper.searchPRs.mockResolvedValueOnce(mockPRs); await initFakeRepo(); expect(await gitea.getBranchPr('missing')).toBeNull(); }); }); describe('addAssignees', () => { it('should add assignees to the issue', async () => { await initFakeRepo(); await gitea.addAssignees(1, ['me', 'you']); expect(helper.updateIssue).toHaveBeenCalledTimes(1); expect(helper.updateIssue).toHaveBeenCalledWith(mockRepo.full_name, 1, { assignees: ['me', 'you'], }); }); }); describe('addReviewers', () => { it('should assign reviewers', async () => { expect.assertions(3); await initFakePlatform(); const mockPR = mockPRs[0]; await expect( gitea.addReviewers(mockPR.number, ['me', 'you']) ).resolves.not.toThrow(); expect(helper.requestPrReviewers).toHaveBeenCalledTimes(1); expect(logger.warn).not.toHaveBeenCalled(); }); it('should should do nothing if version to old', async () => { expect.assertions(3); const mockPR = mockPRs[0]; await expect( gitea.addReviewers(mockPR.number, ['me', 'you']) ).resolves.not.toThrow(); expect(helper.requestPrReviewers).not.toHaveBeenCalled(); expect(logger.warn).not.toHaveBeenCalled(); }); it('catches errors', async () => { expect.assertions(2); const mockPR = mockPRs[0]; await initFakePlatform(); helper.requestPrReviewers.mockRejectedValueOnce(null); await expect( gitea.addReviewers(mockPR.number, ['me', 'you']) ).resolves.not.toThrow(); expect(logger.warn).toHaveBeenCalled(); }); }); describe('massageMarkdown', () => { it('should truncate body to 1000000 characters', () => { const excessiveBody = '*'.repeat(1000001); expect(gitea.massageMarkdown(excessiveBody)).toHaveLength(1000000); }); }); describe('getVulnerabilityAlerts', () => { it('should return an empty list - unsupported by platform', async () => { expect(await gitea.getVulnerabilityAlerts()).toEqual([]); }); }); describe('getJsonFile()', () => { it('returns file content', async () => { const data = { foo: 'bar' }; helper.getRepoContents.mockResolvedValueOnce({ contentString: JSON.stringify(data), } as never); await initFakeRepo({ full_name: 'some/repo' }); const res = await gitea.getJsonFile('file.json'); expect(res).toEqual(data); }); it('returns file content from given repo', async () => { const data = { foo: 'bar' }; helper.getRepoContents.mockResolvedValueOnce({ contentString: JSON.stringify(data), } as never); await initFakeRepo({ full_name: 'different/repo' }); const res = await gitea.getJsonFile('file.json', 'different/repo'); expect(res).toEqual(data); }); it('returns file content from branch or tag', async () => { const data = { foo: 'bar' }; helper.getRepoContents.mockResolvedValueOnce({ contentString: JSON.stringify(data), } as never); await initFakeRepo({ full_name: 'some/repo' }); const res = await gitea.getJsonFile('file.json', 'some/repo', 'dev'); expect(res).toEqual(data); }); it('returns file content in json5 format', async () => { const json5Data = ` { // json5 comment foo: 'bar' } `; helper.getRepoContents.mockResolvedValueOnce({ contentString: json5Data, } as never); await initFakeRepo({ full_name: 'some/repo' }); const res = await gitea.getJsonFile('file.json5'); expect(res).toEqual({ foo: 'bar' }); }); it('throws on malformed JSON', async () => { helper.getRepoContents.mockResolvedValueOnce({ contentString: '!@#', } as never); await initFakeRepo({ full_name: 'some/repo' }); await expect(gitea.getJsonFile('file.json')).rejects.toThrow(); }); it('throws on errors', async () => { helper.getRepoContents.mockRejectedValueOnce(new Error('some error')); await initFakeRepo({ full_name: 'some/repo' }); await expect(gitea.getJsonFile('file.json')).rejects.toThrow(); }); }); });