82a4cd2000
Cast di FakeContext a onRequest via 'unknown' invece che diretto: i due tipi non si sovrappongono abbastanza per tsc (FakeContext è un doppio minimale, non un APIContext completo). Il typecheck è il meccanismo di sicurezza di questo motore: uno rosso in permanenza è un cancello che nessuno guarda più. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XhLbMQ1q7wHwJSykgXRwQF
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
// Il difetto più serio di questo ramo era "una funzione di autorizzazione corretta che
|
|
// nessuno chiama": isProtectedPath e canAccessAdminPath erano giuste, ma finché niente le
|
|
// collegava dentro src/middleware.ts le pagine sanitarie restavano pubbliche. Nessun altro
|
|
// test in tutto il repo importa src/middleware.ts: qui lo si carica davvero (vedi l'alias
|
|
// 'astro:middleware' in vitest.config.ts, necessario perché quel modulo è virtuale e
|
|
// risolto solo dentro la pipeline vite di Astro) e si esercita la funzione vera, non una
|
|
// sua reimplementazione.
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
|
|
// getDb() (usato da src/middleware.ts) apre 'data/insanitylab.db' su disco per default:
|
|
// DB_PATH va puntato a :memory: PRIMA che qualcosa lo chiami (il singleton è pigro).
|
|
process.env.DB_PATH = ':memory:';
|
|
|
|
type FakeContext = {
|
|
url: URL;
|
|
cookies: { get: (name: string) => { value: string } | undefined };
|
|
redirect: (path: string, status?: number) => Response;
|
|
locals: Record<string, unknown>;
|
|
};
|
|
|
|
function contesto(pathname: string, cookieValue?: string): FakeContext {
|
|
return {
|
|
url: new URL(`http://localhost${pathname}`),
|
|
cookies: { get: () => (cookieValue !== undefined ? { value: cookieValue } : undefined) },
|
|
redirect: (path, status) => new Response(null, { status: status ?? 302, headers: { Location: path } }),
|
|
locals: {},
|
|
};
|
|
}
|
|
|
|
function next() {
|
|
let chiamata = false;
|
|
const fn = async () => { chiamata = true; return new Response('PAGINA-SERVITA'); };
|
|
return { fn, fuChiamata: () => chiamata };
|
|
}
|
|
|
|
describe('src/middleware.ts — protezione reale delle rotte longevity', () => {
|
|
let onRequest: (context: FakeContext, next: () => Promise<Response>) => Response | Promise<Response>;
|
|
|
|
beforeAll(async () => {
|
|
const mod = await import('../../src/middleware.ts');
|
|
// FakeContext è un doppio minimale di APIContext (solo cio' che onRequest usa
|
|
// davvero: url, cookies, redirect, locals), non un APIContext completo: il cast
|
|
// diretto viene rifiutato dal typecheck perche' i due tipi non si sovrappongono
|
|
// abbastanza. Si passa da 'unknown' perche' e' un cast intenzionale, non un bug.
|
|
onRequest = mod.onRequest as unknown as typeof onRequest;
|
|
});
|
|
|
|
it('una rotta longevity senza sessione (nessun cookie) redirige al login e non serve la pagina', async () => {
|
|
const ctx = contesto('/longevity/io');
|
|
const n = next();
|
|
const res = await onRequest(ctx, n.fn);
|
|
|
|
expect(n.fuChiamata()).toBe(false);
|
|
expect(res).toBeInstanceOf(Response);
|
|
expect([301, 302, 303, 307, 308]).toContain((res as Response).status);
|
|
expect((res as Response).headers.get('Location')).toMatch(/^\/login/);
|
|
});
|
|
|
|
it('una rotta longevity con un token di sessione inesistente redirige comunque al login', async () => {
|
|
const ctx = contesto('/longevity/gestionale', 'token-che-non-esiste-in-nessuna-sessione');
|
|
const n = next();
|
|
const res = await onRequest(ctx, n.fn);
|
|
|
|
expect(n.fuChiamata()).toBe(false);
|
|
expect((res as Response).headers.get('Location')).toMatch(/^\/login/);
|
|
});
|
|
|
|
it('una API longevity senza sessione risponde 401, non serve la richiesta', async () => {
|
|
const ctx = contesto('/api/longevity/qualcosa');
|
|
const n = next();
|
|
const res = await onRequest(ctx, n.fn);
|
|
|
|
expect(n.fuChiamata()).toBe(false);
|
|
expect((res as Response).status).toBe(401);
|
|
const body = await (res as Response).json();
|
|
expect(body.error).toBeTruthy();
|
|
});
|
|
|
|
it('una rotta pubblica non passa da nessun controllo: next() viene chiamata', async () => {
|
|
const ctx = contesto('/blog');
|
|
const n = next();
|
|
const res = await onRequest(ctx, n.fn);
|
|
|
|
expect(n.fuChiamata()).toBe(true);
|
|
expect(await (res as Response).text()).toBe('PAGINA-SERVITA');
|
|
});
|
|
});
|