gendesign/tradein-mvp/backend/tests/test_cian_state_parser.py
bot-backend 10155eb0fb fix(tradein): word-boundary street match + cian unescape robustness (Refs #769 A1+D)
Часть 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).
2026-05-30 20:13:07 +03:00

78 lines
2.9 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.

"""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"