import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
import { join } from 'path';
import { resolveFilePlaceholders, hasFilePlaceholders, extractFilePlaceholders } from '../utils/filePlaceholderResolver.js';
import { FilePlaceholderError, FILE_PLACEHOLDER_ERROR_CODES } from '../utils/filePlaceholderTypes.js';
describe('File Placeholder Resolver', () => {
const testDir = join(process.cwd(), 'test-files');
const testFile1 = join(testDir, 'script1.js');
const testFile2 = join(testDir, 'template.html');
const testFile3 = join(testDir, 'config.json');
const largeFile = join(testDir, 'large.txt');
beforeEach(() => {
// Create test directory
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
// Create test files
writeFileSync(testFile1, 'console.log("Hello from script1");\nconst x = 42;');
writeFileSync(testFile2, '<div>Hello World</div>\n<p>This is a template</p>');
writeFileSync(testFile3, '{"name": "test", "value": 123}');
// Create a large file (2MB) for size testing
const largeContent = 'x'.repeat(2 * 1024 * 1024);
writeFileSync(largeFile, largeContent);
});
afterEach(() => {
// Clean up test files
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
describe('Basic placeholder resolution', () => {
it('should resolve simple file placeholder', () => {
const input = `{{file:${testFile1}}}`;
const result = resolveFilePlaceholders(input);
expect(result.data).toBe('console.log("Hello from script1");\nconst x = 42;');
expect(result.result.resolved).toBe(true);
expect(result.result.placeholderCount).toBe(1);
expect(result.result.resolvedFiles).toEqual([testFile1]);
});
it('should resolve multiple placeholders in single string', () => {
const input = `Script: {{file:${testFile1}}}\nTemplate: {{file:${testFile2}}}`;
const result = resolveFilePlaceholders(input);
expect(result.data).toContain('console.log("Hello from script1")');
expect(result.data).toContain('<div>Hello World</div>');
expect(result.result.placeholderCount).toBe(2);
expect(result.result.resolvedFiles).toEqual([testFile1, testFile2]);
});
it('should handle strings without placeholders', () => {
const input = 'This is a normal string without placeholders';
const result = resolveFilePlaceholders(input);
expect(result.data).toBe(input);
expect(result.result.resolved).toBe(false);
expect(result.result.placeholderCount).toBe(0);
});
});
describe('Object and array resolution', () => {
it('should resolve placeholders in objects', () => {
const input = {
name: 'test',
script: `{{file:${testFile1}}}`,
template: `{{file:${testFile2}}}`,
value: 123
};
const result = resolveFilePlaceholders(input);
expect(result.data.name).toBe('test');
expect(result.data.script).toContain('console.log("Hello from script1")');
expect(result.data.template).toContain('<div>Hello World</div>');
expect(result.data.value).toBe(123);
expect(result.result.placeholderCount).toBe(2);
});
it('should resolve placeholders in arrays', () => {
const input = [
`{{file:${testFile1}}}`,
'normal string',
`{{file:${testFile2}}}`
];
const result = resolveFilePlaceholders(input);
expect(result.data[0]).toContain('console.log("Hello from script1")');
expect(result.data[1]).toBe('normal string');
expect(result.data[2]).toContain('<div>Hello World</div>');
expect(result.result.placeholderCount).toBe(2);
});
it('should resolve placeholders in nested structures', () => {
const input = {
scripts: [
{ name: 'script1', content: `{{file:${testFile1}}}` },
{ name: 'script2', content: 'normal content' }
],
config: {
template: `{{file:${testFile2}}}`,
value: 42
}
};
const result = resolveFilePlaceholders(input);
expect(result.data.scripts[0].content).toContain('console.log("Hello from script1")');
expect(result.data.scripts[1].content).toBe('normal content');
expect(result.data.config.template).toContain('<div>Hello World</div>');
expect(result.data.config.value).toBe(42);
expect(result.result.placeholderCount).toBe(2);
});
});
describe('Error handling', () => {
it('should throw error for non-existent file', () => {
const input = '{{file:/nonexistent/file.js}}';
expect(() => resolveFilePlaceholders(input)).toThrow(FilePlaceholderError);
try {
resolveFilePlaceholders(input);
} catch (error) {
expect(error).toBeInstanceOf(FilePlaceholderError);
expect((error as FilePlaceholderError).code).toBe(FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_NOT_FOUND);
expect((error as FilePlaceholderError).placeholder).toBe('{{file:/nonexistent/file.js}}');
}
});
it('should throw error for file size exceeded', () => {
const input = `{{file:${largeFile}}}`;
expect(() => resolveFilePlaceholders(input)).toThrow(FilePlaceholderError);
try {
resolveFilePlaceholders(input);
} catch (error) {
expect(error).toBeInstanceOf(FilePlaceholderError);
expect((error as FilePlaceholderError).code).toBe(FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_SIZE_EXCEEDED);
}
});
it('should throw error for relative paths', () => {
const input = '{{file:test-files/script1.js}}';
expect(() => resolveFilePlaceholders(input)).toThrow(FilePlaceholderError);
try {
resolveFilePlaceholders(input);
} catch (error) {
expect(error).toBeInstanceOf(FilePlaceholderError);
expect((error as FilePlaceholderError).code).toBe(FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_INVALID_PATH);
expect((error as FilePlaceholderError).message).toContain('must be absolute');
}
});
it('should throw error for directory traversal attempts', () => {
const input = '{{file:/etc/../../../etc/passwd}}';
expect(() => resolveFilePlaceholders(input)).toThrow(FilePlaceholderError);
try {
resolveFilePlaceholders(input);
} catch (error) {
expect(error).toBeInstanceOf(FilePlaceholderError);
expect((error as FilePlaceholderError).code).toBe(FILE_PLACEHOLDER_ERROR_CODES.FILE_PLACEHOLDER_INVALID_PATH);
}
});
});
describe('Configuration', () => {
it('should use custom max file size', () => {
const input = `{{file:${largeFile}}}`;
// Should work with larger size limit
const result = resolveFilePlaceholders(input, { maxFileSize: 5 * 1024 * 1024 });
expect(result.result.resolved).toBe(true);
});
});
describe('Utility functions', () => {
it('should detect file placeholders', () => {
expect(hasFilePlaceholders('{{file:test.js}}')).toBe(true);
expect(hasFilePlaceholders('normal string')).toBe(false);
expect(hasFilePlaceholders('{{file:test.js}} and {{file:other.js}}')).toBe(true);
});
it('should extract file placeholders', () => {
const input = '{{file:test.js}} and {{file:other.js}} and normal text';
const placeholders = extractFilePlaceholders(input);
expect(placeholders).toEqual(['test.js', 'other.js']);
});
it('should handle malformed placeholders gracefully', () => {
const input = '{{file:test.js} and {{file:}} and normal text';
const result = resolveFilePlaceholders(input);
// Should not resolve any placeholders (all are malformed)
expect(result.data).toBe(input);
expect(result.result.placeholderCount).toBe(0);
});
});
describe('Edge cases', () => {
it('should handle empty strings', () => {
const result = resolveFilePlaceholders('');
expect(result.data).toBe('');
expect(result.result.resolved).toBe(false);
});
it('should handle null and undefined', () => {
expect(resolveFilePlaceholders(null).data).toBe(null);
expect(resolveFilePlaceholders(undefined).data).toBe(undefined);
});
it('should handle numbers and booleans', () => {
expect(resolveFilePlaceholders(42).data).toBe(42);
expect(resolveFilePlaceholders(true).data).toBe(true);
});
it('should preserve file content exactly', () => {
const input = '{{file:test-files/script1.js}}';
const result = resolveFilePlaceholders(input);
// Should preserve exact content including newlines
expect(result.data).toBe('console.log("Hello from script1");\nconst x = 42;');
});
});
});