# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # tests/test_mortgage_providers.py : les 12 connecteurs sur fixtures réelles # (payloads capturés le 2026-08-24). Chaque provider doit produire un lot # propre (validate_batch sans problème) et retrouver des valeurs connues. # ----------------------------------------------------------------------------- import unittest from pathlib import Path from immoka.mortgage.providers import PROVIDERS from immoka.mortgage.validate import validate_batch FIX = Path(__file__).parent / "fixtures" / "mortgage" # slug -> (fixture, nombre de produits attendu) EXPECTED = { "bank_of_canada": ("bank-of-canada.json", 5), "bmo": ("bmo.json", 20), "cibc": ("cibc.json", 24), "desjardins": ("desjardins.html", 19), "eq_bank": ("eq-bank.html", 13), "first_national": ("first-national.html", 41), "mcap": ("mcap.html", 1), "national_bank": ("national-bank.html", 19), "rbc": ("rbc.json", 19), "scotiabank": ("scotiabank.json", 21), "tangerine": ("tangerine.json", 8), "td": ("td.json", 30), } def parse_fixture(slug: str) -> list[dict]: prov = PROVIDERS[slug]() fixture, _ = EXPECTED[slug] products = prov.parse((FIX / fixture).read_text()) if slug == "scotiabank": # deuxième flux : promos variables products += prov.parse_promos( (FIX / "scotiabank-varmortgage.json").read_text()) return products def find(products, **crit): return [p for p in products if all(p.get(k) == v for k, v in crit.items())] class TestRegistry(unittest.TestCase): def test_twelve_providers_registered(self): self.assertEqual(set(PROVIDERS), set(EXPECTED)) class TestAllFixturesClean(unittest.TestCase): def test_counts_and_validation(self): for slug, (_, expected_n) in EXPECTED.items(): with self.subTest(provider=slug): products = parse_fixture(slug) ok, problems = validate_batch(products) self.assertEqual(problems, []) self.assertEqual(len(ok), expected_n) self.assertEqual(len(products), expected_n) def test_every_product_has_provenance(self): for slug in EXPECTED: for p in parse_fixture(slug): with self.subTest(provider=slug, product=p.get("product_name")): self.assertTrue(p.get("source_url")) self.assertTrue(p.get("institution")) class TestKnownValues(unittest.TestCase): """Valeurs lues à la main dans les fixtures commitées.""" def test_td_5y_special(self): products = parse_fixture("td") specials = find(products, rate_type="fixed", term_months=60, kind="special", insured_status="uninsured") self.assertEqual(len(specials), 1) self.assertEqual(specials[0]["rate"], 4.84) # l'« affiché » des codes variables TD est le prime : jamais émis # comme taux variable, une seule entrée rate_type=other primes = find(products, rate_type="other") self.assertEqual(len(primes), 1) self.assertEqual(primes[0]["product_name"], "TD Mortgage Prime") def test_cibc_prime_and_special(self): products = parse_fixture("cibc") prime = find(products, product_name="Taux préférentiel CIBC") self.assertEqual(len(prime), 1) self.assertEqual(prime[0]["rate"], 4.45) self.assertEqual(prime[0]["purpose"], "unknown") # exclu du best_rate var5 = find(products, rate_type="variable", term_months=60, kind="special") self.assertEqual({p["rate"] for p in var5} & {4.10}, {4.10}) def test_bmo_specials(self): products = parse_fixture("bmo") fixed3 = find(products, rate_type="fixed", term_months=36, kind="special", product_name="Fixe fermé 3 ans (offre spéciale)") self.assertEqual(len(fixed3), 1) self.assertEqual(fixed3[0]["rate"], 4.64) def test_national_bank_posted_grid(self): products = parse_fixture("national_bank") closed_1y = find(products, rate_type="fixed", term_months=12, kind="posted", product_name="Fixe fermé 1 an") self.assertEqual([p["rate"] for p in closed_1y], [5.49]) def test_eq_bank_showcase(self): products = parse_fixture("eq_bank") fixe5 = find(products, rate_type="fixed", term_months=60, kind="special", product_name="Fixe 5 ans (vitrine EQ)") self.assertEqual(len(fixe5), 1) self.assertEqual(fixe5[0]["rate"], 4.04) def test_boc_reference_series_excluded_from_market(self): # Les séries de référence BdC ne doivent jamais concourir au meilleur # taux d'achat : purpose=unknown pour les taux « cible » et prime. products = parse_fixture("bank_of_canada") for p in find(products, rate_type="other"): self.assertEqual(p["purpose"], "unknown") def test_no_prime_rate_masquerades_as_product(self): # Aucun provider ne doit publier un « préférentiel » comme taux # hypothécaire fixe/variable comparable (purpose=purchase). for slug in EXPECTED: for p in parse_fixture(slug): name = (p.get("product_name") or "").lower() if "préférentiel" in name or "prime" in name: with self.subTest(provider=slug, product=name): self.assertIn(p["rate_type"], ("other",)) self.assertEqual(p.get("purpose"), "unknown") if __name__ == "__main__": unittest.main()