groupe-meditation/backend/tests/test_business_rules.py

133 lines
5.3 KiB
Python
Raw Normal View History

2026-05-26 09:05:58 -04:00
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
2026-06-01 06:45:47 -04:00
from app.core.security import create_token, decode_token
from app.models.poste import Poste
2026-06-01 06:35:09 -04:00
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.instance_admin import InstanceAdmin
2026-06-01 06:35:09 -04:00
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
2026-05-26 09:05:58 -04:00
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"})
2026-06-01 06:45:47 -04:00
class SecurityTokenTests(unittest.TestCase):
def test_sysadmin_instance_token_has_no_group_context(self):
token = create_token("admin-id", None, expire_jours=1, subject_type="instance_admin")
2026-06-01 06:45:47 -04:00
payload = decode_token(token)
self.assertEqual(payload["sub"], "admin-id")
self.assertEqual(payload["typ"], "instance_admin")
2026-06-01 06:45:47 -04:00
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_instance_admin_is_not_group_scoped(self):
self.assertNotIn("groupe_id", InstanceAdmin.__table__.columns)
self.assertTrue(InstanceAdmin.__table__.columns["identifiant"].unique)
2026-06-01 06:35:09 -04:00
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)
2026-05-26 09:05:58 -04:00
if __name__ == "__main__":
unittest.main()