test: couvrir adminAuth, réparer la CI Python et une couverture annoncée à tort
Some checks are pending
CI / javascript (push) Waiting to run
CI / python (push) Waiting to run
CI / mutation (push) Waiting to run

adminAuth.js était le fichier le moins couvert du dépôt — 52 % de score de
mutation, 22 mutants sans aucune couverture — alors qu'il porte JWT, bcrypt et
le verrouillage de compte. seedAdminUser, recordLoginAttempt, markLoginSuccess
et writeAuditLog n'avaient aucun test direct. writeAuditLog était le plus
gênant : il peut cesser d'enregistrer sans qu'aucune action admin n'échoue et
sans qu'aucun test ne rougisse, et un journal d'audit muet ne se remarque que
le jour où on en a besoin.

Mesuré après coup : adminAuth.js passe de 52,31 % à 84,29 %, les mutants sans
couverture de 22 à 1, et le score global de 82,20 % à 88,85 %.

Le job Python de la CI n'installait que ruff, pytest et pip-audit : psycopg2,
beautifulsoup4, lxml et hypothesis manquaient, et il aurait échoué dès la
collecte — ce que personne n'a jamais vu, faute de runner enregistré. Il part
désormais de requirements-dev.txt, et lance aussi la suite du crawler, qui
n'était pas exécutée du tout.

requirements-dev.txt épinglait psycopg2-binary==2.9.9, sans roue pour Python
>= 3.13 : la suite entière s'arrêtait dès la collecte sur un poste par ailleurs
sain. Épinglages assouplis en bornes basses.

Corrige enfin le commentaire de a11y.test.js, qui renvoyait à « l'audit
Lighthouse manuel du README de la CI » pour justifier deux règles désactivées.
Cet audit n'existe pas : README-CI.md documente le test e2e de l'admin, qui
couvre bien le contraste mais UNIQUEMENT pour le dashboard. Pour le site
public, ces règles ne sont vérifiées nulle part. La couverture n'a pas changé,
seule sa description était fausse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas FRYDER 2026-08-22 14:49:04 +02:00
parent a5de45dd9f
commit b5e3ef91ab
4 changed files with 102 additions and 13 deletions

View file

@ -49,15 +49,23 @@ jobs:
steps:
- uses: actions/checkout@v4
# requirements-dev.txt, et non une liste recopiée : l'ancienne installait
# ruff/pytest/pip-audit seulement. psycopg2, beautifulsoup4, lxml et
# hypothesis manquaient, et le job aurait échoué dès la collecte — ce que
# personne n'a jamais vu, faute de runner enregistré.
- name: Installer l'outillage
run: pip install --no-cache-dir ruff pytest pip-audit
run: pip install --no-cache-dir -r requirements-dev.txt
- name: Ruff (lint + règles de sécurité bandit)
run: ruff check .
- name: Tests du parseur de géocodage
- name: Tests du geocoder
run: cd apps/geocoder && python -m pytest tests -q
# La suite du crawler n'était pas lancée du tout.
- name: Tests du crawler
run: cd apps/crawler && python -m pytest tests -q
- name: Audit des dépendances
run: pip-audit -r apps/crawler/requirements.txt -r apps/geocoder/requirements.txt
continue-on-error: true

View file

@ -9,6 +9,10 @@ const {
verifyPassword,
requireAdminSession,
ADMIN_COOKIE_NAME,
seedAdminUser,
recordLoginAttempt,
markLoginSuccess,
writeAuditLog,
} = await import("../src/adminAuth.js");
const { makeFakePool, rows } = await import("./helpers/fakePool.js");
@ -144,3 +148,69 @@ describe("isLockedOut", () => {
expect(call.values).toEqual([15, "nico", "1.2.3.4"]);
});
});
/**
* Les quatre fonctions ci-dessous n'avaient AUCUN test direct elles portaient
* l'essentiel des 22 mutants sans couverture du fichier (score 52 %, le plus
* bas du dépôt, sur le module qui gère JWT, bcrypt et le verrouillage).
*
* writeAuditLog est le cas le plus gênant : il peut cesser d'enregistrer sans
* qu'aucune action admin n'échoue et sans qu'aucun test ne rougisse. Un journal
* d'audit muet ne se remarque que le jour on en a besoin.
*/
describe("écritures non couvertes jusqu'ici", () => {
it("seedAdminUser ne fait rien sans identifiants", async () => {
const pool = makeFakePool();
delete process.env.ADMIN_SEED_USERNAME;
delete process.env.ADMIN_SEED_PASSWORD_HASH;
await seedAdminUser(pool);
expect(pool.calls).toHaveLength(0);
});
it("seedAdminUser crée le compte sans écraser un existant", async () => {
const pool = makeFakePool();
process.env.ADMIN_SEED_USERNAME = " nico ";
process.env.ADMIN_SEED_PASSWORD_HASH = " $2b$12$hash ";
await seedAdminUser(pool);
const call = pool.find("INSERT INTO admin_users");
expect(call.values).toEqual(["nico", "$2b$12$hash"]);
// Sans ON CONFLICT DO NOTHING, un redémarrage écraserait le mot de passe.
expect(call.sql).toMatch(/ON CONFLICT \(username\) DO NOTHING/);
delete process.env.ADMIN_SEED_USERNAME;
delete process.env.ADMIN_SEED_PASSWORD_HASH;
});
it("recordLoginAttempt enregistre l'issue réelle", async () => {
const pool = makeFakePool();
await recordLoginAttempt(pool, "nico", "1.2.3.4", false);
expect(pool.find("INSERT INTO admin_login_attempts").values).toEqual(["nico", "1.2.3.4", false]);
await recordLoginAttempt(pool, "nico", "1.2.3.4", true);
expect(pool.findAll("INSERT INTO admin_login_attempts")[1].values[2]).toBe(true);
});
it("markLoginSuccess horodate le bon compte", async () => {
const pool = makeFakePool();
await markLoginSuccess(pool, "nico");
const call = pool.find("UPDATE admin_users");
expect(call.values).toEqual(["nico"]);
expect(call.sql).toMatch(/last_login_at = now\(\)/);
});
it("writeAuditLog sérialise avant/après en JSON", async () => {
const pool = makeFakePool();
await writeAuditLog(pool, "nico", "update", "bands", 42, { a: 1 }, { a: 2 });
const call = pool.find("INSERT INTO admin_audit_log");
expect(call.values[0]).toBe("nico");
expect(call.values[3]).toBe("42"); // target_id est du texte
expect(JSON.parse(call.values[4])).toEqual({ a: 1 });
expect(JSON.parse(call.values[5])).toEqual({ a: 2 });
});
it("writeAuditLog accepte l'absence d'avant/après", async () => {
const pool = makeFakePool();
await writeAuditLog(pool, "nico", "cleanup", "crawl_run", null, null, null);
const call = pool.find("INSERT INTO admin_audit_log");
expect(call.values[4]).toBeNull();
expect(call.values[5]).toBeNull();
});
});

View file

@ -11,10 +11,17 @@ import axe from "axe-core";
* On charge le HTML livré tel quel dans jsdom et on passe axe-core dessus.
* Pas de navigateur, pas de conteneur : ~1s pour les deux pages.
*
* Limite assumée : les règles qui nécessitent un vrai moteur de rendu
* (contraste de couleurs, cibles tactiles) ne peuvent pas s'évaluer sous jsdom
* et sont désactivées. Elles ne sont donc PAS couvertes ici voir le README
* de la CI pour l'audit Lighthouse manuel qui les complète.
* Limite RÉELLEMENT non couverte : les règles qui exigent un moteur de rendu
* (contraste de couleurs, taille des cibles tactiles) ne s'évaluent pas sous
* jsdom et sont désactivées ci-dessous.
*
* Ce commentaire renvoyait auparavant à « l'audit Lighthouse manuel du README
* de la CI ». Cet audit n'existe pas, et n'a jamais existé : README-CI.md
* documente apps/admin/test/e2e/a11y.spec.js, qui couvre bien le contraste
* dans un vrai navigateur mais UNIQUEMENT pour le dashboard admin. Pour le
* site public, ces deux règles ne sont vérifiées nulle part. Le dire
* franchement vaut mieux que de renvoyer vers un filet imaginaire : la
* couverture n'a pas changé, seule sa description était fausse.
*/
const __dirname = path.dirname(fileURLToPath(import.meta.url));

View file

@ -1,12 +1,16 @@
# Outillage qualité Python (hors runtime des apps).
# pip install -r requirements-dev.txt
ruff==0.8.4
pytest==8.3.4
pip-audit==2.7.3
ruff>=0.8.4
pytest>=8.3.4
pip-audit>=2.7.3
# Les tests importent src.db, qui importe psycopg2 au chargement du module.
# Aucune connexion n'est ouverte : get_conn est remplacé dans les tests.
psycopg2-binary==2.9.9
beautifulsoup4==4.12.3
lxml==5.3.0
hypothesis==6.122.3
#
# Épinglage souple volontaire : 2.9.9 n'a pas de roue pour Python >= 3.13, et
# la compilation depuis les sources échoue sans en-têtes PostgreSQL. La suite
# entière s'arrêtait alors dès la collecte, sur un poste par ailleurs sain.
psycopg2-binary>=2.9.10
beautifulsoup4>=4.12.3
lxml>=5.3.0
hypothesis>=6.122.3