# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # tests/test_mortgage_validate.py : garde-fous anti-données-aberrantes — # 4,19 % devenu 419 % DOIT être rejeté, jamais publié. # ----------------------------------------------------------------------------- import unittest from immoka.mortgage.validate import validate_batch, validate_product def product(**over) -> dict: base = { "provider": "test", "institution": "Banque Test", "rate_type": "fixed", "term_months": 60, "kind": "posted", "rate": 4.19, "apr": None, "insured_status": "unknown", "purpose": "purchase", "product_name": "Fixe fermé 5 ans", "source_url": "https://example.com/taux", } base.update(over) return base class TestValidateProduct(unittest.TestCase): def test_valid(self): ok, issues = validate_product(product()) self.assertTrue(ok, issues) def test_419_aberration_rejected(self): ok, issues = validate_product(product(rate=419.0)) self.assertFalse(ok) self.assertTrue(any("impossible" in i for i in issues)) def test_zero_and_negative_rejected(self): self.assertFalse(validate_product(product(rate=0.0))[0]) self.assertFalse(validate_product(product(rate=-1.5))[0]) self.assertFalse(validate_product(product(rate=0.1))[0]) # < 0,25 % self.assertFalse(validate_product(product(rate=25.0))[0]) # > 20 % def test_missing_fields(self): for field in ("provider", "institution", "rate_type", "term_months", "kind", "rate", "source_url"): ok, issues = validate_product(product(**{field: None})) self.assertFalse(ok) self.assertTrue(any(field in i for i in issues)) def test_term_bounds(self): self.assertFalse(validate_product(product(term_months=1))[0]) self.assertFalse(validate_product(product(term_months=180))[0]) self.assertFalse(validate_product(product(term_months="60"))[0]) self.assertTrue(validate_product(product(term_months=3))[0]) self.assertTrue(validate_product(product(term_months=120))[0]) def test_enums(self): self.assertFalse(validate_product(product(rate_type="promo"))[0]) self.assertFalse(validate_product(product(kind="teaser"))[0]) self.assertFalse(validate_product(product(insured_status="peut-être"))[0]) self.assertFalse(validate_product(product(purpose="spéculation"))[0]) def test_apr_below_rate_rejected(self): # Un APR < taux contractuel est mathématiquement impossible. self.assertFalse(validate_product(product(rate=4.84, apr=4.20))[0]) # Tolérance d'arrondi de 0,02 point. self.assertTrue(validate_product(product(rate=4.84, apr=4.83))[0]) self.assertTrue(validate_product(product(rate=4.84, apr=4.861))[0]) def test_apr_bounds(self): self.assertFalse(validate_product(product(apr=419.0))[0]) self.assertFalse(validate_product(product(apr="4.9"))[0]) class TestValidateBatch(unittest.TestCase): def test_filters_and_reports(self): ok, problems = validate_batch([ product(), product(rate=419.0, product_name="Aberrant"), product(term_months=60, rate=4.19), # duplicata exact → ignoré product(term_months=36, product_name="Fixe fermé 3 ans"), ]) self.assertEqual(len(ok), 2) self.assertEqual(len(problems), 1) self.assertIn("Aberrant", problems[0]) def test_same_product_different_rate_kept(self): # même clé produit mais taux différent : les deux passent (le store # tranchera) — seul le duplicata EXACT est éliminé ok, _ = validate_batch([product(rate=4.19), product(rate=4.24)]) self.assertEqual(len(ok), 2) if __name__ == "__main__": unittest.main()