"""Tests for cian_state_parser."""
from app.services.scrapers.cian_state_parser import extract_state
def test_extract_state_simple_json():
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 = ""
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("")
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"
# ── #639: новый формат Cian (2026-05) — .concat([{"key":..,"value":..}]) ──
def test_extract_state_concat_format() -> None:
"""Новый формат: .concat([{...}]) с JSON-кавычками вместо .push({key:..})."""
html = (
""
)
state = extract_state(html, mfe="frontend-serp", key="initialState")
assert state is not None
assert state["results"]["offers"][0]["id"] == 7
def test_extract_state_concat_bracket_in_string() -> None:
"""Балансировщик массива не ломается на ']'/'[' внутри JSON-строк."""
html = (
"window._cianConfig['header-frontend'] = "
"(window._cianConfig['header-frontend'] || []).concat("
'[{"key":"initialState","value":{"user":{"isAuthenticated":true,"userId":42,'
'"note":"text with ] and [ inside"}},"priority":1}]'
");"
)
state = extract_state(html, mfe="header-frontend", key="initialState")
assert state is not None
assert state["user"]["isAuthenticated"] is True
assert state["user"]["userId"] == 42
def test_extract_state_legacy_push_still_works() -> None:
"""Старый .push формат остаётся рабочим (fallback)."""
html = (
"window._cianConfig['frontend-serp'].push({"
'key: \'initialState\', value: {"results":{"offers":[{"id":3}]}}, priority: 1});'
)
state = extract_state(html, mfe="frontend-serp", key="initialState")
assert state is not None
assert state["results"]["offers"][0]["id"] == 3