Sezione Campus: layout dedicato, route protette e documentazione

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 15:40:07 +02:00
parent 7baebebc09
commit c4107b84a5
15 changed files with 565 additions and 52 deletions
+71
View File
@@ -0,0 +1,71 @@
import { describe, it, expect } from 'vitest';
import { getNav, findArea, findLesson, chapterOf, readLessonHtml, resolveAsset } from '../src/lib/campus';
// I test girano sui contenuti reali generati da `npm run sync-campus`.
const nav = getNav();
describe('getNav', () => {
it('carica aree e capitoli dai contenuti generati', () => {
expect(nav).not.toBeNull();
expect(nav!.count).toBeGreaterThan(100);
expect(nav!.areas.map((a) => a.slug)).toEqual(
['corso-base', 'slide', 'live-e-ospiti', 'consulenze-masterclass', 'qa']);
for (const a of nav!.areas) expect(a.chapters.length).toBeGreaterThan(0);
});
});
describe('lookup', () => {
it('trova area e guida, e il capitolo della guida', () => {
const area = findArea(nav!, 'corso-base');
expect(area?.title).toBe('Corso base');
const l = findLesson(nav!, 'lezione-01-ottimizzazione-del-sonno');
expect(l).toMatchObject({ area: 'corso-base', chapter: 1, label: 'L1' });
expect(chapterOf(nav!, l!)?.title).toBe('Sonno e ritmi');
});
it('ritorna null per slug inesistenti', () => {
expect(findArea(nav!, 'inventata')).toBeNull();
expect(findLesson(nav!, 'lezione-99-inventata')).toBeNull();
});
it('la catena prev/next collega tutte le guide in ordine', () => {
const all = nav!.areas.flatMap((a) => a.chapters.flatMap((c) => c.lessons));
expect(all[0].prev).toBeNull();
expect(all.at(-1)!.next).toBeNull();
for (let i = 0; i < all.length - 1; i++) expect(all[i].next?.slug).toBe(all[i + 1].slug);
});
});
describe('readLessonHtml', () => {
it('legge il frammento di una guida esistente', () => {
const html = readLessonHtml('lezione-01-ottimizzazione-del-sonno');
expect(html).toContain('<h2');
expect(html).not.toContain('Guida allo studio</h1>');
});
it('ritorna null per una guida inesistente', () => {
expect(readLessonHtml('non-esiste')).toBeNull();
});
it('le immagini puntano alla route protetta', () => {
const html = readLessonHtml('slide-hrv')!;
expect(html).toContain('src="/campus/assets/');
expect(html).not.toContain('src="assets/');
});
});
describe('resolveAsset', () => {
it('risolve un asset esistente', () => {
expect(resolveAsset('slide/slide-hrv')).toMatch(/campus-content\/assets\/slide\/slide-hrv$/);
});
it('rifiuta i path traversal e i path assoluti', () => {
expect(resolveAsset('../pages/lezione-01-ottimizzazione-del-sonno.html')).toBeNull();
expect(resolveAsset('../../package.json')).toBeNull();
expect(resolveAsset('/etc/passwd')).toBeNull();
});
it('ritorna null per un asset inesistente', () => {
expect(resolveAsset('slide/non-esiste.png')).toBeNull();
});
});
+7 -8
View File
@@ -1,7 +1,5 @@
import { describe, it, expect } from 'vitest';
// @ts-expect-error — modulo JS senza tipi, usato solo dallo script di sync
import { parseGuide, kindOf, rewriteAssets } from '../scripts/campus/parse.mjs';
// @ts-expect-error — idem
import { sourceDirFor, CHAPTERS, AREAS, CH_COLORS } from '../scripts/campus/structure.mjs';
const body = `
@@ -104,18 +102,19 @@ describe('rewriteAssets', () => {
});
describe('struttura del corso', () => {
const chapterNums = CHAPTERS.map((c) => c[0] as number);
it('ogni capitolo ha un colore e ogni area capitoli esistenti', () => {
const nums = CHAPTERS.map((c: [number, string, string, string[]]) => c[0]);
expect(CH_COLORS.length).toBe(CHAPTERS.length);
for (const [, , , , chapterNums] of AREAS as [string, string, string, string, number[]][])
for (const n of chapterNums) expect(nums).toContain(n);
for (const area of AREAS)
for (const n of area[4] as number[]) expect(chapterNums).toContain(n);
});
it('nessuno slug duplicato e ogni capitolo è in una sola area', () => {
const slugs = CHAPTERS.flatMap((c: [number, string, string, string[]]) => c[3]);
const slugs = CHAPTERS.flatMap((c) => c[3] as string[]);
expect(new Set(slugs).size).toBe(slugs.length);
const assigned = (AREAS as [string, string, string, string, number[]][]).flatMap((a) => a[4]);
const assigned = AREAS.flatMap((a) => a[4] as number[]);
expect(new Set(assigned).size).toBe(assigned.length);
expect(assigned.sort((a, b) => a - b)).toEqual(CHAPTERS.map((c: [number, string, string, string[]]) => c[0]));
expect(assigned.sort((a, b) => a - b)).toEqual(chapterNums);
});
});