diff --git a/packages/insomnia-app/app/ui/components/editors/__tests__/environment-editor.test.ts b/packages/insomnia-app/app/ui/components/editors/__tests__/environment-editor.test.ts index 0fe317ddcd..5b6997c153 100644 --- a/packages/insomnia-app/app/ui/components/editors/__tests__/environment-editor.test.ts +++ b/packages/insomnia-app/app/ui/components/editors/__tests__/environment-editor.test.ts @@ -1,26 +1,149 @@ import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '../../../../templating'; -import { ensureKeyIsValid } from '../environment-editor'; +import { checkNestedKeys, ensureKeyIsValid } from '../environment-editor'; -describe('ensureKeyIsValid', () => { - it.each(['$', '$a', '$ab'])('%s should be invalid when as key begins with $', key => { - expect(ensureKeyIsValid(key)).toBe(`"${key}" cannot begin with '$' or contain a '.'`); +describe('ensureKeyIsValid()', () => { + it.each([ + '$', + '$a', + '$ab', + ])('"%s" should be invalid when key begins with $', key => { + expect(ensureKeyIsValid(key, false)).toBe(`"${key}" cannot begin with '$' or contain a '.'`); }); - it.each(['.', 'a.', '.a', 'a.b'])('%s should be invalid when key contains .', key => { - expect(ensureKeyIsValid(key)).toBe(`"${key}" cannot begin with '$' or contain a '.'`); + it.each([ + '.', + 'a.', + '.a', + 'a.b', + ])('"%s" should be invalid when key contains .', key => { + expect(ensureKeyIsValid(key, false)).toBe(`"${key}" cannot begin with '$' or contain a '.'`); }); - it.each(['$a.b', '$.'])('%s should be invalid when key starts with $ and contains .', key => { - expect(ensureKeyIsValid(key)).toBe(`"${key}" cannot begin with '$' or contain a '.'`); + it.each([ + '$a.b', + '$.', + ])('"%s" should be invalid when key starts with $ and contains .', key => { + expect(ensureKeyIsValid(key, false)).toBe(`"${key}" cannot begin with '$' or contain a '.'`); }); - const name = NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME; - - it(`${name} as a key should be invalid`, () => { - expect(ensureKeyIsValid(name)).toBe(`"${name}" is a reserved key`); + it.each([ + '_', + ])('"%s" should be invalid when key is _', key => { + expect(ensureKeyIsValid(key, true)).toBe(`"${key}" is a reserved key`); }); - it.each(['a', 'ab', 'a$', 'a$b', 'a-b', `a${name}b`, `${name}ab`])('%s should be valid', key => { - expect(ensureKeyIsValid(key)).toBe(null); + it.each([ + '_', + 'a', + 'ab', + 'a$', + 'a$b', + 'a-b', + `a${NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME}b`, + `${NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME}ab`, + ])('"%s" should be valid as a nested key', key => { + expect(ensureKeyIsValid(key, false)).toBe(null); + }); + + it.each([ + 'a', + 'ab', + 'a$', + 'a$b', + 'a-b', + `a${NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME}b`, + `${NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME}ab`, + ])('"%s" should be valid as a root value', key => { + expect(ensureKeyIsValid(key, true)).toBe(null); + }); +}); + +describe('checkNestedKeys()', () => { + it('should check root property and error', () => { + const obj = { + 'base-url': 'https://api.insomnia.rest', + '$nes.ted': { + 'path-with-hyphens': '/path-with-hyphen', + }, + 'ar-ray': [ + '/first', + { + 'second': 'second', + }, + { + 'third': 'third', + }, + ], + }; + + const err = checkNestedKeys(obj); + + expect(err).toBe('"$nes.ted" cannot begin with \'$\' or contain a \'.\''); + }); + + it('should check nested property and error', () => { + const obj = { + 'base-url': 'https://api.insomnia.rest', + 'nested': { + '$path-wi.th-hyphens': '/path-with-hyphen', + }, + 'ar-ray': [ + '/first', + { + 'second': 'second', + }, + { + 'third': 'third', + }, + ], + }; + + const err = checkNestedKeys(obj); + + expect(err).toBe('"$path-wi.th-hyphens" cannot begin with \'$\' or contain a \'.\''); + }); + + it('should check for complex objects inside array', () => { + const obj = { + 'base-url': 'https://api.insomnia.rest', + 'nested': { + 'path-with-hyphens': '/path-with-hyphen', + }, + 'ar-ray': [ + '/first', + { + 'second': 'second', + }, + { + 'thi.rd': 'third', + }, + ], + }; + + const err = checkNestedKeys(obj); + + expect(err).toBe('"thi.rd" cannot begin with \'$\' or contain a \'.\''); + }); + + it('should check nested properties and pass', () => { + const obj = { + 'base-url': 'https://api.insomnia.rest', + 'nested': { + 'path-with-hyphens': '/path-with-hyphen', + }, + 'ar-ray': [ + '/first', + { + 'second': 'second', + }, + { + 'third': 'third', + }, + ], + }; + + const err = checkNestedKeys(obj); + + expect(err).toBe(null); }); }); diff --git a/packages/insomnia-app/app/ui/components/editors/environment-editor.tsx b/packages/insomnia-app/app/ui/components/editors/environment-editor.tsx index b7add5446d..fbded178ff 100644 --- a/packages/insomnia-app/app/ui/components/editors/environment-editor.tsx +++ b/packages/insomnia-app/app/ui/components/editors/environment-editor.tsx @@ -5,21 +5,52 @@ import React, { PureComponent } from 'react'; import { AUTOBIND_CFG, JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from '../../../common/constants'; import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '../../../templating'; import CodeEditor from '../codemirror/code-editor'; + // NeDB field names cannot begin with '$' or contain a period '.' // Docs: https://github.com/DeNA/nedb#inserting-documents const INVALID_NEDB_KEY_REGEX = /^\$|\./; -export const ensureKeyIsValid = (key: string): string | null => { + +export const ensureKeyIsValid = (key: string, isRoot: boolean): string | null => { if (key.match(INVALID_NEDB_KEY_REGEX)) { return `"${key}" cannot begin with '$' or contain a '.'`; } - if (key === NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME) { - return `"${NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME}" is a reserved key`; // verbiage WIP + if (key === NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME && isRoot) { + return `"${NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME}" is a reserved key`; } return null; }; +/** + * Recursively check nested keys in and immediately return when an invalid key found + */ +export function checkNestedKeys(obj: Record, isRoot = true): string | null { + for (const key in obj) { + let result: string | null = null; + + // Check current key + result = ensureKeyIsValid(key, isRoot); + + // Exit if necessary + if (result) { + return result; + } + + // Check nested keys + if (typeof obj[key] === 'object') { + result = checkNestedKeys(obj[key], false); + } + + // Exit if necessary + if (result) { + return result; + } + } + + return null; +} + export interface EnvironmentInfo { object: Record; propertyOrder: Record | null; @@ -64,14 +95,11 @@ class EnvironmentEditor extends PureComponent { } // Check for invalid key names - // TODO: these only check root properties, not nested properties if (value && value.object) { - for (const key of Object.keys(value.object)) { - error = ensureKeyIsValid(key); - - if (error) { - break; - } + // Check root and nested properties + const err = checkNestedKeys(value.object); + if (err) { + error = err; } }