diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index e2da28d..a60ee6c 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -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 diff --git a/apps/api/test/adminAuth.test.js b/apps/api/test/adminAuth.test.js index 92d1fe5..397f070 100644 --- a/apps/api/test/adminAuth.test.js +++ b/apps/api/test/adminAuth.test.js @@ -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 où 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(); + }); +}); diff --git a/apps/web/test/a11y.test.js b/apps/web/test/a11y.test.js index 1f04c72..cbd85b3 100644 --- a/apps/web/test/a11y.test.js +++ b/apps/web/test/a11y.test.js @@ -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)); diff --git a/requirements-dev.txt b/requirements-dev.txt index 5198dc5..42c017c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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