gendesign/backend/tests/services/scrapers/test_domrf_kn_normalize.py
bot-backend 5b3799a23a
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Successful in 3m26s
Deploy / build-frontend (push) Successful in 3m38s
Deploy / deploy (push) Successful in 2m28s
Deploy / build-worker (push) Successful in 4m20s
feat(scrapers): wire per-flat DOM.РФ catalog scraper + propagate catalog_url_hash (#2442) (#2444)
2026-07-05 17:00:58 +00:00

88 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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
class TestCatalogUrlHash:
"""catalog_url_hash ОБЯЗАН пробрасываться из elemId (#2442 Task 1).
Ранее хардкодился в None → flat-catalog scraper выбирал 0 строк
(WHERE catalog_url_hash IS NOT NULL). Теперь берётся из того же elemId,
что и fallback flat_id, и живёт независимо от того, пришёл ли flatId.
"""
def test_hash_populated_from_elem_id(self) -> None:
row = {"elemId": "abc-hash-123"}
out = _norm_flat(row, region_cd=66)
assert out["catalog_url_hash"] == "abc-hash-123"
def test_hash_populated_even_when_flat_id_present(self) -> None:
# flatId присутствует → fallback-ветка flat_id НЕ выполняется, но hash
# всё равно должен прочитаться (регрессия, которую чинит Task 1).
row = {"flatId": 999, "elemId": "hash-with-flatid"}
out = _norm_flat(row, region_cd=66)
assert out["id"] == 999
assert out["catalog_url_hash"] == "hash-with-flatid"
def test_hash_none_when_elem_id_absent(self) -> None:
out = _norm_flat({"flatId": 1}, region_cd=66)
assert out["catalog_url_hash"] is None
def test_hash_empty_string_normalized_to_none(self) -> None:
# Пустую строку трактуем как отсутствие (не пишем "" в БД).
out = _norm_flat({"flatId": 1, "elemId": ""}, region_cd=66)
assert out["catalog_url_hash"] is None