- .env.example: vider les secrets (à rotater côté Plesk)
- src/lib/auth.ts: fail-fast lazy si ADMIN_JWT_SECRET < 32 chars
- src/pages/api/upload.ts: retirer .svg (anti-XSS) + try/catch FS
- src/pages/api/content/[section].ts: masquer err.message (sauf ZodError)
- retirer gsap/three/@types/three/@react-three/{fiber,drei}/lucide-react
- bump astro 6.1.10 (GHSA-xr5h-phrj-8vxv)
- zod 4: z.email()/z.url() au lieu de .email()/.url()
- nettoyer vars inutilisées + Header script is:inline
Vitest (38 tests, 615ms):
- tests/lib/auth.test.ts: verifyPassword + verifySession (round-trip, secrets distincts)
- tests/lib/content.test.ts: load/save + comportement seed
- tests/api/files.test.ts: path traversal (../, /, imbriqué) + 200
- tests/api/upload.test.ts: extension/taille/sanitize folder + bucket
- tests/middleware.test.ts: gating admin pages vs APIs vs routes publiques
- astro check: 0 errors / 0 warnings / 0 hints
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
78 lines
2.9 KiB
TypeScript
78 lines
2.9 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { mkdtemp, mkdir, readFile, writeFile, rm, copyFile, readdir } from 'node:fs/promises';
|
|
import { join, resolve } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
const PROJECT_ROOT = resolve(import.meta.dirname, '..', '..');
|
|
const REAL_SEED_DIR = join(PROJECT_ROOT, 'public', 'content');
|
|
let workDir: string;
|
|
type ContentModule = typeof import('../../src/lib/content');
|
|
let content: ContentModule;
|
|
|
|
async function setupTmpRoot() {
|
|
workDir = await mkdtemp(join(tmpdir(), 'portfolio-content-'));
|
|
await mkdir(join(workDir, 'data', 'content'), { recursive: true });
|
|
const tmpSeedDir = join(workDir, 'public', 'content');
|
|
await mkdir(tmpSeedDir, { recursive: true });
|
|
for (const file of await readdir(REAL_SEED_DIR)) {
|
|
await copyFile(join(REAL_SEED_DIR, file), join(tmpSeedDir, file));
|
|
}
|
|
vi.spyOn(process, 'cwd').mockReturnValue(workDir);
|
|
vi.resetModules();
|
|
content = await import('../../src/lib/content');
|
|
}
|
|
|
|
async function cleanup() {
|
|
vi.restoreAllMocks();
|
|
if (workDir) await rm(workDir, { recursive: true, force: true });
|
|
}
|
|
|
|
beforeEach(setupTmpRoot);
|
|
afterEach(cleanup);
|
|
|
|
describe('loadContent', () => {
|
|
it('copie le seed depuis public/content quand data/content/<key>.json est absent', async () => {
|
|
const profile = await content.loadContent('profile');
|
|
expect(profile.name).toBeTruthy();
|
|
const dataFile = await readFile(join(workDir, 'data', 'content', 'profile.json'), 'utf-8');
|
|
expect(JSON.parse(dataFile).name).toBe(profile.name);
|
|
});
|
|
|
|
it('lit data/content/<key>.json quand il existe (pas le seed)', async () => {
|
|
const seed = JSON.parse(
|
|
await readFile(join(workDir, 'public', 'content', 'profile.json'), 'utf-8'),
|
|
);
|
|
const customized = { ...seed, name: 'CustomFromData' };
|
|
await writeFile(
|
|
join(workDir, 'data', 'content', 'profile.json'),
|
|
JSON.stringify(customized),
|
|
'utf-8',
|
|
);
|
|
const profile = await content.loadContent('profile');
|
|
expect(profile.name).toBe('CustomFromData');
|
|
});
|
|
});
|
|
|
|
describe('saveContent', () => {
|
|
it('écrit data/content/<key>.json et retourne les données validées', async () => {
|
|
const seed = JSON.parse(
|
|
await readFile(join(workDir, 'public', 'content', 'profile.json'), 'utf-8'),
|
|
);
|
|
const updated = { ...seed, name: 'NouveauNom' };
|
|
const saved = await content.saveContent('profile', updated);
|
|
expect(saved.name).toBe('NouveauNom');
|
|
const onDisk = JSON.parse(
|
|
await readFile(join(workDir, 'data', 'content', 'profile.json'), 'utf-8'),
|
|
);
|
|
expect(onDisk.name).toBe('NouveauNom');
|
|
});
|
|
|
|
it('rejette un payload invalide (ZodError)', async () => {
|
|
await expect(content.saveContent('profile', { bogus: true })).rejects.toThrow();
|
|
});
|
|
|
|
it('rejette null', async () => {
|
|
await expect(content.saveContent('profile', null)).rejects.toThrow();
|
|
});
|
|
});
|