- .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>
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { onRequest } from '../src/middleware';
|
|
import { createSession, ADMIN_COOKIE } from '../src/lib/auth';
|
|
|
|
const NEXT_SENTINEL = new Response('next-ok', { status: 200 });
|
|
const next = async () => NEXT_SENTINEL;
|
|
|
|
function makeContext(url: string, cookieValue?: string) {
|
|
return {
|
|
url: new URL(url),
|
|
cookies: {
|
|
get: (name: string) =>
|
|
name === ADMIN_COOKIE && cookieValue !== undefined ? { value: cookieValue } : undefined,
|
|
},
|
|
redirect: (path: string) =>
|
|
new Response(null, { status: 302, headers: { Location: path } }),
|
|
} as Parameters<typeof onRequest>[0];
|
|
}
|
|
|
|
async function run(url: string, cookieValue?: string) {
|
|
return (await onRequest(makeContext(url, cookieValue), next as Parameters<typeof onRequest>[1])) as Response;
|
|
}
|
|
|
|
describe('middleware — routes publiques', () => {
|
|
it('/ sans cookie → next', async () => {
|
|
const res = await run('http://localhost/');
|
|
expect(res).toBe(NEXT_SENTINEL);
|
|
});
|
|
|
|
it('/admin/login sans cookie → next (page de login publique)', async () => {
|
|
const res = await run('http://localhost/admin/login');
|
|
expect(res).toBe(NEXT_SENTINEL);
|
|
});
|
|
|
|
it('/api/auth/login sans cookie → next (route publique)', async () => {
|
|
const res = await run('http://localhost/api/auth/login');
|
|
expect(res).toBe(NEXT_SENTINEL);
|
|
});
|
|
});
|
|
|
|
describe('middleware — pages admin', () => {
|
|
it('/admin sans cookie → redirect /admin/login', async () => {
|
|
const res = await run('http://localhost/admin');
|
|
expect(res.status).toBe(302);
|
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
|
});
|
|
|
|
it('/admin/projets sans cookie → redirect /admin/login', async () => {
|
|
const res = await run('http://localhost/admin/projets');
|
|
expect(res.status).toBe(302);
|
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
|
});
|
|
|
|
it('/admin/projets avec token valide → next', async () => {
|
|
const token = await createSession();
|
|
const res = await run('http://localhost/admin/projets', token);
|
|
expect(res).toBe(NEXT_SENTINEL);
|
|
});
|
|
|
|
it('/admin/projets avec token invalide → redirect /admin/login', async () => {
|
|
const res = await run('http://localhost/admin/projets', 'not.a.real.jwt');
|
|
expect(res.status).toBe(302);
|
|
expect(res.headers.get('Location')).toBe('/admin/login');
|
|
});
|
|
});
|
|
|
|
describe('middleware — APIs admin', () => {
|
|
it('/api/content/profile sans cookie → 401 JSON', async () => {
|
|
const res = await run('http://localhost/api/content/profile');
|
|
expect(res.status).toBe(401);
|
|
expect(await res.json()).toEqual({ error: 'unauthorized' });
|
|
});
|
|
|
|
it('/api/upload sans cookie → 401 JSON', async () => {
|
|
const res = await run('http://localhost/api/upload');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('/api/auth/logout sans cookie → 401 JSON', async () => {
|
|
const res = await run('http://localhost/api/auth/logout');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('/api/content/profile avec token invalide → 401 JSON', async () => {
|
|
const res = await run('http://localhost/api/content/profile', 'garbage');
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('/api/content/profile avec token valide → next', async () => {
|
|
const token = await createSession();
|
|
const res = await run('http://localhost/api/content/profile', token);
|
|
expect(res).toBe(NEXT_SENTINEL);
|
|
});
|
|
});
|