From 9c3063d4b5b8cb670abce3b263b73831aa9008cf Mon Sep 17 00:00:00 2001 From: Mathieu Drouet Date: Mon, 24 Aug 2026 15:27:02 +0200 Subject: [PATCH] test(helpers): couvrir les trois helpers publics sans test (#TEST-004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `getSeriesCover`, `serializeSeries` et `getParentCollection` étaient exportés par l'API publique et n'apparaissaient nulle part dans `tests/` — zéro occurrence du symbole sur tout le répertoire. Trou ponctuel plutôt que négligence de fond : le ratio du dépôt est bon par ailleurs. 19 tests ajoutés, 293 verts au total. Plus aucun helper public sans couverture. Trois d'entre eux fixent un contrat plutôt qu'ils ne vérifient un calcul : - `render` doit être omis par `serializeSeries` — c'est sa raison d'être, une île React recevrait la fonction comme `undefined` après passage de props ; - l'ordre de `images[]` fait foi pour la cover, contre tout tri introduit en amont ; - `getParentCollection('/x')` rend une chaîne vide, cas dégénéré documenté pour que sa modification soit un jour délibérée. Validés par mutation, cinq fois : `indexOf`→`lastIndexOf` (2 tests tombent), retour du suffixe au lieu du préfixe (3), `Date` non convertie en ISO (1), `date` non retirée de `data` (1), dernière image au lieu de la première (2). Aucune mutation ne passe inaperçue. Aucune entrée au CHANGELOG : rien ne change pour un consommateur, et le gonfler d'ajouts internes lui ferait perdre sa valeur de signal. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AgEyJQRrNvugcuu9WsujGy --- docs/todo.md | 19 +--- tests/unit/helpers-uncovered.test.ts | 162 +++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 tests/unit/helpers-uncovered.test.ts diff --git a/docs/todo.md b/docs/todo.md index 23e93e5..0498cc1 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -30,20 +30,6 @@ prefixes: ## Todo -- [ ] #TEST-004 [P2] Trois helpers publics sans aucun test #tests #effort-s - - **Source** : audit 2026-08-24 - **Zone** : `tests/unit/helpers.test.ts` - - `getSeriesCover`, `serializeSeries` et `getParentCollection` sont exportés par l'API publique et n'apparaissent **nulle part** dans `tests/` — vérifié par recherche du symbole sur tout le répertoire, zéro occurrence pour les trois. Le ratio global est pourtant bon (1,38 ligne de test par ligne de code) : c'est un trou ponctuel, pas une négligence de fond. - - `serializeSeries` est le plus exposé — il façonne ce qu'un site passe à ses îlots client. - - **Checklist** : - - [ ] `getSeriesCover` — cover déclarée, cover absente, série introuvable - - [ ] `serializeSeries` — forme du retour, champs optionnels absents - - [ ] `getParentCollection` — slug imbriqué, slug racine - ## In Progress ## Blocked @@ -52,6 +38,11 @@ prefixes: ## Done +- [x] #TEST-004 [P2] Trois helpers publics sans aucun test #tests #effort-s + > ✅ **Terminé** le 2026-08-24 — audit `docs/reports/audit-summary-2026-08-24.md` + **Zone** : `tests/unit/helpers-uncovered.test.ts` (nouveau) + **Résumé** : `getSeriesCover`, `serializeSeries` et `getParentCollection` étaient exportés par l'API publique et n'apparaissaient **nulle part** dans `tests/` — zéro occurrence du symbole sur tout le répertoire. Trou ponctuel et non négligence de fond : le ratio du dépôt est bon par ailleurs. 19 tests ajoutés, **293 verts** au total, et plus aucun helper public sans couverture. **Validés par mutation, cinq fois** : `indexOf`→`lastIndexOf` (2 tombent), retour du suffixe au lieu du préfixe (3), `Date` non convertie en ISO (1), `date` non retirée de `data` (1), dernière image au lieu de la première (2). Aucune mutation ne passe inaperçue. Trois tests méritent d'être signalés parce qu'ils fixent un contrat plutôt qu'ils ne vérifient un calcul : `render` doit être **omis** — c'est la raison d'être de `serializeSeries`, une île React recevrait la fonction comme `undefined` — l'ordre de `images[]` fait foi contre tout tri introduit en amont, et `getParentCollection('/x')` rend une chaîne vide, cas dégénéré documenté pour que sa modification soit délibérée. **Aucune entrée au CHANGELOG** : rien ne change pour un consommateur, et le gonfler d'ajouts internes lui ferait perdre sa valeur de signal. + - [x] #SEC-002 [P1] `embeds[].id` pouvait détourner l'URL de lecture #sécurité #effort-xs > ✅ **Terminé** le 2026-08-24 — audit `docs/reports/audit-summary-2026-08-24.md` **Zone** : `src/components/SeriesEmbeds.astro`, `tests/e2e/routes.test.ts`, fixture `garde-injection` diff --git a/tests/unit/helpers-uncovered.test.ts b/tests/unit/helpers-uncovered.test.ts new file mode 100644 index 0000000..3d6ce5c --- /dev/null +++ b/tests/unit/helpers-uncovered.test.ts @@ -0,0 +1,162 @@ +/** + * Couverture de trois helpers publics restés sans aucun test (#TEST-004). + * + * Repérés par l'audit du 2026-08-24 : `getSeriesCover`, `serializeSeries` et + * `getParentCollection` sont exportés par l'API publique et n'apparaissaient + * nulle part dans `tests/`. Le ratio global du dépôt est pourtant bon — c'était + * un trou ponctuel, pas une négligence de fond. + */ + +import { describe, it, expect } from 'vitest'; +import { + getSeriesCover, + getParentCollection, + serializeSeries, + resetSeriesCache, +} from '../../src/helpers/index.js'; +import type { Series } from '../../src/helpers/index.js'; + +/** Entrée `series` minimale — même fabrique que les autres suites unitaires. */ +function makeSeries(data: Record, extra: Record = {}): Series { + return { + id: 'demo', + collection: 'series', + data: { title: 'Demo', ...data }, + body: '', + ...extra, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; +} + +// ─── getParentCollection (spec §1.7) ───────────────────────────────────────── + +describe('getParentCollection', () => { + it('extrait le premier segment d\'un slug hiérarchique', () => { + expect(getParentCollection('voyages/asie/tokyo-2024')).toBe('voyages'); + }); + + it('extrait le parent d\'un slug à deux niveaux', () => { + expect(getParentCollection('music/festival-2024')).toBe('music'); + }); + + it('retourne null pour un slug plat', () => { + expect(getParentCollection('bretagne-2024')).toBeNull(); + }); + + it('retourne null pour une chaîne vide', () => { + expect(getParentCollection('')).toBeNull(); + }); + + it('retourne une chaîne vide sur un slash en tête — le segment est vide, pas absent', () => { + // Cas dégénéré : `/x` a bien un parent au sens de la fonction, il est vide. + // Documenté ici pour que le jour où ce comportement change, il change + // délibérément. + expect(getParentCollection('/tokyo-2024')).toBe(''); + }); + + it('ne retient que le premier segment, jamais le dernier', () => { + // Garde contre une inversion : `lastIndexOf` au lieu d'`indexOf` rendrait + // 'voyages/asie' et passerait les trois premiers tests. + expect(getParentCollection('voyages/asie/tokyo')).not.toBe('voyages/asie'); + }); +}); + +// ─── serializeSeries (#MVP-005) ────────────────────────────────────────────── + +describe('serializeSeries', () => { + it('convertit la date en chaîne ISO', () => { + const series = makeSeries({ date: new Date('2024-06-15T00:00:00.000Z') }); + const out = serializeSeries(series); + expect(out.data['date']).toBe('2024-06-15T00:00:00.000Z'); + expect(typeof out.data['date']).toBe('string'); + }); + + it('reporte id et collection', () => { + const out = serializeSeries(makeSeries({})); + expect(out.id).toBe('demo'); + expect(out.collection).toBe('series'); + }); + + it('préserve les autres champs de data', () => { + const out = serializeSeries(makeSeries({ title: 'Bretagne', location: 'Finistère' })); + expect(out.data['title']).toBe('Bretagne'); + expect(out.data['location']).toBe('Finistère'); + }); + + it('omet la clé date quand elle est absente', () => { + const out = serializeSeries(makeSeries({})); + expect('date' in out.data).toBe(false); + }); + + it('omet la clé date quand la valeur n\'est pas une Date', () => { + // Le schéma garantit une `Date`, mais `serializeSeries` accepte n'importe + // quelle `Series` — dont une construite à la main par un consommateur. + const out = serializeSeries(makeSeries({ date: '2024-06-15' })); + expect('date' in out.data).toBe(false); + }); + + it('omet la méthode render — c\'est la raison d\'être de la fonction', () => { + // Une `Series` brute n'est pas sérialisable en JSON : `render` est une + // fonction, et une île React la recevrait comme `undefined` après passage + // de props. Le test garde ce contrat. + const series = makeSeries({}, { render: () => Promise.resolve({ Content: null }) }); + const out = serializeSeries(series); + expect('render' in out).toBe(false); + expect(() => JSON.stringify(out)).not.toThrow(); + }); + + it('reporte le body quand il est présent', () => { + expect(serializeSeries(makeSeries({}, { body: 'texte' })).body).toBe('texte'); + }); + + it('omet la clé body quand elle est absente', () => { + const series = { id: 'demo', collection: 'series', data: { title: 'Demo' } } as unknown as Series; + expect('body' in serializeSeries(series)).toBe(false); + }); + + it('ne mute pas la série passée en argument', () => { + const date = new Date('2024-06-15T00:00:00.000Z'); + const series = makeSeries({ date }); + serializeSeries(series); + expect(series.data['date']).toBeInstanceOf(Date); + }); +}); + +// ─── getSeriesCover (spec §1.6) ────────────────────────────────────────────── + +describe('getSeriesCover', () => { + it('retourne la première image de la série', async () => { + resetSeriesCache(); + const series = makeSeries({ + images: [ + { url: 'https://cdn.test/01.jpg', alt: 'un', width: 100, height: 50 }, + { url: 'https://cdn.test/02.jpg', alt: 'deux', width: 100, height: 50 }, + ], + }); + const cover = await getSeriesCover('demo', series); + expect(cover?.src).toBe('https://cdn.test/01.jpg'); + expect(cover?.alt).toBe('un'); + }); + + it('suit l\'ordre du frontmatter, pas l\'alphabétique', async () => { + // Garde contre un tri introduit en amont : c'est `images[]` qui fait foi. + resetSeriesCache(); + const series = makeSeries({ + images: [ + { url: 'https://cdn.test/zzz.jpg', width: 1, height: 1 }, + { url: 'https://cdn.test/aaa.jpg', width: 1, height: 1 }, + ], + }); + expect((await getSeriesCover('demo', series))?.src).toBe('https://cdn.test/zzz.jpg'); + }); + + it('retourne undefined quand la série n\'a aucune image', async () => { + resetSeriesCache(); + expect(await getSeriesCover('demo', makeSeries({}))).toBeUndefined(); + }); + + it('retourne undefined sur un tableau images vide', async () => { + resetSeriesCache(); + expect(await getSeriesCover('demo', makeSeries({ images: [] }))).toBeUndefined(); + }); +});