groupe-meditation/backend/tests/test_business_rules.py

126 lines
5 KiB
Python

import unittest
import sys
from pathlib import Path
from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from app.core.decision_types import type_decision_depuis_proposition
from app.core.financial_position import PositionTresorerie
from app.core.sanitization import safe_query_params
from app.core.security import create_token, decode_token
from app.models.poste import Poste
from app.models.affectation import Affectation
from app.models.candidature import Candidature
from app.models.collecte import Collecte
from app.models.depense import Depense
from app.models.evenement import Evenement
from app.models.jeton import Jeton
from app.models.litterature import Litterature
from app.models.presence import Presence
from app.models.proposition import Proposition
from app.models.pv_reunion import PvReunion
from app.models.reunion import Reunion
from app.models.vente_litterature import VenteLitterature
from app.models.vente_jeton import VenteJeton
class DecisionTypeTests(unittest.TestCase):
def test_resolution_prefix(self):
prop = SimpleNamespace(numero_resolution="RES-2026-001", candidature_id=None)
self.assertEqual(type_decision_depuis_proposition(prop), "resolution")
def test_nomination_from_candidature(self):
prop = SimpleNamespace(numero_resolution=None, candidature_id="candidate-id")
self.assertEqual(type_decision_depuis_proposition(prop), "nomination")
def test_contribution_prefix(self):
prop = SimpleNamespace(numero_resolution="DON-2026-001", candidature_id=None)
self.assertEqual(type_decision_depuis_proposition(prop), "contribution")
def test_plain_proposition_without_decision_marker(self):
prop = SimpleNamespace(numero_resolution=None, candidature_id=None)
self.assertEqual(type_decision_depuis_proposition(prop), "proposition")
class PositionTresorerieTests(unittest.TestCase):
def test_available_bank_excludes_reserves_and_commitments(self):
position = PositionTresorerie(
solde_banque=100.0,
encaisse=25.0,
total_reserves=40.0,
engagements_non_debites=15.0,
)
self.assertEqual(position.disponible_bancaire, 45.0)
self.assertEqual(position.disponible_total, 70.0)
class AuditTests(unittest.TestCase):
def test_sensitive_query_params_are_masked(self):
request = SimpleNamespace(query_params={"pin": "1234", "membre_id": "abc"})
self.assertEqual(safe_query_params(request.query_params), {"pin": "***", "membre_id": "abc"})
class SecurityTokenTests(unittest.TestCase):
def test_sysadmin_instance_token_has_no_group_context(self):
token = create_token("member-id", None, expire_jours=1)
payload = decode_token(token)
self.assertEqual(payload["sub"], "member-id")
self.assertNotIn("grp", payload)
class MultiTenantTests(unittest.TestCase):
def test_postes_are_group_scoped(self):
self.assertIn("groupe_id", Poste.__table__.columns)
self.assertFalse(Poste.__table__.columns["groupe_id"].nullable)
def test_core_business_tables_are_group_scoped(self):
for model in [
Affectation,
Candidature,
Collecte,
Depense,
Evenement,
Jeton,
Litterature,
Proposition,
Reunion,
VenteLitterature,
VenteJeton,
]:
with self.subTest(model=model.__name__):
self.assertIn("groupe_id", model.__table__.columns)
self.assertFalse(model.__table__.columns["groupe_id"].nullable)
def test_child_tables_without_group_id_are_documented_exceptions(self):
exceptions = {
"presences": "scoped_by_reunion",
"pv_reunions": "scoped_by_reunion",
}
for model in [Presence, PvReunion]:
with self.subTest(model=model.__name__):
self.assertNotIn("groupe_id", model.__table__.columns)
self.assertIn(model.__tablename__, exceptions)
def test_role_fallback_queries_are_group_bounded(self):
root = Path(__file__).resolve().parents[1]
for rel in [
"app/routers/jetons.py",
"app/routers/ventes_litterature.py",
"app/routers/litterature.py",
"app/routers/rapports_rsg.py",
"app/routers/commandes_jetons.py",
]:
source = (root / rel).read_text(encoding="utf-8")
with self.subTest(router=rel):
self.assertIn("Affectation.groupe_id == membre.groupe_id", source)
self.assertIn("Poste.groupe_id == membre.groupe_id", source)
def test_presence_batch_validates_members_in_current_group(self):
source = (Path(__file__).resolve().parents[1] / "app/routers/presences.py").read_text(encoding="utf-8")
self.assertIn("mbr.groupe_id != membre.groupe_id", source)
self.assertIn("reunion.groupe_id != invitation.groupe_id", source)
if __name__ == "__main__":
unittest.main()