diff --git a/packages/insomnia-app/app/common/__tests__/validate-insomnia-config.test.ts b/packages/insomnia-app/app/common/__tests__/validate-insomnia-config.test.ts index bdf45139db..a7c66ed20b 100644 --- a/packages/insomnia-app/app/common/__tests__/validate-insomnia-config.test.ts +++ b/packages/insomnia-app/app/common/__tests__/validate-insomnia-config.test.ts @@ -11,7 +11,26 @@ const electronShowErrorBox = mocked(electron.dialog.showErrorBox); const getConfigSettings = mocked(_getConfigSettings); describe('validateInsomniaConfig', () => { - it('should show error box and exit if there is an error', () => { + it('should show error box and exit if there is a parse error', () => { + // Arrange + const errorReturn = { + error: { + syntaxError: new SyntaxError('mock syntax error'), + fileContents: '{ "mock": ["insomnia", "config"] }', + configPath: '/mock/insomnia/config/path', + }, + }; + getConfigSettings.mockReturnValue(errorReturn); + + // Act + validateInsomniaConfig(); + + // Assert + expect(electronShowErrorBox).toHaveBeenCalled(); + expect(electronAppExit).toHaveBeenCalled(); + }); + + it('should show error box and exit if there is a config error', () => { // Arrange const errorReturn = { error: { diff --git a/packages/insomnia-app/app/common/validate-insomnia-config.ts b/packages/insomnia-app/app/common/validate-insomnia-config.ts index dd6b8adb08..f66cac8417 100644 --- a/packages/insomnia-app/app/common/validate-insomnia-config.ts +++ b/packages/insomnia-app/app/common/validate-insomnia-config.ts @@ -1,27 +1,48 @@ import electron from 'electron'; -import { getConfigSettings } from '../models/helpers/settings'; +import { getConfigSettings, isConfigError, isParseError } from '../models/helpers/settings'; import { exitApp } from './electron-helpers'; export const validateInsomniaConfig = () => { const configSettings = getConfigSettings(); - if ('error' in configSettings) { - const errors = configSettings.error.humanReadableErrors?.map(({ message, path, suggestion }, index) => ([ + + if (!('error' in configSettings)) { + return; + } + + if (isParseError(configSettings)) { + const { syntaxError, configPath } = configSettings.error; + electron.dialog.showErrorBox('Invalid Insomnia Config', [ + 'Failed to parse JSON file for Insomnia Config.', + '', + '[Path]', + configPath, + '', + '[Syntax Error]', + syntaxError.message, + ].join('\n')); + } else if (isConfigError(configSettings)) { + const { humanReadableErrors, configPath } = configSettings.error; + const errors = humanReadableErrors.map(({ message, path, suggestion }, index) => ([ `[Error ${index + 1}]`, `Path: ${path}`, `${message}.${suggestion ? ` ${suggestion}` : ''}`, ]).join('\n')).join('\n\n'); - electron.dialog.showErrorBox('Invalid Insomnia Config', - [ - `Your Insomnia Config was found to be invalid. Please check the path below for the following error${configSettings.error.humanReadableErrors?.length > 1 ? 's' : ''}:`, - '', - '[Path]', - configSettings.error.configPath, - '', - errors, - ].join('\n'), + electron.dialog.showErrorBox('Invalid Insomnia Config', [ + `Your Insomnia Config was found to be invalid. Please check the path below for the following error${configSettings.error.humanReadableErrors?.length > 1 ? 's' : ''}:`, + '', + '[Path]', + configPath, + '', + errors, + ].join('\n')); + } else { + electron.dialog.showErrorBox( + 'An unexpected error occured while parsing Insomnia Config', + JSON.stringify(configSettings), ); - exitApp(); } + + exitApp(); }; diff --git a/packages/insomnia-app/app/models/helpers/settings.ts b/packages/insomnia-app/app/models/helpers/settings.ts index 240ba3e291..9f61fc197b 100644 --- a/packages/insomnia-app/app/models/helpers/settings.ts +++ b/packages/insomnia-app/app/models/helpers/settings.ts @@ -9,27 +9,49 @@ import { ValueOf } from 'type-fest'; import { isDevelopment } from '../../common/constants'; import { getDataDirectory, getPortableExecutableDir } from '../../common/electron-helpers'; +interface FailedParseResult { + syntaxError: SyntaxError; + fileContents: string; + configPath: string; +} + +const isFailedParseResult = (input: any): input is FailedParseResult => ( + input ? input.syntaxError instanceof SyntaxError : false +); + /** takes an unresolved (or resolved will work fine too) filePath of the insomnia config and reads the insomniaConfig from disk */ -export const readConfigFile = (filePath?: string) => { - if (!filePath) { +export const readConfigFile = (configPath?: string): unknown | FailedParseResult | undefined => { + if (!configPath) { return undefined; } let fileContents = ''; try { - fileContents = readFileSync(filePath, 'utf-8'); + fileContents = readFileSync(configPath, 'utf-8'); } catch (error: unknown) { + // file not found return undefined; } - if (!fileContents) { + const fileIsFoundButEmpty = fileContents === ''; + if (fileIsFoundButEmpty) { return undefined; } try { return JSON.parse(fileContents) as unknown; - } catch (error: unknown) { - console.error('failed to parse insomnia config', { filePath, fileContents }, error); + } catch (syntaxError: unknown) { + // note: all JSON.parse errors are SyntaxErrors + // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/JSON_bad_parse + if (syntaxError instanceof SyntaxError) { + console.error('failed to parse insomnia config', { configPath, fileContents, syntaxError }); + const failedParseResult: FailedParseResult = { + syntaxError, + fileContents, + configPath: configPath, + }; + return failedParseResult; + } return undefined; } }; @@ -55,10 +77,14 @@ export const getConfigFile = () => { // note: this is written as to avoid unnecessary (synchronous) reads from disk. // The paths above are in priority order such that if we already found what we're looking for, there's no reason to keep reading other files. for (const configPath of configPaths) { - const insomniaConfig = readConfigFile(configPath); - if (insomniaConfig !== undefined && configPath !== undefined) { + const fileReadResult = readConfigFile(configPath); + if (isFailedParseResult(fileReadResult)) { + return fileReadResult; + } + + if (fileReadResult !== undefined && configPath !== undefined) { return { - insomniaConfig, + insomniaConfig: fileReadResult, configPath, }; } @@ -79,14 +105,33 @@ interface ConfigError { }; } +export const isConfigError = (input: any): input is ConfigError => ( + input ? input.humanErrors?.length > 0 : false +); + +interface ParseError { + error: FailedParseResult; +} + +export const isParseError = (input: any): input is ParseError => ( + input ? isFailedParseResult(input.error) : false +); + /** * gets settings from the `insomnia.config.json` * * note that it is a business rule that the config is never read again after startup, hence the `once` usage. */ -export const getConfigSettings: () => (NonNullable | ConfigError) = once(() => { - const { configPath, insomniaConfig } = getConfigFile(); +export const getConfigSettings: () => (NonNullable | ConfigError | ParseError) = once(() => { + const configFileResult = getConfigFile(); + if (isFailedParseResult(configFileResult)) { + return { + error: configFileResult, + }; + } + + const { insomniaConfig, configPath } = configFileResult; const validationResult = validate(insomniaConfig as InsomniaConfig); if (isErrorResult(validationResult)) {