Files
InsanityLab-web/apps/platforms/tests/campus-parse.test.ts
T
AdrianoDev 6301f521fd Separazione: le aree riservate diventano un'app propria (apps/platforms)
Campus, Stress Index e Longevity escono dal sito e diventano
apps.insanitylab.it, con utenti, sessioni, login e database propri.
Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni
autosufficienti, cosi' che ognuna resti estraibile.

Misura che ha guidato il taglio: le tre aree condividevano col sito
SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts.
Layout, librerie e dati erano gia' disgiunti.

Lato piattaforme:
- ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali
- canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non
  elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno)
- tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti
- /api risponde 401/403, non un redirect verso una pagina di login
- tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi

Lato sito:
- ROLES tornano i tre redazionali; via le regole e il login pubblico
- il login del pannello RESPINGE un account emigrato invece di aprirgli una
  sessione: senza, landingFor lo mandava su /admin, il middleware glielo
  negava, e si formava un ciclo di redirect che sembra un guasto del sito
- l'header non mostra piu' un menu delle piattaforme: un link che esce

Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi
i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra
in Campus e viene rimbalzato da Longevity, il cliente il contrario.

Non ancora in produzione: manca il record DNS di apps.insanitylab.it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:21:12 +00:00

121 lines
5.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { parseGuide, kindOf, rewriteAssets } from '../scripts/campus/parse.mjs';
import { sourceDirFor, CHAPTERS, AREAS, CH_COLORS } from '../scripts/campus/structure.mjs';
const body = `
> Sintesi di studio basata sulla trascrizione della lezione del corso.
## In sintesi
Testo di prova.
## Concetti chiave
- **Il sonno è il pilastro.** Spiegazione.
- **Ritmi circadiani.** Altra spiegazione.
`;
describe('kindOf / sourceDirFor', () => {
it('riconosce il tipo dallo slug', () => {
expect(kindOf('lezione-01-ottimizzazione-del-sonno')).toBe('lezione');
expect(kindOf('lezione-live-1-2026-02-24')).toBe('live');
expect(kindOf('accelerator-1-2025-11-11')).toBe('accelerator');
expect(kindOf('slide-hrv')).toBe('slide');
expect(kindOf('speaker-resta-marketing')).toBe('speaker');
expect(kindOf('consulenza-2025-02-04-cristina-ruggeri')).toBe('consulenza');
expect(kindOf('masterclass-2025-10-08-luca-ruggeri')).toBe('masterclass');
expect(kindOf('qa-01-tony-04')).toBe('qa');
});
it('mappa lo slug alla cartella sorgente', () => {
expect(sourceDirFor('lezione-01-ottimizzazione-del-sonno')).toBe('studio');
expect(sourceDirFor('lezione-live-1-2026-02-24')).toBe('lezioni-live');
expect(sourceDirFor('qa-01-tony-04')).toBe('qa');
expect(sourceDirFor('consulenza-2025-02-04-cristina-ruggeri')).toBe('consulenze');
});
});
describe('parseGuide', () => {
it('lezione numerata con parte e relatore', () => {
const raw = '# Lezione 2 — Movimento e fitness (Parte 1) (Tony Manfron) — Guida allo studio' + body;
const g = parseGuide(raw, 'lezione-02-movimento-fitness-parte1', 1);
expect(g).toMatchObject({
kind: 'lezione', num: 2, relatore: 'Tony Manfron', parte: 'Parte 1',
titleFull: 'Movimento e fitness (Parte 1)', titleShort: 'Movimento e fitness',
label: 'L2 · Parte 1',
});
});
it('lezione live e accelerator usano la data come titolo', () => {
const live = parseGuide(
'# Lezione live 1 — 24 febbraio 2026 (Massimo Filippi) — Guida allo studio' + body,
'lezione-live-1-2026-02-24', 1);
expect(live).toMatchObject({
kind: 'live', num: 1, titleFull: 'Lezione live 1 — 24 febbraio 2026',
titleShort: 'Lezione live 1', relatore: 'Massimo Filippi · 24 febbraio 2026', label: 'LV1',
});
const acc = parseGuide(
'# Accelerator 3 — 28 gennaio 2026 (Massimo Filippi) — Guida allo studio' + body,
'accelerator-3-2026-01-28', 3);
expect(acc).toMatchObject({ kind: 'accelerator', num: 3, titleShort: 'Accelerator 3', label: 'A3' });
});
it('slide, speaker, consulenza, masterclass e Q&A', () => {
expect(parseGuide('# Slide — HRV: Variabilità della frequenza cardiaca — Guida allo studio' + body, 'slide-hrv', 1))
.toMatchObject({ kind: 'slide', num: 1, titleFull: 'HRV: Variabilità della frequenza cardiaca', label: 'SL1' });
expect(parseGuide('# Intervento — Marketing nel BioHacking (Roberto Resta) — Guida allo studio' + body, 'speaker-resta-marketing', 5))
.toMatchObject({ kind: 'speaker', titleFull: 'Marketing nel BioHacking', relatore: 'Roberto Resta', label: 'SP5' });
expect(parseGuide('# Consulenza — Cristina Ruggeri (04/02/2025) — Guida allo studio' + body, 'consulenza-2025-02-04-cristina-ruggeri', 1))
.toMatchObject({ kind: 'consulenza', titleFull: 'Cristina Ruggeri', relatore: '04/02/2025', label: 'C1' });
expect(parseGuide('# Masterclass — Luca Ruggeri (08/10/2025) — Guida allo studio' + body, 'masterclass-2025-10-08-luca-ruggeri', 1))
.toMatchObject({ kind: 'masterclass', titleFull: 'Luca Ruggeri', relatore: '08/10/2025', label: 'MC1' });
expect(parseGuide('# Q&A Tony n°4 — Guida allo studio' + body, 'qa-01-tony-04', 1))
.toMatchObject({ kind: 'qa', titleFull: 'Q&A Tony n°4', label: 'QA1' });
});
it('estrae i concetti chiave in grassetto e rimuove H1 e blockquote', () => {
const g = parseGuide('# Lezione 1 — Sonno (Tony Manfron) — Guida allo studio' + body,
'lezione-01-ottimizzazione-del-sonno', 1);
expect(g.concetti).toEqual(['Il sonno è il pilastro', 'Ritmi circadiani']);
expect(g.html).not.toContain('Guida allo studio');
expect(g.html).not.toContain('Sintesi di studio basata');
expect(g.html).toContain('<h2');
expect(g.html).toContain('Testo di prova');
});
it('rifiuta un H1 non riconosciuto', () => {
expect(() => parseGuide('# Titolo qualunque\n\ntesto', 'lezione-99-x', 1)).toThrow(/H1 non riconosciuto/);
});
});
describe('rewriteAssets', () => {
it('riscrive i path relativi delle immagini verso la route protetta', () => {
const html = '<img src="assets/slide/slide-hrv/fig-01.png" alt="x"><img src="/altro.png">';
expect(rewriteAssets(html)).toBe(
'<img src="/campus/assets/slide/slide-hrv/fig-01.png" alt="x"><img src="/altro.png">');
});
});
describe('struttura del corso', () => {
const chapterNums = CHAPTERS.map((c) => c[0] as number);
it('ogni capitolo ha un colore e ogni area capitoli esistenti', () => {
expect(CH_COLORS.length).toBe(CHAPTERS.length);
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) => c[3] as string[]);
expect(new Set(slugs).size).toBe(slugs.length);
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(chapterNums);
});
});