import { git } from '~test/util.ts'; import * as httpMock from '../../../../test/http-mock.ts'; import { GlobalConfig } from '../../../config/global.ts'; import * as hostRules from '../../../util/host-rules.ts'; import type { Pr } from '../types.ts'; import * as util from '../util.ts'; import * as scmPlatform from './index.ts'; import { mapPrFromScmToRenovate } from './mapper.ts'; import type { PullRequest, Repo, User } from './schema.ts'; import type { PrFilterByState } from './types.ts'; vi.mock('../util.ts'); vi.mock('../../../util/git/index.ts'); const endpoint = 'https://localhost:8080/scm/'; const baseUrl = `${endpoint}api/v2`; const token = 'TEST_TOKEN'; const user: User = { mail: 'test@user.de', displayName: 'Test User', name: 'testUser1337', }; const repo: Repo = { contact: 'test@test.com', creationDate: '2023-08-02T10:48:24.762Z', description: 'Default Repo', lastModified: '2023-08-10T10:48:24.762Z', namespace: 'default', name: 'repo', type: 'git', archived: false, exporting: false, healthCheckRunning: false, _links: { protocol: [ { name: 'http', href: 'https://localhost:8080/scm/default/repo' }, ], defaultBranch: { href: 'https://localhost:8080/scm/api/v2/config/git/default/repo/default-branch', }, }, }; const pullRequest: PullRequest = { id: '1', author: { displayName: 'Thomas Zerr', id: 'tzerr' }, source: 'feature/test', target: 'develop', title: 'The PullRequest', description: 'Another PullRequest', creationDate: '2023-08-02T10:48:24.762Z', status: 'OPEN', labels: [], tasks: { todo: 2, done: 4 }, _links: {}, _embedded: { defaultConfig: { mergeStrategy: 'SQUASH', deleteBranchOnMerge: true, }, }, }; const renovatePr = mapPrFromScmToRenovate(pullRequest); describe('modules/platform/scm-manager/index', () => { beforeEach(() => { GlobalConfig.reset(); hostRules.add({ token, username: user.name }); scmPlatform.invalidatePrCache(); }); describe('initPlatform', () => { it('should throw error, when endpoint is not configured', async () => { await expect(scmPlatform.initPlatform({ token })).rejects.toThrow( 'SCM-Manager endpoint not configured', ); }); it('should throw error, when token is not configured', async () => { await expect(scmPlatform.initPlatform({ endpoint })).rejects.toThrow( 'SCM-Manager API token not configured', ); }); it('should throw error, when token is invalid', async () => { httpMock.scope(baseUrl).get(`/me`).reply(401); await expect( scmPlatform.initPlatform({ endpoint, token: 'invalid' }), ).rejects.toThrow('Init: Authentication failure'); }); it('should init platform', async () => { httpMock.scope(baseUrl).get('/me').reply(200, user); await expect( scmPlatform.initPlatform({ endpoint, token }), ).resolves.toEqual({ endpoint: baseUrl, gitAuthor: 'Test User ', }); }); }); describe('initRepo', () => { it('should init repo', async () => { const repository = `${repo.namespace}/${repo.name}`; const expectedFingerprint = 'expectedFingerprint'; const expectedDefaultBranch = 'expectedDefaultBranch'; GlobalConfig.set({ ignorePrAuthor: true }); httpMock .scope(baseUrl) .get(`/repositories/${repository}`) .reply(200, repo); httpMock .scope(baseUrl) .get(`/config/git/${repository}/default-branch`) .reply(200, { defaultBranch: expectedDefaultBranch }); vi.mocked(util.repoFingerprint).mockReturnValueOnce(expectedFingerprint); await expect( scmPlatform.initRepo({ repository: `${repo.namespace}/${repo.name}`, }), ).resolves.toEqual({ defaultBranch: expectedDefaultBranch, isFork: false, repoFingerprint: expectedFingerprint, }); expect(git.initRepo).toHaveBeenCalledExactlyOnceWith({ url: `https://${user.name}:${token}@localhost:8080/scm/default/repo`, repository, defaultBranch: expectedDefaultBranch, ignorePrAuthor: true, }); }); }); describe('getRepos', () => { it('should return all available repos', async () => { httpMock .scope(baseUrl) .get(`/repositories?pageSize=1000000`) .reply(200, { page: 0, pageTotal: 1, _embedded: { repositories: [ repo, { ...repo, namespace: 'other', name: 'repository' }, { ...repo, namespace: 'other', name: 'mercurial', type: 'hg' }, { ...repo, namespace: 'other', name: 'subversion', type: 'svn' }, ], }, }); await expect(scmPlatform.getRepos()).resolves.toEqual([ 'default/repo', 'other/repository', ]); }); }); describe('getPrList', () => { it('should return empty array, because no PR could be found', async () => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: [], }, }); await expect(scmPlatform.getPrList()).resolves.toBeEmptyArray(); }); it('should return empty array, because API request failed', async () => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(400); await expect(scmPlatform.getPrList()).resolves.toBeEmptyArray(); }); it('should return all PRs of a repo', async () => { const expectedResult: Pr[] = [ { sourceBranch: pullRequest.source, createdAt: pullRequest.creationDate, labels: pullRequest.labels, number: parseInt(pullRequest.id, 10), state: 'open', targetBranch: pullRequest.target, title: pullRequest.title, hasAssignees: false, isDraft: false, reviewers: [], }, ]; httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: [pullRequest], }, }); //Fetching from client await expect(scmPlatform.getPrList()).resolves.toIncludeAllMembers( expectedResult, ); //Fetching from cache await expect(scmPlatform.getPrList()).resolves.toIncludeAllMembers( expectedResult, ); }); }); describe('findPr', () => { it('search in Pull Request without explicitly setting the state as argument', async () => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: [pullRequest], }, }); await expect( scmPlatform.findPr({ branchName: pullRequest.source, prTitle: pullRequest.title, }), ).resolves.toEqual(renovatePr); }); it.each` availablePullRequest | branchName | prTitle | state | result ${[]} | ${pullRequest.source} | ${pullRequest.title} | ${'all'} | ${null} ${[pullRequest]} | ${'invalid branchName'} | ${pullRequest.title} | ${'all'} | ${null} ${[pullRequest]} | ${pullRequest.source} | ${'invalid title'} | ${'all'} | ${null} ${[pullRequest]} | ${pullRequest.source} | ${null} | ${'all'} | ${renovatePr} ${[pullRequest]} | ${pullRequest.source} | ${undefined} | ${'all'} | ${renovatePr} ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'all'} | ${renovatePr} ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'open'} | ${renovatePr} ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'!open'} | ${null} ${[pullRequest]} | ${pullRequest.source} | ${pullRequest.title} | ${'closed'} | ${null} `( 'search within available pull requests for branch name "$branchName", pr title "$prTitle" and state "$state" with result $result', async ({ availablePullRequest, branchName, prTitle, state, result, }: { availablePullRequest: PullRequest[]; branchName: string; prTitle: string | undefined | null; state: string; result: Pr | null; }) => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: availablePullRequest, }, }); await expect( scmPlatform.findPr({ branchName, prTitle, state: state as PrFilterByState, }), ).resolves.toEqual(result); }, ); }); describe('getBranchPr', () => { it.each` availablePullRequest | branchName | result ${[]} | ${pullRequest.source} | ${null} ${[pullRequest]} | ${'invalid branchName'} | ${null} ${[pullRequest]} | ${pullRequest.source} | ${renovatePr} `( 'search within available pull requests for branch name "$branchName" with result $result', async ({ availablePullRequest, branchName, result, }: { availablePullRequest: PullRequest[]; branchName: string; result: Pr | null; }) => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: availablePullRequest, }, }); await expect(scmPlatform.getBranchPr(branchName)).resolves.toEqual( result, ); }, ); }); describe('getPr', () => { it('should return null, because PR was not found', async () => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: [], }, }); httpMock .scope(baseUrl) .get(`/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`) .reply(404); await expect(scmPlatform.getPr(1)).resolves.toBeNull(); }); it('should return PR from cache', async () => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: [pullRequest], }, }); await expect( scmPlatform.getPr(parseInt(pullRequest.id, 10)), ).resolves.toEqual(renovatePr); }); it('should return fetched pr', async () => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}?status=ALL&pageSize=1000000`, ) .reply(200, { page: 0, pageTotal: 1, _embedded: { pullRequests: [], }, }); httpMock .scope(baseUrl) .get(`/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`) .reply(200, pullRequest); await expect( scmPlatform.getPr(parseInt(pullRequest.id, 10)), ).resolves.toEqual(renovatePr); }); }); describe('createPr', () => { it.each` draftPr | expectedIsDraft ${undefined} | ${false} ${false} | ${false} ${true} | ${true} `( 'should create PR with $draftPR and state $expectedState', async ({ draftPr, expectedIsDraft, }: { draftPr: boolean | undefined; expectedState: string; expectedIsDraft: boolean; }) => { httpMock .scope(baseUrl) .post(`/pull-requests/${repo.namespace}/${repo.name}`) .reply(201, undefined, { location: `${baseUrl}/pull-requests/${repo.namespace}/${repo.name}/1337`, }); httpMock .scope(baseUrl) .get(`/pull-requests/${repo.namespace}/${repo.name}/1337`) .reply(200, { id: '1337', source: 'feature/test', target: 'develop', title: 'PR Title', description: 'PR Body', creationDate: '2023-01-01T13:37:00.000Z', status: draftPr ? 'DRAFT' : 'OPEN', labels: [], tasks: { todo: 0, done: 0 }, _links: {}, _embedded: { defaultConfig: { mergeStrategy: 'FAST_FORWARD_IF_POSSIBLE', deleteBranchOnMerge: false, }, }, }); await expect( scmPlatform.createPr({ sourceBranch: 'feature/test', targetBranch: 'develop', prTitle: 'PR Title', prBody: 'PR Body', draftPR: draftPr, }), ).resolves.toEqual({ sourceBranch: 'feature/test', targetBranch: 'develop', title: 'PR Title', createdAt: '2023-01-01T13:37:00.000Z', hasAssignees: false, isDraft: expectedIsDraft, labels: [], number: 1337, reviewers: [], state: 'open', }); }, ); }); describe('updatePr', () => { it.each` state | body ${'open'} | ${'prBody'} ${'closed'} | ${'prBody'} ${undefined} | ${'prBody'} ${'open'} | ${undefined} `( 'should update PR with state $state and bdoy $body', async ({ state, body, }: { state: string | undefined; body: string | undefined; }) => { httpMock .scope(baseUrl) .get( `/pull-requests/${repo.namespace}/${repo.name}/${pullRequest.id}`, ) .reply(200, pullRequest); httpMock .scope(baseUrl) .put(`/pull-requests/${repo.namespace}/${repo.name}/1`) .reply(204); await expect( scmPlatform.updatePr({ number: 1, prTitle: 'PR Title', prBody: body, state: state as 'open' | 'closed' | undefined, targetBranch: 'Target/Branch', }), ).resolves.not.toThrow(); }, ); }); describe('mergePr', () => { it('should Not implemented and return false', async () => { const result = await scmPlatform.mergePr({ id: 1 }); expect(result).toBeFalse(); }); }); describe('getBranchStatus', () => { it('should Not implemented and return red', async () => { const result = await scmPlatform.getBranchStatus('test/branch', false); expect(result).toBe('red'); }); }); describe('setBranchStatus', () => { it('should Not implemented', async () => { await expect( scmPlatform.setBranchStatus({ branchName: 'test/branch', context: 'context', description: 'description', state: 'red', }), ).resolves.not.toThrow(); }); }); describe('getBranchStatusCheck', () => { it('should Not implemented and return null', async () => { const result = await scmPlatform.getBranchStatusCheck( 'test/branch', null, ); expect(result).toBeNull(); }); }); describe('addReviewers', () => { it('should Not implemented', async () => { await expect( scmPlatform.addReviewers(1, ['reviewer']), ).resolves.not.toThrow(); }); }); describe('addAssignees', () => { it('should Not implemented', async () => { await expect( scmPlatform.addAssignees(1, ['assignee']), ).resolves.not.toThrow(); }); }); describe('deleteLabel', () => { it('should Not implemented', async () => { await expect(scmPlatform.deleteLabel(1, 'label')).resolves.not.toThrow(); }); }); describe('getIssueList', () => { it('should Not implemented and return empty list', async () => { const result = await scmPlatform.getIssueList(); expect(result).toEqual([]); }); }); describe('findIssue', () => { it('should Not implemented and return null', async () => { const result = await scmPlatform.findIssue('issue'); expect(result).toBeNull(); }); }); describe('ensureIssue', () => { it('should Not implemented and return null', async () => { const result = await scmPlatform.ensureIssue({ title: 'issue', body: 'body', }); expect(result).toBeNull(); }); }); describe('ensureIssueClosing', () => { it('should Not implemented', async () => { await expect( scmPlatform.ensureIssueClosing('issue'), ).resolves.not.toThrow(); }); }); describe('ensureCommentRemoval', () => { it('should Not implemented', async () => { await expect( scmPlatform.ensureCommentRemoval({ type: 'by-content', number: 1, content: 'content', }), ).resolves.not.toThrow(); }); }); describe('ensureComment', () => { it('should Not implemented', async () => { await expect( scmPlatform.ensureComment({ number: 1, topic: 'comment', content: 'content', }), ).resolves.toBeFalse(); }); }); describe('massageMarkdown', () => { it('should adjust smart link for Pull Requests', () => { const result = scmPlatform.massageMarkdown('[PR](../pull/1)'); expect(result).toBe('[PR](pulls/1)'); }); }); describe('getRepoForceRebase', () => { it('should Not implemented and return false', async () => { const result = await scmPlatform.getRepoForceRebase(); expect(result).toBeFalse(); }); }); describe('getRawFile', () => { it('should Not implemented and return null', async () => { const result = await scmPlatform.getRawFile('file'); expect(result).toBeNull(); }); }); describe('getJsonFile', () => { it('should Not implemented and return undefined', async () => { const result = await scmPlatform.getJsonFile('package.json'); expect(result).toBeNull(); }); }); describe('maxBodyLength', () => { it('should return the max body length allowed for an SCM-Manager request body', () => { expect(scmPlatform.maxBodyLength()).toBe(200000); }); }); });