Remplace la regle de longueur seule (12 caracteres) par une exigence de composition (8 caracteres minimum, majuscule, minuscule, chiffre, caractere special), non documentee dans les exigences officielles du projet, par une regle explicite partagee entre le backend (validateur Pydantic) et le frontend. Ajoute un flux "mot de passe oublie" en libre-service, absent jusqu'ici : jeton a usage unique hache en base (meme principe que les refresh tokens), expirant a 15 minutes, envoye par email via un service SMTP (aiosmtplib, Mailpit en dev), avec limitation de debit dediee et reponse generique pour eviter l'enumeration des comptes. Closes #87
121 lines
3.8 KiB
Python
121 lines
3.8 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app import cli
|
|
from app.schemas.auth import valide_complexite
|
|
|
|
|
|
def test_build_parser_reads_the_create_admin_arguments() -> None:
|
|
arguments = cli.build_parser().parse_args(
|
|
["create-admin", "--email", "admin@enervision.fr", "--generate", "--force"]
|
|
)
|
|
|
|
assert arguments.commande == "create-admin"
|
|
assert arguments.email == "admin@enervision.fr"
|
|
assert arguments.generate is True
|
|
assert arguments.force is True
|
|
|
|
|
|
def test_build_parser_requires_a_subcommand() -> None:
|
|
with pytest.raises(SystemExit):
|
|
cli.build_parser().parse_args([])
|
|
|
|
|
|
def test_build_parser_requires_an_email() -> None:
|
|
with pytest.raises(SystemExit):
|
|
cli.build_parser().parse_args(["create-admin"])
|
|
|
|
|
|
def test_read_password_generates_a_long_secret_when_asked(
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
mot_de_passe = cli.read_password(generate=True)
|
|
|
|
assert len(mot_de_passe) >= cli.LONGUEUR_MOT_DE_PASSE_GENERE
|
|
assert mot_de_passe in capsys.readouterr().out
|
|
valide_complexite(mot_de_passe)
|
|
|
|
|
|
def test_read_password_accepts_two_matching_entries(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
saisies = iter(["Un-mot-de-passe-valide1", "Un-mot-de-passe-valide1"])
|
|
monkeypatch.setattr(cli, "getpass", lambda _: next(saisies))
|
|
|
|
assert cli.read_password(generate=False) == "Un-mot-de-passe-valide1"
|
|
|
|
|
|
def test_read_password_refuses_a_password_below_the_minimum_length(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(cli, "getpass", lambda _: "Court1!")
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli.read_password(generate=False)
|
|
|
|
|
|
def test_read_password_refuses_a_password_missing_a_character_class(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setattr(cli, "getpass", lambda _: "un-mot-de-passe-sans-majuscule-ni-chiffre")
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli.read_password(generate=False)
|
|
|
|
|
|
def test_read_password_refuses_two_different_entries(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
saisies = iter(["Un-mot-de-passe-valide1", "Un-autre-mot-de-passe2"])
|
|
monkeypatch.setattr(cli, "getpass", lambda _: next(saisies))
|
|
|
|
with pytest.raises(SystemExit):
|
|
cli.read_password(generate=False)
|
|
|
|
|
|
def test_build_parser_reads_the_export_openapi_arguments() -> None:
|
|
arguments = cli.build_parser().parse_args(
|
|
["export-openapi", "--output", "ailleurs/contrat.json"]
|
|
)
|
|
|
|
assert arguments.commande == "export-openapi"
|
|
assert arguments.output == "ailleurs/contrat.json"
|
|
|
|
|
|
def test_build_parser_defaults_the_export_to_the_versioned_contract() -> None:
|
|
arguments = cli.build_parser().parse_args(["export-openapi"])
|
|
|
|
assert arguments.output == str(cli.CHEMIN_CONTRAT)
|
|
|
|
|
|
def test_settings_of_the_contract_ignore_the_local_environment(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("APP_API_PREFIX", "/api/v9")
|
|
monkeypatch.setenv("APP_NAME", "API du poste de Johan")
|
|
|
|
settings = cli.settings_du_contrat()
|
|
|
|
assert settings.api_prefix == "/api/v1"
|
|
assert settings.name == "EnerVision API"
|
|
|
|
|
|
def test_export_openapi_writes_a_readable_schema_where_asked(tmp_path: Path) -> None:
|
|
destination = tmp_path / "contrat.json"
|
|
|
|
cli.export_openapi(destination)
|
|
|
|
assert json.loads(destination.read_text(encoding="utf-8"))["openapi"].startswith("3.")
|
|
|
|
|
|
# Piège : `main()` réclamait un mot de passe avant de lire la commande. Sans le branchement,
|
|
# l'export resterait bloqué sur `getpass` et aucune CI ne pourrait le rejouer.
|
|
def test_main_exports_the_contract_without_asking_for_a_password(
|
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
destination = tmp_path / "contrat.json"
|
|
|
|
code = cli.main(["export-openapi", "--output", str(destination)])
|
|
|
|
assert code == 0
|
|
assert destination.exists()
|
|
assert str(destination) in capsys.readouterr().out
|