Часть A1 (#10 correctness): address ILIKE '%street%' давал ложные совпадения («Мира» матчит «Адмирала Макарова») → раздувало аналоги оценки. Добавлен word-boundary regex (~* :street_regex, \m..\M) рядом с ILIKE-prefilter в trade_in.py (street-deals) и estimator.py (_fetch_dkp_corridor). Bind-param, не f-string. Только предикат street-match, anchor/pricing не тронуты. Часть D (#16): cian_state_parser hand-rolled unescape был order-dependent (\" заменялся раньше \ → порча) → backslash в описании ронял парс всей страницы. Заменён на атомарный json.loads внешней quoted-строки. Тесты: «Мира»≠«Адмирала Макарова» но =«Мира 5»; backslash-в-описании парсится. 22 passed, ruff clean. A2 (trgm index) / B (FDW options) / C (matview refresh) / E (geo centroid) ОТЛОЖЕНЫ — нужна live tradein-DB / investigation (follow-up в #769).
78 lines
2.9 KiB
Python
78 lines
2.9 KiB
Python
"""Tests for cian_state_parser."""
|
||
|
||
from app.services.scrapers.cian_state_parser import extract_state
|
||
|
||
|
||
def test_extract_state_simple_json():
|
||
html = """
|
||
<html><body>
|
||
<script>
|
||
window._cianConfig['frontend-serp'] = window._cianConfig['frontend-serp'] || [];
|
||
window._cianConfig['frontend-serp'].push({
|
||
key: 'initialState',
|
||
value: {"results": {"offers": [{"id": 1}]}},
|
||
priority: 1
|
||
});
|
||
</script>
|
||
</body></html>
|
||
"""
|
||
state = extract_state(html, mfe="frontend-serp", key="initialState")
|
||
assert state is not None
|
||
assert state["results"]["offers"][0]["id"] == 1
|
||
|
||
|
||
def test_extract_state_json_parse_form():
|
||
html = r"""
|
||
window._cianConfig['frontend-offer-card'].push({
|
||
key: 'defaultState',
|
||
value: JSON.parse("{\"offerData\":{\"offer\":{\"cianId\":123}}}"),
|
||
priority: 1
|
||
});
|
||
"""
|
||
state = extract_state(html, mfe="frontend-offer-card", key="defaultState")
|
||
assert state is not None
|
||
assert state["offerData"]["offer"]["cianId"] == 123
|
||
|
||
|
||
def test_extract_state_missing_returns_none():
|
||
html = "<html></html>"
|
||
assert extract_state(html, mfe="frontend-serp") is None
|
||
|
||
|
||
def test_extract_state_wrong_mfe_returns_none():
|
||
html = """
|
||
window._cianConfig['other-mfe'].push({key: 'initialState', value: {}, priority: 1});
|
||
"""
|
||
assert extract_state(html, mfe="frontend-serp", key="initialState") is None
|
||
|
||
|
||
def test_extract_state_backslash_in_description() -> None:
|
||
"""Backslash in a listing description must not break page parsing (Part D fix).
|
||
|
||
The hand-rolled .replace() chain was order-dependent: '\\\\' → '\\' after
|
||
'\\"' → '"' left stray backslashes that broke json.loads on the inner payload.
|
||
The fix uses json.loads on the outer quoted string atomically.
|
||
"""
|
||
# Build a JSON payload where the description contains a literal backslash.
|
||
# In the wire format (JSON.parse("...")), the inner JSON is double-escaped:
|
||
# inner JSON: {"desc": "с \\ слешем"}
|
||
# outer string: "{\"desc\": \"с \\\\ слешем\"}"
|
||
import json
|
||
|
||
inner_payload = {"offerData": {"offer": {"cianId": 42, "description": r"описание с \ слешем"}}}
|
||
inner_json_str = json.dumps(inner_payload, ensure_ascii=False)
|
||
# Simulate what Cian emits: JSON.parse("<double-escaped inner>")
|
||
outer_escaped = json.dumps(inner_json_str) # produces "\"...\"" with double escaping
|
||
html = f"""
|
||
window._cianConfig['frontend-offer-card'].push({{
|
||
key: 'defaultState',
|
||
value: JSON.parse({outer_escaped}),
|
||
priority: 1
|
||
}});
|
||
"""
|
||
state = extract_state(html, mfe="frontend-offer-card", key="defaultState")
|
||
assert state is not None, "extract_state returned None — page parse failed"
|
||
assert state["offerData"]["offer"]["cianId"] == 42
|
||
assert (
|
||
"\\" in state["offerData"]["offer"]["description"]
|
||
), "Backslash in description was lost during unescape"
|