diff --git a/index.ts b/index.ts index 553d7a2c4..433e8cd0a 100755 --- a/index.ts +++ b/index.ts @@ -48,7 +48,7 @@ if (argv.v) { process.exit(0); } -console.log('validating config'); +console.log('Validating config'); validate(); console.log('Setting up the proxy and Service'); diff --git a/src/config/ConfigLoader.ts b/src/config/ConfigLoader.ts index 22dd6abfd..9c4d70625 100644 --- a/src/config/ConfigLoader.ts +++ b/src/config/ConfigLoader.ts @@ -5,8 +5,9 @@ import { execFile } from 'child_process'; import { promisify } from 'util'; import { EventEmitter } from 'events'; import envPaths from 'env-paths'; -import { GitProxyConfig, Convert } from './generated/config'; +import { GitProxyConfig } from './generated/config'; import { Configuration, ConfigurationSource, FileSource, HttpSource, GitSource } from './types'; +import { loadConfig, validateConfig } from './validators'; const execFileAsync = promisify(execFile); @@ -148,7 +149,7 @@ export class ConfigLoader extends EventEmitter { ); console.log(`Found ${enabledSources.length} enabled configuration sources`); - const configs = await Promise.all( + const loadedConfigs = await Promise.all( enabledSources.map(async (source: ConfigurationSource) => { try { console.log(`Loading configuration from ${source.type} source`); @@ -161,10 +162,12 @@ export class ConfigLoader extends EventEmitter { ); // Filter out null results from failed loads - const validConfigs = configs.filter((config): config is GitProxyConfig => config !== null); + const nonNullConfigs = loadedConfigs.filter( + (config): config is GitProxyConfig => config !== null, + ); - if (validConfigs.length === 0) { - console.log('No valid configurations loaded from any source'); + if (nonNullConfigs.length === 0) { + console.log('All loaded configurations are empty, skipping reload'); return; } @@ -173,15 +176,20 @@ export class ConfigLoader extends EventEmitter { console.log(`Using ${shouldMerge ? 'merge' : 'override'} strategy for configuration`); const newConfig = shouldMerge - ? validConfigs.reduce( + ? nonNullConfigs.reduce( (acc, curr) => { return this.deepMerge(acc, curr) as Configuration; }, { ...this.config }, ) - : { ...this.config, ...validConfigs[validConfigs.length - 1] }; // Use last config for override + : { ...this.config, ...nonNullConfigs[nonNullConfigs.length - 1] }; // Use last config for override + + if (!validateConfig(newConfig)) { + console.error('Invalid configuration, skipping reload'); + return; + } - // Emit change event if config changed + // Emit change event if config changed and is valid if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) { console.log('Configuration has changed, updating and emitting change event'); this.config = newConfig; @@ -218,16 +226,7 @@ export class ConfigLoader extends EventEmitter { throw new Error('Invalid configuration file path'); } console.log(`Loading configuration from file: ${configPath}`); - const content = await fs.promises.readFile(configPath, 'utf8'); - - // Use QuickType to validate and parse the configuration - try { - return Convert.toGitProxyConfig(content); - } catch (error) { - throw new Error( - `Invalid configuration file format: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - } + return loadConfig(`file: ${configPath}`, async () => fs.promises.readFile(configPath, 'utf8')); } async loadFromHttp(source: HttpSource): Promise { @@ -237,18 +236,10 @@ export class ConfigLoader extends EventEmitter { ...(source.auth?.type === 'bearer' ? { Authorization: `Bearer ${source.auth.token}` } : {}), }; - const response = await axios.get(source.url, { headers }); - - // Use QuickType to validate and parse the configuration from HTTP response - try { - const configJson = - typeof response.data === 'string' ? response.data : JSON.stringify(response.data); - return Convert.toGitProxyConfig(configJson); - } catch (error) { - throw new Error( - `Invalid configuration format from HTTP source: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - } + return loadConfig(`HTTP: ${source.url}`, async () => { + const response = await axios.get(source.url, { headers }); + return typeof response.data === 'string' ? response.data : JSON.stringify(response.data); + }); } async loadFromGit(source: GitSource): Promise { @@ -344,17 +335,7 @@ export class ConfigLoader extends EventEmitter { throw new Error(`Configuration file not found at ${configPath}`); } - try { - const content = await fs.promises.readFile(configPath, 'utf8'); - - // Use QuickType to validate and parse the configuration from Git - const config = Convert.toGitProxyConfig(content); - console.log('Configuration loaded successfully from Git'); - return config; - } catch (error: any) { - console.error('Failed to read or parse configuration file:', error.message); - throw new Error(`Failed to read or parse configuration file: ${error.message}`); - } + return loadConfig(`git: ${configPath}`, async () => fs.promises.readFile(configPath, 'utf8')); } deepMerge(target: Record, source: Record): Record { diff --git a/src/config/index.ts b/src/config/index.ts index ca35c8b06..133750dbb 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -6,6 +6,7 @@ import { ConfigLoader } from './ConfigLoader'; import { Configuration } from './types'; import { serverConfig } from './env'; import { getConfigFile } from './file'; +import { validateConfig } from './validators'; // Cache for current configuration let _currentConfig: GitProxyConfig | null = null; @@ -69,6 +70,15 @@ function loadFullConfiguration(): GitProxyConfig { _currentConfig = mergeConfigurations(defaultConfig, userSettings); + if (!validateConfig(_currentConfig)) { + console.error( + 'Invalid configuration: Please check your configuration file and restart GitProxy.', + ); + throw new Error( + 'Invalid configuration: Please check your configuration file and restart GitProxy.', + ); + } + return _currentConfig; } @@ -325,7 +335,7 @@ const handleConfigUpdate = async (newConfig: Configuration) => { // Initialize config loader function initializeConfigLoader() { - const config = loadFullConfiguration() as Configuration; + const config = loadFullConfiguration(); _configLoader = new ConfigLoader(config); // Handle configuration updates @@ -352,7 +362,6 @@ export const reloadConfiguration = async () => { // Initialize configuration on module load try { - loadFullConfiguration(); initializeConfigLoader(); console.log('Configuration loaded successfully'); } catch (error) { diff --git a/src/config/validators.ts b/src/config/validators.ts new file mode 100644 index 000000000..17da0617b --- /dev/null +++ b/src/config/validators.ts @@ -0,0 +1,111 @@ +import { Convert, GitProxyConfig } from './generated/config'; + +const validationChain = [validateCommitConfig]; + +/** + * Executes all custom validators on the configuration + * @param config The configuration to validate + * @returns true if the configuration is valid, false otherwise + */ +export const validateConfig = (config: GitProxyConfig): boolean => { + return validationChain.every((validator) => validator(config)); +}; + +/** + * Validates that commit configuration uses valid regular expressions. + * @param config The commit configuration to validate + * @returns true if the commit configuration is valid, false otherwise + */ +function validateCommitConfig(config: GitProxyConfig): boolean { + return ( + validateConfigRegex(config, 'commitConfig.author.email.local.block') && + validateConfigRegex(config, 'commitConfig.author.email.domain.allow') && + validateConfigRegex(config, 'commitConfig.message.block.patterns') && + validateConfigRegex(config, 'commitConfig.diff.block.patterns') && + validateConfigRegex(config, 'commitConfig.diff.block.providers') + ); +} + +/** + * Validates that a regular expression is valid. + * @param pattern The regular expression to validate + * @param context The context of the regular expression + * @returns true if the regular expression is valid, false otherwise + */ +function isValidRegex(pattern: string, context: string): boolean { + try { + new RegExp(pattern); + return true; + } catch { + console.error(`Invalid regular expression for ${context}: ${pattern}`); + return false; + } +} + +/** + * Validates that a value in the configuration is a valid regular expression. + * @param config The configuration to validate + * @param path The path to the value to validate + * @returns true if the value is a valid regular expression, false otherwise + */ +function validateConfigRegex(config: GitProxyConfig, path: string): boolean { + const getValueAtPath = (obj: unknown, path: string): unknown => { + return path.split('.').reduce((current, key) => { + if (current == null || typeof current !== 'object') { + return undefined; + } + return (current as Record)[key]; + }, obj); + }; + + const value = getValueAtPath(config, path); + + if (!value) return true; + + if (typeof value === 'string') { + return isValidRegex(value, path); + } + + if (Array.isArray(value)) { + for (const pattern of value) { + if (!isValidRegex(pattern, path)) return false; + } + return true; + } + + if (typeof value === 'object') { + return Object.values(value).every((pattern) => isValidRegex(pattern as string, path)); + } + + return true; +} + +/** + * Loads and parses a GitProxyConfig object from a given context and loading strategy. + * @param context The context of the configuration + * @param loader The loading strategy to use + * @returns The parsed GitProxyConfig object + */ +export async function loadConfig( + context: string, + loader: () => Promise, +): Promise { + const raw = await loader(); + return parseGitProxyConfig(raw, context); +} + +/** + * Parses a raw string into a GitProxyConfig object. + * @param raw The raw string to parse + * @param context The context of the configuration + * @returns The parsed GitProxyConfig object + */ +function parseGitProxyConfig(raw: string, context: string): GitProxyConfig { + try { + return Convert.toGitProxyConfig(raw); + } catch (error) { + throw new Error( + `Invalid configuration format in ${context}: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } +} diff --git a/test/ConfigLoader.test.ts b/test/ConfigLoader.test.ts index 9a6fb0cfd..6fea3b27c 100644 --- a/test/ConfigLoader.test.ts +++ b/test/ConfigLoader.test.ts @@ -192,6 +192,111 @@ describe('ConfigLoader', () => { expect(spy).not.toHaveBeenCalled(); }); + + it('should skip reload and log error when configuration is invalid', async () => { + const invalidConfig = { + proxyUrl: 'https://test.com', + commitConfig: { + author: { + email: { + local: { + block: '[invalid(regex', + }, + }, + }, + }, + }; + + fs.writeFileSync(tempConfigFile, JSON.stringify(invalidConfig)); + + const initialConfig: Configuration = { + configurationSources: { + enabled: true, + sources: [ + { + type: 'file', + enabled: true, + path: tempConfigFile, + }, + ], + reloadIntervalSeconds: 0, + }, + }; + + configLoader = new ConfigLoader(initialConfig); + + const changeEventSpy = vi.fn(); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + configLoader.on('configurationChanged', changeEventSpy); + + await configLoader.reloadConfiguration(); + + expect(changeEventSpy).not.toHaveBeenCalled(); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.author.email.local.block: [invalid(regex', + ); + + expect(consoleErrorSpy).toHaveBeenCalledWith('Invalid configuration, skipping reload'); + + consoleErrorSpy.mockRestore(); + }); + + it('should successfully reload when configuration is valid', async () => { + const validConfig = { + proxyUrl: 'https://test.com', + commitConfig: { + author: { + email: { + local: { + block: '^admin.*', // Valid regex pattern + }, + }, + }, + message: { + block: { + patterns: ['WIP:', 'TODO'], + }, + }, + }, + }; + + fs.writeFileSync(tempConfigFile, JSON.stringify(validConfig)); + + const initialConfig: Configuration = { + configurationSources: { + enabled: true, + sources: [ + { + type: 'file', + enabled: true, + path: tempConfigFile, + }, + ], + reloadIntervalSeconds: 0, + }, + }; + + configLoader = new ConfigLoader(initialConfig); + + const changeEventSpy = vi.fn(); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + configLoader.on('configurationChanged', changeEventSpy); + + await configLoader.reloadConfiguration(); + + expect(changeEventSpy).toHaveBeenCalledOnce(); + expect(changeEventSpy.mock.calls[0][0]).toMatchObject(validConfig); + + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Invalid regular expression'), + ); + expect(consoleErrorSpy).not.toHaveBeenCalledWith('Invalid configuration, skipping reload'); + + consoleErrorSpy.mockRestore(); + }); }); describe('initialize', () => { @@ -509,7 +614,7 @@ describe('ConfigLoader', () => { }; await expect(configLoader.loadFromSource(source)).rejects.toThrow( - /Failed to read or parse configuration file/, + /Invalid configuration format in git/, ); }, { timeout: 30000 }, @@ -707,7 +812,7 @@ describe('ConfigLoader Error Handling', () => { enabled: true, path: tempConfigFile, }), - ).rejects.toThrow(/Invalid configuration file format/); + ).rejects.toThrow(/Invalid configuration format in file/); }); it('should handle HTTP request errors', async () => { @@ -733,6 +838,7 @@ describe('ConfigLoader Error Handling', () => { enabled: true, url: 'http://config-service/config', }), - ).rejects.toThrow(/Invalid configuration format from HTTP source/); + // Check that the error message CONTAINS the following string: + ).rejects.toThrow(/Invalid configuration format in HTTP: http:\/\/config-service\/config/); }); }); diff --git a/test/configValidators.test.ts b/test/configValidators.test.ts new file mode 100644 index 000000000..326196882 --- /dev/null +++ b/test/configValidators.test.ts @@ -0,0 +1,402 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { validateConfig } from '../src/config/validators'; +import { GitProxyConfig } from '../src/config/generated/config'; + +describe('validators', () => { + let consoleErrorSpy: ReturnType; + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + describe('validateConfig', () => { + it('should return true for empty config', () => { + const config: GitProxyConfig = {}; + expect(validateConfig(config)).toBe(true); + }); + + it('should return true for config without commitConfig', () => { + const config: GitProxyConfig = { + proxyUrl: 'https://test.com', + cookieSecret: 'secret', + }; + expect(validateConfig(config)).toBe(true); + }); + + it('should return true for valid commitConfig with all valid regex patterns', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '^admin.*', + }, + domain: { + allow: '.*@example\\.com$', + }, + }, + }, + message: { + block: { + patterns: ['^WIP:', 'TODO', '[Tt]est'], + }, + }, + diff: { + block: { + patterns: ['password', 'secret.*key'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should return false when commitConfig.author.email.local.block has invalid regex', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '[invalid(regex', + }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.author.email.local.block: [invalid(regex', + ); + }); + + it('should return false when commitConfig.author.email.domain.allow has invalid regex', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + domain: { + allow: '(unclosed group', + }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.author.email.domain.allow: (unclosed group', + ); + }); + + it('should return false when commitConfig.message.block.patterns contains invalid regex', () => { + const config: GitProxyConfig = { + commitConfig: { + message: { + block: { + patterns: ['valid-pattern', '[invalid[bracket', 'another-valid'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.message.block.patterns: [invalid[bracket', + ); + }); + + it('should return false when commitConfig.diff.block.patterns contains invalid regex', () => { + const config: GitProxyConfig = { + commitConfig: { + diff: { + block: { + patterns: ['password', '*invalid-quantifier'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.diff.block.patterns: *invalid-quantifier', + ); + }); + + it('should return false on first invalid pattern in message.block.patterns array', () => { + const config: GitProxyConfig = { + commitConfig: { + message: { + block: { + patterns: ['[invalid1', '[invalid2'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + // Only logs the first invalid pattern + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.message.block.patterns: [invalid1', + ); + }); + + it('should validate all regex fields independently', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '^valid.*', + }, + domain: { + allow: '.*@valid\\.com$', + }, + }, + }, + message: { + block: { + patterns: ['valid-message'], + }, + }, + diff: { + block: { + patterns: ['valid-diff'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle partial commitConfig with only author.email.local.block', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '^admin', + }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle partial commitConfig with only author.email.domain.allow', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + domain: { + allow: 'example\\.com', + }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle partial commitConfig with only message.block.patterns', () => { + const config: GitProxyConfig = { + commitConfig: { + message: { + block: { + patterns: ['WIP', 'TODO'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle partial commitConfig with only diff.block.patterns', () => { + const config: GitProxyConfig = { + commitConfig: { + diff: { + block: { + patterns: ['password', 'secret'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle empty providers object for diff.block.providers', () => { + const config: GitProxyConfig = { + commitConfig: { + diff: { + block: { + providers: {}, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle empty patterns array for message.block.patterns', () => { + const config: GitProxyConfig = { + commitConfig: { + message: { + block: { + patterns: [], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle empty patterns array for diff.block.patterns', () => { + const config: GitProxyConfig = { + commitConfig: { + diff: { + block: { + patterns: [], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should validate complex regex patterns correctly', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '^(?!.*[._]{2})(?!^[._])(?!.*[._]$)[a-z0-9._]+$', + }, + domain: { + allow: '^([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}$', + }, + }, + }, + message: { + block: { + patterns: [ + '\\b(password|secret|api[_-]?key)\\b', + '^(fixup!|squash!)', + '\\[skip[\\s-]ci\\]', + ], + }, + }, + diff: { + block: { + patterns: [ + '-----BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----', + '(AWS|aws)_?(SECRET|secret)_?(ACCESS|access)_?(KEY|key)', + ], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle invalid regex with special characters', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '???', + }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.author.email.local.block: ???', + ); + }); + + it('should stop validation at first error and return false', () => { + const config: GitProxyConfig = { + commitConfig: { + author: { + email: { + local: { + block: '[invalid', + }, + domain: { + allow: '(also-invalid', + }, + }, + }, + message: { + block: { + patterns: ['*invalid-too'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + // Stops at first error (local.block) + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.author.email.local.block: [invalid', + ); + }); + + it('should handle regex with unicode characters', () => { + const config: GitProxyConfig = { + commitConfig: { + message: { + block: { + patterns: ['[\\u4e00-\\u9fff]+', '\\p{Emoji}'], + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle invalid regex with special characters in providers', () => { + const config: GitProxyConfig = { + commitConfig: { + diff: { + block: { + providers: { type: '???' }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.diff.block.providers: ???', + ); + }); + + it('should work with valid regex in providers', () => { + const config: GitProxyConfig = { + commitConfig: { + diff: { + block: { + providers: { type: 'valid' }, + }, + }, + }, + }; + expect(validateConfig(config)).toBe(true); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/test/testConfig.test.ts b/test/testConfig.test.ts index 862f7c90d..972f5e8cb 100644 --- a/test/testConfig.test.ts +++ b/test/testConfig.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi, MockInstance } from 'vitest'; import fs from 'fs'; import path from 'path'; import defaultSettings from '../proxy.config.json'; @@ -453,3 +453,103 @@ describe('Configuration Update Handling', () => { vi.resetModules(); }); }); + +describe('loadFullConfiguration', () => { + let tempDir: string; + let tempUserFile: string; + let oldEnv: NodeJS.ProcessEnv; + let consoleErrorSpy: MockInstance; + + beforeEach(async () => { + vi.resetModules(); + oldEnv = { ...process.env }; + tempDir = fs.mkdtempSync('gitproxy-test'); + tempUserFile = path.join(tempDir, 'test-settings.json'); + + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const fileModule = await import('../src/config/file'); + fileModule.setConfigFile(tempUserFile); + }); + + afterEach(() => { + if (fs.existsSync(tempUserFile)) { + fs.rmSync(tempUserFile); + } + if (fs.existsSync(tempDir)) { + fs.rmdirSync(tempDir); + } + process.env = { ...oldEnv }; + consoleErrorSpy.mockRestore(); + vi.resetModules(); + }); + + describe('validation', () => { + it('should load successfully when user config contains valid regex patterns', async () => { + const validUser = { + commitConfig: { + author: { + email: { + local: { + block: '^admin.*', + }, + domain: { + allow: '.*@example\\.com$', + }, + }, + }, + message: { + block: { + patterns: ['^WIP:', 'TODO', '[Tt]est'], + }, + }, + diff: { + block: { + patterns: ['password', 'secret.*key'], + }, + }, + }, + }; + fs.writeFileSync(tempUserFile, JSON.stringify(validUser)); + + const config = await import('../src/config'); + config.invalidateCache(); + + expect(() => { + config.reloadConfiguration(); // Calls loadFullConfiguration + }).not.toThrow(); + + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Invalid regular expression'), + ); + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + 'Invalid configuration: Please check your configuration file and restart GitProxy.', + ); + }); + + it('should throw error when config file has invalid commitConfig entry', async () => { + const invalidConfig = { + commitConfig: { + author: { + email: { + local: { + block: '[invalid(regex', + }, + }, + }, + }, + }; + + fs.writeFileSync(tempUserFile, JSON.stringify(invalidConfig)); + + // Needed since loadFullConfiguration is executed on import too + await expect(import('../src/config')).rejects.toThrow( + 'Invalid configuration: Please check your configuration file and restart GitProxy.', + ); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Invalid regular expression for commitConfig.author.email.local.block: [invalid(regex', + ); + }); + }); +});