/** * Unit tests for validation utility functions */ import { describe, it, expect } from 'vitest'; import { sanitizeAssetName, normalizeAndSanitizeAssetPath, validatePathLength, validateAssetParams, ensureVector3, ensureRotation, resolveSkeletalMeshPath } from './validation.js'; import { sanitizePath } from '../paths/path-security.js'; describe('sanitizeAssetName', () => { it('removes invalid characters', () => { // ! is replaced with _, then trailing _ is stripped expect(sanitizeAssetName('My Asset!')).toBe('My_Asset'); }); it('removes leading/trailing whitespace', () => { expect(sanitizeAssetName(' MyAsset ')).toBe('MyAsset'); }); it('preserves valid names', () => { expect(sanitizeAssetName('ValidName_123')).toBe('ValidName_123'); }); it('replaces spaces with underscores', () => { expect(sanitizeAssetName('My Cool Asset')).toBe('My_Cool_Asset'); }); it('handles empty strings', () => { const result = sanitizeAssetName(''); expect(result.length).toBeGreaterThan(0); }); it('removes consecutive underscores', () => { const result = sanitizeAssetName('My__Asset'); expect(result).not.toContain('__'); }); it('sanitizes SQL-like patterns consistently across repeated calls', () => { expect(sanitizeAssetName('DROP DELETE Table')).toBe('Table'); expect(sanitizeAssetName('DROP DELETE Table')).toBe('Table'); }); it('handles reserved keywords case-insensitively', () => { expect(sanitizeAssetName('None')).toBe('None_Asset'); expect(sanitizeAssetName('CLASS')).toBe('CLASS_Asset'); expect(sanitizeAssetName('native')).toBe('native_Asset'); }); }); describe('sanitizePath', () => { it('normalizes forward slashes', () => { const result = sanitizePath('/Game/MyAsset'); expect(result).toContain('/'); expect(result).not.toContain('\\\\'); }); it('removes double slashes', () => { const result = sanitizePath('/Game//MyAsset'); expect(result).not.toContain('//'); }); it('handles backslashes', () => { const result = sanitizePath('\\Game\\MyAsset'); expect(result).toContain('/'); }); it('sanitizes path segments with dots', () => { expect(() => sanitizePath('/Game/../MyAsset')).toThrow( 'directory traversal (..) is not allowed' ); }); it('preserves Niagara root paths', () => { expect(normalizeAndSanitizeAssetPath('/Niagara/Modules/EmitterState')).toBe('/Niagara/Modules/EmitterState'); }); }); describe('validatePathLength', () => { it('accepts valid short paths', () => { const result = validatePathLength('/Game/MyAsset'); expect(result.valid).toBe(true); }); it('accepts empty paths (length 0 < 260)', () => { const result = validatePathLength(''); // Empty string has length 0, which is < 260, so it's valid expect(result.valid).toBe(true); }); it('rejects excessively long paths', () => { const longPath = '/Game/' + 'a'.repeat(300); const result = validatePathLength(longPath); expect(result.valid).toBe(false); expect(result.error).toBeDefined(); }); }); describe('validateAssetParams', () => { it('validates correct create params', () => { const result = validateAssetParams({ action: 'create', name: 'MyAsset', path: '/Game/Assets' }); expect(result.valid).toBe(true); }); it('handles names that need sanitization', () => { const result = validateAssetParams({ name: '', savePath: '/Game' }); // Empty name gets sanitized to 'Asset', so this should be valid expect(result.valid).toBe(true); }); it('validates path params', () => { const result = validateAssetParams({ name: 'Target', savePath: '/Game/Source' }); expect(result.valid).toBe(true); }); }); describe('ensureVector3', () => { it('accepts object format with x, y, z', () => { const result = ensureVector3({ x: 1, y: 2, z: 3 }, 'location'); expect(result).toEqual([1, 2, 3]); }); it('accepts array format', () => { const result = ensureVector3([1, 2, 3], 'location'); expect(result).toEqual([1, 2, 3]); }); it('throws on invalid object', () => { expect(() => ensureVector3({ x: 1 }, 'location')).toThrow(); }); it('throws on wrong array length', () => { expect(() => ensureVector3([1, 2], 'location')).toThrow(); }); it('throws on non-number values', () => { expect(() => ensureVector3({ x: 'a', y: 2, z: 3 }, 'location')).toThrow(); }); }); describe('ensureRotation', () => { it('accepts object format with pitch, yaw, roll', () => { const result = ensureRotation({ pitch: 0, yaw: 90, roll: 0 }, 'rotation'); expect(result).toEqual([0, 90, 0]); }); it('accepts array format', () => { const result = ensureRotation([0, 90, 0], 'rotation'); expect(result).toEqual([0, 90, 0]); }); it('throws on invalid input', () => { expect(() => ensureRotation('invalid', 'rotation')).toThrow(); }); }); describe('resolveSkeletalMeshPath', () => { it('maps known skeleton paths to their mesh paths', () => { expect(resolveSkeletalMeshPath('/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton')).toBe( '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple' ); }); it('converts common skeleton names to skeletal mesh names', () => { expect(resolveSkeletalMeshPath('/Game/Characters/UE5_Quinn_Skeleton')).toBe( '/Game/Characters/SKM_Quinn' ); }); it('returns null for path traversal attempts', () => { expect(resolveSkeletalMeshPath('/Game/../Bad_Skeleton')).toBeNull(); }); });