Files
renovate/lib/workers/global/config/parse/additional-config-file.spec.ts
Sebastian Poxhofer 05e1271376 test: use expect().resolves instead of awaiting inside expect (#45672)
Enables the vitest/prefer-expect-resolves oxlint rule and applies its
autofix across the spec suite.
2026-09-04 10:40:46 +00:00

319 lines
10 KiB
TypeScript

import fs from 'node:fs';
import { codeBlock } from 'common-tags';
import fsExtra from 'fs-extra';
import type { DirectoryResult } from 'tmp-promise';
import { dir } from 'tmp-promise';
import upath from 'upath';
import { logger } from '../../../../logger/index.ts';
import customConfig from './__fixtures__/config.cjs';
import * as file from './additional-config-file.ts';
describe('workers/global/config/parse/additional-config-file', () => {
const processExitSpy = vi.spyOn(process, 'exit');
const fsPathExistsSpy = vi.spyOn(fsExtra, 'pathExists');
const fsRemoveSpy = vi.spyOn(fsExtra, 'remove');
let tmp: DirectoryResult;
beforeAll(async () => {
tmp = await dir({ unsafeCleanup: true });
});
afterAll(async () => {
await tmp.cleanup();
});
describe('.getConfig()', () => {
it.each([
['custom js config file', 'config.js'],
['custom js config file', 'config.cjs'],
['custom js config file', 'config.mjs'],
['custom js config file exporting a Promise', 'config-promise.js'],
['custom js config file exporting a function', 'config-function.js'],
// The next two are different syntactic ways of expressing the same thing
[
'custom js config file exporting a function returning a Promise',
'config-function-promise.js',
],
[
'custom js config file exporting an async function',
'config-async-function.js',
],
['.renovaterc', '.renovaterc'],
['JSON5 config file', 'config.json5'],
['YAML config file', 'config.yaml'],
])('parses %s > %s', async (_fileType, filePath) => {
const configFile = upath.resolve(
import.meta.dirname,
'./__fixtures__/',
filePath,
);
await expect(
file.getConfig({ RENOVATE_ADDITIONAL_CONFIG_FILE: configFile }),
).resolves.toEqual(customConfig);
});
it('migrates', async () => {
const configFile = upath.resolve(
import.meta.dirname,
'./__fixtures__/config2.js',
);
// for coverage
const relativePath = upath.relative(process.cwd(), configFile);
const res = await file.getConfig({
RENOVATE_ADDITIONAL_CONFIG_FILE: relativePath,
});
expect(res.rangeStrategy).toBe('bump');
});
it('warns if config is invalid', async () => {
const configFile = upath.resolve(tmp.path, 'config.js');
const fileContent = codeBlock`
module.exports = {
"enabled": "invalid-value",
"prTitle":"something",
};
`;
await fs.promises.writeFile(configFile, fileContent, {
encoding: 'utf8',
});
await file.getConfig({ RENOVATE_ADDITIONAL_CONFIG_FILE: configFile });
expect(logger.warn).toHaveBeenCalledTimes(2);
await fs.promises.unlink(configFile);
});
it('parse and returns empty config if there is no RENOVATE_ADDITIONAL_CONFIG_FILE in env', async () => {
await expect(file.getConfig({})).resolves.toBeDefined();
});
it.each([
[
'config.invalid.js',
codeBlock`
module.exports = {
"platform": "github",
"token":"abcdef",
"onboarding": false,
"gitAuthor": "Renovate Bot <renovate@whitesourcesoftware.com>"
"onboardingConfig": {
"extends": ["config:recommended"],
},
"repositories": [ "test/test" ],
};
`,
],
['config.invalid.json5', `"invalid":`],
['config.invalid.yaml', `clearly: "invalid" "yaml"`],
])(
'fatal error and exit if error in parsing %s',
async (fileName, fileContent) => {
processExitSpy.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, fileName);
await fs.promises.writeFile(configFile, fileContent, {
encoding: 'utf8',
});
await file.getConfig({ RENOVATE_ADDITIONAL_CONFIG_FILE: configFile });
expect(processExitSpy).toHaveBeenCalledExactlyOnceWith(1);
await fs.promises.unlink(configFile);
},
);
it('fatal error and exit if custom config file does not exist', async () => {
processExitSpy
.mockImplementationOnce(() => undefined as never)
.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, './file4.js');
await file.getConfig({ RENOVATE_ADDITIONAL_CONFIG_FILE: configFile });
// TODO this should be called exactly once, but it is 2 times
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('fatal error and exit if config.js contains unresolved env var', async () => {
processExitSpy.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(
import.meta.dirname,
'./__fixtures__/config-ref-error.js-invalid',
);
const tmpDir = tmp.path;
await fsExtra.ensureDir(tmpDir);
const tmpConfigFile = upath.resolve(tmpDir, 'config-ref-error.js');
await fsExtra.copy(configFile, tmpConfigFile);
await file.getConfig({ RENOVATE_ADDITIONAL_CONFIG_FILE: tmpConfigFile });
expect(logger.fatal).toHaveBeenCalledWith(
{
err: expect.objectContaining({
message: 'CI_API_V4_URL is not defined',
}),
},
'Error parsing additional config file due to unresolved variable(s)',
);
expect(processExitSpy).toHaveBeenCalledExactlyOnceWith(1);
});
it.each([
['invalid config file type', './file.txt'],
['missing config file type', './file'],
])('fatal error and exit if %s', async (fileType, filePath) => {
processExitSpy.mockImplementationOnce(() => undefined as never);
const configFile = upath.resolve(tmp.path, filePath);
await fs.promises.writeFile(configFile, `{"token": "abc"}`, {
encoding: 'utf8',
});
await file.getConfig({ RENOVATE_ADDITIONAL_CONFIG_FILE: configFile });
expect(processExitSpy).toHaveBeenCalledExactlyOnceWith(1);
expect(logger.fatal).toHaveBeenCalledWith('Unsupported file type');
await fs.promises.unlink(configFile);
});
it('exports env variables to environment from processEnv object', async () => {
const configFile = upath.resolve(tmp.path, 'config2.js');
const fileContent1 = codeBlock`
module.exports = {
"processEnv": {
"SOME_KEY": "SOME_VALUE"
},
"labels": ["renovate"]
}
`;
await fs.promises.writeFile(configFile, fileContent1, {
encoding: 'utf8',
});
const fileConfig = await file.getConfig({
RENOVATE_ADDITIONAL_CONFIG_FILE: configFile,
});
expect(fileConfig).toMatchObject({
labels: ['renovate'],
});
expect(fileConfig.processEnv).toBeUndefined();
expect(process.env.SOME_KEY).toBe('SOME_VALUE');
await fs.promises.unlink(configFile);
vi.stubEnv('SOME_KEY', undefined);
});
it('does not export env variables to environment from processEnv object if key/value is invalid', async () => {
const configFile = upath.resolve(tmp.path, 'config3.js');
const fileContent1 = codeBlock`
module.exports = {
"processEnv": {
"SOME_KEY": "SOME_VALUE",
"SOME_OTHER_KEY": true,
"valid_Key": "true",
},
"labels": ["renovate"]
}
`;
await fs.promises.writeFile(configFile, fileContent1, {
encoding: 'utf8',
});
const fileConfig = await file.getConfig({
RENOVATE_ADDITIONAL_CONFIG_FILE: configFile,
});
expect(fileConfig).toMatchObject({
labels: ['renovate'],
});
expect(fileConfig.processEnv).toBeUndefined();
expect(process.env.SOME_KEY).toBe('SOME_VALUE');
expect(process.env.valid_Key).toBe('true');
expect(process.env.SOME_OTHER_KEY).toBeUndefined();
await fs.promises.unlink(configFile);
vi.stubEnv('SOME_KEY', undefined);
vi.stubEnv('valid_Key', undefined);
});
});
describe('deleteConfigFile()', () => {
it.each([[undefined], [' ']])(
'skip when RENOVATE_ADDITIONAL_CONFIG_FILE is not set ("%s")',
async (configFile) => {
await file.deleteNonDefaultConfig(
{ RENOVATE_ADDITIONAL_CONFIG_FILE: configFile },
true,
);
expect(fsRemoveSpy).toHaveBeenCalledTimes(0);
},
);
it('skip when config file does not exist', async () => {
fsPathExistsSpy.mockResolvedValueOnce(false as never);
await file.deleteNonDefaultConfig(
{
RENOVATE_ADDITIONAL_CONFIG_FILE: 'path',
},
true,
);
expect(fsRemoveSpy).toHaveBeenCalledTimes(0);
});
it.each([['false'], [' ']])(
'skip if deleteConfigFile is not set ("%s")',
async (deleteConfig) => {
fsPathExistsSpy.mockResolvedValueOnce(true as never);
await file.deleteNonDefaultConfig(
{
RENOVATE_ADDITIONAL_CONFIG_FILE: '/path/to/config.js',
},
deleteConfig === 'true',
);
expect(fsRemoveSpy).toHaveBeenCalledTimes(0);
},
);
it('removes the specified config file', async () => {
fsRemoveSpy.mockImplementationOnce(() => {
// no-op
});
fsPathExistsSpy.mockResolvedValueOnce(true as never);
const configFile = '/path/to/config.js';
await file.deleteNonDefaultConfig(
{
RENOVATE_ADDITIONAL_CONFIG_FILE: configFile,
},
true,
);
expect(fsRemoveSpy).toHaveBeenCalledExactlyOnceWith(configFile);
expect(logger.trace).toHaveBeenCalledWith(
expect.anything(),
'Additional config file successfully deleted',
);
});
it('fails silently when attempting to delete the config file', async () => {
fsRemoveSpy.mockImplementationOnce(() => {
throw new Error();
});
fsPathExistsSpy.mockResolvedValueOnce(true as never);
const configFile = '/path/to/config.js';
await file.deleteNonDefaultConfig(
{
RENOVATE_ADDITIONAL_CONFIG_FILE: configFile,
},
true,
);
expect(fsRemoveSpy).toHaveBeenCalledExactlyOnceWith(configFile);
expect(logger.warn).toHaveBeenCalledWith(
expect.anything(),
'Error deleting additional config file',
);
});
});
});