136 lines
5.7 KiB
TypeScript
136 lines
5.7 KiB
TypeScript
/**
|
|
* Unit tests for FileSystemProjectScaffolder.
|
|
*
|
|
* Drives against a real temp directory so end-to-end file IO + path-traversal
|
|
* defenses are exercised.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
FileSystemProjectScaffolder,
|
|
validateProjectName,
|
|
} from '../src/scaffolder/projectScaffolder';
|
|
|
|
function tmp(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'astra-scaffold-test-'));
|
|
}
|
|
|
|
describe('validateProjectName', () => {
|
|
test('accepts allowed names', () => {
|
|
expect(validateProjectName('foo')).toBe('foo');
|
|
expect(validateProjectName('foo-bar_v2')).toBe('foo-bar_v2');
|
|
expect(validateProjectName(' trimmed ')).toBe('trimmed');
|
|
});
|
|
|
|
test('rejects too short', () => {
|
|
expect(validateProjectName('a')).toBeNull();
|
|
});
|
|
|
|
test('rejects too long', () => {
|
|
expect(validateProjectName('a'.repeat(41))).toBeNull();
|
|
});
|
|
|
|
test('rejects invalid chars', () => {
|
|
expect(validateProjectName('foo bar')).toBeNull();
|
|
expect(validateProjectName('foo/bar')).toBeNull();
|
|
expect(validateProjectName('foo.bar')).toBeNull();
|
|
expect(validateProjectName('한글이름')).toBeNull();
|
|
});
|
|
|
|
test('rejects empty / non-string', () => {
|
|
expect(validateProjectName('')).toBeNull();
|
|
expect(validateProjectName(undefined as any)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('FileSystemProjectScaffolder', () => {
|
|
let root: string;
|
|
let scaffolder: FileSystemProjectScaffolder;
|
|
|
|
beforeEach(() => {
|
|
root = tmp();
|
|
scaffolder = new FileSystemProjectScaffolder();
|
|
});
|
|
|
|
afterEach(() => {
|
|
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
});
|
|
|
|
test('listTemplates returns the catalog', () => {
|
|
const list = scaffolder.listTemplates();
|
|
const ids = list.map(t => t.id);
|
|
expect(ids).toContain('static');
|
|
expect(ids).toContain('vite-vanilla');
|
|
expect(ids).toContain('vite-react');
|
|
});
|
|
|
|
test('static template writes index.html + README.md', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'demo-static', template: 'static', rootDir: root });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
expect(fs.existsSync(path.join(result.projectPath, 'site', 'index.html'))).toBe(true);
|
|
expect(fs.existsSync(path.join(result.projectPath, 'README.md'))).toBe(true);
|
|
const html = fs.readFileSync(path.join(result.projectPath, 'site', 'index.html'), 'utf8');
|
|
expect(html).toContain('demo-static');
|
|
});
|
|
|
|
test('vite-vanilla template writes package.json + main.js', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'vv', template: 'vite-vanilla', rootDir: root });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(result.projectPath, 'site', 'package.json'), 'utf8'));
|
|
expect(pkg.name).toBe('vv');
|
|
expect(pkg.devDependencies.vite).toBeDefined();
|
|
expect(fs.existsSync(path.join(result.projectPath, 'site', 'main.js'))).toBe(true);
|
|
});
|
|
|
|
test('vite-react template includes tsconfig + main.tsx', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'vr', template: 'vite-react', rootDir: root });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
expect(fs.existsSync(path.join(result.projectPath, 'site', 'tsconfig.json'))).toBe(true);
|
|
expect(fs.existsSync(path.join(result.projectPath, 'site', 'src', 'main.tsx'))).toBe(true);
|
|
const tsx = fs.readFileSync(path.join(result.projectPath, 'site', 'src', 'main.tsx'), 'utf8');
|
|
expect(tsx).toContain('<h1>vr</h1>');
|
|
});
|
|
|
|
test('rejects invalid name with INVALID_NAME', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'foo bar', template: 'static', rootDir: root });
|
|
expect(result).toEqual(expect.objectContaining({ ok: false, code: 'INVALID_NAME' }));
|
|
});
|
|
|
|
test('rejects unknown template with UNKNOWN_TEMPLATE', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'foo', template: 'made-up' as any, rootDir: root });
|
|
expect(result).toEqual(expect.objectContaining({ ok: false, code: 'UNKNOWN_TEMPLATE' }));
|
|
});
|
|
|
|
test('rejects empty rootDir with NO_ROOT_DIR', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'foo', template: 'static', rootDir: '' });
|
|
expect(result).toEqual(expect.objectContaining({ ok: false, code: 'NO_ROOT_DIR' }));
|
|
});
|
|
|
|
test('rejects relative rootDir with ROOT_NOT_ABSOLUTE', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'foo', template: 'static', rootDir: 'relative/path' });
|
|
expect(result).toEqual(expect.objectContaining({ ok: false, code: 'ROOT_NOT_ABSOLUTE' }));
|
|
});
|
|
|
|
test('rejects when target already exists with ALREADY_EXISTS', async () => {
|
|
const r1 = await scaffolder.scaffold({ name: 'twice', template: 'static', rootDir: root });
|
|
expect(r1.ok).toBe(true);
|
|
const r2 = await scaffolder.scaffold({ name: 'twice', template: 'static', rootDir: root });
|
|
expect(r2).toEqual(expect.objectContaining({ ok: false, code: 'ALREADY_EXISTS' }));
|
|
});
|
|
|
|
test('reports filesWritten for downstream UI', async () => {
|
|
const result = await scaffolder.scaffold({ name: 'count', template: 'vite-react', rootDir: root });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
expect(result.filesWritten.length).toBeGreaterThanOrEqual(5);
|
|
for (const file of result.filesWritten) {
|
|
expect(fs.existsSync(file)).toBe(true);
|
|
}
|
|
});
|
|
});
|