import { codeBlock } from 'common-tags'; import { logger } from '~test/util.ts'; import { GlobalConfig } from '../config/global.ts'; import { InheritConfig } from '../config/inherit.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://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('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); }); }); }); });