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