mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 02:00:10 +00:00
05e1271376
Enables the vitest/prefer-expect-resolves oxlint rule and applies its autofix across the spec suite.
3034 lines
90 KiB
TypeScript
3034 lines
90 KiB
TypeScript
import { Readable } from 'node:stream';
|
|
import { isString } from '@sindresorhus/is';
|
|
import type { ICoreApi } from 'azure-devops-node-api/CoreApi.js';
|
|
import type { IGitApi } from 'azure-devops-node-api/GitApi.js';
|
|
import type {
|
|
GitPullRequest,
|
|
GitVersionDescriptor,
|
|
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
|
|
import {
|
|
GitPullRequestMergeStrategy,
|
|
GitStatusState,
|
|
GitVersionType,
|
|
PullRequestStatus,
|
|
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
|
|
import type {
|
|
PolicyConfiguration,
|
|
PolicyEvaluationRecord,
|
|
PolicyTypeRef,
|
|
} from 'azure-devops-node-api/interfaces/PolicyInterfaces.js';
|
|
import { PolicyEvaluationStatus } from 'azure-devops-node-api/interfaces/PolicyInterfaces.js';
|
|
import type { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi.js';
|
|
import type { Mocked, MockedObject } from 'vitest';
|
|
import { vi } from 'vitest';
|
|
import { mockDeep } from 'vitest-mock-extended';
|
|
import { partial } from '~test/util.ts';
|
|
import { GlobalConfig } from '../../../config/global.ts';
|
|
import {
|
|
REPOSITORY_ARCHIVED,
|
|
REPOSITORY_NOT_FOUND,
|
|
} from '../../../constants/error-messages.ts';
|
|
import type { logger as _logger } from '../../../logger/index.ts';
|
|
import type * as _git from '../../../util/git/index.ts';
|
|
import type * as _hostRules from '../../../util/host-rules.ts';
|
|
import type { Platform, RepoParams } from '../types.ts';
|
|
import { AzurePrVote } from './types.ts';
|
|
|
|
vi.mock('../../../config/global.ts', async (importOriginal) =>
|
|
importOriginal<typeof import('../../../config/global.ts')>(),
|
|
);
|
|
vi.mock('./azure-got-wrapper.ts', () => mockDeep());
|
|
vi.mock('./azure-helper.ts', () => mockDeep());
|
|
vi.mock('../../../util/sanitize.ts', () =>
|
|
mockDeep({ sanitize: (s: string) => s }),
|
|
);
|
|
vi.mock('timers/promises');
|
|
|
|
describe('modules/platform/azure/index', () => {
|
|
let hostRules: typeof _hostRules;
|
|
let azure: Platform;
|
|
let azureApi: Mocked<typeof import('./azure-got-wrapper.ts')>;
|
|
let azureHelper: Mocked<typeof import('./azure-helper.ts')>;
|
|
let git: Mocked<typeof _git>;
|
|
let logger: MockedObject<typeof _logger>;
|
|
|
|
beforeEach(async () => {
|
|
// reset module
|
|
vi.resetModules();
|
|
GlobalConfig.reset();
|
|
hostRules = await vi.importActual('../../../util/host-rules.ts');
|
|
azure = await vi.importActual('./index.ts');
|
|
azureApi = await vi.importMock('./azure-got-wrapper.ts');
|
|
azureHelper = await vi.importMock('./azure-helper.ts');
|
|
logger = (
|
|
await vi.importMock<typeof import('../../../logger/index.ts')>(
|
|
'../../../logger/index.ts',
|
|
)
|
|
).logger;
|
|
git = await vi.importMock('../../../util/git/index.ts');
|
|
git.branchExists.mockReturnValue(true);
|
|
git.isBranchBehindBase.mockResolvedValue(false);
|
|
hostRules.clear();
|
|
hostRules.add({ token: 'token' });
|
|
azureHelper.getPolicyEvaluations.mockResolvedValue([]);
|
|
azureApi.getAuthenticatedUserId.mockResolvedValue('renovate-user-id');
|
|
// Default to the hosted (cloud) endpoint used across these tests.
|
|
azureApi.isHosted.mockResolvedValue(true);
|
|
await azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
token: 'token',
|
|
});
|
|
});
|
|
|
|
// do we need the args?
|
|
|
|
function getRepos(_token: string, _endpoint: string) {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getRepositories: vi.fn().mockResolvedValue([
|
|
{
|
|
name: 'repo1',
|
|
project: {
|
|
name: 'prj1',
|
|
},
|
|
},
|
|
{
|
|
name: 'repo2',
|
|
project: {
|
|
name: 'prj1',
|
|
},
|
|
isDisabled: false,
|
|
},
|
|
{
|
|
name: 'repoDisabled',
|
|
project: {
|
|
name: 'prj1',
|
|
},
|
|
isDisabled: true,
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
return azure.getRepos();
|
|
}
|
|
|
|
describe('initPlatform()', () => {
|
|
it('should throw if no endpoint', () => {
|
|
expect.assertions(1);
|
|
expect(() => azure.initPlatform({})).toThrow(
|
|
'Init: You must configure an Azure DevOps endpoint',
|
|
);
|
|
});
|
|
|
|
it('should throw if no token nor a username and password', () => {
|
|
expect.assertions(1);
|
|
expect(() =>
|
|
azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
}),
|
|
).toThrow(
|
|
'Init: You must configure an Azure DevOps token, or a username and',
|
|
);
|
|
});
|
|
|
|
it('should throw if a username but no password', () => {
|
|
expect.assertions(1);
|
|
expect(() =>
|
|
azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
username: 'user',
|
|
}),
|
|
).toThrow(
|
|
'Init: You must configure an Azure DevOps token, or a username and',
|
|
);
|
|
});
|
|
|
|
it('should throw if a password but no username', () => {
|
|
expect.assertions(1);
|
|
expect(() =>
|
|
azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
password: 'pass',
|
|
}),
|
|
).toThrow(
|
|
'Init: You must configure an Azure DevOps token, or a username and',
|
|
);
|
|
});
|
|
|
|
it('should init', async () => {
|
|
await expect(
|
|
azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
token: 'token',
|
|
}),
|
|
).resolves.toEqual({
|
|
endpoint: 'https://dev.azure.com/renovate12345/',
|
|
});
|
|
expect(azureApi.getAuthenticatedUserId).toHaveBeenLastCalledWith({
|
|
token: 'token',
|
|
});
|
|
});
|
|
|
|
it('should discover the authenticated user with basic credentials', async () => {
|
|
await azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
username: 'user',
|
|
password: 'pass',
|
|
});
|
|
|
|
expect(azureApi.getAuthenticatedUserId).toHaveBeenLastCalledWith({
|
|
username: 'user',
|
|
password: 'pass',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getRepos()', () => {
|
|
it('should return an array of repos', async () => {
|
|
const repos = await getRepos(
|
|
'sometoken',
|
|
'https://dev.azure.com/renovate12345',
|
|
);
|
|
expect(azureApi.gitApi.mock.calls).toEqual([[]]);
|
|
expect(repos).toEqual(['prj1/repo1', 'prj1/repo2']);
|
|
});
|
|
});
|
|
|
|
function initRepo(args?: Partial<RepoParams> | string) {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getRepositories: vi.fn().mockResolvedValue([
|
|
{
|
|
name: 'repo',
|
|
id: '1',
|
|
privateRepo: true,
|
|
isFork: false,
|
|
defaultBranch: 'defBr',
|
|
project: {
|
|
name: 'some',
|
|
},
|
|
},
|
|
{
|
|
name: 'repo2',
|
|
project: {
|
|
name: 'prj2',
|
|
},
|
|
},
|
|
{
|
|
name: 'repo3',
|
|
project: {
|
|
name: 'some',
|
|
},
|
|
isDisabled: true,
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
|
|
if (isString(args)) {
|
|
return azure.initRepo({
|
|
repository: args,
|
|
});
|
|
}
|
|
|
|
return azure.initRepo({
|
|
repository: 'some/repo',
|
|
...args,
|
|
});
|
|
}
|
|
|
|
describe('initRepo', () => {
|
|
it(`should initialise the config for a repo`, async () => {
|
|
const config = await initRepo({
|
|
repository: 'some/repo',
|
|
});
|
|
expect(azureApi.gitApi.mock.calls).toEqual([[]]);
|
|
expect(config).toEqual({
|
|
defaultBranch: 'defBr',
|
|
isFork: false,
|
|
repoFingerprint:
|
|
'02574de485149547c1a071aa7921da3d0afadcd6162f3bd49ba3ced29be589f8b9bac689fd0badb212bd21c3f48bd8566beaf31cdca2b083bd855808a9c129e2',
|
|
});
|
|
});
|
|
|
|
it(`throws if repo is disabled`, async () => {
|
|
await expect(
|
|
initRepo({
|
|
repository: 'some/repo3',
|
|
}),
|
|
).rejects.toThrow(REPOSITORY_ARCHIVED);
|
|
});
|
|
|
|
it(`throws if repo is not in repos list`, async () => {
|
|
await expect(
|
|
initRepo({
|
|
repository: 'some/missing',
|
|
}),
|
|
).rejects.toThrow(REPOSITORY_NOT_FOUND);
|
|
});
|
|
});
|
|
|
|
describe('findPr(branchName, prTitle, state, targetBranch)', () => {
|
|
it('returns pr if found it open', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Active,
|
|
},
|
|
]),
|
|
|
|
getPullRequestCommits: vi.fn().mockReturnValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
state: 'open',
|
|
});
|
|
expect(res).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
number: 1,
|
|
pullRequestId: 1,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'open',
|
|
status: 1,
|
|
targetBranch: 'branch-b',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
|
|
it('returns pr if found not open', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Completed,
|
|
},
|
|
]),
|
|
|
|
getPullRequestCommits: vi.fn().mockReturnValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
state: '!open',
|
|
});
|
|
expect(res).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
number: 1,
|
|
pullRequestId: 1,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'merged',
|
|
status: 3,
|
|
targetBranch: 'branch-b',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
|
|
it('returns pr if found it close', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Abandoned,
|
|
},
|
|
]),
|
|
|
|
getPullRequestCommits: vi.fn().mockReturnValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
state: 'closed',
|
|
});
|
|
expect(res).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
number: 1,
|
|
pullRequestId: 1,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'closed',
|
|
status: 2,
|
|
targetBranch: 'branch-b',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
|
|
it('returns pr if found it all state', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Abandoned,
|
|
},
|
|
]),
|
|
|
|
getPullRequestCommits: vi.fn().mockReturnValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
});
|
|
expect(res).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
number: 1,
|
|
pullRequestId: 1,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'closed',
|
|
status: 2,
|
|
targetBranch: 'branch-b',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
|
|
it('returns pr if found matches targetBranch', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Active,
|
|
},
|
|
{
|
|
pullRequestId: 2,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-c',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Active,
|
|
},
|
|
]),
|
|
getPullRequestCommits: vi.fn().mockReturnValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
state: 'open',
|
|
targetBranch: 'branch-c',
|
|
});
|
|
expect(res).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
number: 2,
|
|
pullRequestId: 2,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'open',
|
|
status: 1,
|
|
targetBranch: 'branch-c',
|
|
targetRefName: 'refs/heads/branch-c',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
|
|
it('returns first pr if found does not match targetBranch', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Active,
|
|
},
|
|
{
|
|
pullRequestId: 2,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-c',
|
|
title: 'branch a pr',
|
|
status: PullRequestStatus.Active,
|
|
},
|
|
]),
|
|
getPullRequestCommits: vi.fn().mockReturnValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
state: 'open',
|
|
targetBranch: 'branch-d',
|
|
});
|
|
expect(res).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
number: 1,
|
|
pullRequestId: 1,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'open',
|
|
status: 1,
|
|
targetBranch: 'branch-b',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
|
|
it('queries the exact branch when including other authors', async () => {
|
|
await initRepo();
|
|
const getPullRequests = vi.fn().mockResolvedValue([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: 1,
|
|
},
|
|
]);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests,
|
|
}),
|
|
);
|
|
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
state: 'open',
|
|
targetBranch: 'branch-b',
|
|
includeOtherAuthors: true,
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
number: 1,
|
|
sourceBranch: 'branch-a',
|
|
state: 'open',
|
|
targetBranch: 'branch-b',
|
|
});
|
|
expect(getPullRequests).toHaveBeenCalledExactlyOnceWith(
|
|
'1',
|
|
{
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
sourceRepositoryId: '1',
|
|
status: 1,
|
|
targetRefName: 'refs/heads/branch-b',
|
|
},
|
|
'some',
|
|
0,
|
|
0,
|
|
1,
|
|
);
|
|
});
|
|
|
|
it('returns null when no PR from another author matches', async () => {
|
|
await initRepo();
|
|
const getPullRequests = vi.fn().mockResolvedValue([]);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests,
|
|
}),
|
|
);
|
|
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
state: 'open',
|
|
includeOtherAuthors: true,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
expect(getPullRequests).toHaveBeenCalledExactlyOnceWith(
|
|
'1',
|
|
{
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
sourceRepositoryId: '1',
|
|
status: 1,
|
|
},
|
|
'some',
|
|
0,
|
|
0,
|
|
1,
|
|
);
|
|
});
|
|
|
|
it('catches errors', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi.fn().mockRejectedValueOnce(new Error()),
|
|
}),
|
|
);
|
|
const res = await azure.findPr({
|
|
branchName: 'branch-a',
|
|
prTitle: 'branch a pr',
|
|
});
|
|
expect(res).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getPrList()', () => {
|
|
it('filters PRs by repository and authenticated user', async () => {
|
|
await initRepo();
|
|
const getPullRequests = vi.fn().mockResolvedValue([]);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests,
|
|
}),
|
|
);
|
|
await expect(azure.getPrList()).resolves.toEqual([]);
|
|
expect(azureApi.getAuthenticatedUserId).toHaveBeenCalledExactlyOnceWith({
|
|
token: 'token',
|
|
});
|
|
expect(azureApi.gitApi).toHaveBeenLastCalledWith();
|
|
expect(getPullRequests).toHaveBeenCalledExactlyOnceWith(
|
|
'1',
|
|
{
|
|
creatorId: 'renovate-user-id',
|
|
sourceRepositoryId: '1',
|
|
status: 4,
|
|
},
|
|
'some',
|
|
0,
|
|
0,
|
|
100,
|
|
);
|
|
});
|
|
|
|
it('does not filter by authenticated user when ignorePrAuthor is enabled', async () => {
|
|
GlobalConfig.set({ ignorePrAuthor: true });
|
|
await initRepo();
|
|
const getPullRequests = vi.fn().mockResolvedValue([]);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests,
|
|
}),
|
|
);
|
|
|
|
await expect(azure.getPrList()).resolves.toEqual([]);
|
|
expect(getPullRequests).toHaveBeenCalledExactlyOnceWith(
|
|
'1',
|
|
{
|
|
sourceRepositoryId: '1',
|
|
status: 4,
|
|
},
|
|
'some',
|
|
0,
|
|
0,
|
|
100,
|
|
);
|
|
});
|
|
|
|
it('does not filter by authenticated user when the ID is unavailable', async () => {
|
|
azureApi.getAuthenticatedUserId.mockResolvedValueOnce(undefined);
|
|
await azure.initPlatform({
|
|
endpoint: 'https://dev.azure.com/renovate12345',
|
|
token: 'token',
|
|
});
|
|
await initRepo();
|
|
const getPullRequests = vi.fn().mockResolvedValue([]);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests,
|
|
}),
|
|
);
|
|
|
|
await expect(azure.getPrList()).resolves.toEqual([]);
|
|
expect(getPullRequests).toHaveBeenCalledExactlyOnceWith(
|
|
'1',
|
|
{
|
|
sourceRepositoryId: '1',
|
|
status: 4,
|
|
},
|
|
'some',
|
|
0,
|
|
0,
|
|
100,
|
|
);
|
|
});
|
|
|
|
it('reuses the cached PR list', async () => {
|
|
await initRepo();
|
|
const getPullRequests = vi.fn().mockResolvedValue([]);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({ getPullRequests }),
|
|
);
|
|
|
|
await azure.getPrList();
|
|
await azure.getPrList();
|
|
|
|
expect(getPullRequests).toHaveBeenCalledOnce();
|
|
});
|
|
});
|
|
|
|
describe('getBranchPr(branchName, targetBranch)', () => {
|
|
it('should return null if no PR exists', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi.fn().mockResolvedValueOnce([]),
|
|
}),
|
|
);
|
|
const pr = await azure.getBranchPr('somebranch');
|
|
expect(pr).toBeNull();
|
|
});
|
|
|
|
it('should return the pr', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockResolvedValueOnce([
|
|
{
|
|
pullRequestId: 1,
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
status: 1,
|
|
},
|
|
])
|
|
.mockResolvedValueOnce([]),
|
|
getPullRequestLabels: vi.fn().mockResolvedValue([]),
|
|
}),
|
|
);
|
|
const pr = await azure.getBranchPr('branch-a', 'branch-b');
|
|
expect(pr).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdAt: undefined,
|
|
labels: [],
|
|
number: 1,
|
|
pullRequestId: 1,
|
|
sourceBranch: 'branch-a',
|
|
sourceRefName: 'refs/heads/branch-a',
|
|
state: 'open',
|
|
status: 1,
|
|
targetBranch: 'branch-b',
|
|
targetRefName: 'refs/heads/branch-b',
|
|
title: 'branch a pr',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getBranchStatusCheck(branchName, context)', () => {
|
|
it('should return green if status is succeeded', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Succeeded,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('green');
|
|
});
|
|
|
|
it('should return green if status is not applicable', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.NotApplicable,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('green');
|
|
});
|
|
|
|
it('should return red if status is failed', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Failed,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('red');
|
|
});
|
|
|
|
it('should return red if context status is error', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Error,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('red');
|
|
});
|
|
|
|
it('should return yellow if status is pending', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Pending,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('yellow');
|
|
});
|
|
|
|
it('should return yellow if status is not set', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.NotSet,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('yellow');
|
|
});
|
|
|
|
it('should return yellow if status is unknown', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: -1,
|
|
context: { genre: 'a-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBe('yellow');
|
|
});
|
|
|
|
it('should return null if status not found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Pending,
|
|
context: { genre: 'another-genre', name: 'a-name' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatusCheck(
|
|
'somebranch',
|
|
'a-genre/a-name',
|
|
);
|
|
expect(res).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('getBranchStatus(branchName, ignoreTests)', () => {
|
|
it('should pass through success', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Succeeded,
|
|
context: { genre: 'renovate' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatus('somebranch', true);
|
|
expect(res).toBe('green');
|
|
});
|
|
|
|
it('should not treat internal checks as success', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
|
|
getStatuses: vi.fn().mockResolvedValue([
|
|
{
|
|
state: GitStatusState.Succeeded,
|
|
context: { genre: 'renovate' },
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatus('somebranch', false);
|
|
expect(res).toBe('yellow');
|
|
});
|
|
|
|
it('should pass through failed', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
getStatuses: vi
|
|
.fn()
|
|
.mockResolvedValue([{ state: GitStatusState.Error }]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatus('somebranch', true);
|
|
expect(res).toBe('red');
|
|
});
|
|
|
|
it('should pass through pending', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
getStatuses: vi
|
|
.fn()
|
|
.mockResolvedValue([{ state: GitStatusState.Pending }]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatus('somebranch', true);
|
|
expect(res).toBe('yellow');
|
|
});
|
|
|
|
it('should fall back to yellow if no statuses returned', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
getStatuses: vi.fn().mockResolvedValue([]),
|
|
}),
|
|
);
|
|
const res = await azure.getBranchStatus('somebranch', true);
|
|
expect(res).toBe('yellow');
|
|
});
|
|
});
|
|
|
|
describe('getPr(prNo)', () => {
|
|
it('should return null if no prNo is passed', async () => {
|
|
const pr = await azure.getPr(0);
|
|
expect(pr).toBeNull();
|
|
});
|
|
|
|
it('should return null if no PR is returned from azure', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi.fn().mockResolvedValue([]),
|
|
}),
|
|
);
|
|
const pr = await azure.getPr(1234);
|
|
expect(pr).toBeNull();
|
|
});
|
|
|
|
it('should return a pr in the right format', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getPullRequests: vi
|
|
.fn()
|
|
.mockReturnValue([])
|
|
.mockReturnValueOnce([
|
|
{
|
|
pullRequestId: 1234,
|
|
},
|
|
]),
|
|
|
|
getPullRequestLabels: vi
|
|
.fn()
|
|
.mockReturnValue([{ active: true, name: 'renovate' }]),
|
|
|
|
getPullRequestCommits: vi.fn().mockReturnValue([
|
|
{
|
|
author: {
|
|
name: 'renovate',
|
|
},
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const pr = await azure.getPr(1234);
|
|
expect(pr).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
labels: ['renovate'],
|
|
number: 1234,
|
|
pullRequestId: 1234,
|
|
state: 'open',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createPr()', () => {
|
|
it('should create and return a PR object', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn().mockResolvedValue({
|
|
pullRequestId: 456,
|
|
}),
|
|
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
}),
|
|
);
|
|
const pr = await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'master',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
});
|
|
expect(pr).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
number: 456,
|
|
pullRequestId: 456,
|
|
state: 'open',
|
|
});
|
|
});
|
|
|
|
it('should create and return a PR object from base branch', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn().mockResolvedValue({
|
|
pullRequestId: 456,
|
|
}),
|
|
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
}),
|
|
);
|
|
const pr = await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'master',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
});
|
|
expect(pr).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
number: 456,
|
|
pullRequestId: 456,
|
|
state: 'open',
|
|
});
|
|
});
|
|
|
|
describe('when usePlatformAutomerge is set', () => {
|
|
it('should create and return a PR object with auto-complete set', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const prResult = {
|
|
pullRequestId: 456,
|
|
title: 'The Title',
|
|
createdBy: {
|
|
id: '123',
|
|
},
|
|
};
|
|
const prUpdateResult = {
|
|
...prResult,
|
|
autoCompleteSetBy: {
|
|
id: prResult.createdBy.id,
|
|
},
|
|
completionOptions: {
|
|
mergeStrategy: GitPullRequestMergeStrategy.Squash,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'The Title',
|
|
},
|
|
};
|
|
const updateFn = vi.fn().mockResolvedValue(prUpdateResult);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn().mockResolvedValue(prResult),
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
updatePullRequest: updateFn,
|
|
}),
|
|
);
|
|
const pr = await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'dev',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
platformPrOptions: { usePlatformAutomerge: true },
|
|
});
|
|
expect(updateFn).toHaveBeenCalled();
|
|
expect(pr).toEqual({
|
|
autoCompleteSetBy: {
|
|
id: '123',
|
|
},
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
completionOptions: {
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'The Title',
|
|
mergeStrategy: GitPullRequestMergeStrategy.Squash,
|
|
},
|
|
createdBy: {
|
|
id: '123',
|
|
},
|
|
number: 456,
|
|
pullRequestId: 456,
|
|
state: 'open',
|
|
title: 'The Title',
|
|
});
|
|
});
|
|
|
|
it('should only call getMergeMethod once per run when automergeStrategy is auto', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const prResult = [
|
|
{
|
|
pullRequestId: 456,
|
|
title: 'The Title',
|
|
createdBy: {
|
|
id: '123',
|
|
},
|
|
},
|
|
{
|
|
pullRequestId: 457,
|
|
title: 'The Second Title',
|
|
createdBy: {
|
|
id: '123',
|
|
},
|
|
},
|
|
];
|
|
const prUpdateResults = [
|
|
{
|
|
...prResult[0],
|
|
autoCompleteSetBy: {
|
|
id: prResult[0].createdBy.id,
|
|
},
|
|
completionOptions: {
|
|
mergeStrategy: GitPullRequestMergeStrategy.Squash,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'The Title',
|
|
},
|
|
},
|
|
{
|
|
...prResult[1],
|
|
autoCompleteSetBy: {
|
|
id: prResult[1].createdBy.id,
|
|
},
|
|
completionOptions: {
|
|
mergeStrategy: GitPullRequestMergeStrategy.Squash,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'The Second Title',
|
|
},
|
|
},
|
|
];
|
|
const updateFn = vi.fn(() => Promise.resolve(prUpdateResults.shift()!));
|
|
|
|
azureHelper.getMergeMethod.mockResolvedValueOnce(
|
|
GitPullRequestMergeStrategy.Squash,
|
|
);
|
|
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn(() => Promise.resolve(prResult.shift()!)),
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
updatePullRequest: updateFn,
|
|
}),
|
|
);
|
|
await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'dev',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
platformPrOptions: {
|
|
automergeStrategy: 'auto',
|
|
usePlatformAutomerge: true,
|
|
},
|
|
});
|
|
|
|
await azure.createPr({
|
|
sourceBranch: 'some-other-branch',
|
|
targetBranch: 'dev',
|
|
prTitle: 'The Second Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
platformPrOptions: {
|
|
automergeStrategy: 'auto',
|
|
usePlatformAutomerge: true,
|
|
},
|
|
});
|
|
|
|
expect(updateFn).toHaveBeenCalledTimes(2);
|
|
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it.each`
|
|
automergeStrategy
|
|
${'fast-forward'}
|
|
${'merge-commit'}
|
|
${'rebase'}
|
|
${'rebase-merge'}
|
|
${'squash'}
|
|
`(
|
|
'should not call getMergeMethod when automergeStrategy is $automergeStrategy',
|
|
async (automergeStrategy) => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const prResult = {
|
|
pullRequestId: 123,
|
|
title: 'The Title',
|
|
createdBy: {
|
|
id: '123',
|
|
},
|
|
};
|
|
const prUpdateResults = {
|
|
...prResult,
|
|
autoCompleteSetBy: {
|
|
id: prResult.createdBy.id,
|
|
},
|
|
completionOptions: {
|
|
mergeStrategy: GitPullRequestMergeStrategy.Squash,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'The Title',
|
|
},
|
|
};
|
|
const updateFn = vi.fn(() => Promise.resolve(prUpdateResults));
|
|
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn(() => Promise.resolve(prResult)),
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
updatePullRequest: updateFn,
|
|
}),
|
|
);
|
|
await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'dev',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
platformPrOptions: {
|
|
automergeStrategy,
|
|
usePlatformAutomerge: true,
|
|
},
|
|
});
|
|
|
|
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(0);
|
|
},
|
|
);
|
|
|
|
it.each`
|
|
automergeStrategy | prMergeStrategy
|
|
${'fast-forward'} | ${GitPullRequestMergeStrategy.Rebase}
|
|
${'merge-commit'} | ${GitPullRequestMergeStrategy.NoFastForward}
|
|
${'rebase'} | ${GitPullRequestMergeStrategy.Rebase}
|
|
${'rebase-merge'} | ${GitPullRequestMergeStrategy.RebaseMerge}
|
|
${'squash'} | ${GitPullRequestMergeStrategy.Squash}
|
|
`(
|
|
'should create PR with mergeStrategy $prMergeStrategy',
|
|
async ({ automergeStrategy, prMergeStrategy }) => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const prResult = {
|
|
pullRequestId: 456,
|
|
title: 'The Title',
|
|
createdBy: {
|
|
id: '123',
|
|
},
|
|
};
|
|
const prUpdateResult = {
|
|
...prResult,
|
|
autoCompleteSetBy: {
|
|
id: prResult.createdBy.id,
|
|
},
|
|
completionOptions: {
|
|
mergeStrategy: prMergeStrategy,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'The Title',
|
|
},
|
|
};
|
|
const updateFn = vi.fn().mockResolvedValue(prUpdateResult);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn().mockResolvedValue(prResult),
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
updatePullRequest: updateFn,
|
|
}),
|
|
);
|
|
const pr = await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'dev',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
platformPrOptions: {
|
|
automergeStrategy,
|
|
usePlatformAutomerge: true,
|
|
},
|
|
});
|
|
|
|
expect((pr as GitPullRequest).completionOptions?.mergeStrategy).toBe(
|
|
prMergeStrategy,
|
|
);
|
|
expect(updateFn).toHaveBeenCalled();
|
|
expect(
|
|
updateFn.mock.calls[0][0].completionOptions.mergeStrategy,
|
|
).toBe(prMergeStrategy);
|
|
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(0);
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should create and return an approved PR object', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const prResult = {
|
|
pullRequestId: 456,
|
|
createdBy: {
|
|
id: 123,
|
|
url: 'user-url',
|
|
},
|
|
};
|
|
const prUpdateResult = {
|
|
reviewerUrl: prResult.createdBy.url,
|
|
vote: AzurePrVote.Approved,
|
|
isFlagged: false,
|
|
isRequired: false,
|
|
};
|
|
const updateFn = vi
|
|
.fn()
|
|
.mockResolvedValue(prUpdateResult)
|
|
.mockName('createPullRequestReviewer');
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn().mockResolvedValue(prResult),
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
createPullRequestReviewer: updateFn,
|
|
}),
|
|
);
|
|
const pr = await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'dev',
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
labels: ['deps', 'renovate'],
|
|
platformPrOptions: { autoApprove: true },
|
|
});
|
|
expect(updateFn).toHaveBeenCalled();
|
|
expect(pr).toEqual({
|
|
bodyStruct: {
|
|
hash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
|
|
},
|
|
createdBy: {
|
|
id: 123,
|
|
url: 'user-url',
|
|
},
|
|
number: 456,
|
|
pullRequestId: 456,
|
|
state: 'open',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updatePr(prNo, title, body, platformPrOptions)', () => {
|
|
it('should update the PR', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const updatePullRequest = vi.fn().mockResolvedValue({});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
updatePullRequest,
|
|
}),
|
|
);
|
|
await azure.updatePr({
|
|
number: 1234,
|
|
prTitle: 'The New Title',
|
|
prBody: 'Hello world again',
|
|
targetBranch: 'new_base',
|
|
});
|
|
expect(updatePullRequest).toHaveBeenCalledTimes(1);
|
|
expect(updatePullRequest).toHaveBeenCalledWith(
|
|
{
|
|
description: 'Hello world again',
|
|
targetRefName: 'refs/heads/new_base',
|
|
title: 'The New Title',
|
|
},
|
|
'1',
|
|
1234,
|
|
);
|
|
});
|
|
|
|
it('should update the PR including cache', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
createPullRequest: vi.fn().mockResolvedValue({
|
|
pullRequestId: 456,
|
|
title: 'Title 1',
|
|
}),
|
|
|
|
createPullRequestLabel: vi.fn().mockResolvedValue({}),
|
|
getPullRequests: vi.fn().mockResolvedValue([]),
|
|
|
|
updatePullRequest: vi.fn().mockResolvedValue({
|
|
pullRequestId: 456,
|
|
title: 'Title 2',
|
|
}),
|
|
}),
|
|
);
|
|
await expect(azure.getPrList()).resolves.toEqual([]);
|
|
const createdPr = await azure.createPr({
|
|
sourceBranch: 'some-branch',
|
|
targetBranch: 'master',
|
|
prTitle: 'Title 1',
|
|
prBody: 'Hello world',
|
|
labels: [],
|
|
});
|
|
expect(createdPr).toMatchObject({ number: 456, title: 'Title 1' });
|
|
await expect(azure.getPrList()).resolves.toHaveLength(1);
|
|
await azure.updatePr({
|
|
number: 456,
|
|
prTitle: 'Title 2',
|
|
});
|
|
const prList = await azure.getPrList();
|
|
expect(prList).toHaveLength(1);
|
|
expect(prList[0]).toMatchObject({ number: 456, title: 'Title 2' });
|
|
});
|
|
|
|
it('should update the PR without description', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const updatePullRequest = vi.fn().mockResolvedValue({});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
updatePullRequest,
|
|
}),
|
|
);
|
|
await azure.updatePr({
|
|
number: 1234,
|
|
prTitle: 'The New Title - autoclose',
|
|
});
|
|
expect(updatePullRequest).toHaveBeenCalledTimes(1);
|
|
expect(updatePullRequest).toHaveBeenCalledWith(
|
|
{
|
|
title: 'The New Title - autoclose',
|
|
},
|
|
'1',
|
|
1234,
|
|
);
|
|
});
|
|
|
|
it('should close the PR', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const updatePullRequest = vi.fn().mockResolvedValue({});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
updatePullRequest,
|
|
}),
|
|
);
|
|
await azure.updatePr({
|
|
number: 1234,
|
|
prTitle: 'The New Title',
|
|
prBody: 'Hello world again',
|
|
state: 'closed',
|
|
});
|
|
expect(updatePullRequest).toHaveBeenCalledTimes(1);
|
|
expect(updatePullRequest).toHaveBeenCalledWith(
|
|
{
|
|
description: 'Hello world again',
|
|
status: PullRequestStatus.Abandoned,
|
|
title: 'The New Title',
|
|
},
|
|
'1',
|
|
1234,
|
|
);
|
|
});
|
|
|
|
it('should reopen the PR', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const updatePullRequest = vi.fn().mockResolvedValue({});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
updatePullRequest,
|
|
}),
|
|
);
|
|
await azure.updatePr({
|
|
number: 1234,
|
|
prTitle: 'The New Title',
|
|
prBody: 'Hello world again',
|
|
state: 'open',
|
|
});
|
|
expect(updatePullRequest.mock.calls).toEqual([
|
|
[
|
|
{
|
|
status: PullRequestStatus.Active,
|
|
},
|
|
'1',
|
|
1234,
|
|
],
|
|
[
|
|
{
|
|
description: 'Hello world again',
|
|
title: 'The New Title',
|
|
},
|
|
'1',
|
|
1234,
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should re-approve the PR', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const prResult = {
|
|
pullRequestId: 456,
|
|
createdBy: {
|
|
id: 123,
|
|
url: 'user-url',
|
|
},
|
|
};
|
|
const prUpdateResult = {
|
|
reviewerUrl: prResult.createdBy.url,
|
|
vote: AzurePrVote.Approved,
|
|
isFlagged: false,
|
|
isRequired: false,
|
|
};
|
|
const updateFn = vi.fn().mockResolvedValue(prUpdateResult);
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
updatePullRequest: vi.fn().mockResolvedValue(prResult),
|
|
createPullRequestReviewer: updateFn,
|
|
|
|
getPullRequestById: vi.fn().mockResolvedValue({
|
|
pullRequestId: prResult.pullRequestId,
|
|
createdBy: prResult.createdBy,
|
|
}),
|
|
}),
|
|
);
|
|
const pr = await azure.updatePr({
|
|
number: prResult.pullRequestId,
|
|
prTitle: 'The Title',
|
|
prBody: 'Hello world',
|
|
platformPrOptions: { autoApprove: true },
|
|
});
|
|
expect(updateFn).toHaveBeenCalled();
|
|
expect(pr).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('ensureComment', () => {
|
|
it('adds comment if missing', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const gitApiMock = {
|
|
createThread: vi.fn(() => [{ id: 123 }]),
|
|
getThreads: vi.fn().mockReturnValue([
|
|
{
|
|
comments: [{ content: 'end-user comment', id: 1 }],
|
|
id: 2,
|
|
},
|
|
]),
|
|
updateComment: vi.fn(() => ({ id: 123 })),
|
|
};
|
|
azureApi.gitApi.mockImplementation(() => gitApiMock as any);
|
|
await azure.ensureComment({
|
|
number: 42,
|
|
topic: 'some-subject',
|
|
content: 'some\ncontent',
|
|
});
|
|
expect(gitApiMock.createThread).toHaveBeenCalledTimes(1);
|
|
expect(gitApiMock.createThread).toHaveBeenCalledWith(
|
|
{
|
|
comments: [
|
|
{
|
|
commentType: 1,
|
|
content: '### some-subject\n\nsome\ncontent',
|
|
parentCommentId: 0,
|
|
},
|
|
],
|
|
status: 1,
|
|
},
|
|
'1',
|
|
42,
|
|
);
|
|
expect(gitApiMock.updateComment).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('updates comment if missing', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const gitApiMock = {
|
|
createThread: vi.fn(() => [{ id: 123 }]),
|
|
getThreads: vi.fn().mockReturnValue([
|
|
{
|
|
comments: [{ content: 'end-user comment', id: 1 }],
|
|
id: 3,
|
|
},
|
|
{
|
|
comments: [{ content: '### some-subject\n\nsome\ncontent', id: 2 }],
|
|
id: 4,
|
|
},
|
|
]),
|
|
updateComment: vi.fn(() => ({ id: 123 })),
|
|
};
|
|
azureApi.gitApi.mockImplementation(() => gitApiMock as any);
|
|
await azure.ensureComment({
|
|
number: 42,
|
|
topic: 'some-subject',
|
|
content: 'some\nnew\ncontent',
|
|
});
|
|
expect(gitApiMock.createThread).not.toHaveBeenCalled();
|
|
expect(gitApiMock.updateComment).toHaveBeenCalledTimes(1);
|
|
expect(gitApiMock.updateComment).toHaveBeenCalledWith(
|
|
{
|
|
content: '### some-subject\n\nsome\nnew\ncontent',
|
|
},
|
|
'1',
|
|
42,
|
|
4,
|
|
2,
|
|
);
|
|
});
|
|
|
|
it('does nothing if comment exists and is the same', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const gitApiMock = {
|
|
createThread: vi.fn(() => [{ id: 123 }]),
|
|
getThreads: vi.fn().mockReturnValue([
|
|
{
|
|
comments: [{ content: 'end-user comment', id: 1 }],
|
|
id: 3,
|
|
},
|
|
{
|
|
comments: [{ content: '### some-subject\n\nsome\ncontent', id: 2 }],
|
|
id: 4,
|
|
},
|
|
]),
|
|
updateComment: vi.fn(() => ({ id: 123 })),
|
|
};
|
|
azureApi.gitApi.mockImplementation(() => gitApiMock as any);
|
|
await azure.ensureComment({
|
|
number: 42,
|
|
topic: 'some-subject',
|
|
content: 'some\ncontent',
|
|
});
|
|
expect(gitApiMock.createThread).not.toHaveBeenCalled();
|
|
expect(gitApiMock.updateComment).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does nothing if comment exists and is the same when there is no topic', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const gitApiMock = {
|
|
createThread: vi.fn(() => [{ id: 123 }]),
|
|
getThreads: vi.fn().mockReturnValue([
|
|
{
|
|
comments: [{ content: 'some\ncontent', id: 2 }],
|
|
id: 4,
|
|
},
|
|
]),
|
|
updateComment: vi.fn(() => ({ id: 123 })),
|
|
};
|
|
azureApi.gitApi.mockImplementation(() => gitApiMock as any);
|
|
await azure.ensureComment({
|
|
number: 42,
|
|
topic: null,
|
|
content: 'some\ncontent',
|
|
});
|
|
expect(gitApiMock.createThread).not.toHaveBeenCalled();
|
|
expect(gitApiMock.updateComment).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('passes comment through massageMarkdown', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const gitApiMock = {
|
|
createThread: vi.fn().mockResolvedValue([{ id: 123 }]),
|
|
getThreads: vi.fn().mockResolvedValue([
|
|
{
|
|
comments: [{ content: 'end-user comment', id: 1 }],
|
|
id: 2,
|
|
},
|
|
]),
|
|
updateComment: vi.fn().mockResolvedValue({ id: 123 }),
|
|
};
|
|
azureApi.gitApi.mockResolvedValue(partial<IGitApi>(gitApiMock));
|
|
|
|
await azure.ensureComment({
|
|
number: 42,
|
|
topic: 'some-subject',
|
|
content:
|
|
'You can manually request rebase by checking the rebase/retry box above.',
|
|
});
|
|
|
|
const commentContent = gitApiMock.createThread.mock.calls[0];
|
|
expect(JSON.stringify(commentContent)).not.toContain(
|
|
'checking the rebase/retry box above',
|
|
);
|
|
expect(JSON.stringify(commentContent)).toContain(
|
|
'renaming the PR to start with \\"rebase!\\"',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('ensureCommentRemoval', () => {
|
|
// TODO: fix types #22198
|
|
let gitApiMock: any;
|
|
|
|
beforeEach(() => {
|
|
gitApiMock = {
|
|
getThreads: vi.fn(() => [
|
|
{
|
|
comments: [{ content: '### some-subject\n\nblabla' }],
|
|
id: 123,
|
|
},
|
|
{
|
|
comments: [{ content: 'some-content\n' }],
|
|
id: 124,
|
|
},
|
|
]),
|
|
updateThread: vi.fn(),
|
|
};
|
|
azureApi.gitApi.mockImplementation(() => gitApiMock);
|
|
});
|
|
|
|
it('deletes comment by topic if found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
await azure.ensureCommentRemoval({
|
|
type: 'by-topic',
|
|
number: 42,
|
|
topic: 'some-subject',
|
|
});
|
|
expect(gitApiMock.getThreads).toHaveBeenCalledExactlyOnceWith('1', 42);
|
|
expect(gitApiMock.updateThread).toHaveBeenCalledExactlyOnceWith(
|
|
{ status: 4 },
|
|
'1',
|
|
42,
|
|
123,
|
|
);
|
|
});
|
|
|
|
it('deletes comment by content if found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
await azure.ensureCommentRemoval({
|
|
type: 'by-content',
|
|
number: 42,
|
|
content: 'some-content',
|
|
});
|
|
expect(gitApiMock.getThreads).toHaveBeenCalledExactlyOnceWith('1', 42);
|
|
expect(gitApiMock.updateThread).toHaveBeenCalledExactlyOnceWith(
|
|
{ status: 4 },
|
|
'1',
|
|
42,
|
|
124,
|
|
);
|
|
});
|
|
|
|
it('comment not found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
await azure.ensureCommentRemoval({
|
|
type: 'by-topic',
|
|
number: 42,
|
|
topic: 'does-not-exist',
|
|
});
|
|
expect(gitApiMock.getThreads).toHaveBeenCalledExactlyOnceWith('1', 42);
|
|
expect(gitApiMock.updateThread).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Assignees', () => {
|
|
it('addAssignees', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getRepositories: vi
|
|
.fn()
|
|
.mockResolvedValue([{ id: '1', project: { id: 2 } }]),
|
|
createThread: vi.fn().mockResolvedValue([{ id: 123 }]),
|
|
getThreads: vi.fn().mockResolvedValue([]),
|
|
}),
|
|
);
|
|
azureApi.coreApi.mockResolvedValue(
|
|
partial<ICoreApi>({
|
|
getTeamMembersWithExtendedProperties: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
{ identity: { displayName: 'jyc', uniqueName: 'jyc', id: 123 } },
|
|
]),
|
|
}),
|
|
);
|
|
azureHelper.getAllProjectTeams = vi.fn().mockReturnValue([
|
|
{ id: 3, name: 'abc' },
|
|
{ id: 4, name: 'def' },
|
|
]);
|
|
await azure.addAssignees(123, ['test@bonjour.fr', 'jyc', 'def']);
|
|
expect(azureApi.gitApi).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
describe('Reviewers', () => {
|
|
it('addReviewers one valid', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getRepositories: vi
|
|
.fn()
|
|
.mockResolvedValue([{ id: '1', project: { id: 2 } }]),
|
|
createPullRequestReviewer: vi.fn(),
|
|
}),
|
|
);
|
|
azureApi.coreApi.mockResolvedValue(
|
|
partial<ICoreApi>({
|
|
getTeamMembersWithExtendedProperties: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
{ identity: { displayName: 'jyc', uniqueName: 'jyc', id: 123 } },
|
|
]),
|
|
}),
|
|
);
|
|
azureHelper.getAllProjectTeams = vi.fn().mockReturnValue([
|
|
{ id: 3, name: 'abc' },
|
|
{ id: 4, name: 'def' },
|
|
]);
|
|
await azure.addReviewers(123, ['test@bonjour.fr', 'jyc', 'required:def']);
|
|
expect(azureApi.gitApi).toHaveBeenCalledTimes(3);
|
|
expect(logger.once.info).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('addReviewers all valid', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getRepositories: vi
|
|
.fn()
|
|
.mockResolvedValue([{ id: '1', project: { id: 2 } }]),
|
|
createPullRequestReviewer: vi.fn(),
|
|
}),
|
|
);
|
|
azureApi.coreApi.mockResolvedValue(
|
|
partial<ICoreApi>({
|
|
getTeamMembersWithExtendedProperties: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
{ identity: { displayName: 'jyc', uniqueName: 'jyc', id: 123 } },
|
|
]),
|
|
}),
|
|
);
|
|
azureHelper.getAllProjectTeams = vi.fn().mockReturnValue([
|
|
{ id: 3, name: 'abc' },
|
|
{ id: 4, name: 'def' },
|
|
]);
|
|
await azure.addReviewers(123, ['required:jyc']);
|
|
expect(azureApi.gitApi).toHaveBeenCalledTimes(3);
|
|
expect(logger.once.info).toHaveBeenCalledTimes(0);
|
|
});
|
|
});
|
|
|
|
describe('massageMarkdown(input)', () => {
|
|
it('returns updated pr body', () => {
|
|
const prBody =
|
|
'\n---\n\n - [ ] <!-- rebase-check --> rebase\n<!--renovate-config-hash:-->' +
|
|
'plus also [a link](https://github.com/foo/bar/issues/5)';
|
|
expect(azure.massageMarkdown(prBody)).toBe(
|
|
'plus also [a link](https://github.com/foo/bar/issues/5)',
|
|
);
|
|
});
|
|
|
|
it('returns updated comment content', () => {
|
|
const commentContent =
|
|
'You can manually request rebase by checking the rebase/retry box above.\n\n' +
|
|
'plus also [a link](https://github.com/foo/bar/issues/5)';
|
|
expect(azure.massageMarkdown(commentContent)).toBe(
|
|
'You can manually request rebase by renaming the PR to start with "rebase!".\n\nplus also [a link](https://github.com/foo/bar/issues/5)',
|
|
);
|
|
});
|
|
|
|
it('converts standalone PR references to Azure format', () => {
|
|
expect(azure.massageMarkdown('see #123 and (#456)')).toBe(
|
|
'see !123 and (!456)',
|
|
);
|
|
});
|
|
|
|
it('does not corrupt HTML entities or URL anchors', () => {
|
|
const input =
|
|
'[#​32124](https://github.com/org/repo/issues/32124) ' +
|
|
'[`v4.78.0`](https://github.com/org/repo/blob/HEAD/CHANGELOG.md#4780-june-18-2026)';
|
|
expect(azure.massageMarkdown(input)).toBe(input);
|
|
});
|
|
});
|
|
|
|
describe('setBranchStatus', () => {
|
|
it('should build and call the create status api properly', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const createCommitStatusMock = vi.fn();
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
createCommitStatus: createCommitStatusMock,
|
|
}),
|
|
);
|
|
await azure.setBranchStatus({
|
|
branchName: 'test',
|
|
context: 'test',
|
|
description: 'test',
|
|
state: 'yellow',
|
|
url: 'test.com',
|
|
});
|
|
expect(createCommitStatusMock).toHaveBeenCalledExactlyOnceWith(
|
|
{
|
|
context: {
|
|
genre: undefined,
|
|
name: 'test',
|
|
},
|
|
description: 'test',
|
|
state: GitStatusState.Pending,
|
|
targetUrl: 'test.com',
|
|
},
|
|
'abcd1234',
|
|
'1',
|
|
);
|
|
});
|
|
|
|
it('should build and call the create status api properly with a complex context', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const createCommitStatusMock = vi.fn();
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getBranch: vi
|
|
.fn()
|
|
.mockResolvedValue({ commit: { commitId: 'abcd1234' } }),
|
|
createCommitStatus: createCommitStatusMock,
|
|
}),
|
|
);
|
|
await azure.setBranchStatus({
|
|
branchName: 'test',
|
|
context: 'renovate/artifact/test',
|
|
description: 'test',
|
|
state: 'green',
|
|
url: 'test.com',
|
|
});
|
|
expect(createCommitStatusMock).toHaveBeenCalledExactlyOnceWith(
|
|
{
|
|
context: {
|
|
genre: 'renovate/artifact',
|
|
name: 'test',
|
|
},
|
|
description: 'test',
|
|
state: GitStatusState.Succeeded,
|
|
targetUrl: 'test.com',
|
|
},
|
|
'abcd1234',
|
|
'1',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('mergePr', () => {
|
|
it('should not complete the PR if there are pending blocking policy evaluations', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const updatePullRequestMock = vi.fn();
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: vi.fn(),
|
|
updatePullRequest: updatePullRequestMock,
|
|
}),
|
|
);
|
|
const pendingEvaluationMock = partial<PolicyEvaluationRecord>({
|
|
configuration: partial<PolicyConfiguration>({
|
|
isBlocking: true,
|
|
type: partial<PolicyTypeRef>({
|
|
displayName: 'Minimum number of reviewers',
|
|
}),
|
|
}),
|
|
status: PolicyEvaluationStatus.Running,
|
|
});
|
|
azureHelper.getPolicyEvaluations.mockResolvedValueOnce([
|
|
pendingEvaluationMock,
|
|
]);
|
|
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
strategy: 'auto',
|
|
});
|
|
|
|
expect(logger.debug).toHaveBeenCalledWith(
|
|
{
|
|
pullRequestId: pullRequestIdMock,
|
|
artifactId: `vstfs:///CodeReview/CodeReviewId/undefined/${pullRequestIdMock}`,
|
|
policyEvaluations: [pendingEvaluationMock],
|
|
},
|
|
'Retrieved policy evaluations for PR',
|
|
);
|
|
expect(logger.debug).toHaveBeenCalledWith(
|
|
{
|
|
pullRequestId: pullRequestIdMock,
|
|
pendingPolicies: [
|
|
{ name: 'Minimum number of reviewers', status: 'Running' },
|
|
],
|
|
},
|
|
'Not completing PR because branch policies have not been satisfied yet',
|
|
);
|
|
expect(updatePullRequestMock).not.toHaveBeenCalled();
|
|
expect(res).toBeFalse();
|
|
});
|
|
|
|
it('should complete the PR if blocking policies have been approved or are not applicable', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
|
|
const updatePullRequestMock = vi.fn().mockResolvedValue({
|
|
status: 3,
|
|
});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
targetRefName: 'refs/heads/ding',
|
|
title: 'title',
|
|
}),
|
|
updatePullRequest: updatePullRequestMock,
|
|
}),
|
|
);
|
|
azureHelper.getMergeMethod = vi
|
|
.fn()
|
|
.mockReturnValue(GitPullRequestMergeStrategy.Squash);
|
|
azureHelper.getPolicyEvaluations.mockResolvedValueOnce([
|
|
partial<PolicyEvaluationRecord>({
|
|
configuration: partial<PolicyConfiguration>({ isBlocking: true }),
|
|
status: PolicyEvaluationStatus.Approved,
|
|
}),
|
|
partial<PolicyEvaluationRecord>({
|
|
configuration: partial<PolicyConfiguration>({ isBlocking: true }),
|
|
status: PolicyEvaluationStatus.NotApplicable,
|
|
}),
|
|
partial<PolicyEvaluationRecord>({
|
|
configuration: partial<PolicyConfiguration>({ isBlocking: false }),
|
|
status: PolicyEvaluationStatus.Running,
|
|
}),
|
|
]);
|
|
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
strategy: 'auto',
|
|
});
|
|
|
|
expect(updatePullRequestMock).toHaveBeenCalledOnce();
|
|
expect(res).toBeTrue();
|
|
});
|
|
|
|
it('should complete the PR', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
|
|
const updatePullRequestMock = vi.fn().mockResolvedValue({
|
|
status: 3,
|
|
});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
targetRefName: 'refs/heads/ding',
|
|
title: 'title',
|
|
}),
|
|
|
|
updatePullRequest: updatePullRequestMock,
|
|
}),
|
|
);
|
|
|
|
azureHelper.getMergeMethod = vi
|
|
.fn()
|
|
.mockReturnValue(GitPullRequestMergeStrategy.Squash);
|
|
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
strategy: 'auto',
|
|
});
|
|
|
|
expect(updatePullRequestMock).toHaveBeenCalledExactlyOnceWith(
|
|
{
|
|
status: PullRequestStatus.Completed,
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
completionOptions: {
|
|
mergeStrategy: GitPullRequestMergeStrategy.Squash,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'title',
|
|
},
|
|
},
|
|
'1',
|
|
pullRequestIdMock,
|
|
);
|
|
expect(res).toBeTrue();
|
|
});
|
|
|
|
it.each`
|
|
automergeStrategy | prMergeStrategy
|
|
${'fast-forward'} | ${GitPullRequestMergeStrategy.Rebase}
|
|
${'merge-commit'} | ${GitPullRequestMergeStrategy.NoFastForward}
|
|
${'rebase'} | ${GitPullRequestMergeStrategy.Rebase}
|
|
${'rebase-merge'} | ${GitPullRequestMergeStrategy.RebaseMerge}
|
|
${'squash'} | ${GitPullRequestMergeStrategy.Squash}
|
|
`(
|
|
'should complete PR with mergeStrategy $prMergeStrategy',
|
|
async ({ automergeStrategy, prMergeStrategy }) => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
|
|
const updatePullRequestMock = vi.fn().mockResolvedValue({
|
|
status: 3,
|
|
});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
targetRefName: 'refs/heads/ding',
|
|
title: 'title',
|
|
}),
|
|
|
|
updatePullRequest: updatePullRequestMock,
|
|
}),
|
|
);
|
|
|
|
azureHelper.getMergeMethod = vi.fn().mockReturnValue(prMergeStrategy);
|
|
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
strategy: automergeStrategy,
|
|
});
|
|
|
|
expect(updatePullRequestMock).toHaveBeenCalledExactlyOnceWith(
|
|
{
|
|
status: PullRequestStatus.Completed,
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
completionOptions: {
|
|
mergeStrategy: prMergeStrategy,
|
|
deleteSourceBranch: true,
|
|
mergeCommitMessage: 'title',
|
|
},
|
|
},
|
|
'1',
|
|
pullRequestIdMock,
|
|
);
|
|
expect(res).toBeTrue();
|
|
},
|
|
);
|
|
|
|
it('should return false if the PR does not update successfully', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
}),
|
|
|
|
updatePullRequest: vi
|
|
.fn()
|
|
.mockRejectedValue(new Error(`oh no pr couldn't be updated`)),
|
|
}),
|
|
);
|
|
|
|
azureHelper.getMergeMethod = vi
|
|
.fn()
|
|
.mockReturnValue(GitPullRequestMergeStrategy.Squash);
|
|
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
});
|
|
expect(res).toBeFalse();
|
|
});
|
|
|
|
it('should cache the mergeMethod for subsequent merges', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValue(
|
|
partial<IGitApi>({
|
|
getPullRequestById: vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: { commitId: 'abcd1234' },
|
|
targetRefName: 'refs/heads/ding',
|
|
}),
|
|
|
|
updatePullRequest: vi.fn(),
|
|
}),
|
|
);
|
|
azureHelper.getMergeMethod = vi
|
|
.fn()
|
|
.mockReturnValue(GitPullRequestMergeStrategy.Squash);
|
|
|
|
await azure.mergePr({
|
|
branchName: 'test-branch-1',
|
|
id: 1234,
|
|
strategy: 'auto',
|
|
});
|
|
await azure.mergePr({
|
|
branchName: 'test-branch-2',
|
|
id: 5678,
|
|
strategy: 'auto',
|
|
});
|
|
|
|
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should refetch the PR if the update response has not yet been set to completed', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
|
|
const getPullRequestByIdMock = vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
targetRefName: 'refs/heads/ding',
|
|
status: 3,
|
|
});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: getPullRequestByIdMock,
|
|
|
|
updatePullRequest: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
}),
|
|
}),
|
|
);
|
|
azureHelper.getMergeMethod = vi
|
|
.fn()
|
|
.mockReturnValue(GitPullRequestMergeStrategy.Squash);
|
|
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
});
|
|
expect(getPullRequestByIdMock).toHaveBeenCalledTimes(2);
|
|
expect(res).toBeTrue();
|
|
});
|
|
|
|
it('should log a warning after retrying if the PR has still not yet been set to completed', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const pullRequestIdMock = 12345;
|
|
const branchNameMock = 'test';
|
|
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
|
|
const expectedNumRetries = 5;
|
|
const getPullRequestByIdMock = vi.fn().mockResolvedValue({
|
|
lastMergeSourceCommit: lastMergeSourceCommitMock,
|
|
targetRefName: 'refs/heads/ding',
|
|
status: 1,
|
|
});
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getPullRequestById: getPullRequestByIdMock,
|
|
|
|
updatePullRequest: vi.fn().mockResolvedValue({
|
|
status: 1,
|
|
}),
|
|
}),
|
|
);
|
|
azureHelper.getMergeMethod = vi
|
|
.fn()
|
|
.mockReturnValue(GitPullRequestMergeStrategy.Squash);
|
|
const res = await azure.mergePr({
|
|
branchName: branchNameMock,
|
|
id: pullRequestIdMock,
|
|
});
|
|
expect(getPullRequestByIdMock).toHaveBeenCalledTimes(
|
|
expectedNumRetries + 1,
|
|
);
|
|
expect(logger.warn).toHaveBeenCalled();
|
|
expect(res).toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('deleteLabel()', () => {
|
|
it('Should delete a label', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
deletePullRequestLabels: vi.fn(),
|
|
}),
|
|
);
|
|
await azure.deleteLabel(1234, 'rebase');
|
|
expect(azureApi.gitApi.mock.calls).toEqual([[], []]);
|
|
});
|
|
});
|
|
|
|
describe('getJsonFile()', () => {
|
|
beforeEach(async () => {
|
|
await initRepo();
|
|
});
|
|
|
|
it('returns file content', async () => {
|
|
const data = { foo: 'bar' };
|
|
azureApi.gitApi.mockImplementationOnce(
|
|
vi.fn().mockImplementationOnce(() => ({
|
|
getItem: vi.fn(() =>
|
|
Promise.resolve({ content: JSON.stringify(data) }),
|
|
),
|
|
})),
|
|
);
|
|
const res = await azure.getJsonFile('file.json');
|
|
expect(res).toEqual(data);
|
|
});
|
|
|
|
it('returns null when file not found', async () => {
|
|
azureApi.gitApi.mockImplementationOnce(
|
|
vi.fn().mockImplementationOnce(() => ({
|
|
getItem: vi.fn(() => Promise.resolve(null)),
|
|
})),
|
|
);
|
|
const res = await azure.getJsonFile('file.json');
|
|
expect(res).toBeNull();
|
|
});
|
|
|
|
it('returns file content in json5 format', async () => {
|
|
const json5Data = `
|
|
{
|
|
// json5 comment
|
|
foo: 'bar'
|
|
}
|
|
`;
|
|
azureApi.gitApi.mockImplementationOnce(
|
|
vi.fn().mockImplementationOnce(() => ({
|
|
getItem: vi.fn(() => Promise.resolve({ content: json5Data })),
|
|
})),
|
|
);
|
|
const res = await azure.getJsonFile('file.json5');
|
|
expect(res).toEqual({ foo: 'bar' });
|
|
});
|
|
|
|
it('returns file content from branch or tag', async () => {
|
|
const data = { foo: 'bar' };
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getItem: vi.fn(() =>
|
|
Promise.resolve({ content: JSON.stringify(data) }),
|
|
),
|
|
}),
|
|
);
|
|
const res = await azure.getJsonFile('file.json', undefined, 'dev');
|
|
expect(res).toEqual(data);
|
|
});
|
|
|
|
it('throws on malformed JSON', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getItemContent: vi.fn(() => Promise.resolve(Readable.from('!@#'))),
|
|
}),
|
|
);
|
|
await expect(azure.getJsonFile('file.json')).rejects.toThrow(
|
|
'azureApiGit.getItem is not a function',
|
|
);
|
|
});
|
|
|
|
it('throws on errors', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getItemContent: vi.fn(() => {
|
|
throw new Error('some error');
|
|
}),
|
|
}),
|
|
);
|
|
await expect(azure.getJsonFile('file.json')).rejects.toThrow(
|
|
'azureApiGit.getItem is not a function',
|
|
);
|
|
});
|
|
|
|
it('supports fetch from another repo', async () => {
|
|
const data = { foo: 'bar' };
|
|
const getItemFn = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({ content: JSON.stringify(data) });
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getItem: getItemFn,
|
|
getRepositories: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
{ id: '123456', name: 'bar', project: { name: 'foo' } },
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.getJsonFile('file.json', 'foo/bar');
|
|
expect(res).toEqual(data);
|
|
expect(getItemFn.mock.calls).toEqual([
|
|
[
|
|
'123456',
|
|
'file.json',
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('returns null', async () => {
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getRepositories: vi.fn(() => Promise.resolve([])),
|
|
}),
|
|
);
|
|
const res = await azure.getJsonFile('file.json', 'foo/bar');
|
|
expect(res).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('getRawFile should check tag first and then return branch if tag was not found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
const callArgs: any[] = [];
|
|
const getItemMock = vi.fn(
|
|
(
|
|
repositoryId: string,
|
|
path: string,
|
|
project?: string,
|
|
scopePath?: string,
|
|
recursionLevel?: any,
|
|
includeContentMetadata?: boolean,
|
|
latestProcessedChange?: boolean,
|
|
download?: boolean,
|
|
versionDescriptor?: GitVersionDescriptor,
|
|
_includeContent?: boolean,
|
|
_resolveLfs?: boolean,
|
|
_sanitize?: boolean,
|
|
) => {
|
|
callArgs.push(versionDescriptor?.versionType);
|
|
if (versionDescriptor?.versionType === GitVersionType.Branch) {
|
|
return Promise.resolve({ content: '{ "test": "branch content" }' });
|
|
}
|
|
if (versionDescriptor?.versionType === GitVersionType.Tag) {
|
|
return Promise.resolve(null);
|
|
}
|
|
throw new Error('Unexpected call');
|
|
},
|
|
);
|
|
|
|
azureApi.gitApi.mockResolvedValueOnce(
|
|
partial<IGitApi>({
|
|
getItem: getItemMock as IGitApi['getItem'],
|
|
|
|
getRepositories: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
{ id: '123456', name: 'repo', project: { name: 'some' } },
|
|
]),
|
|
}),
|
|
);
|
|
const result = await azure.getJsonFile(
|
|
'file.json',
|
|
'some/repo',
|
|
'some-branch',
|
|
);
|
|
expect(result).toEqual({ test: 'branch content' });
|
|
expect(callArgs[0]).toBe(GitVersionType.Tag);
|
|
expect(callArgs[1]).toBe(GitVersionType.Branch);
|
|
});
|
|
|
|
it('findIssue returns null if not found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.workItemTrackingApi.mockResolvedValueOnce(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({ workItems: [] }),
|
|
}),
|
|
);
|
|
const res = await azure.findIssue('Nonexistent');
|
|
expect(res).toBeNull();
|
|
});
|
|
|
|
it('findIssue return first issue if multiple found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValueOnce(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }, { id: 456 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'The title',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Active',
|
|
'System.Description': 'fake',
|
|
},
|
|
},
|
|
{
|
|
id: 456,
|
|
fields: {
|
|
'System.Title': 'The title',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'New',
|
|
'System.Description': 'another fake description',
|
|
},
|
|
},
|
|
]),
|
|
}),
|
|
);
|
|
const res = await azure.findIssue('The title');
|
|
expect(res).toMatchObject({
|
|
number: 123,
|
|
title: 'The title',
|
|
state: 'open',
|
|
body: 'fake',
|
|
});
|
|
});
|
|
|
|
it('ensureIssue creates a new issue if none existing found', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const createWorkItemMock = vi.fn().mockResolvedValue({
|
|
id: 123,
|
|
});
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValueOnce(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({ workItems: [] }),
|
|
createWorkItem: createWorkItemMock,
|
|
getWorkItemTypes: vi.fn().mockResolvedValue([{ name: 'Issue' }]),
|
|
}),
|
|
);
|
|
const result = await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'Test body',
|
|
});
|
|
expect(result).toBe('created');
|
|
expect(createWorkItemMock).toHaveBeenCalledWith(
|
|
undefined, // no headers
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
op: 'add',
|
|
path: '/fields/System.Title',
|
|
value: 'Test Issue',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'add',
|
|
path: '/fields/System.Description',
|
|
value: 'Test body',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'add',
|
|
path: '/multilineFieldsFormat/System.Description',
|
|
value: 'Markdown',
|
|
}),
|
|
]),
|
|
'some',
|
|
'Issue',
|
|
);
|
|
});
|
|
it('ensureIssue updates an existing issue', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi.fn();
|
|
const createWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Active',
|
|
'System.Description': 'fake',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
createWorkItem: createWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
const result = await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'Updated body',
|
|
});
|
|
expect(result).toBe('updated');
|
|
expect(createWorkItemMock).not.toHaveBeenCalled();
|
|
expect(updateWorkItemMock).toHaveBeenCalledWith(
|
|
undefined, // no headers
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.Title',
|
|
value: 'Test Issue',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.Description',
|
|
value: 'Updated body',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/multilineFieldsFormat/System.Description',
|
|
value: 'Markdown',
|
|
}),
|
|
]),
|
|
123,
|
|
'some',
|
|
);
|
|
});
|
|
it('ensureIssue do not touch issues when not needed', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi.fn();
|
|
const createWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Active',
|
|
'System.Description': 'fake body',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
createWorkItem: createWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
const result = await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'fake body',
|
|
});
|
|
expect(result).toBe('updated');
|
|
expect(createWorkItemMock).not.toHaveBeenCalled();
|
|
expect(updateWorkItemMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ensureIssue do not recreate issue when once == true', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi.fn();
|
|
const createWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Closed',
|
|
'System.Description': 'fake body',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
createWorkItem: createWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
const result = await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'Updated body',
|
|
once: true,
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('ensureIssue handles Azure API error', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('Test error'));
|
|
const createWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Closed',
|
|
'System.Description': 'fake body',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
createWorkItem: createWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
const result = await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'Updated body',
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('ensureIssue updates an existing issue and reopens it if needed', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi.fn();
|
|
const createWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Closed',
|
|
'System.Description': 'fake',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
createWorkItem: createWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'Updated body',
|
|
});
|
|
expect(createWorkItemMock).not.toHaveBeenCalled();
|
|
expect(updateWorkItemMock).toHaveBeenCalledWith(
|
|
undefined, // no headers
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.Title',
|
|
value: 'Test Issue',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.Description',
|
|
value: 'Updated body',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.State',
|
|
value: 'New',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/multilineFieldsFormat/System.Description',
|
|
value: 'Markdown',
|
|
}),
|
|
]),
|
|
123,
|
|
'some',
|
|
);
|
|
});
|
|
|
|
it('ensureIssue close duplicates and updates an existing issue', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi.fn();
|
|
const createWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }, { id: 456 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Active',
|
|
'System.Description': 'fake',
|
|
},
|
|
},
|
|
{
|
|
id: 456,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'New',
|
|
'System.Description': 'another fake',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
createWorkItem: createWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
await azure.ensureIssue({
|
|
title: 'Test Issue',
|
|
body: 'Updated body',
|
|
});
|
|
expect(createWorkItemMock).not.toHaveBeenCalled();
|
|
expect(updateWorkItemMock).toHaveBeenCalledWith(
|
|
undefined, // no headers
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.Title',
|
|
value: 'Test Issue',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.Description',
|
|
value: 'Updated body',
|
|
}),
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/multilineFieldsFormat/System.Description',
|
|
value: 'Markdown',
|
|
}),
|
|
]),
|
|
123,
|
|
'some',
|
|
);
|
|
});
|
|
|
|
it('ensureIssueClosing closes an existing issue', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi.fn();
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Active',
|
|
'System.Description': 'fake',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
await azure.ensureIssueClosing('Test Issue');
|
|
expect(updateWorkItemMock).toHaveBeenCalledWith(
|
|
undefined, // no headers
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.State',
|
|
value: 'Closed',
|
|
}),
|
|
]),
|
|
123,
|
|
'some',
|
|
);
|
|
});
|
|
|
|
it('ensureIssueClosing handles Azure API error', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
|
|
const updateWorkItemMock = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('Test error'));
|
|
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [{ id: 123 }],
|
|
}),
|
|
getWorkItems: vi.fn().mockResolvedValue([
|
|
{
|
|
id: 123,
|
|
fields: {
|
|
'System.Title': 'Test Issue',
|
|
'System.WorkItemType': 'Issue',
|
|
'System.State': 'Active',
|
|
'System.Description': 'fake',
|
|
},
|
|
},
|
|
]),
|
|
updateWorkItem: updateWorkItemMock,
|
|
}),
|
|
);
|
|
|
|
await azure.ensureIssueClosing('Test Issue');
|
|
expect(updateWorkItemMock).toHaveBeenCalledWith(
|
|
undefined, // no headers
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
op: 'replace',
|
|
path: '/fields/System.State',
|
|
value: 'Closed',
|
|
}),
|
|
]),
|
|
123,
|
|
'some',
|
|
);
|
|
});
|
|
|
|
it('getIssueList handles empty wiql result', async () => {
|
|
await initRepo({ repository: 'some/repo' });
|
|
azureApi.workItemTrackingApi.mockResolvedValue(
|
|
partial<IWorkItemTrackingApi>({
|
|
queryByWiql: vi.fn().mockResolvedValue({
|
|
workItems: [],
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const result = await azure.getIssueList();
|
|
expect(result).toBeEmptyArray();
|
|
});
|
|
});
|