"""Tests unitaires du contrôle de secrets de déploiement.""" from __future__ import annotations import importlib.util import subprocess import sys from pathlib import Path from types import ModuleType from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from _pytest.capture import CaptureFixture @pytest.fixture def secret_checker() -> ModuleType: """Charge le script de vérification sans l'exécuter comme programme. :return: Module du script de contrôle de secrets. :rtype: ModuleType """ script_path = Path(__file__).parents[2] / "scripts" / "check_secrets.py" specification = importlib.util.spec_from_file_location("check_secrets", script_path) assert specification is not None assert specification.loader is not None module = importlib.util.module_from_spec(specification) sys.modules[specification.name] = module try: specification.loader.exec_module(module) finally: del sys.modules[specification.name] return module def test_main_accepts_clean_files_and_ignores_environment_file( secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] ) -> None: """Vérifie qu'un dépôt propre réussit sans analyser le fichier d'environnement. :param secret_checker: Module du script sous test. :param tmp_path: Répertoire temporaire représentant un dépôt. :param capsys: Fixture de capture de sortie. :return: None """ (tmp_path / "application.py").write_text("value = 'safe'\n", encoding="utf-8") ignored_environment_secret = 'password = "private-value"\n' # pragma: allowlist secret (tmp_path / ".env").write_text( ignored_environment_secret, encoding="utf-8" ) # secret-check: allow assert secret_checker.main([], root=tmp_path) == 0 assert "OK:" in capsys.readouterr().out def test_main_reports_a_literal_secret_without_disclosing_its_value( secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] ) -> None: """Vérifie qu'un secret littéral échoue sans fuite de sa valeur. :param secret_checker: Module du script sous test. :param tmp_path: Répertoire temporaire représentant un dépôt. :param capsys: Fixture de capture de sortie. :return: None """ sentinel = "m14-literal-sentinel" (tmp_path / "settings.py").write_text( f'password = "{sentinel}"\n', encoding="utf-8" ) # secret-check: allow assert secret_checker.main([], root=tmp_path) == 1 output = capsys.readouterr().out assert "settings.py:1 (affectation-litterale)" in output assert sentinel not in output def test_main_reports_an_unquoted_configuration_secret_without_disclosing_its_value( secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] ) -> None: """Vérifie qu'un secret de configuration non cité échoue sans fuite de sa valeur. :param secret_checker: Module du script sous test. :param tmp_path: Répertoire temporaire représentant un dépôt. :param capsys: Fixture de capture de sortie. :return: None """ sentinel = "m14-unquoted-sentinel" (tmp_path / "settings.yaml").write_text( f"password: {sentinel}\n", encoding="utf-8" ) # secret-check: allow assert secret_checker.main([], root=tmp_path) == 1 output = capsys.readouterr().out assert "settings.yaml:1 (affectation-litterale)" in output assert sentinel not in output def test_main_detects_sensitive_url_parameter( secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] ) -> None: """Vérifie qu'un paramètre URL sensible déclenche un échec. :param secret_checker: Module du script sous test. :param tmp_path: Répertoire temporaire représentant un dépôt. :param capsys: Fixture de capture de sortie. :return: None """ sentinel = "m14-url-sentinel" (tmp_path / "settings.yaml").write_text( f"url: https://example.invalid/calendar?icalsecurise={sentinel}\n", encoding="utf-8" ) # secret-check: allow assert secret_checker.main([], root=tmp_path) == 1 output = capsys.readouterr().out assert "settings.yaml:1 (parametre-url)" in output assert sentinel not in output def test_staged_mode_inspects_only_paths_provided_by_git( secret_checker: ModuleType, tmp_path: Path, capsys: CaptureFixture[str] ) -> None: """Vérifie que l'option staged ignore les fichiers non indexés. :param secret_checker: Module du script sous test. :param tmp_path: Répertoire temporaire représentant un dépôt. :param capsys: Fixture de capture de sortie. :return: None """ (tmp_path / "indexed.py").write_text("answer = 42\n", encoding="utf-8") untracked_secret = 'api_key = "m14-untracked-sentinel"\n' # pragma: allowlist secret (tmp_path / "untracked.py").write_text( untracked_secret, encoding="utf-8" ) # secret-check: allow def runner(*_args: object, **_kwargs: object) -> subprocess.CompletedProcess[str]: """Simule Git avec un seul fichier indexé. :return: Résultat Git simulé. :rtype: subprocess.CompletedProcess[str] """ return subprocess.CompletedProcess([], 0, stdout="indexed.py\0", stderr="") assert secret_checker.main(["--staged"], root=tmp_path, runner=runner) == 0 assert "OK:" in capsys.readouterr().out