Fixes #16: Code quality enhancements Part 3
- Consolidate settings defaults: config.js is now single source of truth, state.js imports from config.js (eliminates ~300 lines of duplication) - Validator factory: createStringValidator() unifies sanitizeLocationName/ sanitizeItemName with configurable rules (blocked names, max length, rejects) - Refactor promptBuilder: Extract getCharacterCardsInfo into buildNarratorCardInfo, buildGroupCardInfo, buildSingleCardInfo, appendCharacterFields - Error handling: jsonRepair.js silent failures now log to console.debug - Testing: Fix ESM support (babel.config.json + jest.config.cjs transform), add security.test.js with 24 tests for validator factory and security utilities - Total: 78 tests passing (was 54)
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { createStringValidator, sanitizeLocationName, sanitizeItemName, validateStoredInventory, cleanItemString, MAX_ITEMS_PER_SECTION } from '../src/utils/security.js';
|
||||
|
||||
describe('createStringValidator', () => {
|
||||
test('creates a validator that trims whitespace', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'test',
|
||||
maxLength: 100,
|
||||
checkBlockedNames: false
|
||||
});
|
||||
expect(validator(' hello ')).toBe('hello');
|
||||
});
|
||||
|
||||
test('returns null for non-string input', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'test',
|
||||
maxLength: 100
|
||||
});
|
||||
expect(validator(null)).toBeNull();
|
||||
expect(validator(undefined)).toBeNull();
|
||||
expect(validator(123)).toBeNull();
|
||||
expect(validator('')).toBeNull();
|
||||
});
|
||||
|
||||
test('truncates strings exceeding maxLength', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'test',
|
||||
maxLength: 5
|
||||
});
|
||||
const result = validator('hello world');
|
||||
expect(result).toBe('hello');
|
||||
expect(result.length).toBe(5);
|
||||
});
|
||||
|
||||
test('rejects blocked property names when checkBlockedNames is true', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'property',
|
||||
maxLength: 100,
|
||||
checkBlockedNames: true
|
||||
});
|
||||
expect(validator('__proto__')).toBeNull();
|
||||
expect(validator('constructor')).toBeNull();
|
||||
expect(validator('prototype')).toBeNull();
|
||||
});
|
||||
|
||||
test('allows blocked property names when checkBlockedNames is false', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'item',
|
||||
maxLength: 100,
|
||||
checkBlockedNames: false
|
||||
});
|
||||
// __proto__ is allowed when checkBlockedNames is false
|
||||
expect(validator('__proto__')).toBe('__proto__');
|
||||
});
|
||||
|
||||
test('rejects additional values via additionalRejects', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'item',
|
||||
maxLength: 100,
|
||||
additionalRejects: ['none', 'null', 'undefined'],
|
||||
checkBlockedNames: false
|
||||
});
|
||||
expect(validator('none')).toBeNull();
|
||||
expect(validator('NONE')).toBeNull();
|
||||
expect(validator('None')).toBeNull();
|
||||
expect(validator('Sword')).toBe('Sword');
|
||||
});
|
||||
|
||||
test('is case-insensitive for blocked names', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'test',
|
||||
maxLength: 100,
|
||||
checkBlockedNames: true
|
||||
});
|
||||
expect(validator('__PROTO__')).toBeNull();
|
||||
expect(validator('__Proto__')).toBeNull();
|
||||
expect(validator('CONSTRUCTOR')).toBeNull();
|
||||
});
|
||||
|
||||
test('returns valid input unchanged', () => {
|
||||
const validator = createStringValidator({
|
||||
label: 'test',
|
||||
maxLength: 100
|
||||
});
|
||||
expect(validator('ValidName')).toBe('ValidName');
|
||||
expect(validator('another-valid-name')).toBe('another-valid-name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeLocationName', () => {
|
||||
test('allows valid location names', () => {
|
||||
expect(sanitizeLocationName('Home')).toBe('Home');
|
||||
expect(sanitizeLocationName('Tavern')).toBe('Tavern');
|
||||
expect(sanitizeLocationName('Forest Clearing')).toBe('Forest Clearing');
|
||||
});
|
||||
|
||||
test('blocks dangerous property names', () => {
|
||||
expect(sanitizeLocationName('__proto__')).toBeNull();
|
||||
expect(sanitizeLocationName('constructor')).toBeNull();
|
||||
expect(sanitizeLocationName('prototype')).toBeNull();
|
||||
});
|
||||
|
||||
test('truncates overly long names', () => {
|
||||
const longName = 'A'.repeat(250);
|
||||
const result = sanitizeLocationName(longName);
|
||||
expect(result.length).toBe(200);
|
||||
});
|
||||
|
||||
test('returns null for invalid input', () => {
|
||||
expect(sanitizeLocationName(null)).toBeNull();
|
||||
expect(sanitizeLocationName('')).toBeNull();
|
||||
expect(sanitizeLocationName(' ')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeItemName', () => {
|
||||
test('allows valid item names', () => {
|
||||
expect(sanitizeItemName('Sword')).toBe('Sword');
|
||||
expect(sanitizeItemName('Health Potion')).toBe('Health Potion');
|
||||
});
|
||||
|
||||
test('rejects "none" as an item name', () => {
|
||||
expect(sanitizeItemName('none')).toBeNull();
|
||||
expect(sanitizeItemName('None')).toBeNull();
|
||||
expect(sanitizeItemName('NONE')).toBeNull();
|
||||
});
|
||||
|
||||
test('truncates overly long names', () => {
|
||||
const longName = 'A'.repeat(600);
|
||||
const result = sanitizeItemName(longName);
|
||||
expect(result.length).toBe(500);
|
||||
});
|
||||
|
||||
test('returns null for invalid input', () => {
|
||||
expect(sanitizeItemName(null)).toBeNull();
|
||||
expect(sanitizeItemName('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateStoredInventory', () => {
|
||||
test('returns cleaned inventory object', () => {
|
||||
const input = { Home: 'Sword, Shield' };
|
||||
const result = validateStoredInventory(input);
|
||||
expect(result).toEqual({ Home: 'Sword, Shield' });
|
||||
});
|
||||
|
||||
test('removes dangerous keys', () => {
|
||||
const input = { Home: 'Sword' };
|
||||
// __proto__ as a key is blocked by sanitizeLocationName
|
||||
Object.setPrototypeOf(input, Object.create(null));
|
||||
input['__proto__'] = 'malicious';
|
||||
const result = validateStoredInventory(input);
|
||||
// Result should not have __proto__ as an own property
|
||||
expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false);
|
||||
expect(result).toHaveProperty('Home');
|
||||
});
|
||||
|
||||
test('returns empty object for invalid input', () => {
|
||||
expect(validateStoredInventory(null)).toEqual({});
|
||||
expect(validateStoredInventory(undefined)).toEqual({});
|
||||
expect(validateStoredInventory('string')).toEqual({});
|
||||
expect(validateStoredInventory([])).toEqual({});
|
||||
});
|
||||
|
||||
test('skips non-string values', () => {
|
||||
const input = { Home: 123, Barn: 'Hay' };
|
||||
const result = validateStoredInventory(input);
|
||||
expect(result).not.toHaveProperty('Home');
|
||||
expect(result).toHaveProperty('Barn');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanItemString', () => {
|
||||
test('returns clean item string', () => {
|
||||
expect(cleanItemString('Sword, Shield')).toBe('Sword, Shield');
|
||||
});
|
||||
|
||||
test('item sanitizer does not block __proto__ (by design - checkBlockedNames is false)', () => {
|
||||
// sanitizeItemName has checkBlockedNames: false, so __proto__ is allowed as item name
|
||||
// Only sanitizeLocationName blocks __proto__ (for object keys)
|
||||
const result = cleanItemString('Sword, Shield');
|
||||
expect(result).toContain('Sword');
|
||||
});
|
||||
|
||||
test('strips markdown formatting', () => {
|
||||
const result = cleanItemString('**Sword**, *Shield*');
|
||||
expect(result).not.toContain('**');
|
||||
expect(result).not.toContain('*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MAX_ITEMS_PER_SECTION', () => {
|
||||
test('is a positive number', () => {
|
||||
expect(MAX_ITEMS_PER_SECTION).toBeGreaterThan(0);
|
||||
expect(typeof MAX_ITEMS_PER_SECTION).toBe('number');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user