"""Тесты `app.services.payments.notification` — строгий разбор нотификации. `parse_notification()` вызывается ПОСЛЕ `verify_notification_token(...) is True` (эта функция подпись не проверяет) — тесты здесь работают с payload напрямую, без пересчёта подписи. """ from __future__ import annotations import pytest from app.services.payments.notification import ( NotificationParseError, TBankNotification, parse_notification, ) _VALID_PAYLOAD = { "TerminalKey": "1234567890DEMO", "OrderId": "order-1", "Success": True, "Status": "CONFIRMED", "PaymentId": "0000000", "Amount": 111100, "Token": "irrelevant-here", } # ── happy path ──────────────────────────────────────────────────────────────── def test_parse_notification_happy_path_returns_typed_object() -> None: result = parse_notification(_VALID_PAYLOAD) assert isinstance(result, TBankNotification) assert result.success is True assert result.status == "CONFIRMED" assert result.order_id == "order-1" assert result.payment_id == "0000000" assert result.terminal_key == "1234567890DEMO" assert result.amount_kopecks == 111100 def test_parse_notification_ignores_extra_fields() -> None: """Лишние поля (ErrorCode, CardId, Pan, ...) в payload не мешают разбору.""" payload = {**_VALID_PAYLOAD, "ErrorCode": "0", "CardId": "000000", "Pan": "200000******0000"} result = parse_notification(payload) assert result.order_id == "order-1" # ── payload не dict ───────────────────────────────────────────────────────── def test_parse_notification_rejects_non_dict_payload() -> None: with pytest.raises(NotificationParseError): parse_notification([_VALID_PAYLOAD]) # type: ignore[arg-type] # ── Success: только настоящий bool ────────────────────────────────────────── def test_parse_notification_rejects_success_as_string_true() -> None: payload = {**_VALID_PAYLOAD, "Success": "true"} with pytest.raises(NotificationParseError): parse_notification(payload) def test_parse_notification_rejects_success_as_int_one() -> None: payload = {**_VALID_PAYLOAD, "Success": 1} with pytest.raises(NotificationParseError): parse_notification(payload) def test_parse_notification_rejects_missing_success() -> None: payload = {k: v for k, v in _VALID_PAYLOAD.items() if k != "Success"} with pytest.raises(NotificationParseError): parse_notification(payload) # ── Amount: только int, bool отдельно отсекается ──────────────────────────── def test_parse_notification_rejects_amount_as_bool_true() -> None: """`bool` — подкласс `int` в Python, поэтому отсекается ДО общей int-проверки.""" payload = {**_VALID_PAYLOAD, "Amount": True} with pytest.raises(NotificationParseError): parse_notification(payload) def test_parse_notification_rejects_amount_as_bool_false() -> None: payload = {**_VALID_PAYLOAD, "Amount": False} with pytest.raises(NotificationParseError): parse_notification(payload) def test_parse_notification_rejects_amount_as_float() -> None: payload = {**_VALID_PAYLOAD, "Amount": 111100.0} with pytest.raises(NotificationParseError): parse_notification(payload) def test_parse_notification_rejects_amount_as_string() -> None: payload = {**_VALID_PAYLOAD, "Amount": "111100"} with pytest.raises(NotificationParseError): parse_notification(payload) # ── строковые поля: только непустой str ───────────────────────────────────── @pytest.mark.parametrize("field", ["Status", "OrderId", "PaymentId", "TerminalKey"]) def test_parse_notification_rejects_empty_string_field(field: str) -> None: payload = {**_VALID_PAYLOAD, field: ""} with pytest.raises(NotificationParseError): parse_notification(payload) @pytest.mark.parametrize("field", ["Status", "OrderId", "PaymentId", "TerminalKey"]) def test_parse_notification_rejects_non_string_field(field: str) -> None: payload = {**_VALID_PAYLOAD, field: 12345} with pytest.raises(NotificationParseError): parse_notification(payload) @pytest.mark.parametrize("field", ["Status", "OrderId", "PaymentId", "TerminalKey"]) def test_parse_notification_rejects_missing_field(field: str) -> None: payload = {k: v for k, v in _VALID_PAYLOAD.items() if k != field} with pytest.raises(NotificationParseError): parse_notification(payload) def test_parse_notification_result_is_frozen() -> None: """`TBankNotification` — frozen dataclass, случайная мутация после разбора невозможна.""" result = parse_notification(_VALID_PAYLOAD) with pytest.raises(AttributeError): result.amount_kopecks = 1 # type: ignore[misc]