mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
0583e54718
* refactor(constants): add a platform family capability table Every consumer of `detectPlatform` carried its own mapping from the detected platform to a tags datasource or a web UI path, and the seven `*_API_USING_HOST_TYPES` arrays were checked one by one to produce that platform in the first place. Introduce `PLATFORM_FAMILIES`, one record keyed by family holding the host types, the hostnames which identify the family outright, the tags datasource, the API base URL derivation and the web UI path to a directory at the default branch. A family is a group of platforms, so the ids are checked against `PlatformId`: a family which is not a platform is a compile error. Not every platform is a family, because codecommit, gerrit, local and scm-manager are never recognized from a URL. `detectPlatform` loops the table for both its hostname and its hostType matching, and the consumers look their answers up: - `github-actions` and `pre-commit` read `tagsDatasource` instead of importing the tags datasource classes. - The PR body reads `webDirPath` instead of special-casing Bitbucket and Bitbucket Server. `apiBaseUrl` has no consumer yet; the changelog sources pick it up in the next commit. `lib/util/http/auth.ts` and `lib/util/http/host-rules.ts` still read six of the old arrays by name, so those stay as aliases of the table and are migrated in a follow-up. `AZURE_API_USING_HOST_TYPES` had no other consumer and is dropped. Behaviour changes: none. `detectPlatform` now tests every known hostname before its name heuristics instead of interleaving the two, which is equivalent because the hostname lists are pairwise disjoint and no known hostname contains the name of another family. * Apply suggestion from @secustor
312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
import { codeBlock } from 'common-tags';
|
|
import { logger } from '~test/util.ts';
|
|
import { GlobalConfig } from '../config/global.ts';
|
|
import { InheritConfig } from '../config/inherit.ts';
|
|
import { PLATFORM_FAMILIES } from '../constants/index.ts';
|
|
import { detectPlatform, getInheritedOrGlobal, parseJson } from './common.ts';
|
|
import * as hostRules from './host-rules.ts';
|
|
|
|
const validJsonString = `
|
|
{
|
|
"name": "John Doe",
|
|
"age": 30,
|
|
"city": "New York"
|
|
}
|
|
`;
|
|
const invalidJsonString = `
|
|
{
|
|
"name": "Alice",
|
|
"age": 25,
|
|
"city": "Los Angeles",
|
|
"hobbies": ["Reading", "Running", "Cooking"]
|
|
"isStudent": true
|
|
}
|
|
`;
|
|
const onlyJson5parsableString = `
|
|
{
|
|
name: "Bob",
|
|
age: 35,
|
|
city: 'San Francisco',
|
|
// This is a comment
|
|
"isMarried": false,
|
|
}
|
|
`;
|
|
const validJsoncString = `
|
|
{
|
|
// This is a comment
|
|
"name": "John Doe",
|
|
"age": 30,
|
|
"city": "New York"
|
|
}
|
|
`;
|
|
|
|
describe('util/common', () => {
|
|
beforeEach(() => hostRules.clear());
|
|
|
|
describe('detectPlatform', () => {
|
|
it.each`
|
|
url | hostType
|
|
${'some-invalid@url:::'} | ${null}
|
|
${'https://enterprise.example.com/chalk/chalk'} | ${null}
|
|
${'https://dev.azure.com/my-organization/my-project/_git/my-repo.git'} | ${'azure'}
|
|
${'https://myorg.visualstudio.com/my-project/_git/my-repo.git'} | ${'azure'}
|
|
${'https://bitbucket.org/some-org/some-repo'} | ${'bitbucket'}
|
|
${'https://bitbucket.com/some-org/some-repo'} | ${'bitbucket'}
|
|
${'https://bitbucket.example.com/some-org/some-repo'} | ${'bitbucket-server'}
|
|
${'https://gitea.com/semantic-release/gitlab'} | ${'gitea'}
|
|
${'https://gitea.example.com/semantic-release/gitlab'} | ${'gitea'}
|
|
${'https://forgejo.example.com/semantic-release/gitlab'} | ${'forgejo'}
|
|
${'https://codeberg.org/forgejo/forgejo'} | ${'forgejo'}
|
|
${'https://codefloe.com/some-org/some-repo'} | ${'forgejo'}
|
|
${'https://github.com/semantic-release/gitlab'} | ${'github'}
|
|
${'https://github-enterprise.example.com/chalk/chalk'} | ${'github'}
|
|
${'https://gitlab.com/chalk/chalk'} | ${'gitlab'}
|
|
${'https://gitlab-enterprise.example.com/chalk/chalk'} | ${'gitlab'}
|
|
`('("$url") === $hostType', ({ url, hostType }) => {
|
|
expect(detectPlatform(url)).toBe(hostType);
|
|
});
|
|
|
|
it.each(
|
|
Object.entries(PLATFORM_FAMILIES).flatMap(([family, { knownHosts }]) =>
|
|
knownHosts.map((host) => ({ family, host })),
|
|
),
|
|
)('knows $host as $family', ({ family, host }) => {
|
|
expect(detectPlatform(`https://${host}/some-org/some-repo`)).toBe(family);
|
|
});
|
|
|
|
it('uses host rules', () => {
|
|
hostRules.add({
|
|
hostType: 'azure',
|
|
matchHost: 'az.example.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'bitbucket',
|
|
matchHost: 'bb.example.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'gitea',
|
|
matchHost: 'gt.example.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'forgejo',
|
|
matchHost: 'fj.example.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'github-changelog',
|
|
matchHost: 'gh.example.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'gitlab-changelog',
|
|
matchHost: 'gl.example.com',
|
|
});
|
|
hostRules.add({
|
|
hostType: 'unknown',
|
|
matchHost: 'f.example.com',
|
|
});
|
|
expect(detectPlatform('https://az.example.com/chalk/chalk')).toBe(
|
|
'azure',
|
|
);
|
|
expect(detectPlatform('https://bb.example.com/chalk/chalk')).toBe(
|
|
'bitbucket',
|
|
);
|
|
expect(detectPlatform('https://gt.example.com/chalk/chalk')).toBe(
|
|
'gitea',
|
|
);
|
|
expect(detectPlatform('https://fj.example.com/chalk/chalk')).toBe(
|
|
'forgejo',
|
|
);
|
|
expect(detectPlatform('https://gh.example.com/chalk/chalk')).toBe(
|
|
'github',
|
|
);
|
|
expect(detectPlatform('https://gl.example.com/chalk/chalk')).toBe(
|
|
'gitlab',
|
|
);
|
|
expect(detectPlatform('https://f.example.com/chalk/chalk')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parseJson', () => {
|
|
it('returns null', () => {
|
|
expect(parseJson(null, 'renovate.json')).toBeNull();
|
|
});
|
|
|
|
it('returns parsed json', () => {
|
|
expect(parseJson(validJsonString, 'renovate.json')).toEqual({
|
|
name: 'John Doe',
|
|
age: 30,
|
|
city: 'New York',
|
|
});
|
|
});
|
|
|
|
it('supports jsonc', () => {
|
|
const jsoncString = codeBlock`
|
|
{
|
|
// This is a comment (valid in JSONC, invalid in JSON5 when outside object properties)
|
|
"name": "Alice", // inline comment is valid in JSONC but not in JSON5
|
|
"age": 25, // JSON5 supports trailing commas, so this line is okay in both
|
|
"city": "Atlanta",
|
|
// JSONC allows comments here too
|
|
}
|
|
`;
|
|
|
|
expect(parseJson(jsoncString, 'renovate.json')).toEqual({
|
|
name: 'Alice',
|
|
age: 25,
|
|
city: 'Atlanta',
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid json', () => {
|
|
expect(() => parseJson(invalidJsonString, 'renovate.json')).toThrow(
|
|
"JSON5: invalid character '\\\"' at 7:3",
|
|
);
|
|
});
|
|
|
|
it('catches and warns if content parsing failed with JSONC.parse but not with JSON5.parse', () => {
|
|
expect(parseJson(onlyJson5parsableString, 'renovate.json')).toEqual({
|
|
name: 'Bob',
|
|
age: 35,
|
|
city: 'San Francisco',
|
|
isMarried: false,
|
|
});
|
|
|
|
expect(logger.logger.warn).toHaveBeenCalledWith(
|
|
{ context: 'renovate.json' },
|
|
'File contents are invalid JSONC but parse using JSON5. Support for this will be removed in a future release so please change to a support .json5 file name or ensure correct JSON syntax.',
|
|
);
|
|
});
|
|
|
|
it('does not warn if filename ends with .jsonc', () => {
|
|
parseJson(validJsoncString, 'renovate.jsonc');
|
|
expect(logger.logger.warn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not warn if filename ends with .json5', () => {
|
|
parseJson(onlyJson5parsableString, 'renovate.json5');
|
|
expect(logger.logger.warn).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('parseJsonc', () => {
|
|
it('returns parsed jsonc', () => {
|
|
expect(parseJson(validJsoncString, 'renovate.jsonc')).toEqual({
|
|
name: 'John Doe',
|
|
age: 30,
|
|
city: 'New York',
|
|
});
|
|
});
|
|
|
|
it('throws error for invalid jsonc', () => {
|
|
expect(() => parseJson(invalidJsonString, 'renovate.jsonc')).toThrow(
|
|
'Parse error: Expected comma on line 6 column 47',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('getInheritedOrGlobal', () => {
|
|
beforeEach(() => {
|
|
GlobalConfig.reset();
|
|
InheritConfig.reset();
|
|
});
|
|
|
|
it('returns undefined if not set', () => {
|
|
expect(getInheritedOrGlobal('configFileNames')).toBeUndefined();
|
|
});
|
|
|
|
it('returns inherited value if only inherited value is set', () => {
|
|
InheritConfig.set({
|
|
configFileNames: ['inherited'],
|
|
});
|
|
expect(getInheritedOrGlobal('configFileNames')).toEqual(['inherited']);
|
|
});
|
|
|
|
it('returns global value if only global value is set', () => {
|
|
GlobalConfig.set({
|
|
configFileNames: ['global'],
|
|
});
|
|
expect(getInheritedOrGlobal('configFileNames')).toEqual(['global']);
|
|
});
|
|
|
|
it('returns inherited value - when both global + inherited are set', () => {
|
|
InheritConfig.set({
|
|
configFileNames: ['inherited'],
|
|
});
|
|
GlobalConfig.set({
|
|
configFileNames: ['global'],
|
|
});
|
|
expect(getInheritedOrGlobal('configFileNames')).toEqual(['inherited']);
|
|
});
|
|
|
|
// is not possiblle as config validation will error out: only for coverage
|
|
it('handles null inherited values', () => {
|
|
InheritConfig.set({
|
|
configFileNames: null as never,
|
|
});
|
|
GlobalConfig.set({
|
|
configFileNames: ['global'],
|
|
});
|
|
expect(getInheritedOrGlobal('configFileNames')).toBeNull();
|
|
});
|
|
|
|
// is not possiblle as config validation will error out: only for coverage
|
|
it('handles undefined inherited values', () => {
|
|
InheritConfig.set({
|
|
configFileNames: undefined,
|
|
});
|
|
GlobalConfig.set({
|
|
configFileNames: ['global'],
|
|
});
|
|
expect(getInheritedOrGlobal('configFileNames')).toBeUndefined();
|
|
});
|
|
|
|
describe('when requesting onboardingAutoCloseAge, do not allow inherit config to override global config', () => {
|
|
it('returns inherited value when inherited < global', () => {
|
|
GlobalConfig.set({
|
|
onboardingAutoCloseAge: 10,
|
|
});
|
|
InheritConfig.set({
|
|
onboardingAutoCloseAge: 5,
|
|
});
|
|
expect(getInheritedOrGlobal('onboardingAutoCloseAge')).toBe(5);
|
|
});
|
|
|
|
it('returns global value when inherited > global value', () => {
|
|
GlobalConfig.set({
|
|
onboardingAutoCloseAge: 5,
|
|
});
|
|
InheritConfig.set({
|
|
onboardingAutoCloseAge: 10,
|
|
});
|
|
expect(getInheritedOrGlobal('onboardingAutoCloseAge')).toBe(5);
|
|
});
|
|
|
|
it('returns inherited value when inherited == global', () => {
|
|
GlobalConfig.set({
|
|
onboardingAutoCloseAge: 5,
|
|
});
|
|
InheritConfig.set({
|
|
onboardingAutoCloseAge: 5,
|
|
});
|
|
expect(getInheritedOrGlobal('onboardingAutoCloseAge')).toBe(5);
|
|
});
|
|
|
|
it('returns inherited value when global value is not set', () => {
|
|
GlobalConfig.set({
|
|
onboardingAutoCloseAge: undefined,
|
|
});
|
|
InheritConfig.set({
|
|
onboardingAutoCloseAge: 10,
|
|
});
|
|
expect(getInheritedOrGlobal('onboardingAutoCloseAge')).toBe(10);
|
|
});
|
|
|
|
it('returns global value when inherited value is not set', () => {
|
|
GlobalConfig.set({
|
|
onboardingAutoCloseAge: 10,
|
|
});
|
|
expect(getInheritedOrGlobal('onboardingAutoCloseAge')).toBe(10);
|
|
});
|
|
});
|
|
});
|
|
});
|