gendesign/tradein-mvp/backend/tests/test_payments_notification.py
bot-backend 00d1f78668
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m10s
fix(tradein/payments): строгий разбор нотификации и отказ вместо догадок на враждебном входе
2026-08-06 15:48:57 +03:00

136 lines
5.4 KiB
Python
Raw Permalink 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.

"""Тесты `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]