import { hostRules } from '~test/host-rules.ts'; import { partial } from '~test/util.ts'; import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages.ts'; import type { BranchStatus } from '../../../types/index.ts'; import { setBaseUrl } from '../../../util/http/gerrit.ts'; import { hashBody } from '../pr-body.ts'; import type { GerritAccountInfo, GerritChange, GerritChangeMessageInfo, GerritLabelTypeInfo, GerritRevisionInfo, } from './schema.ts'; import type { GerritChangeStatus } from './types.ts'; import * as utils from './utils.ts'; import { mapBranchStatusToLabel } from './utils.ts'; const baseUrl = 'https://gerrit.example.com'; describe('modules/platform/gerrit/utils', () => { const currentRevision = '0123456789abcdef0123456789abcdef01234567'; beforeEach(() => { setBaseUrl(baseUrl); }); describe('getGerritRepoUrl()', () => { describe('no gitUrl provided', () => { it('create a git url with username/password', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', baseUrl, undefined, 'abc', ); expect(repoUrl).toBe('https://abc:123@gerrit.example.com/a/web/apps'); }); it('preserves slashes for deeply nested repository paths', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'group/subgroup/my-repo', baseUrl, undefined, 'abc', ); expect(repoUrl).toBe( 'https://abc:123@gerrit.example.com/a/group/subgroup/my-repo', ); }); it('create a git url without username/password', () => { expect(() => utils.getGerritRepoUrl('web/apps', baseUrl, undefined, 'abc'), ).toThrow('Init: You must configure a Gerrit Server username/password'); }); it('throws on invalid endpoint', () => { expect(() => utils.getGerritRepoUrl('web/apps', '...', undefined, 'abc'), ).toThrow(Error(CONFIG_GIT_URL_UNAVAILABLE)); }); }); describe('default gitUrl', () => { it('create a git url with username/password', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', baseUrl, 'default', 'abc', ); expect(repoUrl).toBe('https://abc:123@gerrit.example.com/a/web/apps'); }); }); describe('endpoint gitUrl', () => { it('create a git url with username/password', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', baseUrl, 'endpoint', 'abc', ); expect(repoUrl).toBe('https://abc:123@gerrit.example.com/a/web/apps'); }); }); describe('ssh gitUrl', () => { it('create a simple url', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', baseUrl, 'ssh', 'abc', ); expect(repoUrl).toBe('ssh://abc@gerrit.example.com:29418/web/apps'); }); it('create a url with trailing slash', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', 'https://gerrit.example.com/', 'ssh', 'abc', ); expect(repoUrl).toBe('ssh://abc@gerrit.example.com:29418/web/apps'); }); it('create a url when base has context', () => { hostRules.add({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', 'https://gerrit.example.com/context', 'ssh', 'abc', ); expect(repoUrl).toBe('ssh://abc@gerrit.example.com:29418/web/apps'); }); it('ignores non-default HTTP port from the endpoint', () => { vi.spyOn(hostRules, 'find').mockReturnValue({ username: 'abc', password: '123', }); const repoUrl = utils.getGerritRepoUrl( 'web/apps', 'http://gerrit.example.com:8080/', 'ssh', 'abc', ); expect(repoUrl).toBe('ssh://abc@gerrit.example.com:29418/web/apps'); }); }); }); describe('mapPrStateToGerritFilter()', () => { it.each([ ['closed', 'status:abandoned'], ['merged', 'status:merged'], ['!open', '-status:open'], ['open', 'status:open'], ['all', null], [undefined, null], ])( 'maps pr state %p to gerrit filter %p', (prState: any, filter: string | null) => { expect(utils.mapPrStateToGerritFilter(prState)).toEqual(filter); }, ); }); describe('mapGerritChangeStateToPrState()', () => { it.each([ ['NEW', 'open'], ['MERGED', 'merged'], ['ABANDONED', 'closed'], ['unknown', undefined], ])('maps gerrit change state %p to PrState %p', (state, prState) => { expect( utils.mapGerritChangeStateToPrState(state as GerritChangeStatus), ).toEqual(prState); }); }); describe('mapGerritChangeToPr()', () => { it('map a gerrit change to to Pr', () => { const change = partial({ _number: 123456, status: 'NEW', branch: 'main', subject: 'Fix for', created: '2025-04-14 16:33:37.000000000', hashtags: ['hashtag1', 'hashtag2'], reviewers: { REVIEWER: [partial({ username: 'username' })], }, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Some change\n\nRenovate-Branch: renovate/dependency-1.x\nChange-Id: ...', }), }, messages: [ partial({ id: '9d78ac236714cee8c2d86e95d638358925cf6853', tag: 'pull-request', message: 'Patch Set 1:\n\nOld PR-Body', }), partial({ id: '1d17c930381e88e177bbc59595c3ec941bd21028', tag: 'pull-request', message: 'Patch Set 12:\n\nLast PR-Body', }), partial({ id: '9d78ac236714cee8c2d86e95d638358925cf6853', message: 'other message...', }), ], }); expect(utils.mapGerritChangeToPr(change)).toEqual({ number: 123456, state: 'open', title: 'Fix for', createdAt: '2025-04-14T16:33:37.000000000', sourceBranch: 'renovate/dependency-1.x', targetBranch: 'main', labels: ['hashtag1', 'hashtag2'], reviewers: ['username'], bodyStruct: { hash: hashBody('Last PR-Body'), }, sha: currentRevision, }); }); it('map a gerrit change without reviewers to Pr', () => { const change = partial({ _number: 123456, status: 'NEW', branch: 'main', subject: 'Fix for', reviewers: {}, current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Some change\n\nRenovate-Branch: renovate/dependency-1.x\nChange-Id: ...', }), }, created: '2025-04-14 16:33:37.000000000', }); expect(utils.mapGerritChangeToPr(change)).toEqual({ number: 123456, state: 'open', title: 'Fix for', sourceBranch: 'renovate/dependency-1.x', targetBranch: 'main', reviewers: [], sha: currentRevision, bodyStruct: { hash: hashBody(''), }, createdAt: '2025-04-14T16:33:37.000000000', }); }); it('does not map a gerrit change without source branch to Pr', () => { const change = partial({ _number: 123456, status: 'NEW', branch: 'main', subject: 'Fix for', current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Some change\n\nRenovate-Broke: renovate/dependency-1.x\nChange-Id: ...', }), }, created: '2025-04-14 16:33:37.000000000', }); expect(utils.mapGerritChangeToPr(change)).toBeNull(); }); it('does not reject a broken commit message if knownProperties.sourceBranch is passed', () => { const change = partial({ _number: 123456, status: 'NEW', branch: 'main', subject: 'Fix for', current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Some change\n\nRenovate-Broke: renovate/dependency-1.x\nChange-Id: ...', }), }, created: '2025-04-14 16:33:37.000000000', }); expect( utils.mapGerritChangeToPr(change, { sourceBranch: 'renovate/dependency-1.x', }), ).toEqual({ number: 123456, state: 'open', title: 'Fix for', sourceBranch: 'renovate/dependency-1.x', targetBranch: 'main', reviewers: [], sha: currentRevision, bodyStruct: { hash: hashBody(''), }, createdAt: '2025-04-14T16:33:37.000000000', }); }); it('avoids iterating through change messages knownProperties.prBody is passed', () => { const change = partial({ _number: 123456, status: 'NEW', branch: 'main', subject: 'Fix for', current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Some change\n\nRenovate-Branch: renovate/dependency-1.x\nChange-Id: ...', }), }, created: '2025-04-14 16:33:37.000000000', }); expect( utils.mapGerritChangeToPr(change, { prBody: 'PR Body', }), ).toEqual({ number: 123456, state: 'open', title: 'Fix for', sourceBranch: 'renovate/dependency-1.x', targetBranch: 'main', reviewers: [], sha: currentRevision, bodyStruct: { hash: hashBody('PR Body'), }, createdAt: '2025-04-14T16:33:37.000000000', }); }); }); describe('extractSourceBranch()', () => { it('no commit message', () => { const change = partial(); expect(utils.extractSourceBranch(change)).toBeUndefined(); }); it('commit message with no footer', () => { const change = partial({ current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'some message...', }), }, }); expect(utils.extractSourceBranch(change)).toBeUndefined(); }); it('commit message with footer', () => { const change = partial({ current_revision: currentRevision, revisions: { [currentRevision]: partial({ commit_with_footers: 'Some change\n\nRenovate-Branch: renovate/dependency-1.x\nChange-Id: ...', }), }, }); expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); }); }); describe('findPullRequestBody()', () => { it('find pull-request-body', () => { const change = partial({ messages: [ partial({ id: '9d78ac236714cee8c2d86e95d638358925cf6853', tag: 'pull-request', message: 'Patch Set 1:\n\nOld PR-Body', }), partial({ id: '1d17c930381e88e177bbc59595c3ec941bd21028', tag: 'pull-request', message: 'Patch Set 12:\n\nLast PR-Body', }), partial({ id: '9d78ac236714cee8c2d86e95d638358925cf6853', message: 'other message...', }), ], }); expect(utils.findPullRequestBody(change)).toBe('Last PR-Body'); }); it('no pull-request-body message found', () => { const change = partial({}); expect(utils.findPullRequestBody(change)).toBeUndefined(); change.messages = []; expect(utils.findPullRequestBody(change)).toBeUndefined(); change.messages = [ partial({ tag: 'other-tag', message: 'message', }), ]; expect(utils.findPullRequestBody(change)).toBeUndefined(); }); }); describe('mapBranchStatusToLabel()', () => { const labelWithOne: GerritLabelTypeInfo = { values: { '-1': 'rejected', '0': 'default', '1': 'accepted' }, default_value: 0, }; it.each([ ['red', -1], ['yellow', -1], ['green', 1], ])( 'Label with +1/-1 map branchState=%p to %p', (branchState, expectedValue) => { expect( mapBranchStatusToLabel(branchState as BranchStatus, labelWithOne), ).toEqual(expectedValue); }, ); const labelWithTwo: GerritLabelTypeInfo = { values: { '-2': 'rejected', '-1': 'disliked', '0': 'default', '1': 'looksOkay', '2': 'approved', }, default_value: 0, }; it.each([ ['red', -2], ['yellow', -2], ['green', 2], ])( 'Label with +2/-2, map branchState=%p to %p', (branchState, expectedValue) => { expect( mapBranchStatusToLabel(branchState as BranchStatus, labelWithTwo), ).toEqual(expectedValue); }, ); }); describe('convertGerritDateToISO()', () => { it('converts Gerrit date format to ISO format', () => { expect( utils.convertGerritDateToISO('2023-05-20 14:25:30.123456789'), ).toBe('2023-05-20T14:25:30.123456789'); }); }); });