SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%
5.7 KB · 138 lines python
Raw Blame History
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