mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-17 20:58:23 -04:00
chore: add more tests for allowed folder paths (#9679)
This commit is contained in:
@@ -266,7 +266,33 @@ export function unescapeForwardSlash(str: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
export const normalizeFolderPath = (p: string) => path.normalize(p).replace(/[/\\]+$/, '');
|
||||
export const normalizeFolderPath = (p: string) => {
|
||||
const normalized = path.normalize(p);
|
||||
// Preserve filesystem roots as-is (e.g. "/" on POSIX, "C:\" on Windows)
|
||||
if (normalized === path.parse(normalized).root) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.replace(/[/\\]+$/, '');
|
||||
};
|
||||
|
||||
export type FolderValidationResult =
|
||||
| { ok: true; normalizedValue: string }
|
||||
| { ok: false; error: string };
|
||||
|
||||
export function validateFolderInput(input: string, existing: string[]): FolderValidationResult {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') {
|
||||
return { ok: false, error: 'Enter a folder path to add.' };
|
||||
}
|
||||
const normalized = normalizeFolderPath(trimmed);
|
||||
if (trimmed !== normalized) {
|
||||
return { ok: false, error: `Invalid folder path format. Did you mean "${normalized}"?` };
|
||||
}
|
||||
if (existing.some(v => normalizeFolderPath(v) === normalized)) {
|
||||
return { ok: false, error: 'Duplicate folders are not allowed.' };
|
||||
}
|
||||
return { ok: true, normalizedValue: normalized };
|
||||
}
|
||||
|
||||
export function cannotAccessPathError(accessingPath: string): string {
|
||||
return process.type === 'renderer' || process.type === 'browser'
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { normalizeFolderPath } from '../../../common/misc';
|
||||
import { normalizeFolderPath, validateFolderInput } from '../../../common/misc';
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
describe('normalizeFolderPath', () => {
|
||||
describe.skipIf(isWindows)('POSIX paths', () => {
|
||||
it.each([
|
||||
// root is preserved as-is
|
||||
{ input: '/', expected: '/' },
|
||||
{ input: '///', expected: '/' },
|
||||
// trailing slash removal
|
||||
{ input: '/Users/foo/bar', expected: '/Users/foo/bar' },
|
||||
{ input: '/Users/foo/bar/', expected: '/Users/foo/bar' },
|
||||
{ input: '/Users/foo/bar///', expected: '/Users/foo/bar' },
|
||||
// duplicate separator collapse
|
||||
{ input: '/Users//foo//bar', expected: '/Users/foo/bar' },
|
||||
// real-world paths
|
||||
{ input: '/Volumes/External/data/', expected: '/Volumes/External/data' },
|
||||
{ input: '/Applications/Insomnia.app/Contents/', expected: '/Applications/Insomnia.app/Contents' },
|
||||
{ input: '/Users/名前/docs', expected: '/Users/名前/docs' },
|
||||
{ input: '/Users/my folder/docs', expected: '/Users/my folder/docs' },
|
||||
// ".." and "./" resolution (format error fires in the UI before storing)
|
||||
{ input: '/Users/foo/../bar', expected: '/Users/bar' },
|
||||
{ input: './relative/path', expected: 'relative/path' },
|
||||
])('normalizes "$input" to "$expected"', ({ input, expected }) => {
|
||||
expect(normalizeFolderPath(input)).toBe(expected);
|
||||
});
|
||||
@@ -20,6 +31,10 @@ describe('normalizeFolderPath', () => {
|
||||
|
||||
describe.runIf(isWindows)('Windows paths', () => {
|
||||
it.each([
|
||||
// root is preserved as-is
|
||||
{ input: 'C:\\', expected: 'C:\\' },
|
||||
{ input: 'C:\\\\', expected: 'C:\\' },
|
||||
// trailing backslash removal
|
||||
{ input: 'C:\\Users\\foo\\bar', expected: 'C:\\Users\\foo\\bar' },
|
||||
{ input: 'C:\\Users\\foo\\bar\\', expected: 'C:\\Users\\foo\\bar' },
|
||||
{ input: 'C:\\Users\\foo\\bar\\\\\\', expected: 'C:\\Users\\foo\\bar' },
|
||||
@@ -28,4 +43,131 @@ describe('normalizeFolderPath', () => {
|
||||
expect(normalizeFolderPath(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not throw on a path longer than 4096 characters', () => {
|
||||
expect(() => normalizeFolderPath('/' + 'a'.repeat(4096))).not.toThrow();
|
||||
});
|
||||
|
||||
it('is case sensitive', () => {
|
||||
expect(normalizeFolderPath('/Users/foo')).not.toBe(normalizeFolderPath('/Users/FOO'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateFolderInput', () => {
|
||||
describe('empty input', () => {
|
||||
it('returns error for empty string', () => {
|
||||
expect(validateFolderInput('', [])).toEqual({ ok: false, error: 'Enter a folder path to add.' });
|
||||
});
|
||||
|
||||
it('returns error for whitespace-only input (trimmed to empty)', () => {
|
||||
expect(validateFolderInput(' ', [])).toEqual({ ok: false, error: 'Enter a folder path to add.' });
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(isWindows)('format validation (POSIX)', () => {
|
||||
it('suggests normalized form when input has a trailing slash', () => {
|
||||
expect(validateFolderInput('/Users/foo/', [])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "/Users/foo"?',
|
||||
});
|
||||
});
|
||||
|
||||
it('suggests normalized form when input has ".." segments', () => {
|
||||
expect(validateFolderInput('/Users/foo/../bar', [])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "/Users/bar"?',
|
||||
});
|
||||
});
|
||||
|
||||
it('suggests normalized form when input has a "./" prefix', () => {
|
||||
expect(validateFolderInput('./relative/path', [])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "relative/path"?',
|
||||
});
|
||||
});
|
||||
|
||||
it('format error takes priority over duplicate error', () => {
|
||||
// "/Users/foo/" !== "/Users/foo", so format check fires before duplicate check
|
||||
expect(validateFolderInput('/Users/foo/', ['/Users/foo'])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "/Users/foo"?',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(isWindows)('format validation (Windows)', () => {
|
||||
it('suggests normalized form when input has a trailing backslash', () => {
|
||||
expect(validateFolderInput('C:\\Users\\foo\\', [])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "C:\\Users\\foo"?',
|
||||
});
|
||||
});
|
||||
|
||||
it('suggests normalized form when input has ".." segments', () => {
|
||||
expect(validateFolderInput('C:\\Users\\foo\\..\\bar', [])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "C:\\Users\\bar"?',
|
||||
});
|
||||
});
|
||||
|
||||
it('format error takes priority over duplicate error', () => {
|
||||
expect(validateFolderInput('C:\\Users\\foo\\', ['C:\\Users\\foo'])).toEqual({
|
||||
ok: false,
|
||||
error: 'Invalid folder path format. Did you mean "C:\\Users\\foo"?',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(isWindows)('duplicate detection (POSIX)', () => {
|
||||
it('returns duplicate error when the exact path is already in the list', () => {
|
||||
expect(validateFolderInput('/Users/foo', ['/Users/foo'])).toEqual({
|
||||
ok: false,
|
||||
error: 'Duplicate folders are not allowed.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no error when the path is not in the list', () => {
|
||||
expect(validateFolderInput('/Users/bar', ['/Users/foo'])).toEqual({
|
||||
ok: true,
|
||||
normalizedValue: '/Users/bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no error when the list is empty', () => {
|
||||
expect(validateFolderInput('/Users/foo', [])).toEqual({
|
||||
ok: true,
|
||||
normalizedValue: '/Users/foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(isWindows)('duplicate detection (Windows)', () => {
|
||||
it('returns duplicate error when the exact path is already in the list', () => {
|
||||
expect(validateFolderInput('C:\\Users\\foo', ['C:\\Users\\foo'])).toEqual({
|
||||
ok: false,
|
||||
error: 'Duplicate folders are not allowed.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no error when the path is not in the list', () => {
|
||||
expect(validateFolderInput('C:\\Users\\bar', ['C:\\Users\\foo'])).toEqual({
|
||||
ok: true,
|
||||
normalizedValue: 'C:\\Users\\bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns no error when the list is empty', () => {
|
||||
expect(validateFolderInput('C:\\Users\\foo', [])).toEqual({
|
||||
ok: true,
|
||||
normalizedValue: 'C:\\Users\\foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skipIf(isWindows)('trims leading and trailing whitespace before processing', () => {
|
||||
expect(validateFolderInput(' /Users/docs ', [])).toEqual({
|
||||
ok: true,
|
||||
normalizedValue: '/Users/docs',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ListBox, ListBoxItem } from 'react-aria-components';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import { normalizeFolderPath } from '../../../common/misc';
|
||||
import { validateFolderInput } from '../../../common/misc';
|
||||
import type { SettingsOfType } from '../../../common/settings';
|
||||
import { useSettingsPatcher } from '../../hooks/use-request';
|
||||
import { PromptButton } from '../base/prompt-button';
|
||||
@@ -30,22 +30,12 @@ export const TextArraySetting: FC<{
|
||||
}
|
||||
|
||||
const onAddDataFolder = useCallback(async () => {
|
||||
const validValue = folderToAdd ? folderToAdd.trim() : '';
|
||||
if (validValue === '') {
|
||||
setValidationError('Enter a folder path to add.');
|
||||
const result = validateFolderInput(folderToAdd, currentValue);
|
||||
if (!result.ok) {
|
||||
setValidationError(result.error);
|
||||
return;
|
||||
}
|
||||
const normalizedValue = normalizeFolderPath(validValue);
|
||||
if (validValue !== normalizedValue) {
|
||||
setValidationError(`Invalid path format. Did you mean "${normalizedValue}"?`);
|
||||
return;
|
||||
}
|
||||
const exists = currentValue.some(v => normalizeFolderPath(v) === normalizedValue);
|
||||
if (exists) {
|
||||
setValidationError('Duplicate folders are not allowed.');
|
||||
return;
|
||||
}
|
||||
const updatedValue = [...currentValue, validValue];
|
||||
const updatedValue = [...currentValue, result.normalizedValue];
|
||||
patchSettings({ [setting]: updatedValue });
|
||||
setFolderToAdd('');
|
||||
setValidationError('');
|
||||
@@ -115,7 +105,7 @@ export const TextArraySetting: FC<{
|
||||
confirmMessage=""
|
||||
doneMessage=""
|
||||
onClick={() => onDeleteDataFolder(dataFolderPath)}
|
||||
title="Delete cookie"
|
||||
title="Delete folder"
|
||||
>
|
||||
<i className="fa fa-trash-o" />
|
||||
</PromptButton>
|
||||
|
||||
Reference in New Issue
Block a user