763e5611e6
- creaCliente: il codice si deduce dal massimo fra identity E longevity (non piu' solo identity), cosi' un client_code cancellato dall'anagrafica non torna mai disponibile e non si attribuiscono le misure di un vecchio cliente a uno nuovo. Compensazione se la scrittura su longevity fallisce dopo quella su identity. - ROLES unica fonte in auth.ts (Role e isRole derivati); users.astro e users/new.astro usano quella lista invece di array scritti a mano che dimenticavano cliente/trainer. - Commento falso su cosa protegge il middleware, riscritto: rimanda a isProtectedPath. - vitest.config.ts: alias per astro:middleware (stesso bersaglio della pipeline vite di Astro), cosi' src/middleware.ts e' finalmente importabile e testabile in isolamento - prima nessun test lo caricava davvero. - export.test.ts: asserzione posizionale per colonna, non piu' solo intestazioni scritte a mano; intercetta un riordino della SELECT che disallinea etichette e valori. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
3.4 KiB
TypeScript
84 lines
3.4 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');
|
|
onRequest = mod.onRequest 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');
|
|
});
|
|
});
|