Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)
Python 47.5%
HTML 27.9%
TypeScript 15.5%
CSS 7.2%
JavaScript 2%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# tests/test_mortgage_validate.py : garde-fous anti-données-aberrantes —5# 4,19 % devenu 419 % DOIT être rejeté, jamais publié.6# -----------------------------------------------------------------------------7import unittest89from immoka.mortgage.validate import validate_batch, validate_product101112def product(**over) -> dict:13 base = {14 "provider": "test", "institution": "Banque Test",15 "rate_type": "fixed", "term_months": 60, "kind": "posted",16 "rate": 4.19, "apr": None, "insured_status": "unknown",17 "purpose": "purchase", "product_name": "Fixe fermé 5 ans",18 "source_url": "https://example.com/taux",19 }20 base.update(over)21 return base222324class TestValidateProduct(unittest.TestCase):25 def test_valid(self):26 ok, issues = validate_product(product())27 self.assertTrue(ok, issues)2829 def test_419_aberration_rejected(self):30 ok, issues = validate_product(product(rate=419.0))31 self.assertFalse(ok)32 self.assertTrue(any("impossible" in i for i in issues))3334 def test_zero_and_negative_rejected(self):35 self.assertFalse(validate_product(product(rate=0.0))[0])36 self.assertFalse(validate_product(product(rate=-1.5))[0])37 self.assertFalse(validate_product(product(rate=0.1))[0]) # < 0,25 %38 self.assertFalse(validate_product(product(rate=25.0))[0]) # > 20 %3940 def test_missing_fields(self):41 for field in ("provider", "institution", "rate_type", "term_months",42 "kind", "rate", "source_url"):43 ok, issues = validate_product(product(**{field: None}))44 self.assertFalse(ok)45 self.assertTrue(any(field in i for i in issues))4647 def test_term_bounds(self):48 self.assertFalse(validate_product(product(term_months=1))[0])49 self.assertFalse(validate_product(product(term_months=180))[0])50 self.assertFalse(validate_product(product(term_months="60"))[0])51 self.assertTrue(validate_product(product(term_months=3))[0])52 self.assertTrue(validate_product(product(term_months=120))[0])5354 def test_enums(self):55 self.assertFalse(validate_product(product(rate_type="promo"))[0])56 self.assertFalse(validate_product(product(kind="teaser"))[0])57 self.assertFalse(validate_product(product(insured_status="peut-être"))[0])58 self.assertFalse(validate_product(product(purpose="spéculation"))[0])5960 def test_apr_below_rate_rejected(self):61 # Un APR < taux contractuel est mathématiquement impossible.62 self.assertFalse(validate_product(product(rate=4.84, apr=4.20))[0])63 # Tolérance d'arrondi de 0,02 point.64 self.assertTrue(validate_product(product(rate=4.84, apr=4.83))[0])65 self.assertTrue(validate_product(product(rate=4.84, apr=4.861))[0])6667 def test_apr_bounds(self):68 self.assertFalse(validate_product(product(apr=419.0))[0])69 self.assertFalse(validate_product(product(apr="4.9"))[0])707172class TestValidateBatch(unittest.TestCase):73 def test_filters_and_reports(self):74 ok, problems = validate_batch([75 product(),76 product(rate=419.0, product_name="Aberrant"),77 product(term_months=60, rate=4.19), # duplicata exact → ignoré78 product(term_months=36, product_name="Fixe fermé 3 ans"),79 ])80 self.assertEqual(len(ok), 2)81 self.assertEqual(len(problems), 1)82 self.assertIn("Aberrant", problems[0])8384 def test_same_product_different_rate_kept(self):85 # même clé produit mais taux différent : les deux passent (le store86 # tranchera) — seul le duplicata EXACT est éliminé87 ok, _ = validate_batch([product(rate=4.19), product(rate=4.24)])88 self.assertEqual(len(ok), 2)899091if __name__ == "__main__":92 unittest.main()93