renovate/lib/workers/repository/config-migration/pr/index.spec.ts

274 lines
9.3 KiB
TypeScript

import type { Indent } from 'detect-indent';
import type { RequestError, Response } from 'got';
import { mock } from 'jest-mock-extended';
import { Fixtures } from '../../../../../test/fixtures';
import type { RenovateConfig } from '../../../../../test/util';
import { partial, platform, scm } from '../../../../../test/util';
import { getConfig } from '../../../../config/defaults';
import { GlobalConfig } from '../../../../config/global';
import { logger } from '../../../../logger';
import type { Pr } from '../../../../modules/platform';
import { hashBody } from '../../../../modules/platform/pr-body';
import { ConfigMigrationCommitMessageFactory } from '../branch/commit-message';
import type { MigratedData } from '../branch/migrated-data';
import { ensureConfigMigrationPr } from '.';
describe('workers/repository/config-migration/pr/index', () => {
const spy = jest.spyOn(platform, 'massageMarkdown');
const { configFileName, migratedContent } = Fixtures.getJson(
'./migrated-data.json',
);
const prTitle = new ConfigMigrationCommitMessageFactory(
{},
configFileName,
).getPrTitle();
const migratedData: MigratedData = {
content: migratedContent,
filename: configFileName,
indent: partial<Indent>(),
};
let config: RenovateConfig;
beforeEach(() => {
GlobalConfig.set({
dryRun: null,
});
config = {
...getConfig(),
configMigration: true,
defaultBranch: 'main',
description: [],
};
});
describe('ensureConfigMigrationPr()', () => {
beforeEach(() => {
spy.mockImplementation((input) => input);
platform.createPr.mockResolvedValueOnce(partial<Pr>());
});
let createPrBody: string;
let hash: string;
it('creates PR', async () => {
await ensureConfigMigrationPr(config, migratedData);
expect(platform.getBranchPr).toHaveBeenCalledTimes(1);
expect(platform.createPr).toHaveBeenCalledTimes(1);
createPrBody = platform.createPr.mock.calls[0][0].prBody;
});
it('creates PR with default PR title', async () => {
await ensureConfigMigrationPr(
{ ...config, onboardingPrTitle: '' },
migratedData,
);
expect(platform.getBranchPr).toHaveBeenCalledTimes(1);
expect(platform.createPr).toHaveBeenCalledTimes(1);
createPrBody = platform.createPr.mock.calls[0][0].prBody;
});
it('Founds an open PR and as it is up to date and returns', async () => {
hash = hashBody(createPrBody);
platform.getBranchPr.mockResolvedValueOnce(
mock<Pr>({ bodyStruct: { hash }, title: prTitle }),
);
await ensureConfigMigrationPr(config, migratedData);
expect(platform.updatePr).toHaveBeenCalledTimes(0);
expect(platform.createPr).toHaveBeenCalledTimes(0);
});
it('Founds an open PR and updates it', async () => {
platform.getBranchPr.mockResolvedValueOnce(
mock<Pr>({ bodyStruct: { hash: '' } }),
);
await ensureConfigMigrationPr(config, migratedData);
expect(platform.updatePr).toHaveBeenCalledTimes(1);
expect(platform.createPr).toHaveBeenCalledTimes(0);
});
it('updates an open PR with unexpected PR title', async () => {
hash = hashBody(createPrBody);
platform.getBranchPr.mockResolvedValueOnce(
mock<Pr>({ bodyStruct: { hash }, title: 'unexpected PR title' }),
);
await ensureConfigMigrationPr(config, migratedData);
expect(platform.updatePr).toHaveBeenCalledTimes(1);
expect(platform.updatePr.mock.calls[0][0]).toMatchObject({ prTitle });
expect(platform.createPr).toHaveBeenCalledTimes(0);
});
it('Dry runs and does not update out of date PR', async () => {
GlobalConfig.set({
dryRun: 'full',
});
platform.getBranchPr.mockResolvedValueOnce(
mock<Pr>({ bodyStruct: { hash: '' } }),
);
await ensureConfigMigrationPr(config, migratedData);
expect(platform.updatePr).toHaveBeenCalledTimes(0);
expect(platform.createPr).toHaveBeenCalledTimes(0);
expect(logger.debug).toHaveBeenCalledWith('Found open migration PR');
expect(logger.debug).not.toHaveBeenLastCalledWith(
`does not need updating`,
);
expect(logger.info).toHaveBeenLastCalledWith(
'DRY-RUN: Would update migration PR',
);
});
it('Creates PR in dry run mode', async () => {
GlobalConfig.set({
dryRun: 'full',
});
await ensureConfigMigrationPr(config, migratedData);
expect(platform.getBranchPr).toHaveBeenCalledTimes(1);
expect(platform.createPr).toHaveBeenCalledTimes(0);
expect(logger.info).toHaveBeenLastCalledWith(
'DRY-RUN: Would create migration PR',
);
});
it('creates PR with labels', async () => {
await ensureConfigMigrationPr(
{
...config,
labels: ['label'],
addLabels: ['label', 'additional-label'],
},
migratedData,
);
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].labels).toEqual([
'additional-label',
'label',
]);
});
it('creates PR with empty footer and header', async () => {
await ensureConfigMigrationPr(
{
...config,
prHeader: '',
prFooter: '',
},
migratedData,
);
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot();
});
it('creates PR for JSON5 config file', async () => {
await ensureConfigMigrationPr(config, {
content: migratedContent,
filename: 'renovate.json5',
indent: partial<Indent>(),
});
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot();
});
it('creates PR with footer and header with trailing and leading newlines', async () => {
await ensureConfigMigrationPr(
{
...config,
prHeader: '\r\r\nThis should not be the first line of the PR',
prFooter:
'There should be several empty lines at the end of the PR\r\n\n\n',
},
migratedData,
);
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot();
});
it('creates non-semantic PR title', async () => {
await ensureConfigMigrationPr(
{
...config,
prHeader: '\r\r\nThis should not be the first line of the PR',
prFooter:
'There should be several empty lines at the end of the PR\r\n\n\n',
},
migratedData,
);
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].prTitle).toBe(
'Migrate renovate config',
);
});
it('creates semantic PR title', async () => {
await ensureConfigMigrationPr(
{
...config,
commitMessagePrefix: '',
semanticCommits: 'enabled',
prHeader: '\r\r\nThis should not be the first line of the PR',
prFooter:
'There should be several empty lines at the end of the PR\r\n\n\n',
},
migratedData,
);
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].prTitle).toBe(
'chore(config): migrate renovate config',
);
});
it('creates PR with footer and header using templating', async () => {
config.baseBranch = 'some-branch';
config.repository = 'test';
await ensureConfigMigrationPr(
{
...config,
prHeader: 'This is a header for platform:{{platform}}',
prFooter:
'And this is a footer for repository:{{repository}} baseBranch:{{baseBranch}}',
},
migratedData,
);
expect(platform.createPr).toHaveBeenCalledTimes(1);
expect(platform.createPr.mock.calls[0][0].prBody).toMatch(
/platform:github/,
);
expect(platform.createPr.mock.calls[0][0].prBody).toMatch(
/repository:test/,
);
expect(platform.createPr.mock.calls[0][0].prBody).toMatch(
/baseBranch:some-branch/,
);
expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot();
});
});
describe('ensureConfigMigrationPr() throws', () => {
const response = partial<Response>({ statusCode: 422 });
const err = partial<RequestError>({ response });
beforeEach(() => {
GlobalConfig.reset();
scm.deleteBranch.mockResolvedValue();
});
it('throws when trying to create a new PR', async () => {
platform.createPr.mockRejectedValueOnce(err);
await expect(ensureConfigMigrationPr(config, migratedData)).toReject();
expect(scm.deleteBranch).toHaveBeenCalledTimes(0);
});
it('deletes branch when PR already exists but cannot find it', async () => {
response.body = {
errors: [{ message: 'A pull request already exists' }],
};
platform.createPr.mockRejectedValue(err);
await expect(ensureConfigMigrationPr(config, migratedData)).toResolve();
expect(logger.warn).toHaveBeenCalledWith(
{ err },
'Migration PR already exists but cannot find it. It was probably created by a different user.',
);
expect(scm.deleteBranch).toHaveBeenCalledTimes(1);
});
});
});