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_providers.py : les 12 connecteurs sur fixtures réelles5# (payloads capturés le 2026-08-24). Chaque provider doit produire un lot6# propre (validate_batch sans problème) et retrouver des valeurs connues.7# -----------------------------------------------------------------------------8import unittest9from pathlib import Path1011from immoka.mortgage.providers import PROVIDERS12from immoka.mortgage.validate import validate_batch1314FIX = Path(__file__).parent / "fixtures" / "mortgage"1516# slug -> (fixture, nombre de produits attendu)17EXPECTED = {18 "bank_of_canada": ("bank-of-canada.json", 5),19 "bmo": ("bmo.json", 20),20 "cibc": ("cibc.json", 24),21 "desjardins": ("desjardins.html", 19),22 "eq_bank": ("eq-bank.html", 13),23 "first_national": ("first-national.html", 41),24 "mcap": ("mcap.html", 1),25 "national_bank": ("national-bank.html", 19),26 "rbc": ("rbc.json", 19),27 "scotiabank": ("scotiabank.json", 21),28 "tangerine": ("tangerine.json", 8),29 "td": ("td.json", 30),30}313233def parse_fixture(slug: str) -> list[dict]:34 prov = PROVIDERS[slug]()35 fixture, _ = EXPECTED[slug]36 products = prov.parse((FIX / fixture).read_text())37 if slug == "scotiabank": # deuxième flux : promos variables38 products += prov.parse_promos(39 (FIX / "scotiabank-varmortgage.json").read_text())40 return products414243def find(products, **crit):44 return [p for p in products45 if all(p.get(k) == v for k, v in crit.items())]464748class TestRegistry(unittest.TestCase):49 def test_twelve_providers_registered(self):50 self.assertEqual(set(PROVIDERS), set(EXPECTED))515253class TestAllFixturesClean(unittest.TestCase):54 def test_counts_and_validation(self):55 for slug, (_, expected_n) in EXPECTED.items():56 with self.subTest(provider=slug):57 products = parse_fixture(slug)58 ok, problems = validate_batch(products)59 self.assertEqual(problems, [])60 self.assertEqual(len(ok), expected_n)61 self.assertEqual(len(products), expected_n)6263 def test_every_product_has_provenance(self):64 for slug in EXPECTED:65 for p in parse_fixture(slug):66 with self.subTest(provider=slug, product=p.get("product_name")):67 self.assertTrue(p.get("source_url"))68 self.assertTrue(p.get("institution"))697071class TestKnownValues(unittest.TestCase):72 """Valeurs lues à la main dans les fixtures commitées."""7374 def test_td_5y_special(self):75 products = parse_fixture("td")76 specials = find(products, rate_type="fixed", term_months=60,77 kind="special", insured_status="uninsured")78 self.assertEqual(len(specials), 1)79 self.assertEqual(specials[0]["rate"], 4.84)80 # l'« affiché » des codes variables TD est le prime : jamais émis81 # comme taux variable, une seule entrée rate_type=other82 primes = find(products, rate_type="other")83 self.assertEqual(len(primes), 1)84 self.assertEqual(primes[0]["product_name"], "TD Mortgage Prime")8586 def test_cibc_prime_and_special(self):87 products = parse_fixture("cibc")88 prime = find(products, product_name="Taux préférentiel CIBC")89 self.assertEqual(len(prime), 1)90 self.assertEqual(prime[0]["rate"], 4.45)91 self.assertEqual(prime[0]["purpose"], "unknown") # exclu du best_rate92 var5 = find(products, rate_type="variable", term_months=60,93 kind="special")94 self.assertEqual({p["rate"] for p in var5} & {4.10}, {4.10})9596 def test_bmo_specials(self):97 products = parse_fixture("bmo")98 fixed3 = find(products, rate_type="fixed", term_months=36,99 kind="special",100 product_name="Fixe fermé 3 ans (offre spéciale)")101 self.assertEqual(len(fixed3), 1)102 self.assertEqual(fixed3[0]["rate"], 4.64)103104 def test_national_bank_posted_grid(self):105 products = parse_fixture("national_bank")106 closed_1y = find(products, rate_type="fixed", term_months=12,107 kind="posted", product_name="Fixe fermé 1 an")108 self.assertEqual([p["rate"] for p in closed_1y], [5.49])109110 def test_eq_bank_showcase(self):111 products = parse_fixture("eq_bank")112 fixe5 = find(products, rate_type="fixed", term_months=60,113 kind="special", product_name="Fixe 5 ans (vitrine EQ)")114 self.assertEqual(len(fixe5), 1)115 self.assertEqual(fixe5[0]["rate"], 4.04)116117 def test_boc_reference_series_excluded_from_market(self):118 # Les séries de référence BdC ne doivent jamais concourir au meilleur119 # taux d'achat : purpose=unknown pour les taux « cible » et prime.120 products = parse_fixture("bank_of_canada")121 for p in find(products, rate_type="other"):122 self.assertEqual(p["purpose"], "unknown")123124 def test_no_prime_rate_masquerades_as_product(self):125 # Aucun provider ne doit publier un « préférentiel » comme taux126 # hypothécaire fixe/variable comparable (purpose=purchase).127 for slug in EXPECTED:128 for p in parse_fixture(slug):129 name = (p.get("product_name") or "").lower()130 if "préférentiel" in name or "prime" in name:131 with self.subTest(provider=slug, product=name):132 self.assertIn(p["rate_type"], ("other",))133 self.assertEqual(p.get("purpose"), "unknown")134135136if __name__ == "__main__":137 unittest.main()138