"""Unit-тесты для нормализаторов scrapers/domrf_kn (#1208). Главный сценарий: детерминированный fallback flat_id через sha256 — НЕ рандомизированный hash(). Раньше при resume упавшего sweep (новый процесс воркера) та же квартира получала другой id → ON CONFLICT не срабатывал → дубли в одном snapshot. """ from __future__ import annotations import hashlib from app.services.scrapers.domrf_kn import _norm_flat def _expected_sha_fallback(elem: str) -> int: """Зеркало формулы из _norm_flat — для прямого сравнения в тестах.""" digest = hashlib.sha256(str(elem).encode("utf-8")).digest()[:8] return int.from_bytes(digest, "big") % (2**63 - 1) class TestFlatIdFallbackDeterministic: """flat_id fallback ОБЯЗАН быть детерминированным между процессами/перезапусками.""" def test_fallback_matches_sha256_formula(self) -> None: row = {"elemId": "abc123"} out = _norm_flat(row, region_cd=66) assert out["id"] == _expected_sha_fallback("abc123") def test_fallback_stable_across_calls(self) -> None: # Тот же elemId → тот же id. (Раньше hash() мог дать другой id в новом процессе.) row = {"elemId": "stable-elem-id"} a = _norm_flat(row, region_cd=66)["id"] b = _norm_flat(row, region_cd=66)["id"] assert a == b assert a == _expected_sha_fallback("stable-elem-id") def test_different_elems_give_different_ids(self) -> None: a = _norm_flat({"elemId": "elem-A"}, region_cd=66)["id"] b = _norm_flat({"elemId": "elem-B"}, region_cd=66)["id"] assert a != b def test_flat_id_present_wins_over_fallback(self) -> None: # Если flatId есть — используем его, fallback не активируется. row = {"flatId": 12345, "elemId": "should-be-ignored"} out = _norm_flat(row, region_cd=66) assert out["id"] == 12345 def test_no_flat_id_no_elem_id_returns_none(self) -> None: out = _norm_flat({}, region_cd=66) assert out["id"] is None def test_fallback_fits_bigint(self) -> None: # 8-байтовый digest усечён в pos-int, всегда < 2**63. out = _norm_flat({"elemId": "x" * 1000}, region_cd=66) assert out["id"] is not None assert 0 <= out["id"] < 2**63