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