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
136 lines
5.4 KiB
Python
136 lines
5.4 KiB
Python
"""Тесты `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]
|