# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : tests/test_connectors.py # Rôle : Tests de régression des connecteurs (rejeu des fixtures hors ligne) # Ré-enregistrer après un changement de site : run.py record # Créé : 2026-08-17 Modifié : 2026-08-17 # ============================================================================= import json import re import pytest from jobka.fixtures import FIXTURES_DIR, recorded_sources, replay_connector, snapshot SOURCES = recorded_sources() pytestmark = pytest.mark.skipif( not SOURCES, reason="aucune fixture enregistrée (run.py record )") @pytest.fixture(scope="module") def parsed(): """Rejoue chaque connecteur une seule fois par session de test.""" cache = {} def _get(source_id): if source_id not in cache: cache[source_id] = replay_connector(source_id).fetch() return cache[source_id] return _get @pytest.mark.parametrize("source_id", SOURCES) def test_parsing_matches_snapshot(source_id, parsed): """Le parsing des fixtures reproduit exactement l'instantané enregistré.""" expected = json.loads( (FIXTURES_DIR / source_id / "expected.json").read_text(encoding="utf-8")) actual = snapshot(parsed(source_id)) assert actual["count"] == expected["count"] assert actual["jobs"] == expected["jobs"] @pytest.mark.parametrize("source_id", SOURCES) def test_jobs_are_valid(source_id, parsed): """Contraintes de schéma : uid uniques, URLs absolues, salaires plausibles.""" postings = parsed(source_id) assert postings, "le connecteur ne retourne aucune offre depuis la fixture" uids = [p.uid for p in postings] assert len(uids) == len(set(uids)), "external_id en double" for p in postings: assert p.source == source_id assert p.external_id assert p.title, f"offre sans titre : {p.uid}" assert p.employer, f"offre sans employeur : {p.uid}" assert re.match(r"^https?://", p.url), f"URL non absolue : {p.url!r}" if p.salary_min is not None: assert p.salary_unit in ("hour", "day", "week", "biweek", "month", "year") assert p.salary_max is None or p.salary_max >= p.salary_min # jamais de valeur inventée : chaîne vide ou None, pas de « N/A » assert (p.employment_type or "") not in ("N/A", "n/a", "?") @pytest.mark.parametrize("source_id", SOURCES) def test_finalize_is_safe(source_id, parsed): """finalize() ne lève pas et produit des valeurs canoniques.""" for p in parsed(source_id): f = p.finalize() assert f.work_mode in (None, "presentiel", "hybride", "teletravail") assert f.employment_type in (None, "temps_plein", "temps_partiel", "contractuel", "stage", "saisonnier") if f.date_posted is not None: assert re.fullmatch(r"\d{4}-\d{2}-\d{2}", f.date_posted) if f.lat is not None: assert 44.5 <= f.lat <= 63.0 and -80.0 <= f.lng <= -56.0