import { codeBlock } from 'common-tags'; import { hostRules } from '~test/host-rules.ts'; import { fakeSha, git, partial } from '~test/util.ts'; import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages.ts'; import type { BranchStatus } from '../../../types/index.ts'; import { repoFingerprint } from '../util.ts'; import { client as _client } from './client.ts'; import * as gerrit from './index.ts'; import { writeToConfig } from './index.ts'; import type { GerritAccountInfo, GerritChange, GerritLabelInfo, GerritLabelTypeInfo, GerritProjectInfo, GerritRevisionInfo, } from './schema.ts'; import { configureScm } from './scm.ts'; import { REQUEST_DETAILS_FOR_PRS, TAG_PULL_REQUEST_BODY, mapGerritChangeToPr, } from './utils.ts'; const gerritEndpointUrl = 'https://dev.gerrit.com/renovate'; const codeReviewLabel: GerritLabelTypeInfo = { values: { '-2': 'bad', '-1': 'unlikely', 0: 'neutral', 1: 'ok', 2: 'good', }, default_value: 0, }; vi.mock('./client.ts'); const clientMock = vi.mocked(_client); describe('modules/platform/gerrit/index', () => { const currentRevision = '0123456789abcdef0123456789abcdef01234567'; beforeEach(async () => { hostRules.add({ username: 'user', password: 'pass', }); writeToConfig({ repository: 'test/repo', labels: {}, }); clientMock.getGerritVersion.mockResolvedValue('3.0.0'); await gerrit.initPlatform({ endpoint: gerritEndpointUrl, username: 'user', password: 'pass', }); }); describe('initPlatform()', () => { it('should throw if no endpoint', async () => { expect.assertions(1); await expect(() => gerrit.initPlatform({})).rejects.toThrow( 'Init: You must configure a Gerrit Server endpoint', ); }); it('should throw if no username/password', async () => { expect.assertions(1); await expect(() => gerrit.initPlatform({ endpoint: 'endpoint' }), ).rejects.toThrow( 'Init: You must configure a Gerrit Server username/password', ); }); it('should init', async () => { await expect( gerrit.initPlatform({ endpoint: gerritEndpointUrl, username: 'abc', password: '123', }), ).resolves.toEqual({ endpoint: 'https://dev.gerrit.com/renovate/' }); }); it('should throw if auth fails', async () => { clientMock.getGerritVersion.mockRejectedValue(new Error('Auth failed')); await expect( gerrit.initPlatform({ endpoint: gerritEndpointUrl, username: 'abc', password: '123', }), ).rejects.toThrow('Init: Authentication failure'); }); it('should throw if version is unparseable', async () => { clientMock.getGerritVersion.mockResolvedValue('not-a-valid-version'); await expect( gerrit.initPlatform({ endpoint: gerritEndpointUrl, username: 'abc', password: '123', }), ).rejects.toThrow('Unable to parse Gerrit version: not-a-valid-version'); }); }); describe('getRepos()', () => { it('returns repos', async () => { clientMock.getRepos.mockResolvedValueOnce(['repo1', 'repo2']); await expect(gerrit.getRepos()).resolves.toEqual(['repo1', 'repo2']); }); }); it('initRepo() - inactive', async () => { clientMock.getProjectInfo.mockRejectedValueOnce( new Error(REPOSITORY_ARCHIVED), ); await expect(gerrit.initRepo({ repository: 'test/repo' })).rejects.toThrow( REPOSITORY_ARCHIVED, ); }); describe('initRepo()', () => { const projectInfo: GerritProjectInfo = { id: 'repo1', name: 'test-repo2', }; beforeEach(() => { clientMock.getBranchInfo.mockResolvedValueOnce({ ref: 'sha-hash....', revision: 'main', }); }); it('initRepo() - active', async () => { clientMock.getProjectInfo.mockResolvedValueOnce(projectInfo); clientMock.findChanges.mockResolvedValueOnce([]); // rejected changes clientMock.findChanges.mockResolvedValueOnce([]); // open changes for branch initialization await expect( gerrit.initRepo({ repository: 'test/repo' }), ).resolves.toEqual({ defaultBranch: 'main', isFork: false, repoFingerprint: repoFingerprint('test/repo', `${gerritEndpointUrl}/`), }); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith({ url: 'https://user:pass@dev.gerrit.com/renovate/a/test/repo', virtualBranches: {}, }); }); it('initRepo() - passes cloneSubmodules', async () => { clientMock.getProjectInfo.mockResolvedValueOnce(projectInfo); clientMock.findChanges.mockResolvedValueOnce([]); // rejected changes clientMock.findChanges.mockResolvedValueOnce([]); // open changes for branch initialization await gerrit.initRepo({ repository: 'test/repo', cloneSubmodules: true, cloneSubmodulesFilter: ['test'], }); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith({ url: 'https://user:pass@dev.gerrit.com/renovate/a/test/repo', cloneSubmodules: true, cloneSubmodulesFilter: ['test'], virtualBranches: {}, }); }); it('initRepo() - uses username in ssh clone url', async () => { clientMock.getProjectInfo.mockResolvedValueOnce(projectInfo); clientMock.findChanges.mockResolvedValueOnce([]); // rejected changes clientMock.findChanges.mockResolvedValueOnce([]); // open changes for branch initialization await gerrit.initRepo({ repository: 'test/repo', gitUrl: 'ssh', }); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith({ url: 'ssh://user@dev.gerrit.com:29418/test/repo', virtualBranches: {}, }); }); it('initRepo() - abandon rejected changes', async () => { clientMock.getProjectInfo.mockResolvedValueOnce({ ...projectInfo, labels: { 'Code-Review': codeReviewLabel }, }); clientMock.findChanges.mockResolvedValueOnce([ partial({ _number: 1 }), partial({ _number: 2 }), ]); clientMock.findChanges.mockResolvedValueOnce([]); // open changes for branch initialization await gerrit.initRepo({ repository: 'test/repo' }); expect(clientMock.findChanges.mock.calls[0]).toEqual([ 'test/repo', { branchName: '', label: '-2', state: 'open' }, ]); expect(clientMock.abandonChange.mock.calls).toEqual([ [ 1, 'This change has been abandoned as it was voted with Code-Review -2.', ], [ 2, 'This change has been abandoned as it was voted with Code-Review -2.', ], ]); }); it('initRepo() - initialize branches from open changes', async () => { const sha1 = fakeSha('dep-1'); const sha2 = fakeSha('dep-2-skip'); const sha3 = fakeSha('dep-3'); clientMock.getProjectInfo.mockResolvedValueOnce(projectInfo); clientMock.findChanges.mockResolvedValueOnce([]); // rejected changes clientMock.findChanges.mockResolvedValueOnce([ partial({ _number: 12345, current_revision: sha1, revisions: { [sha1]: partial({ ref: 'refs/changes/45/12345/1', commit_with_footers: 'commit message\n\nRenovate-Branch: renovate/dep-1', }), }, }), // change without Renovate-Branch footer should be skipped partial({ _number: 99999, current_revision: sha2, revisions: { [sha2]: partial({ ref: 'refs/changes/99/99999/1', commit_with_footers: 'commit message without branch footer', }), }, }), partial({ _number: 12346, current_revision: sha3, revisions: { [sha3]: partial({ ref: 'refs/changes/46/12346/1', commit_with_footers: 'commit message\n\nRenovate-Branch: renovate/dep-2', }), }, }), ]); await gerrit.initRepo({ repository: 'test/repo' }); expect(clientMock.findChanges.mock.calls[1]).toEqual([ 'test/repo', { branchName: '', state: 'open', requestDetails: ['CURRENT_REVISION', 'COMMIT_FOOTERS'], }, ]); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith({ url: 'https://user:pass@dev.gerrit.com/renovate/a/test/repo', virtualBranches: { 'renovate/dep-1': { ref: 'refs/changes/45/12345/1', sha: sha1, }, 'renovate/dep-2': { ref: 'refs/changes/46/12346/1', sha: sha3, }, }, }); }); }); describe('findPr()', () => { it('findPr() - no results', async () => { clientMock.findChanges.mockResolvedValueOnce([]); await expect( gerrit.findPr({ branchName: 'branch', state: 'open', targetBranch: 'master', }), ).resolves.toBeNull(); expect(clientMock.findChanges).toHaveBeenCalledExactlyOnceWith( 'test/repo', { branchName: 'branch', state: 'open', targetBranch: 'master', singleChange: true, requestDetails: REQUEST_DETAILS_FOR_PRS, }, ); }); it('findPr() - found', async () => { const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: source', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.findPr({ branchName: 'branch', state: 'open', targetBranch: 'master', }), ).resolves.toHaveProperty('number', 123456); }); }); describe('getPr()', () => { it('getPr() - found', async () => { const change = partial({ created: '2025-04-14 16:33:37.000000000', }); clientMock.getChange.mockResolvedValueOnce(change); await expect(gerrit.getPr(123456)).resolves.toEqual( mapGerritChangeToPr(change), ); expect(clientMock.getChange).toHaveBeenCalledExactlyOnceWith( 123456, REQUEST_DETAILS_FOR_PRS, ); }); it('getPr() - not found', async () => { clientMock.getChange.mockRejectedValueOnce({ statusCode: 404 }); await expect(gerrit.getPr(123456)).resolves.toBeNull(); }); it('getPr() - other error', async () => { clientMock.getChange.mockRejectedValueOnce(new Error('other error')); await expect(gerrit.getPr(123456)).rejects.toThrow('other error'); }); }); describe('updatePr()', () => { beforeAll(() => { gerrit.writeToConfig({ labels: {} }); }); it('updatePr() - closed => abandon the change', async () => { const change = partial({}); clientMock.getChange.mockResolvedValueOnce(change); await gerrit.updatePr({ number: 123456, prTitle: change.subject, state: 'closed', }); expect(clientMock.abandonChange).toHaveBeenCalledExactlyOnceWith(123456); }); it('updatePr() - body set => add as message if needed', async () => { const change = partial({}); clientMock.getChange.mockResolvedValueOnce(change); await gerrit.updatePr({ number: 123456, prTitle: change.subject, prBody: 'NEW PR-Body', }); expect( clientMock.addMessageIfNotAlreadyExists, ).toHaveBeenCalledExactlyOnceWith( 123456, 'NEW PR-Body', TAG_PULL_REQUEST_BODY, ); }); it('updatePr() - with addLabels => add hashtags', async () => { const change = partial({}); clientMock.getChange.mockResolvedValueOnce(change); await gerrit.updatePr({ number: 123456, prTitle: change.subject, addLabels: ['label1', 'label2'], }); expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { add: ['label1', 'label2'], }); }); it('updatePr() - with removeLabels => remove hashtags', async () => { const change = partial({}); clientMock.getChange.mockResolvedValueOnce(change); await gerrit.updatePr({ number: 123456, prTitle: change.subject, removeLabels: ['old-label'], }); expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { remove: ['old-label'], }); }); it('updatePr() - with addLabels and removeLabels => update hashtags in single call', async () => { const change = partial({}); clientMock.getChange.mockResolvedValueOnce(change); await gerrit.updatePr({ number: 123456, prTitle: change.subject, addLabels: ['new-label'], removeLabels: ['old-label'], }); expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { add: ['new-label'], remove: ['old-label'], }); }); }); it('updatePr() - targetBranch set => move the change', async () => { const change = partial({ branch: 'main' }); clientMock.getChange.mockResolvedValueOnce(change); clientMock.moveChange.mockResolvedValueOnce( partial({ branch: 'new-main' }), ); await gerrit.updatePr({ number: 123456, prTitle: 'title', targetBranch: 'new-main', }); expect(clientMock.moveChange).toHaveBeenCalledExactlyOnceWith( 123456, 'new-main', ); }); describe('createPr()', () => { it('createPr() - creates change by pushing to refs/for/', async () => { git.pushCommit.mockResolvedValueOnce(true); const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ ref: 'refs/changes/56/123456/1', commit_with_footers: 'Renovate-Branch: source', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ sourceBranch: 'source', targetBranch: 'target', prTitle: 'title', prBody: 'body', }); expect(pr).toHaveProperty('number', 123456); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'source', targetRef: 'refs/for/target', files: [], pushOptions: ['notify=NONE', 'ready'], }); expect(git.setVirtualBranch).toHaveBeenCalledExactlyOnceWith( 'source', 'refs/changes/56/123456/1', currentRevision, ); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, 'body', TAG_PULL_REQUEST_BODY, ); }); it('createPr() - with autoApprove', async () => { configureScm('test/repo', { 'Code-Review': codeReviewLabel }); git.pushCommit.mockResolvedValueOnce(true); const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ ref: 'refs/changes/56/123456/1', commit_with_footers: 'Renovate-Branch: source', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ sourceBranch: 'source', targetBranch: 'target', prTitle: 'title', prBody: 'body', platformPrOptions: { autoApprove: true, }, }); expect(pr).toHaveProperty('number', 123456); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'source', targetRef: 'refs/for/target', files: [], pushOptions: ['notify=NONE', 'ready', 'label=Code-Review+2'], }); expect(git.setVirtualBranch).toHaveBeenCalledExactlyOnceWith( 'source', 'refs/changes/56/123456/1', currentRevision, ); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, 'body', TAG_PULL_REQUEST_BODY, ); }); it('createPr() - with labels', async () => { git.pushCommit.mockResolvedValueOnce(true); const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ ref: 'refs/changes/56/123456/1', commit_with_footers: 'Renovate-Branch: source', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ sourceBranch: 'source', targetBranch: 'target', prTitle: 'title', prBody: 'body', labels: ['label1', 'label2'], }); expect(pr).toHaveProperty('number', 123456); expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ sourceRef: 'source', targetRef: 'refs/for/target', files: [], pushOptions: [ 'notify=NONE', 'ready', 'hashtag=label1', 'hashtag=label2', ], }); expect(git.setVirtualBranch).toHaveBeenCalledExactlyOnceWith( 'source', 'refs/changes/56/123456/1', currentRevision, ); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, 'body', TAG_PULL_REQUEST_BODY, ); }); it('createPr() - no change found after push => rejects', async () => { git.pushCommit.mockResolvedValueOnce(true); clientMock.findChanges.mockResolvedValueOnce([]); await expect( gerrit.createPr({ sourceBranch: 'source', targetBranch: 'target', prTitle: 'title', prBody: 'body', }), ).rejects.toThrow( `Could not find the Gerrit change after pushing to refs/for/target`, ); expect(git.setVirtualBranch).not.toHaveBeenCalled(); }); it('createPr() - push fails => rejects', async () => { git.pushCommit.mockResolvedValueOnce(false); await expect( gerrit.createPr({ sourceBranch: 'source', targetBranch: 'target', prTitle: 'title', prBody: 'body', }), ).rejects.toThrow( `Failed to push commit to refs/for/target to create Gerrit change`, ); expect(git.setVirtualBranch).not.toHaveBeenCalled(); }); }); describe('getBranchPr()', () => { it('getBranchPr() - no result', async () => { clientMock.findChanges.mockResolvedValueOnce([]); await expect( gerrit.getBranchPr('renovate/dependency-1.x'), ).resolves.toBeNull(); expect(clientMock.getBranchChange).toHaveBeenCalledExactlyOnceWith( 'test/repo', { branchName: 'renovate/dependency-1.x', state: 'open', requestDetails: REQUEST_DETAILS_FOR_PRS, }, ); }); it('getBranchPr() - found', async () => { const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.getBranchChange.mockResolvedValueOnce(change); await expect( gerrit.getBranchPr('renovate/dependency-1.x', 'master'), ).resolves.toHaveProperty('number', 123456); expect(clientMock.getBranchChange).toHaveBeenCalledExactlyOnceWith( 'test/repo', { state: 'open', branchName: 'renovate/dependency-1.x', targetBranch: 'master', requestDetails: REQUEST_DETAILS_FOR_PRS, }, ); }); it('getBranchPr() - found even without targetBranch', async () => { const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.getBranchChange.mockResolvedValueOnce(change); await expect( gerrit.getBranchPr('renovate/dependency-1.x', undefined), ).resolves.toHaveProperty('number', 123456); expect(clientMock.getBranchChange).toHaveBeenCalledExactlyOnceWith( 'test/repo', { state: 'open', branchName: 'renovate/dependency-1.x', targetBranch: undefined, requestDetails: REQUEST_DETAILS_FOR_PRS, }, ); }); }); describe('getPrList()', () => { it('getPrList() - empty list', async () => { clientMock.findChanges.mockResolvedValueOnce([]); await expect(gerrit.getPrList()).resolves.toEqual([]); expect(clientMock.findChanges).toHaveBeenCalledExactlyOnceWith( 'test/repo', { branchName: '', requestDetails: REQUEST_DETAILS_FOR_PRS, }, ); }); it('getPrList() - multiple results', async () => { const change = partial({ current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change, change, change]); await expect(gerrit.getPrList()).resolves.toHaveLength(3); }); }); describe('mergePr()', () => { it('mergePr() - blocker by Verified', async () => { clientMock.submitChange.mockRejectedValueOnce({ statusCode: 409, message: 'blocked by Verified', }); await expect(gerrit.mergePr({ id: 123456 })).resolves.toBeFalse(); expect(clientMock.submitChange).toHaveBeenCalledExactlyOnceWith(123456); }); it('mergePr() - success', async () => { clientMock.submitChange.mockResolvedValueOnce( partial({ status: 'MERGED' }), ); await expect(gerrit.mergePr({ id: 123456 })).resolves.toBeTrue(); }); it('mergePr() - other errors', async () => { clientMock.submitChange.mockRejectedValueOnce( new Error('any other error'), ); await expect(gerrit.mergePr({ id: 123456 })).rejects.toThrow( 'any other error', ); }); }); describe('getBranchStatus()', () => { it('getBranchStatus() - change not found => yellow', async () => { clientMock.findChanges.mockResolvedValueOnce([]); await expect( gerrit.getBranchStatus('renovate/dependency-1.x'), ).resolves.toBe('yellow'); }); it('getBranchStatus() - change found, submittable and not hasProblems => green', async () => { const change = partial({ submittable: true, }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.getBranchStatus('renovate/dependency-1.x'), ).resolves.toBe('green'); }); it('getBranchStatus() - change found, submittable but hasProblems => red', async () => { const change = partial({ submittable: true, problems: [{ message: 'error1' }, { message: 'error2' }], current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.getBranchStatus('renovate/dependency-1.x'), ).resolves.toBe('red'); }); it('getBranchStatus() - change found and hasProblems => red', async () => { const change = partial({ submittable: false, problems: [{ message: 'error1' }, { message: 'error2' }], current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.getBranchStatus('renovate/dependency-1.x'), ).resolves.toBe('red'); }); it('getBranchStatus() - changes found and hasBlockingLabels but no problems => red', async () => { const change = partial({ submittable: true, problems: [], labels: { Verified: { blocking: true }, }, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.getBranchStatus('renovate/dependency-1.x'), ).resolves.toBe('red'); }); }); describe('getBranchStatusCheck()', () => { describe('GerritLabel is not available', () => { beforeAll(() => { writeToConfig({ labels: {} }); }); it.each([ 'unknownCtx', 'renovate/stability-days', 'renovate/merge-confidence', ])('getBranchStatusCheck() - %s', async (ctx) => { await expect( gerrit.getBranchStatusCheck('renovate/dependency-1.x', ctx), ).resolves.toBe('yellow'); expect(clientMock.findChanges).not.toHaveBeenCalled(); }); }); describe('GerritLabel is available', () => { beforeEach(() => { writeToConfig({ labels: { 'Renovate-Merge-Confidence': { values: { '0': 'default', '-1': 'Unsatisfied', '1': 'Satisfied' }, default_value: 0, }, }, }); }); it.each([ { label: 'Renovate-Merge-Confidence', labelValue: { rejected: partial({}) }, expectedState: 'red' as BranchStatus, }, { label: 'Renovate-Merge-Confidence', labelValue: { approved: partial({}) }, expectedState: 'green' as BranchStatus, }, { label: 'Renovate-Merge-Confidence', labelValue: { approved: partial({}), rejected: partial({}), }, expectedState: 'red' as BranchStatus, }, { label: 'Renovate-Merge-Confidence', labelValue: { approved: partial({}), rejected: partial({}), blocking: true, }, expectedState: 'red' as BranchStatus, }, ])('$ctx/$labels', async ({ label, labelValue, expectedState }) => { const change = partial({ labels: { [label]: partial({ ...labelValue }), }, }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.getBranchStatusCheck('renovate/dependency-1.x', label), ).resolves.toBe(expectedState); }); }); }); describe('setBranchStatus()', () => { describe('GerritLabel is not configured in Renovate', () => { beforeEach(() => { writeToConfig({ labels: {} }); }); it('setBranchStatus(renovate/stability-days)', async () => { await expect( gerrit.setBranchStatus({ branchName: 'branch', context: 'renovate/stability-days', state: 'red', description: 'desc', }), ).resolves.toBeUndefined(); expect(clientMock.setLabel).not.toHaveBeenCalled(); }); it('setBranchStatus(renovate/merge-confidence)', async () => { await expect( gerrit.setBranchStatus({ branchName: 'branch', context: 'renovate/merge-confidence', state: 'red', description: 'desc', }), ).resolves.toBeUndefined(); expect(clientMock.setLabel).not.toHaveBeenCalled(); }); }); describe('GerritLabel is configured in Renovate', () => { beforeEach(() => { writeToConfig({ labels: { 'Renovate-Merge-Confidence': { values: { '0': 'default', '-1': 'Unsatisfied', '1': 'Satisfied' }, default_value: 0, }, }, }); }); it.each([ { ctx: 'Renovate-Merge-Confidence', branchState: 'red' as BranchStatus, expectedVote: -1, expectedLabel: 'Renovate-Merge-Confidence', }, { ctx: 'Renovate-Merge-Confidence', branchState: 'yellow' as BranchStatus, expectedVote: -1, expectedLabel: 'Renovate-Merge-Confidence', }, { ctx: 'Renovate-Merge-Confidence', branchState: 'green' as BranchStatus, expectedVote: 1, expectedLabel: 'Renovate-Merge-Confidence', }, ])( '$ctx/$branchState', async ({ ctx, branchState, expectedVote, expectedLabel }) => { const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, labels: { [ctx]: partial({ approved: partial({}), rejected: partial({}), }), }, }); clientMock.findChanges.mockResolvedValueOnce([change]); await gerrit.setBranchStatus({ branchName: 'renovate/dependency-1.x', context: ctx, state: branchState, description: 'desc', }); expect(clientMock.setLabel).toHaveBeenCalledExactlyOnceWith( 123456, expectedLabel, expectedVote, ); }, ); it('no change found', async () => { clientMock.findChanges.mockResolvedValueOnce([]); await expect( gerrit.setBranchStatus({ branchName: 'renovate/dependency-1.x', context: 'Renovate-Merge-Confidence', state: 'red', description: 'desc', }), ).resolves.toBeUndefined(); expect(clientMock.setLabel).not.toHaveBeenCalled(); }); it('does not call setLabel() if label does not exist in change', async () => { const change = partial({ _number: 123456, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, }); clientMock.findChanges.mockResolvedValueOnce([change]); await expect( gerrit.setBranchStatus({ branchName: 'branch', context: 'renovate/merge-confidence', state: 'red', description: 'desc', }), ).resolves.toBeUndefined(); expect(clientMock.setLabel).not.toHaveBeenCalled(); }); }); }); describe('deleteLabel()', () => { it('deleteLabel() - deletes a label', async () => { const pro = gerrit.deleteLabel(123456, 'hashtag1'); await expect(pro).resolves.toBeUndefined(); expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { remove: ['hashtag1'], }); }); }); describe('addReviewers()', () => { it('addReviewers() - add reviewers', async () => { await expect( gerrit.addReviewers(123456, ['user1', 'user2']), ).resolves.toBeUndefined(); expect(clientMock.addReviewers).toHaveBeenCalledTimes(1); expect(clientMock.addReviewers).toHaveBeenCalledExactlyOnceWith(123456, [ 'user1', 'user2', ]); }); }); describe('addAssignees()', () => { it('addAssignees() - set assignee', async () => { await expect( gerrit.addAssignees(123456, ['user1', 'user2']), ).resolves.toBeUndefined(); expect(clientMock.addAssignee).toHaveBeenCalledTimes(1); expect(clientMock.addAssignee).toHaveBeenCalledExactlyOnceWith( 123456, 'user1', ); }); }); describe('ensureComment()', () => { it('ensureComment() - without tag', async () => { await expect( gerrit.ensureComment({ number: 123456, topic: null, content: 'My-Comment-Msg', }), ).resolves.toBeTrue(); expect( clientMock.addMessageIfNotAlreadyExists, ).toHaveBeenCalledExactlyOnceWith(123456, 'My-Comment-Msg', undefined); }); it('ensureComment() - with tag', async () => { await expect( gerrit.ensureComment({ number: 123456, topic: 'myTopic', content: 'My-Comment-Msg', }), ).resolves.toBeTrue(); expect( clientMock.addMessageIfNotAlreadyExists, ).toHaveBeenCalledExactlyOnceWith(123456, 'My-Comment-Msg', 'myTopic'); }); }); describe('getRawFile()', () => { beforeEach(() => { clientMock.getFile.mockResolvedValueOnce('{}'); }); it('getRawFile() - repo and branch', async () => { await expect( gerrit.getRawFile('renovate.json', 'test/repo', 'main'), ).resolves.toBe('{}'); expect(clientMock.getFile).toHaveBeenCalledExactlyOnceWith( 'test/repo', 'main', 'renovate.json', ); }); it('getRawFile() - repo/branch from config', async () => { writeToConfig({ repository: 'repo', head: 'master', labels: {}, }); await expect(gerrit.getRawFile('renovate.json')).resolves.toBe('{}'); expect(clientMock.getFile).toHaveBeenCalledExactlyOnceWith( 'repo', 'master', 'renovate.json', ); }); it('getRawFile() - branch defaults', async () => { writeToConfig({ repository: 'repo', head: undefined, labels: {}, }); await expect(gerrit.getRawFile('renovate.json')).resolves.toBe('{}'); expect(clientMock.getFile).toHaveBeenCalledExactlyOnceWith( 'repo', 'HEAD', 'renovate.json', ); }); it('getRawFile() - no repo', async () => { writeToConfig({ repository: undefined, head: 'master', labels: {}, }); await expect(gerrit.getRawFile('renovate.json')).resolves.toBe(null); expect(clientMock.getFile).not.toHaveBeenCalled(); }); }); describe('getJsonFile()', () => { //TODO: the wanted semantic is not clear it('getJsonFile()', async () => { clientMock.getFile.mockResolvedValueOnce('{}'); await expect( gerrit.getJsonFile('renovate.json', 'test/repo', 'main'), ).resolves.toEqual({}); }); }); describe('massageMarkdown()', () => { it('massageMarkdown()', () => { expect( gerrit.massageMarkdown( codeBlock` Pull Request PR Branch creation Disabled because a matching PR was automerged previously Whenever PR becomes conflicted close this Pull Request unmerged Close this PR you tick the rebase/retry checkbox checking the rebase/retry box above `, 'rebase', ), ).toBe(codeBlock` change change Change creation Disabled because a matching change was automerged previously Whenever change becomes conflicted abandon or vote this change with Code-Review -2 Abandon or vote this change with Code-Review -2 you add the _rebase_ hashtag to this change adding the _rebase_ hashtag to this change `); }); }); describe('currently unused/not-implemented functions', () => { it('deleteLabel()', async () => { await expect( gerrit.deleteLabel(123456, 'label'), ).resolves.toBeUndefined(); }); it('ensureCommentRemoval()', async () => { await expect( gerrit.ensureCommentRemoval({ type: 'by-topic', number: 123456, topic: 'topic', }), ).resolves.toBeUndefined(); }); it('ensureIssueClosing()', async () => { await expect(gerrit.ensureIssueClosing('title')).resolves.toBeUndefined(); }); it('ensureIssue()', async () => { await expect( gerrit.ensureIssue({ body: 'body', title: 'title' }), ).resolves.toBeNull(); }); it('findIssue()', async () => { await expect(gerrit.findIssue('title')).resolves.toBeNull(); }); it('getIssueList()', async () => { await expect(gerrit.getIssueList()).resolves.toStrictEqual([]); }); }); });