"""Unit tests for Yandex Realty parsing helpers.""" from datetime import date from app.services.scrapers.yandex_helpers import ( RE_FLOOR, RE_JK_ID, RE_OFFER_ID, RE_TITLE_AREA, RE_TITLE_ROOMS, extract_json_ld, find_ld_by_type, parse_dmy, parse_house_class, parse_house_type, parse_ru_date, parse_rub, ) # --- house_type --- def test_parse_house_type_panel(): assert parse_house_type("в тёплом, панельном доме 1981 года") == "panel" def test_parse_house_type_monolith_brick(): assert parse_house_type("кирпично-монолитный дом") == "monolith_brick" def test_parse_house_type_monolith_only(): assert parse_house_type("монолитное здание") == "monolith" def test_parse_house_type_brick_only(): assert parse_house_type("кирпичный дом 1995") == "brick" def test_parse_house_type_unknown(): assert parse_house_type("из железобетона") is None assert parse_house_type("") is None assert parse_house_type(None) is None # --- house_class --- def test_parse_house_class_comfort_plus(): assert parse_house_class("дома класса комфорт+") == "comfort_plus" def test_parse_house_class_comfort(): assert parse_house_class("ЖК класса комфорт в центре") == "comfort" def test_parse_house_class_business(): assert parse_house_class("проект класса бизнес") == "business" def test_parse_house_class_premium(): assert parse_house_class("ЖК премиум-класса") == "premium" def test_parse_house_class_elite(): assert parse_house_class("элитный класс жилья") == "elite" def test_parse_house_class_none(): assert parse_house_class("обычный жилой комплекс") is None assert parse_house_class(None) is None # --- parse_rub --- def test_parse_rub_full_price(): assert parse_rub("4 399 000 ₽") == 4_399_000 def test_parse_rub_mln_comma(): assert parse_rub("4,4 млн ₽") == 4_400_000 def test_parse_rub_mln_dot(): assert parse_rub("4.4 млн ₽") == 4_400_000 def test_parse_rub_ppm2(): assert parse_rub("117 500 ₽ за м²") == 117_500 def test_parse_rub_raw_digits(): assert parse_rub("117500") == 117_500 def test_parse_rub_empty(): assert parse_rub("") is None assert parse_rub(None) is None assert parse_rub("без цены") is None def test_parse_rub_nbsp_separator(): """Yandex SERP форматирует «9\\xa0800\\xa0000» через NBSP, не regular space. До fix'а `.replace(" ", "")` оставлял \\xa0 → int() падал ValueError → parse_rub() возвращал None → весь scraper не парсил lots (0/23 на live HTML). """ assert parse_rub("9\xa0800\xa0000\xa0₽") == 9_800_000 assert parse_rub("4\xa0399\xa0000") == 4_399_000 def test_parse_rub_mln_with_nbsp(): """«4,4\\xa0млн\\xa0₽» — NBSP вокруг unit-keyword тоже встречается.""" assert parse_rub("4,4\xa0млн\xa0₽") == 4_400_000 # --- parse_ru_date --- def test_parse_ru_date_basic(): assert parse_ru_date("9 мая 2026") == date(2026, 5, 9) def test_parse_ru_date_in_sentence(): assert parse_ru_date("опубликовано 17 февраля 2024") == date(2024, 2, 17) def test_parse_ru_date_invalid_month(): assert parse_ru_date("9 invalidmonth 2026") is None def test_parse_ru_date_dmy_not_supported(): # parse_ru_date doesn't handle DD.MM.YYYY (that's parse_dmy's job) assert parse_ru_date("01.02.2026") is None def test_parse_ru_date_empty(): assert parse_ru_date("") is None assert parse_ru_date(None) is None # --- parse_listing_date (extended formats) --- def test_listing_date_today_keyword(): from datetime import timedelta from app.services.scrapers.yandex_helpers import parse_listing_date assert parse_listing_date("сегодня в 12:30") == date.today() assert parse_listing_date("вчера в 18:45") == date.today() - timedelta(days=1) assert parse_listing_date("позавчера") == date.today() - timedelta(days=2) assert parse_listing_date("только что") == date.today() def test_listing_date_absolute_no_year(): """Yandex SERP формат «18 мая» без года — infer current year.""" from app.services.scrapers.yandex_helpers import parse_listing_date today = date.today() # Pick a date в прошлом этого года, чтобы тест стабильный if today.month >= 2: target = date(today.year, today.month - 1, 15) else: target = date(today.year - 1, 12, 15) ru_months = { 1: "января", 2: "февраля", 3: "марта", 4: "апреля", 5: "мая", 6: "июня", 7: "июля", 8: "августа", 9: "сентября", 10: "октября", 11: "ноября", 12: "декабря", } text = f"{target.day} {ru_months[target.month]}" assert parse_listing_date(text) == target def test_listing_date_relative_still_works(): from datetime import timedelta from app.services.scrapers.yandex_helpers import parse_listing_date assert parse_listing_date("5 дней назад") == date.today() - timedelta(days=5) # --- parse_dmy --- def test_parse_dmy_basic(): assert parse_dmy("17.02.2026") == date(2026, 2, 17) def test_parse_dmy_in_sentence(): assert parse_dmy("Дата 31.12.2025 — конец года") == date(2025, 12, 31) def test_parse_dmy_invalid(): assert parse_dmy("99.99.9999") is None assert parse_dmy("") is None assert parse_dmy(None) is None # --- JSON-LD --- def test_extract_json_ld_single(): html = ( '" ) ld = extract_json_ld(html) assert len(ld) == 1 assert ld[0]["@type"] == "Product" def test_extract_json_ld_multiple(): html = ( '' '' ) ld = extract_json_ld(html) assert len(ld) == 2 def test_extract_json_ld_array_wrapper(): html = '' ld = extract_json_ld(html) assert len(ld) == 2 def test_extract_json_ld_invalid_skipped(): html = ( '' '' ) ld = extract_json_ld(html) assert len(ld) == 1 assert ld[0]["@type"] == "Valid" def test_find_ld_by_type_simple(): html = ( '' '' ) product = find_ld_by_type(html, "Product") assert product is not None assert product["price"] == 100 def test_find_ld_by_type_list_form(): # @type can be a list html = '' assert find_ld_by_type(html, "Offer") is not None def test_find_ld_by_type_missing(): html = '' assert find_ld_by_type(html, "Product") is None # --- regex sanity --- def test_offer_id(): m = RE_OFFER_ID.search("https://realty.yandex.ru/offer/7567094292504417257/") assert m and m.group(1) == "7567094292504417257" def test_jk_id(): m = RE_JK_ID.search("/ekaterinburg/kupit/novostrojka/tatlin-1592987/") assert m assert m.group(1) == "tatlin" assert m.group(2) == "1592987" def test_title_parsing(): title = "39 м² · 1-комнатная квартира · 6 этаж из 15" area = RE_TITLE_AREA.search(title) rooms = RE_TITLE_ROOMS.search(title) floor = RE_FLOOR.search(title) assert area and area.group(1) == "39" assert rooms and rooms.group(1) == "1" assert floor and floor.group(1) == "6" and floor.group(2) == "15" def test_title_studio(): rooms = RE_TITLE_ROOMS.search("Студия 28 м²") # group(2) captures студия/студию/студий when no number-room pattern present assert rooms is not None matched = rooms.group(1) or rooms.group(2) assert matched is not None and matched.lower().startswith("студи")