diff --git a/packages/insomnia-app/app/__mocks__/electron.ts b/packages/insomnia-app/app/__mocks__/electron.ts index 1d0fec7cd5..68856c60ea 100644 --- a/packages/insomnia-app/app/__mocks__/electron.ts +++ b/packages/insomnia-app/app/__mocks__/electron.ts @@ -16,6 +16,8 @@ const remote = { getLocale() { return 'en-US'; }, + + exit: jest.fn(), }, net: { request() { @@ -55,9 +57,14 @@ const remote = { }, }; +const dialog = { + showErrorBox: jest.fn(), +}; + const electron = { ...remote, remote, + dialog, ipcMain: { on: jest.fn(), 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 new file mode 100644 index 0000000000..5648acc93a --- /dev/null +++ b/packages/insomnia-app/app/common/__tests__/validate-insomnia-config.test.ts @@ -0,0 +1,45 @@ +import electron from 'electron'; +import { mocked } from 'ts-jest/utils'; + +import { getConfigSettings as _getConfigSettings } from '../../models/helpers/settings'; +import { validateInsomniaConfig } from '../validate-insomnia-config'; + +jest.mock('electron'); +jest.mock('../../models/helpers/settings'); +const electronAppExit = mocked(electron.app.exit); +const electronShowErrorBox = mocked(electron.dialog.showErrorBox); +const getConfigSettings = mocked(_getConfigSettings); + +describe('validateInsomniaConfig', () => { + it('should show error box and exit if there is an error', () => { + // Arrange + const errorReturn = { + error: { + errors: [], + insomniaConfig: 'abc', + configPath: 'configPath', + }, + }; + getConfigSettings.mockReturnValue(errorReturn); + + // Act + validateInsomniaConfig(); + + // Assert + expect(electronShowErrorBox).toHaveBeenCalled(); + expect(electronAppExit).toHaveBeenCalled(); + }); + + it('should not exit if there are no errors', () => { + // Arrange + const validReturn = { enableAnalytics: true }; + getConfigSettings.mockReturnValue(validReturn); + + // Act + validateInsomniaConfig(); + + // Assert + expect(electronShowErrorBox).not.toHaveBeenCalled(); + expect(electronAppExit).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/insomnia-app/app/common/electron-helpers.ts b/packages/insomnia-app/app/common/electron-helpers.ts index dbef15c8e2..81ef7b7788 100644 --- a/packages/insomnia-app/app/common/electron-helpers.ts +++ b/packages/insomnia-app/app/common/electron-helpers.ts @@ -66,6 +66,11 @@ export function restartApp() { app.exit(); } +export const exitApp = () => { + const { app } = electron.remote || electron; + app.exit(); +}; + export const setMenuBarVisibility = (visible: boolean) => { const { BrowserWindow } = electron.remote || electron; BrowserWindow.getAllWindows() diff --git a/packages/insomnia-app/app/common/validate-insomnia-config.ts b/packages/insomnia-app/app/common/validate-insomnia-config.ts new file mode 100644 index 0000000000..2ebfd4bb8f --- /dev/null +++ b/packages/insomnia-app/app/common/validate-insomnia-config.ts @@ -0,0 +1,23 @@ +import electron from 'electron'; +import { omit } from 'ramda'; + +import { getConfigSettings } from '../models/helpers/settings'; +import { exitApp } from './electron-helpers'; + +export const validateInsomniaConfig = () => { + const configSettings = getConfigSettings(); + if ('error' in configSettings) { + const errors = configSettings.error.errors?.map(omit(['parentSchema', 'data'])); + + electron.dialog.showErrorBox('Invalid Insomnia Config', + [ + `Invalid Insomnia Config found at "${configSettings.error.configPath}"`, + '', + 'errors:', + `${JSON.stringify(errors, null, 2)}`, + ].join('\n'), + ); + + exitApp(); + } +}; diff --git a/packages/insomnia-app/app/main.development.ts b/packages/insomnia-app/app/main.development.ts index cc58171b4e..c10b7f7b06 100644 --- a/packages/insomnia-app/app/main.development.ts +++ b/packages/insomnia-app/app/main.development.ts @@ -8,6 +8,7 @@ import { changelogUrl, getAppVersion, isDevelopment, isMac } from './common/cons import { database } from './common/database'; import { disableSpellcheckerDownload } from './common/electron-helpers'; import log, { initializeLogging } from './common/log'; +import { validateInsomniaConfig } from './common/validate-insomnia-config'; import * as errorHandling from './main/error-handling'; import * as grpcIpcMain from './main/grpc-ipc-main'; import { checkIfRestartNeeded } from './main/squirrel-startup'; @@ -38,8 +39,10 @@ if (!isDevelopment()) { // So if (window) checks don't throw global.window = global.window || undefined; + // When the app is first launched app.on('ready', async () => { + validateInsomniaConfig(); disableSpellcheckerDownload(); if (isDevelopment()) { @@ -62,6 +65,7 @@ app.on('ready', async () => { const updatedStats = await _trackStats(); await _updateFlags(updatedStats); await _launchApp(); + // Init the rest await updates.init(); grpcIpcMain.init(); diff --git a/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings-error.test.ts b/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings-error.test.ts new file mode 100644 index 0000000000..1d32e473f4 --- /dev/null +++ b/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings-error.test.ts @@ -0,0 +1,31 @@ +import fs from 'fs'; + +import { getConfigSettings } from '../settings'; + +describe('getConfigSettings error', () => { + it('returns an error if validation result has errors', () => { + // Arrange + const invalidConfig = { + insomniaConfig: '1.0.0', + settings: { + random: 'abcd', + }, + }; + + const readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(invalidConfig)); + const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation(); + + // Act + const result = getConfigSettings(); + + // Assert + expect(result).toHaveProperty('error'); + if ('error' in result) { + expect(consoleErrorMock).toHaveBeenCalledWith('invalid insomnia config', result.error); + } + + // Cleanup + readFileSyncSpy.mockRestore(); + consoleErrorMock.mockRestore(); + }); +}); diff --git a/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings.test.ts b/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings-once.test.ts similarity index 96% rename from packages/insomnia-app/app/models/helpers/__tests__/get-config-settings.test.ts rename to packages/insomnia-app/app/models/helpers/__tests__/get-config-settings-once.test.ts index bf87e9aadf..380992bee8 100644 --- a/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings.test.ts +++ b/packages/insomnia-app/app/models/helpers/__tests__/get-config-settings-once.test.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import { getConfigSettings } from '../settings'; // This test exists outside of settings.test.ts because we need an unmocked `../settings` module -describe('getConfigSettings', () => { +describe('getConfigSettings once', () => { it('only reads the config once on startup and then never again', () => { // Arrange const configOne = { diff --git a/packages/insomnia-app/app/models/helpers/settings.ts b/packages/insomnia-app/app/models/helpers/settings.ts index c718fbc53b..fc4d70247c 100644 --- a/packages/insomnia-app/app/models/helpers/settings.ts +++ b/packages/insomnia-app/app/models/helpers/settings.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import { Settings } from 'insomnia-common'; -import { InsomniaConfig, validate } from 'insomnia-config'; +import { InsomniaConfig, validate, ValidationResult } from 'insomnia-config/dist'; import { resolve } from 'path'; import { mapObjIndexed, once } from 'ramda'; import { omitBy } from 'ramda-adjunct'; @@ -53,7 +53,7 @@ export const getConfigFile = () => { // 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) { + if (insomniaConfig !== undefined && configPath !== undefined) { return { insomniaConfig, configPath, @@ -67,22 +67,37 @@ export const getConfigFile = () => { }; }; +interface ConfigError { + error: { + configPath?: string; + insomniaConfig: unknown; + errors: ValidationResult['errors']; + }; +} + /** * 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 = once(() => { +export const getConfigSettings: () => (NonNullable | ConfigError) = once(() => { const { configPath, insomniaConfig } = getConfigFile(); const { valid, errors } = validate(insomniaConfig as InsomniaConfig); if (!valid) { + const resolvedConfigPath = resolve(configPath); console.error('invalid insomnia config', { - configPath, + configPath: resolvedConfigPath, insomniaConfig, errors, }); - return {}; + return { + error: { + configPath: resolvedConfigPath, + insomniaConfig, + errors, + }, + }; } // This cast is important for testing intentionally bad values (the above validation will catch it, anyway) return (insomniaConfig as InsomniaConfig).settings || {}; diff --git a/packages/insomnia-config/src/entities.ts b/packages/insomnia-config/src/entities.ts index c575a7f932..9d8af2157c 100644 --- a/packages/insomnia-config/src/entities.ts +++ b/packages/insomnia-config/src/entities.ts @@ -1,8 +1,8 @@ import { Settings } from 'insomnia-common'; -type ConfigVersion = '1.0.0'; +export type ConfigVersion = '1.0.0'; -type AllowedSettings = Partial