"""Tests for the ConsentLog model — B-2.1 Loi 25 audit trail.""" import os import sys from datetime import datetime, timedelta sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:') os.environ.setdefault('SECRET_KEY', 'test-secret-key') from src.app import app, db # noqa: E402 from src.models.user import User # noqa: E402 from src.models.consent import ConsentLog # noqa: E402 def _make_user(username='alice', email='alice@example.com'): user = User(username=username, email=email, password='x' * 60) db.session.add(user) db.session.commit() return user def test_consent_log_records_cgu_grant(): """Granting CGU consent at signup creates a row with all audit fields.""" with app.app_context(): db.create_all() try: user = _make_user() log = ConsentLog( user_id=user.id, consent_type='cgu', version='1.0', granted=True, ip_address='192.0.2.1', user_agent='Mozilla/5.0 (Windows NT 10.0)' ) db.session.add(log) db.session.commit() assert ConsentLog.query.count() == 1 assert log.granted_at is not None assert log.granted_at <= datetime.utcnow() assert log.revoked_at is None finally: db.session.rollback() db.drop_all() def test_consent_log_supports_4_consent_types(): """All 4 Loi 25 consent types (cgu, confidentialite, marketing, analytics) are valid.""" with app.app_context(): db.create_all() try: user = _make_user() for consent_type in ('cgu', 'confidentialite', 'marketing', 'analytics'): log = ConsentLog( user_id=user.id, consent_type=consent_type, version='1.0', granted=True, ip_address='192.0.2.1', user_agent='Mozilla/5.0' ) db.session.add(log) db.session.commit() types = sorted([l.consent_type for l in ConsentLog.query.all()]) assert types == ['analytics', 'cgu', 'confidentialite', 'marketing'] finally: db.session.rollback() db.drop_all() def test_consent_log_revoke_creates_separate_row(): """Revoking later does NOT mutate the grant — both rows persist for audit trail.""" with app.app_context(): db.create_all() try: user = _make_user() grant = ConsentLog( user_id=user.id, consent_type='marketing', version='1.0', granted=True, ip_address='192.0.2.1', user_agent='UA' ) db.session.add(grant) db.session.commit() revoke = ConsentLog( user_id=user.id, consent_type='marketing', version='1.0', granted=False, ip_address='192.0.2.1', user_agent='UA', revoked_at=datetime.utcnow() ) db.session.add(revoke) db.session.commit() rows = ConsentLog.query.filter_by(consent_type='marketing').order_by(ConsentLog.id).all() assert len(rows) == 2, "Grant and revoke must each have their own row (audit trail)" assert rows[0].granted is True and rows[0].revoked_at is None assert rows[1].granted is False and rows[1].revoked_at is not None finally: db.session.rollback() db.drop_all() def test_consent_log_user_backref(): """User.consent_logs backref returns the user's consent history.""" with app.app_context(): db.create_all() try: user = _make_user() for ct in ('cgu', 'confidentialite'): db.session.add(ConsentLog( user_id=user.id, consent_type=ct, version='1.0', granted=True, ip_address='192.0.2.1', user_agent='UA' )) db.session.commit() assert len(user.consent_logs) == 2 assert sorted([l.consent_type for l in user.consent_logs]) == ['cgu', 'confidentialite'] finally: db.session.rollback() db.drop_all() def test_consent_log_requires_ip_and_user_agent(): """ip_address and user_agent are NOT NULL — required for Loi 25 traceability.""" from sqlalchemy.exc import IntegrityError with app.app_context(): db.create_all() try: user = _make_user(username='erica', email='erica@example.com') # Missing ip_address log_no_ip = ConsentLog( user_id=user.id, consent_type='cgu', version='1.0', granted=True, user_agent='UA' ) db.session.add(log_no_ip) try: db.session.commit() raise AssertionError("Expected IntegrityError on missing ip_address") except IntegrityError: db.session.rollback() # Missing user_agent log_no_ua = ConsentLog( user_id=user.id, consent_type='cgu', version='1.0', granted=True, ip_address='192.0.2.1' ) db.session.add(log_no_ua) try: db.session.commit() raise AssertionError("Expected IntegrityError on missing user_agent") except IntegrityError: db.session.rollback() finally: db.session.rollback() db.drop_all() def test_user_has_new_b21_fields(): """User model gained: totp_secret_encrypted, totp_enabled, webauthn_credentials, ordre_pro, cabinet, stripe_customer_id, subscription_status.""" with app.app_context(): db.create_all() try: user = _make_user() user.totp_secret_encrypted = 'gAAAAABh-encrypted-fernet-token-placeholder' user.totp_enabled = True user.webauthn_credentials = [{'id': 'cred1', 'public_key': 'abc'}] user.ordre_pro = 'barreau' user.cabinet = 'Cabinet Pilote A' user.stripe_customer_id = 'cus_TestCustomerId' user.subscription_status = 'active' db.session.commit() fetched = User.query.filter_by(username='alice').first() assert fetched.totp_secret_encrypted == 'gAAAAABh-encrypted-fernet-token-placeholder' assert fetched.totp_enabled is True assert fetched.webauthn_credentials == [{'id': 'cred1', 'public_key': 'abc'}] assert fetched.ordre_pro == 'barreau' assert fetched.cabinet == 'Cabinet Pilote A' assert fetched.stripe_customer_id == 'cus_TestCustomerId' assert fetched.subscription_status == 'active' finally: db.session.rollback() db.drop_all() def test_user_b21_fields_default_to_safe_values(): """New User defaults: totp_enabled=False, others None.""" with app.app_context(): db.create_all() try: user = _make_user(username='bob', email='bob@example.com') assert user.totp_enabled is False, "totp_enabled must default to False (no MFA bypass)" assert user.totp_secret_encrypted is None assert user.webauthn_credentials is None assert user.stripe_customer_id is None assert user.subscription_status is None assert user.ordre_pro is None assert user.cabinet is None finally: db.session.rollback() db.drop_all() def test_consent_log_survives_user_deletion_with_null_user_id(): """Loi 25 art. 28.1 right-to-erasure: deleting a User must NOT delete their consent log rows. The user_id is set to NULL, the audit row survives. """ with app.app_context(): db.create_all() try: user = _make_user(username='claire', email='claire@example.com') uid = user.id log = ConsentLog( user_id=uid, consent_type='cgu', version='2026-04-27', granted=True, ip_address='192.0.2.1', user_agent='UA' ) db.session.add(log) db.session.commit() log_id = log.id db.session.delete(user) db.session.commit() # Audit row must survive surviving = ConsentLog.query.get(log_id) assert surviving is not None, "Consent log row must survive user deletion (Loi 25 audit trail)" assert surviving.user_id is None, "user_id must be NULL after user deletion (data minimization)" assert surviving.consent_type == 'cgu' assert surviving.granted is True finally: db.session.rollback() db.drop_all() def test_consent_log_rejects_invalid_consent_type(): """Typos like 'comfidentialite' must be rejected at ORM level.""" with app.app_context(): db.create_all() try: user = _make_user(username='diane', email='diane@example.com') try: ConsentLog( user_id=user.id, consent_type='comfidentialite', # typo version='1.0', granted=True, ip_address='192.0.2.1', user_agent='UA' ) raise AssertionError("ValueError expected on invalid consent_type") except ValueError as e: assert 'Invalid consent_type' in str(e) finally: db.session.rollback() db.drop_all()