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
215 lines
7.5 KiB
Python
215 lines
7.5 KiB
Python
"""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
|