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'); }); });