gendesign/tradein-mvp/backend/tests/test_yandex_session.py
bot-backend 5be64c6688
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m18s
Deploy Trade-In / test (push) Successful in 4m4s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 2m0s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
fix(tradein/scrapers): хранилище авторизованной сессии Яндекс.Недвижимости (#3195)
2026-08-28 19:02:05 +00:00

215 lines
7.5 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 yandex_session — cookie management service (#3192).
Зеркалит test_domclick_session.py (MagicMock db, никакого live DB) — без
verify_session, т.к. yandex_session MVP её не реализует (см. модуль docstring).
"""
from __future__ import annotations
import json
import re
from unittest.mock import MagicMock
import pytest
from app.services.yandex_session import (
ANALYTICS_COOKIE_PREFIXES,
AUTH_MARKER_COOKIE,
YANDEX_CRITICAL_COOKIES,
filter_analytics_cookies,
has_auth_marker,
load_session,
mark_session_invalid,
save_session,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def mock_db() -> MagicMock:
db = MagicMock()
# Default: no row found (load_session → None)
db.execute.return_value.mappings.return_value.first.return_value = None
return db
def _cookie(name: str, value: str = "x") -> dict[str, str]:
return {"name": name, "value": value, "domain": ".yandex.ru", "path": "/"}
# ---------------------------------------------------------------------------
# YANDEX_CRITICAL_COOKIES / AUTH_MARKER_COOKIE
# ---------------------------------------------------------------------------
def test_critical_cookies_set_not_empty() -> None:
assert isinstance(YANDEX_CRITICAL_COOKIES, set)
assert len(YANDEX_CRITICAL_COOKIES) >= 5
def test_critical_cookies_contains_key_names() -> None:
assert "Session_id" in YANDEX_CRITICAL_COOKIES
assert "yandexuid" in YANDEX_CRITICAL_COOKIES
assert "sessguard" in YANDEX_CRITICAL_COOKIES
def test_auth_marker_is_session_id() -> None:
assert AUTH_MARKER_COOKIE == "Session_id"
# ---------------------------------------------------------------------------
# filter_analytics_cookies — NOT an allowlist, only drops analytics prefixes
# ---------------------------------------------------------------------------
def test_filter_analytics_drops_only_analytics_prefixes() -> None:
cookies = [
_cookie("Session_id"),
_cookie("_ym_uid"),
_cookie("_ym_d"),
_cookie("yabs-sid"),
_cookie("_yasc"),
_cookie("some_unknown_cookie_not_in_any_allowlist"),
]
result = filter_analytics_cookies(cookies)
names = {c["name"] for c in result}
assert names == {"Session_id", "some_unknown_cookie_not_in_any_allowlist"}
def test_filter_analytics_keeps_full_dump_minus_analytics() -> None:
"""Замер #3192 сделан на 44 cookies — фильтр НЕ должен сужать до allowlist."""
cookies = [_cookie(f"unknown_cookie_{i}") for i in range(40)] + [
_cookie("Session_id"),
_cookie("_ym_isad"),
_cookie("_yasc"),
_cookie("yabs-sid"),
]
result = filter_analytics_cookies(cookies)
assert len(result) == 41 # 40 unknown + Session_id, 3 analytics dropped
def test_analytics_prefixes_tuple() -> None:
assert ANALYTICS_COOKIE_PREFIXES == ("_ym_", "yabs-", "_yasc")
# ---------------------------------------------------------------------------
# has_auth_marker
# ---------------------------------------------------------------------------
def test_has_auth_marker_true_when_session_id_present() -> None:
assert has_auth_marker([_cookie("Session_id"), _cookie("yandexuid")]) is True
def test_has_auth_marker_false_when_session_id_absent() -> None:
assert has_auth_marker([_cookie("yandexuid"), _cookie("_ym_uid")]) is False
def test_has_auth_marker_false_on_empty_list() -> None:
assert has_auth_marker([]) is False
# ---------------------------------------------------------------------------
# save_session
# ---------------------------------------------------------------------------
def test_save_session_calls_pgp_sym_encrypt(mock_db: MagicMock) -> None:
save_session(mock_db, account_label="test-account", cookies=[_cookie("Session_id")])
args, _ = mock_db.execute.call_args
sql_text = str(args[0])
assert "pgp_sym_encrypt" in sql_text
assert "yandex_session_cookies" in sql_text
def test_save_session_uses_cast_not_colon_colon(mock_db: MagicMock) -> None:
"""Verify psycopg v3 compatibility — no bind-param immediately followed by ::type.
`)::interval` is legit (`::` glued to a closing paren, not to a bind-name — see
`.claude/rules/backend.md` CAST(:x AS type) exception), so we assert against the
actual forbidden pattern (`:name::type`) rather than a blanket "::" absence.
"""
save_session(mock_db, account_label="acc", cookies=[_cookie("Session_id")])
args, _ = mock_db.execute.call_args
sql_text = str(args[0])
assert "CAST(" in sql_text
assert re.search(r":[a-z_]+::[a-z]", sql_text) is None
def test_save_session_commits(mock_db: MagicMock) -> None:
save_session(mock_db, account_label="acc", cookies=[_cookie("Session_id")])
assert mock_db.commit.called
def test_save_session_on_conflict_do_update(mock_db: MagicMock) -> None:
save_session(mock_db, account_label="acc", cookies=[_cookie("Session_id")])
args, _ = mock_db.execute.call_args
sql_text = str(args[0])
assert "ON CONFLICT" in sql_text
assert "DO UPDATE" in sql_text
def test_save_session_passes_correct_params(mock_db: MagicMock) -> None:
cookies = [_cookie("Session_id", "abc123")]
save_session(mock_db, account_label="my-account", cookies=cookies, ttl_days=14)
call_args = mock_db.execute.call_args
params = call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("params", {})
assert params["label"] == "my-account"
assert params["ttl_days"] == 14
cookies_payload = json.loads(params["cookies_json"])
assert cookies_payload == cookies
# ---------------------------------------------------------------------------
# load_session
# ---------------------------------------------------------------------------
def test_load_session_returns_none_when_empty(mock_db: MagicMock) -> None:
result = load_session(mock_db)
assert result is None
def test_load_session_decodes_json(mock_db: MagicMock) -> None:
stored_cookies = [_cookie("Session_id", "abc"), _cookie("yandexuid", "def")]
mock_row = {
"account_label": "acc",
"cookies_json": json.dumps(stored_cookies),
"expires_at_estimate": None,
}
mock_db.execute.return_value.mappings.return_value.first.return_value = mock_row
result = load_session(mock_db)
assert result == stored_cookies
def test_load_session_updates_last_used_at(mock_db: MagicMock) -> None:
mock_row = {
"account_label": "acc",
"cookies_json": json.dumps([_cookie("Session_id")]),
"expires_at_estimate": None,
}
mock_db.execute.return_value.mappings.return_value.first.return_value = mock_row
load_session(mock_db)
# Должно быть минимум 2 вызова execute: SELECT + UPDATE last_used_at
assert mock_db.execute.call_count >= 2
# ---------------------------------------------------------------------------
# mark_session_invalid
# ---------------------------------------------------------------------------
def test_mark_session_invalid_updates_table(mock_db: MagicMock) -> None:
mark_session_invalid(mock_db, account_label="acc")
args, _ = mock_db.execute.call_args
sql = str(args[0])
assert "last_invalid_at" in sql
assert "yandex_session_cookies" in sql
def test_mark_session_invalid_commits(mock_db: MagicMock) -> None:
mark_session_invalid(mock_db, account_label="acc")
assert mock_db.commit.called