mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 18:19:51 +00:00
acb46bc706
With the new hardening on blocking internal hosts with outbound HTTP calls, we should build on top of this to ensure that the UX is better when a preset is blocked. We can do this by making sure that when `HOST_BLOCKED`, we report a more targeted config validation error. We can also make sure to pass around our `internalGrants`, similar to how we currently allow an admin's `headers` are handled. Co-authored-by: Claude Fable 5 <jamie.tanna+claude-code@mend.io> Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
1728 lines
54 KiB
TypeScript
1728 lines
54 KiB
TypeScript
import { mockDeep } from 'vitest-mock-extended';
|
|
import { Fixtures } from '~test/fixtures.ts';
|
|
import { logger } from '~test/util.ts';
|
|
import {
|
|
CONFIG_VALIDATION,
|
|
HOST_BLOCKED,
|
|
PLATFORM_RATE_LIMIT_EXCEEDED,
|
|
} from '../../constants/error-messages.ts';
|
|
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
|
|
import * as memCache from '../../util/cache/memory/index.ts';
|
|
import * as _packageCache from '../../util/cache/package/index.ts';
|
|
import { setCustomEnv } from '../../util/env.ts';
|
|
import { GlobalConfig } from '../global.ts';
|
|
import type { AllConfig } from '../types.ts';
|
|
import * as _forgejo from './forgejo/index.ts';
|
|
import * as _gitea from './gitea/index.ts';
|
|
import * as _github from './github/index.ts';
|
|
import * as _gitlab from './gitlab/index.ts';
|
|
import * as _http from './http/index.ts';
|
|
import * as presets from './index.ts';
|
|
import * as _local from './local/index.ts';
|
|
import * as _npm from './npm/index.ts';
|
|
import {
|
|
PRESET_DEP_NOT_FOUND,
|
|
PRESET_INVALID_JSON,
|
|
PRESET_NOT_FOUND,
|
|
PRESET_RENOVATE_CONFIG_NOT_FOUND,
|
|
} from './util.ts';
|
|
|
|
vi.mock('./forgejo/index.ts');
|
|
vi.mock('./gitea/index.ts');
|
|
vi.mock('./http/index.ts');
|
|
vi.mock('./npm/index.ts');
|
|
vi.mock('./github/index.ts');
|
|
vi.mock('./gitlab/index.ts');
|
|
vi.mock('./local/index.ts');
|
|
vi.mock('../../util/cache/package/index.ts', () => mockDeep());
|
|
|
|
const forgejo = vi.mocked(_forgejo);
|
|
const gitea = vi.mocked(_gitea);
|
|
const http = vi.mocked(_http);
|
|
const npm = vi.mocked(_npm);
|
|
const local = vi.mocked(_local);
|
|
const gitHub = vi.mocked(_github);
|
|
const gitLab = vi.mocked(_gitlab);
|
|
const packageCache = vi.mocked(_packageCache);
|
|
|
|
const presetIkatyang = Fixtures.getJson('renovate-config-ikatyang.json');
|
|
|
|
describe('config/presets/index', () => {
|
|
describe('resolveConfigPresets', () => {
|
|
let config: AllConfig;
|
|
|
|
beforeEach(() => {
|
|
config = {};
|
|
GlobalConfig.reset();
|
|
memCache.init();
|
|
packageCache.get.mockImplementation(
|
|
<T>(namespace: string, key: string): Promise<T> =>
|
|
Promise.resolve(memCache.get(`${namespace}-${key}`)),
|
|
);
|
|
|
|
packageCache.set.mockImplementation(
|
|
(
|
|
namespace: string,
|
|
key: string,
|
|
value: unknown,
|
|
_minutes: number,
|
|
): Promise<void> => {
|
|
memCache.set(`${namespace}-${key}`, value);
|
|
return Promise.resolve();
|
|
},
|
|
);
|
|
|
|
npm.getPreset.mockImplementation(({ repo, presetName }) => {
|
|
if (repo === 'renovate-config-ikatyang') {
|
|
return presetIkatyang.versions[presetIkatyang['dist-tags'].latest][
|
|
'renovate-config'
|
|
][presetName!];
|
|
}
|
|
if (repo === 'renovate-config-notfound') {
|
|
throw new Error(PRESET_DEP_NOT_FOUND);
|
|
}
|
|
if (repo === 'renovate-config-noconfig') {
|
|
throw new Error(PRESET_RENOVATE_CONFIG_NOT_FOUND);
|
|
}
|
|
if (repo === 'renovate-config-throw') {
|
|
throw new Error('whoops');
|
|
}
|
|
if (repo === 'renovate-config-wrongpreset') {
|
|
throw new Error(PRESET_NOT_FOUND);
|
|
}
|
|
return null;
|
|
});
|
|
});
|
|
|
|
it('returns same if no presets', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = [];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(config).toMatchObject(res);
|
|
expect(res).toEqual({ foo: 1 });
|
|
});
|
|
|
|
it('skips duplicate resolves', async () => {
|
|
config.extends = ['local>some/repo:a', 'local>some/repo:b'];
|
|
local.getPreset.mockResolvedValueOnce({ extends: ['local>some/repo:c'] });
|
|
local.getPreset.mockResolvedValueOnce({ extends: ['local>some/repo:c'] });
|
|
local.getPreset.mockResolvedValueOnce({ foo: 1 });
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
foo: 1,
|
|
});
|
|
expect(local.getPreset).toHaveBeenCalledTimes(3);
|
|
|
|
expect(logger.logger.debug).toHaveBeenCalledWith(
|
|
'Already seen preset local>some/repo:c in [local>some/repo:a, local>some/repo:c]',
|
|
);
|
|
});
|
|
|
|
it('throws if invalid preset file', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['local>some/repo'];
|
|
|
|
local.getPreset.mockResolvedValueOnce({ extends: ['notfound'] });
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe(
|
|
"Cannot find preset's package (notfound)." +
|
|
' Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.',
|
|
);
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('throws if the preset host is blocked', async () => {
|
|
config.extends = ['http://10.1.2.3/preset.json'];
|
|
http.getPreset.mockRejectedValueOnce(new Error(HOST_BLOCKED));
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationError).toBe(
|
|
'Preset host is blocked by this Renovate instance (http://10.1.2.3/preset.json). If this is intended, ask your Renovate administrator to permit it with a `hostRules` entry setting `allowInternal=true`, scoped either by `hostType` (for example `preset` or `npm`) or by a URL-prefix `matchHost`',
|
|
);
|
|
expect(logger.logger.warn).toHaveBeenCalledWith(
|
|
{
|
|
preset: 'http://10.1.2.3/preset.json',
|
|
documentationUrl: expect.stringContaining(
|
|
'self-hosted-configuration/#hostrulesallowinternal',
|
|
),
|
|
},
|
|
'Preset host is blocked by this Renovate instance',
|
|
);
|
|
});
|
|
|
|
it('throws if invalid preset', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['wrongpreset:invalid-preset'];
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe(
|
|
'Preset name not found within published preset config (wrongpreset:invalid-preset)',
|
|
);
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('throws if path + invalid syntax', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['github>user/repo//'];
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe('Preset is invalid (github>user/repo//)');
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('throws if path + sub-preset', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['github>user/repo//path:subpreset'];
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe(
|
|
'Sub-presets cannot be combined with a custom path (github>user/repo//path:subpreset)',
|
|
);
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('throws if invalid preset json', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['org/repo'];
|
|
let e: Error | undefined;
|
|
local.getPreset.mockRejectedValueOnce(new Error(PRESET_INVALID_JSON));
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe('Preset is invalid JSON (org/repo)');
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('throws noconfig', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['noconfig:recommended'];
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe(
|
|
'Preset package is missing a renovate-config entry (noconfig:recommended)',
|
|
);
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('throws throw', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['throw:base'];
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe(
|
|
'Preset caused unexpected error (throw:base)',
|
|
);
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('works with valid', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = [':pinVersions'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
description: [
|
|
'Use version pinning (maintain a single version only and not SemVer ranges).',
|
|
],
|
|
foo: 1,
|
|
rangeStrategy: 'pin',
|
|
});
|
|
expect(res.rangeStrategy).toBe('pin');
|
|
});
|
|
|
|
it('replaces preset descriptions with overrideDescription', async () => {
|
|
config.overrideDescription = ['Pin everything.'];
|
|
config.extends = [':pinVersions'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
description: ['Pin everything.'],
|
|
rangeStrategy: 'pin',
|
|
});
|
|
});
|
|
|
|
it('ignores empty overrideDescription', async () => {
|
|
config.overrideDescription = [];
|
|
config.extends = [':pinVersions'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
description: [
|
|
'Use version pinning (maintain a single version only and not SemVer ranges).',
|
|
],
|
|
rangeStrategy: 'pin',
|
|
});
|
|
});
|
|
|
|
it('throws if valid and invalid', async () => {
|
|
// @ts-expect-error -- invalid config
|
|
config.foo = 1;
|
|
config.extends = ['wrongpreset:invalid-preset', ':pinVersions'];
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.resolveConfigPresets(config);
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBe(
|
|
'Preset name not found within published preset config (wrongpreset:invalid-preset)',
|
|
);
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('resolves packageRule', async () => {
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
packageRules: [
|
|
{
|
|
groupName: 'eslint',
|
|
matchPackageNames: [
|
|
'*/eslint-plugin',
|
|
'@babel/eslint-parser',
|
|
'@eslint/**',
|
|
'@eslint-community/**',
|
|
'@stylistic/eslint-plugin**',
|
|
'@types/eslint',
|
|
'@types/eslint__**',
|
|
'@typescript-eslint/**',
|
|
'babel-eslint',
|
|
'eslint**',
|
|
'typescript-eslint',
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('resolves pin GitHub Action digests to SemVer', async () => {
|
|
config.extends = ['helpers:pinGitHubActionDigestsToSemver'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res.packageRules).toEqual([
|
|
{
|
|
matchDepTypes: ['action', 'workflow'],
|
|
pinDigests: true,
|
|
},
|
|
{
|
|
matchDepTypes: ['action', 'workflow'],
|
|
extractVersion: '^(?<version>v?\\d+\\.\\d+\\.\\d+)$',
|
|
versioning:
|
|
'regex:^v?(?<major>\\d+)(\\.(?<minor>\\d+)\\.(?<patch>\\d+))?$',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('resolves eslint', async () => {
|
|
config.extends = ['packages:eslint'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
matchPackageNames: [
|
|
'*/eslint-plugin',
|
|
'@babel/eslint-parser',
|
|
'@eslint/**',
|
|
'@eslint-community/**',
|
|
'@stylistic/eslint-plugin**',
|
|
'@types/eslint',
|
|
'@types/eslint__**',
|
|
'@typescript-eslint/**',
|
|
'babel-eslint',
|
|
'eslint**',
|
|
'typescript-eslint',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('resolves linters', async () => {
|
|
config.extends = ['packages:linters'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
description: ['All lint-related packages.'],
|
|
matchPackageNames: [
|
|
'ember-template-lint**',
|
|
'*/eslint-plugin',
|
|
'@babel/eslint-parser',
|
|
'@eslint/**',
|
|
'@eslint-community/**',
|
|
'@stylistic/eslint-plugin**',
|
|
'@types/eslint',
|
|
'@types/eslint__**',
|
|
'@typescript-eslint/**',
|
|
'babel-eslint',
|
|
'eslint**',
|
|
'typescript-eslint',
|
|
'friendsofphp/php-cs-fixer',
|
|
'squizlabs/php_codesniffer',
|
|
'symplify/easy-coding-standard',
|
|
'stylelint**',
|
|
'codelyzer',
|
|
'/\\btslint\\b/',
|
|
'@oxlint/**',
|
|
'oxlint',
|
|
'prettier',
|
|
'remark-lint',
|
|
'standard',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('resolves nested groups', async () => {
|
|
config.extends = [':automergeLinters'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res.packageRules).toHaveLength(1);
|
|
const rule = res.packageRules![0];
|
|
expect(rule.automerge).toBeTrue();
|
|
expect(rule.matchPackageNames).toHaveLength(23);
|
|
});
|
|
|
|
it('migrates automerge in presets', async () => {
|
|
config.extends = ['ikatyang:library'];
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res.automerge).toBeUndefined();
|
|
expect(res).toMatchObject({
|
|
major: { automerge: false },
|
|
minor: { automerge: true },
|
|
});
|
|
});
|
|
|
|
it('ignores presets', async () => {
|
|
config.extends = ['config:recommended'];
|
|
const { config: res } = await presets.resolveConfigPresets(config, {}, [
|
|
'config:recommended',
|
|
]);
|
|
expect(config).toMatchObject(res);
|
|
expect(res).toBeEmptyObject();
|
|
});
|
|
|
|
it('resolves self-hosted presets without baseConfig', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(local.getPreset.mock.calls).toHaveLength(1);
|
|
expect(res).toEqual({ labels: ['self-hosted resolved'] });
|
|
});
|
|
|
|
it('returns the presets which have been merged into the resulting config', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
extends: ['security:openssf-scorecard'],
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
|
|
const {
|
|
visitedPresets: { merged },
|
|
} = await presets.resolveConfigPresets(config);
|
|
|
|
expect(merged).toEqual([
|
|
'local>username/preset-repo',
|
|
'security:openssf-scorecard',
|
|
]);
|
|
});
|
|
|
|
it('de-duplicates the presets which have been meregd into the resulting config', async () => {
|
|
config.extends = [
|
|
'security:openssf-scorecard',
|
|
'local>username/preset-repo',
|
|
'security:openssf-scorecard',
|
|
];
|
|
|
|
local.getPreset.mockResolvedValueOnce({
|
|
labels: ['self-hosted resolved'],
|
|
extends: ['security:openssf-scorecard'],
|
|
packageRules: [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
],
|
|
});
|
|
|
|
const {
|
|
visitedPresets: { merged },
|
|
} = await presets.resolveConfigPresets(config);
|
|
|
|
expect(merged).toEqual(
|
|
// NOTE that we're not expecting to have a strict or stable ordering
|
|
expect.arrayContaining([
|
|
'local>username/preset-repo',
|
|
'security:openssf-scorecard',
|
|
'packages:eslint',
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('resolves self-hosted preset with templating', async () => {
|
|
setCustomEnv({ GIT_REF: 'abc123' });
|
|
config.extends = ['local>username/preset-repo#{{ env.GIT_REF }}'];
|
|
local.getPreset.mockImplementationOnce(({ tag }) =>
|
|
tag === 'abc123'
|
|
? Promise.resolve({ labels: ['self-hosted with template resolved'] })
|
|
: Promise.reject(new Error('Failed to resolve self-hosted preset')),
|
|
);
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res.labels).toEqual(['self-hosted with template resolved']);
|
|
expect(local.getPreset).toHaveBeenCalledExactlyOnceWith({
|
|
presetName: 'default',
|
|
presetPath: undefined,
|
|
repo: 'username/preset-repo',
|
|
tag: 'abc123',
|
|
});
|
|
});
|
|
|
|
it('resolves self-hosted transitive presets without baseConfig', async () => {
|
|
config.platform = 'gitlab';
|
|
config.endpoint = 'https://dummy.example.com/api/v4';
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset
|
|
.mockResolvedValueOnce({
|
|
extends: ['local>username/preset-repo//subpreset'],
|
|
})
|
|
.mockResolvedValueOnce({ labels: ['self-hosted resolved'] });
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({
|
|
platform: 'gitlab',
|
|
endpoint: 'https://dummy.example.com/api/v4',
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
});
|
|
|
|
it('resolves http presets', async () => {
|
|
config.extends = ['https://example.com/preset.json'];
|
|
http.getPreset.mockResolvedValueOnce({ labels: ['http'] });
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({ labels: ['http'] });
|
|
});
|
|
|
|
it('resolves forgejo presets', async () => {
|
|
config.extends = ['forgejo>user/repo'];
|
|
forgejo.getPreset.mockResolvedValueOnce({ labels: ['forgejo'] });
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({ labels: ['forgejo'] });
|
|
});
|
|
|
|
it('resolves gitea presets', async () => {
|
|
config.extends = ['gitea>user/repo'];
|
|
gitea.getPreset.mockResolvedValueOnce({ labels: ['gitea'] });
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({ labels: ['gitea'] });
|
|
});
|
|
|
|
it('resolves gitlab presets', async () => {
|
|
config.extends = ['gitlab>user/repo'];
|
|
gitLab.getPreset.mockResolvedValueOnce({ labels: ['gitlab'] });
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({ labels: ['gitlab'] });
|
|
});
|
|
|
|
it('gets preset value from cache when it has been seen', async () => {
|
|
config.extends = ['github>username/preset-repo'];
|
|
config.packageRules = [
|
|
{
|
|
matchManagers: ['github-actions'],
|
|
groupName: 'github-actions dependencies',
|
|
},
|
|
];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
packageRules: [
|
|
{
|
|
matchDatasources: ['docker'],
|
|
matchPackageNames: ['ubi'],
|
|
versioning: 'regex',
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(presets.resolveConfigPresets(config)).resolves.toBeDefined();
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
packageRules: [
|
|
{
|
|
matchDatasources: ['docker'],
|
|
matchPackageNames: ['ubi'],
|
|
versioning: 'regex',
|
|
},
|
|
{
|
|
matchManagers: ['github-actions'],
|
|
groupName: 'github-actions dependencies',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('default packageCache TTL should be 15 minutes', async () => {
|
|
GlobalConfig.set({
|
|
presetCachePersistence: true,
|
|
});
|
|
|
|
config.extends = ['github>username/preset-repo'];
|
|
config.packageRules = [
|
|
{
|
|
matchManagers: ['github-actions'],
|
|
groupName: 'github-actions dependencies',
|
|
},
|
|
];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
packageRules: [
|
|
{
|
|
matchDatasources: ['docker'],
|
|
matchPackageNames: ['ubi'],
|
|
versioning: 'regex',
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(presets.resolveConfigPresets(config)).resolves.toBeDefined();
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
packageRules: [
|
|
{
|
|
matchDatasources: ['docker'],
|
|
matchPackageNames: ['ubi'],
|
|
versioning: 'regex',
|
|
},
|
|
{
|
|
matchManagers: ['github-actions'],
|
|
groupName: 'github-actions dependencies',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(packageCache.set.mock.calls[0][3]).toBe(15);
|
|
});
|
|
|
|
it('use packageCache when presetCachePersistence is set', async () => {
|
|
GlobalConfig.set({
|
|
presetCachePersistence: true,
|
|
});
|
|
|
|
config.extends = ['github>username/preset-repo'];
|
|
config.packageRules = [
|
|
{
|
|
matchManagers: ['github-actions'],
|
|
groupName: 'github-actions dependencies',
|
|
},
|
|
];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
packageRules: [
|
|
{
|
|
matchDatasources: ['docker'],
|
|
matchPackageNames: ['ubi'],
|
|
versioning: 'regex',
|
|
},
|
|
],
|
|
});
|
|
|
|
await expect(presets.resolveConfigPresets(config)).resolves.toBeDefined();
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
expect(res).toEqual({
|
|
packageRules: [
|
|
{
|
|
matchDatasources: ['docker'],
|
|
matchPackageNames: ['ubi'],
|
|
versioning: 'regex',
|
|
},
|
|
{
|
|
matchManagers: ['github-actions'],
|
|
groupName: 'github-actions dependencies',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(packageCache.set.mock.calls[0][3]).toBe(15);
|
|
});
|
|
|
|
it('throws', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
|
|
local.getPreset.mockRejectedValueOnce(
|
|
new ExternalHostError(new Error('whoops')),
|
|
);
|
|
await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
|
|
ExternalHostError,
|
|
);
|
|
|
|
local.getPreset.mockRejectedValueOnce(
|
|
new Error(PLATFORM_RATE_LIMIT_EXCEEDED),
|
|
);
|
|
await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
|
|
PLATFORM_RATE_LIMIT_EXCEEDED,
|
|
);
|
|
});
|
|
|
|
describe('relative presets', () => {
|
|
it('resolves a relative preset against its parent', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['./system/registries'],
|
|
});
|
|
gitHub.getPreset.mockResolvedValueOnce({ prCreation: 'not-pending' });
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({ prCreation: 'not-pending' });
|
|
expect(gitHub.getPreset).toHaveBeenCalledWith({
|
|
repo: 'some/repo',
|
|
presetPath: 'system',
|
|
presetName: 'registries',
|
|
tag: 'v1.0.0',
|
|
});
|
|
});
|
|
|
|
it('terminates on self-referencing relative presets', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
gitHub.getPreset.mockResolvedValue({
|
|
extends: ['./default'],
|
|
prCreation: 'not-pending',
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({ prCreation: 'not-pending' });
|
|
expect(gitHub.getPreset).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('fetches the root default preset only once', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
gitHub.getPreset.mockImplementation(({ presetPath, presetName }) => {
|
|
if (presetPath === 'group' && presetName === 'a') {
|
|
return Promise.resolve({ extends: ['/default'] });
|
|
}
|
|
return Promise.resolve({
|
|
extends: ['./group/a'],
|
|
packageRules: [{ matchManagers: ['npm'], automerge: true }],
|
|
});
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({
|
|
packageRules: [{ matchManagers: ['npm'], automerge: true }],
|
|
});
|
|
expect(gitHub.getPreset).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('resolves relative presets which use params', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0(argA)'];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['./{{arg0}}'],
|
|
});
|
|
gitHub.getPreset.mockResolvedValueOnce({ prCreation: 'not-pending' });
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({ prCreation: 'not-pending' });
|
|
expect(gitHub.getPreset).toHaveBeenCalledWith({
|
|
repo: 'some/repo',
|
|
presetPath: undefined,
|
|
presetName: 'argA',
|
|
tag: 'v1.0.0',
|
|
});
|
|
});
|
|
|
|
it('throws if a relative preset has no parent preset', async () => {
|
|
config.extends = ['./x'];
|
|
|
|
await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
|
|
expect.objectContaining({
|
|
message: CONFIG_VALIDATION,
|
|
validationError:
|
|
'Relative preset reference cannot be resolved (./x). Relative presets can only be used within presets from a supported source, must stay inside their repository, and cannot be templated or used outside of a preset (for example in the repository config, inherited config, or globalExtends)',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('throws if a relative preset resolves outside of its repository', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['github>other/repo#v2.0.0'],
|
|
});
|
|
gitHub.getPreset.mockResolvedValueOnce({ extends: ['../x'] });
|
|
|
|
await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
|
|
expect.objectContaining({
|
|
message: CONFIG_VALIDATION,
|
|
validationError:
|
|
'Relative preset reference cannot be resolved (../x). Relative presets can only be used within presets from a supported source, must stay inside their repository, and cannot be templated or used outside of a preset (for example in the repository config, inherited config, or globalExtends). Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('throws if a relative preset is used by an unsupported preset source', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['somepackage:webapp'],
|
|
});
|
|
npm.getPreset.mockResolvedValueOnce({ extends: ['./x'] });
|
|
|
|
await expect(presets.resolveConfigPresets(config)).rejects.toThrow(
|
|
expect.objectContaining({
|
|
message: CONFIG_VALIDATION,
|
|
validationError:
|
|
'Relative preset reference cannot be resolved (./x). Relative presets can only be used within presets from a supported source, must stay inside their repository, and cannot be templated or used outside of a preset (for example in the repository config, inherited config, or globalExtends). Note: this is a *nested* preset so please contact the preset author if you are unable to fix it yourself.',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('resolves relative ignorePresets of a preset', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['./optional'],
|
|
ignorePresets: ['./optional'],
|
|
prCreation: 'not-pending',
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({ prCreation: 'not-pending' });
|
|
expect(gitHub.getPreset).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('allows ignorePresets to neutralize a broken relative preset', async () => {
|
|
config.extends = ['github>some/repo#v1.0.0'];
|
|
config.ignorePresets = ['../oops'];
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['../oops'],
|
|
prCreation: 'not-pending',
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(config);
|
|
|
|
expect(res).toEqual({ prCreation: 'not-pending' });
|
|
});
|
|
});
|
|
|
|
describe('when using mergeInternalPresets=true', () => {
|
|
describe('when resolving an internal preset', () => {
|
|
it('merges `extends`', async () => {
|
|
config.extends = ['security:openssf-scorecard'];
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
description: ['Show OpenSSF badge on pull requests.'],
|
|
packageRules: [
|
|
{
|
|
matchSourceUrls: ['https://github.com/**'],
|
|
prBodyColumns: [
|
|
'Package',
|
|
'Type',
|
|
'Update',
|
|
'Change',
|
|
'Pending',
|
|
'OpenSSF',
|
|
],
|
|
prBodyDefinitions: {
|
|
OpenSSF:
|
|
'[](https://securityscorecards.dev/viewer/?uri=github.com/{{sourceRepo}})',
|
|
},
|
|
},
|
|
{
|
|
groupName: 'eslint',
|
|
matchPackageNames: [
|
|
'*/eslint-plugin',
|
|
'@babel/eslint-parser',
|
|
'@eslint/**',
|
|
'@eslint-community/**',
|
|
'@stylistic/eslint-plugin**',
|
|
'@types/eslint',
|
|
'@types/eslint__**',
|
|
'@typescript-eslint/**',
|
|
'babel-eslint',
|
|
'eslint**',
|
|
'typescript-eslint',
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('does not return any unmerged presets', async () => {
|
|
config.extends = ['security:openssf-scorecard'];
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(unmerged).toBeEmptyArray();
|
|
});
|
|
});
|
|
|
|
// NOTE that this is a **??**
|
|
describe('when resolving an internal preset which includes many other internal presets', () => {
|
|
it('merges `extends`, recursively', async () => {
|
|
config.extends = [':assignAndReview(jamietanna)'];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
assignees: ['jamietanna'],
|
|
description: [
|
|
'Assign PRs to `jamietanna`.',
|
|
'Add `jamietanna` as reviewer for PRs.',
|
|
],
|
|
reviewers: ['jamietanna'],
|
|
});
|
|
});
|
|
|
|
it('does not return any unmerged presets', async () => {
|
|
config.extends = [':assignAndReview(jamietanna)'];
|
|
|
|
const {
|
|
visitedPresets: { merged, unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(merged).toEqual([
|
|
':assignAndReview(jamietanna)',
|
|
':assignee(jamietanna)',
|
|
':reviewer(jamietanna)',
|
|
]);
|
|
expect(unmerged).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('when resolving an external preset which references an internal preset', () => {
|
|
it('merges internal `extends`', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
extends: ['security:openssf-scorecard'],
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
description: ['Show OpenSSF badge on pull requests.'],
|
|
labels: ['self-hosted resolved'],
|
|
packageRules: [
|
|
{
|
|
matchSourceUrls: ['https://github.com/**'],
|
|
prBodyColumns: [
|
|
'Package',
|
|
'Type',
|
|
'Update',
|
|
'Change',
|
|
'Pending',
|
|
'OpenSSF',
|
|
],
|
|
prBodyDefinitions: {
|
|
OpenSSF:
|
|
'[](https://securityscorecards.dev/viewer/?uri=github.com/{{sourceRepo}})',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('does not return any unmerged presets', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
extends: ['security:openssf-scorecard'],
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(unmerged).toBeEmptyArray();
|
|
});
|
|
});
|
|
|
|
describe('when resolving mixed internal and external presets', () => {
|
|
it('merges internal `extends`', async () => {
|
|
config.extends = [
|
|
'security:openssf-scorecard',
|
|
'local>username/preset-repo',
|
|
];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
description: ['Show OpenSSF badge on pull requests.'],
|
|
labels: ['self-hosted resolved'],
|
|
packageRules: [
|
|
{
|
|
matchSourceUrls: ['https://github.com/**'],
|
|
prBodyColumns: [
|
|
'Package',
|
|
'Type',
|
|
'Update',
|
|
'Change',
|
|
'Pending',
|
|
'OpenSSF',
|
|
],
|
|
prBodyDefinitions: {
|
|
OpenSSF:
|
|
'[](https://securityscorecards.dev/viewer/?uri=github.com/{{sourceRepo}})',
|
|
},
|
|
},
|
|
{
|
|
groupName: 'eslint',
|
|
matchPackageNames: [
|
|
'*/eslint-plugin',
|
|
'@babel/eslint-parser',
|
|
'@eslint/**',
|
|
'@eslint-community/**',
|
|
'@stylistic/eslint-plugin**',
|
|
'@types/eslint',
|
|
'@types/eslint__**',
|
|
'@typescript-eslint/**',
|
|
'babel-eslint',
|
|
'eslint**',
|
|
'typescript-eslint',
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('does not return any unmerged presets', async () => {
|
|
config.extends = [
|
|
'security:openssf-scorecard',
|
|
'local>username/preset-repo',
|
|
];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
expect(unmerged).toBeEmptyArray();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when using mergeInternalPresets=false', () => {
|
|
describe('when resolving an internal preset', () => {
|
|
it('does not merge `extends`', async () => {
|
|
config.extends = ['security:openssf-scorecard'];
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
packageRules: [
|
|
{
|
|
groupName: 'eslint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns the presets in the unmerged array', async () => {
|
|
config.extends = ['security:openssf-scorecard'];
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(unmerged).toEqual([
|
|
'security:openssf-scorecard',
|
|
'packages:eslint',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when resolving an internal, parameterised preset', () => {
|
|
it('does not merge `extends`', async () => {
|
|
config.extends = [':assignee(renovate-tests)'];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(res).toEqual({});
|
|
});
|
|
|
|
it('returns the preset in the unmerged array', async () => {
|
|
config.extends = [':assignee(renovate-tests)'];
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(unmerged).toEqual([':assignee(renovate-tests)']);
|
|
});
|
|
});
|
|
|
|
describe('when resolving an internal preset which includes many other internal presets', () => {
|
|
it('does not merge `extends`', async () => {
|
|
config.extends = ['config:recommended'];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(res).toEqual({});
|
|
});
|
|
|
|
it('returns the unmerged internal presets', async () => {
|
|
config.extends = ['config:recommended'];
|
|
|
|
const {
|
|
visitedPresets: { merged, unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(merged).toEqual([]);
|
|
expect(unmerged).toEqual(['config:recommended']);
|
|
});
|
|
});
|
|
|
|
describe('when resolving an external preset which references an internal preset', () => {
|
|
it('does not merge `extends`', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
extends: ['security:openssf-scorecard'],
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
});
|
|
|
|
it('returns the unmerged internal presets', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
extends: ['security:openssf-scorecard'],
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(unmerged).toEqual(['security:openssf-scorecard']);
|
|
});
|
|
});
|
|
|
|
describe('when resolving mixed internal and external presets', () => {
|
|
it('does not expand internal `extends`', async () => {
|
|
config.extends = [
|
|
'security:openssf-scorecard',
|
|
'local>username/preset-repo',
|
|
];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const { config: res } = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(res).toEqual({
|
|
labels: ['self-hosted resolved'],
|
|
packageRules: [
|
|
{
|
|
groupName: 'eslint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('returns the unmerged internal presets', async () => {
|
|
config.extends = [
|
|
'security:openssf-scorecard',
|
|
'local>username/preset-repo',
|
|
];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
labels: ['self-hosted resolved'],
|
|
});
|
|
config.packageRules = [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
];
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(unmerged).toEqual([
|
|
'security:openssf-scorecard',
|
|
'packages:eslint',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when resolving an internal preset inside a nested object config value', () => {
|
|
it('returns the unmerged internal presets from a datasource', async () => {
|
|
config.extends = ['local>username/preset-repo'];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
artifactory: {
|
|
extends: ['security:openssf-scorecard'],
|
|
enabled: true,
|
|
},
|
|
});
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(unmerged).toEqual(['security:openssf-scorecard']);
|
|
});
|
|
});
|
|
|
|
describe('when duplicate internal presets are found', () => {
|
|
it('they are de-duplicated when returned as unmerged', async () => {
|
|
config.extends = [
|
|
'security:openssf-scorecard',
|
|
'local>username/preset-repo',
|
|
];
|
|
local.getPreset.mockResolvedValueOnce({
|
|
extends: ['security:openssf-scorecard'],
|
|
});
|
|
config.packageRules = [
|
|
{
|
|
extends: ['security:openssf-scorecard'],
|
|
},
|
|
];
|
|
|
|
const {
|
|
visitedPresets: { unmerged },
|
|
} = await presets.resolveConfigPresets(
|
|
config,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
expect(unmerged).toEqual(['security:openssf-scorecard']);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('replaceArgs', () => {
|
|
const argMappings = {
|
|
arg0: 'a',
|
|
arg1: 'b',
|
|
arg2: 'c',
|
|
};
|
|
|
|
it('replaces args in strings', () => {
|
|
const str = '{{arg2}} foo {{arg0}}{{arg1}}';
|
|
const res = presets.replaceArgs(str, argMappings);
|
|
expect(res).toBe('c foo ab');
|
|
});
|
|
|
|
it('replaces args twice in same string', () => {
|
|
const str = '{{arg2}}{{arg0}} foo {{arg0}}{{arg1}}';
|
|
const res = presets.replaceArgs(str, argMappings);
|
|
expect(res).toBe('ca foo ab');
|
|
});
|
|
|
|
it('replaces objects', () => {
|
|
const obj = {
|
|
foo: 'ha {{arg0}}',
|
|
bar: {
|
|
baz: '{{arg1}} boo',
|
|
aaa: {
|
|
bbb: 'woo {{arg2}}',
|
|
},
|
|
},
|
|
};
|
|
const res = presets.replaceArgs(obj, argMappings);
|
|
expect(res).toEqual({
|
|
bar: { aaa: { bbb: 'woo c' }, baz: 'b boo' },
|
|
foo: 'ha a',
|
|
});
|
|
});
|
|
|
|
it('replaces arrays', () => {
|
|
const obj = {
|
|
foo: ['{{arg0}}', { bar: '{{arg1}}', baz: 5 }],
|
|
};
|
|
const res = presets.replaceArgs(obj, argMappings);
|
|
expect(res).toEqual({
|
|
foo: ['a', { bar: 'b', baz: 5 }],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getPreset', () => {
|
|
beforeEach(() => {
|
|
memCache.init();
|
|
});
|
|
|
|
it('does not use cache for internal presets', async () => {
|
|
const memCacheGetSpy = vi.spyOn(memCache, 'get');
|
|
await expect(
|
|
presets.getPreset(':dependencyDashboard', {}),
|
|
).resolves.toBeDefined();
|
|
expect(memCacheGetSpy).not.toHaveBeenCalled();
|
|
expect(packageCache.get).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles removed presets with a migration', async () => {
|
|
const res = await presets.getPreset(':base', {});
|
|
expect(res).toEqual({
|
|
extends: [
|
|
':dependencyDashboard',
|
|
':semanticPrefixFixDepsChoreOthers',
|
|
':ignoreModulesAndTests',
|
|
'group:monorepos',
|
|
'group:recommended',
|
|
'mergeConfidence:age-confidence-badges',
|
|
'replacements:all',
|
|
'workarounds:all',
|
|
'helpers:forgejoDigestChangelogs',
|
|
'helpers:giteaDigestChangelogs',
|
|
'helpers:githubDigestChangelogs',
|
|
'helpers:gitlabDigestChangelogs',
|
|
'helpers:goXPackagesChangelogLink',
|
|
'helpers:goXPackagesNameLink',
|
|
'helpers:renovateChangelog',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('handles removed presets with no migration', async () => {
|
|
const res = await presets.getPreset('helpers:oddIsUnstable', {});
|
|
expect(res).toEqual({});
|
|
});
|
|
|
|
it('handles renamed monorepos', async () => {
|
|
const res = await presets.getPreset('monorepo:opentelemetry', {});
|
|
expect(res).toMatchInlineSnapshot(`
|
|
{
|
|
"description": [
|
|
"opentelemetry-js monorepo",
|
|
],
|
|
"matchSourceUrls": [
|
|
"https://github.com/open-telemetry/opentelemetry-js",
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('handles renamed monorepo groups', async () => {
|
|
const res = await presets.getPreset('group:opentelemetryMonorepo', {});
|
|
expect(res).toMatchInlineSnapshot(`
|
|
{
|
|
"packageRules": [
|
|
{
|
|
"description": [
|
|
"Group packages from opentelemetry-js monorepo together.",
|
|
],
|
|
"extends": [
|
|
"monorepo:opentelemetry-js",
|
|
],
|
|
"groupName": "opentelemetry-js monorepo",
|
|
"matchUpdateTypes": [
|
|
"digest",
|
|
"patch",
|
|
"minor",
|
|
"major",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('handles renamed regexManagers presets', async () => {
|
|
const res = await presets.getPreset(
|
|
'regexManagers:dockerfileVersions',
|
|
{},
|
|
);
|
|
expect(res.customManagers).toHaveLength(1);
|
|
});
|
|
|
|
it('gets linters', async () => {
|
|
const res = await presets.getPreset('packages:linters', {});
|
|
expect(res).toEqual({
|
|
description: ['All lint-related packages.'],
|
|
extends: [
|
|
'packages:emberTemplateLint',
|
|
'packages:eslint',
|
|
'packages:phpLinters',
|
|
'packages:stylelint',
|
|
'packages:tslint',
|
|
],
|
|
matchPackageNames: [
|
|
'@oxlint/**',
|
|
'oxlint',
|
|
'prettier',
|
|
'remark-lint',
|
|
'standard',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('gets parameterised configs', async () => {
|
|
const res = await presets.getPreset(
|
|
':group(packages:eslint, eslint)',
|
|
{},
|
|
);
|
|
expect(res).toEqual({
|
|
description: ['Group `eslint` packages into same branch/PR.'],
|
|
packageRules: [
|
|
{
|
|
extends: ['packages:eslint'],
|
|
groupName: 'eslint',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('handles missing params', async () => {
|
|
const res = await presets.getPreset(':group()', {});
|
|
expect(res).toEqual({
|
|
description: ['Group `{{arg1}}` packages into same branch/PR.'],
|
|
packageRules: [
|
|
{
|
|
extends: [],
|
|
groupName: '{{arg1}}',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('ignores irrelevant params', async () => {
|
|
const res = await presets.getPreset(':pinVersions(foo, bar)', {});
|
|
expect(res).toEqual({
|
|
description: [
|
|
'Use version pinning (maintain a single version only and not SemVer ranges).',
|
|
],
|
|
rangeStrategy: 'pin',
|
|
});
|
|
});
|
|
|
|
it('substitutes {{args}}', async () => {
|
|
local.getPreset.mockResolvedValueOnce({
|
|
customManagers: [
|
|
{
|
|
customType: 'regex',
|
|
managerFilePatterns: ['{{args}}'],
|
|
matchStrings: ['# renovate: ...'],
|
|
},
|
|
],
|
|
});
|
|
const res = await presets.getPreset(
|
|
'local>customManager(**/{*.py, *.yaml})',
|
|
{},
|
|
);
|
|
expect(res).toEqual({
|
|
customManagers: [
|
|
{
|
|
customType: 'regex',
|
|
// The space after comma is obviously incorrect here.
|
|
// But the test must ensure that spaces aren't removed.
|
|
managerFilePatterns: ['**/{*.py, *.yaml}'],
|
|
matchStrings: ['# renovate: ...'],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('handles 404 packages', async () => {
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.getPreset('notfound:foo', {});
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBeUndefined();
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('handles no config', async () => {
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.getPreset('noconfig:foo', {});
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBeUndefined();
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('handles throw errors', async () => {
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.getPreset('throw:foo', {});
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBeUndefined();
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
|
|
it('canonicalizes relative presets', async () => {
|
|
gitHub.getPreset.mockResolvedValueOnce({
|
|
extends: ['./system/registries', 'github>other/repo'],
|
|
packageRules: [{ extends: ['./rules/docker'] }],
|
|
});
|
|
|
|
const res = await presets.getPreset('github>some/repo#v1.0.0', {});
|
|
|
|
expect(res).toEqual({
|
|
extends: [
|
|
'github>some/repo//system/registries#v1.0.0',
|
|
'github>other/repo',
|
|
],
|
|
packageRules: [{ extends: ['github>some/repo//rules/docker#v1.0.0'] }],
|
|
});
|
|
});
|
|
|
|
it('leaves relative presets of npm presets unchanged', async () => {
|
|
npm.getPreset.mockResolvedValueOnce({
|
|
extends: ['./system/registries'],
|
|
ignorePresets: ['./optional'],
|
|
});
|
|
|
|
const res = await presets.getPreset('somepackage:webapp', {});
|
|
|
|
expect(res).toEqual({
|
|
extends: ['./system/registries'],
|
|
ignorePresets: ['./optional'],
|
|
});
|
|
});
|
|
|
|
it('handles preset not found', async () => {
|
|
let e: Error | undefined;
|
|
try {
|
|
await presets.getPreset('wrongpreset:foo', {});
|
|
} catch (err) {
|
|
e = err;
|
|
}
|
|
expect(e).toBeDefined();
|
|
expect(e!.validationSource).toBeUndefined();
|
|
expect(e!.validationError).toBeUndefined();
|
|
expect(e!.validationMessage).toBeUndefined();
|
|
});
|
|
});
|
|
});
|