136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
import hashlib
|
|
|
|
import pytest
|
|
|
|
from app.adapters.tbank.base import TBankPaymentNotificationTokenError
|
|
from app.adapters.tbank.client import TBankAdapter
|
|
from app.schemas.payment import TBankPaymentNotification
|
|
|
|
|
|
def _make_adapter() -> TBankAdapter:
|
|
return TBankAdapter(
|
|
http_client=object(), # type: ignore[arg-type]
|
|
init_url="https://securepay.tinkoff.ru/v2/Init",
|
|
notification_url="https://merchant.test/api/v1/delivery/tbank/notifications",
|
|
success_url="https://merchant.test/payment/success",
|
|
terminal_key="TestTerminal",
|
|
password="test-password",
|
|
)
|
|
|
|
|
|
def _base_payload(**overrides: object) -> dict[str, object]:
|
|
payload: dict[str, object] = {
|
|
"TerminalKey": "TestTerminal",
|
|
"OrderId": "order-uuid-1",
|
|
"Success": True,
|
|
"Status": "CONFIRMED",
|
|
"PaymentId": 8347568144,
|
|
"ErrorCode": "0",
|
|
"Amount": 125000,
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
def _build_notification_token(payload: dict[str, object]) -> str:
|
|
token_payload = {
|
|
key: value
|
|
for key, value in payload.items()
|
|
if key != "Token" and not isinstance(value, (dict, list))
|
|
}
|
|
token_payload["Password"] = "test-password"
|
|
token_source = "".join(
|
|
_stringify_token_value(token_payload[key]) for key in sorted(token_payload)
|
|
)
|
|
return hashlib.sha256(token_source.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _stringify_token_value(value: object) -> str:
|
|
if isinstance(value, bool):
|
|
return "true" if value else "false"
|
|
return str(value)
|
|
|
|
|
|
def _signed_notification(**overrides: object) -> TBankPaymentNotification:
|
|
payload = _base_payload(**overrides)
|
|
if "Token" not in payload:
|
|
payload["Token"] = _build_notification_token(payload)
|
|
return TBankPaymentNotification(**payload)
|
|
|
|
|
|
def test_verify_payment_notification_accepts_valid_token() -> None:
|
|
adapter = _make_adapter()
|
|
notification = _signed_notification()
|
|
|
|
adapter.verify_payment_notification(notification)
|
|
|
|
|
|
def test_verify_payment_notification_rejects_invalid_token() -> None:
|
|
adapter = _make_adapter()
|
|
notification = _signed_notification(Token="invalid-token")
|
|
|
|
with pytest.raises(TBankPaymentNotificationTokenError):
|
|
adapter.verify_payment_notification(notification)
|
|
|
|
|
|
def test_verify_payment_notification_excludes_token_from_hash_source() -> None:
|
|
adapter = _make_adapter()
|
|
payload = _base_payload()
|
|
payload["Token"] = _build_notification_token(payload)
|
|
notification = TBankPaymentNotification(**payload)
|
|
|
|
adapter.verify_payment_notification(notification)
|
|
|
|
|
|
def test_verify_payment_notification_excludes_nested_data_and_receipt() -> None:
|
|
adapter = _make_adapter()
|
|
notification = _signed_notification(
|
|
Data={"nested": "ignored"},
|
|
Receipt={"Items": [{"Name": "Box", "Price": 125000}]},
|
|
)
|
|
|
|
adapter.verify_payment_notification(notification)
|
|
|
|
|
|
def test_verify_payment_notification_includes_extra_scalar_fields() -> None:
|
|
adapter = _make_adapter()
|
|
notification = _signed_notification(CardId="card-id-1")
|
|
|
|
adapter.verify_payment_notification(notification)
|
|
|
|
payload_without_extra = notification.model_dump(mode="python")
|
|
del payload_without_extra["CardId"]
|
|
payload_without_extra["Token"] = _build_notification_token(payload_without_extra)
|
|
invalid_notification = TBankPaymentNotification(
|
|
**payload_without_extra,
|
|
CardId="card-id-1",
|
|
)
|
|
|
|
with pytest.raises(TBankPaymentNotificationTokenError):
|
|
adapter.verify_payment_notification(invalid_notification)
|
|
|
|
|
|
def test_verify_payment_notification_serializes_booleans_as_lowercase() -> None:
|
|
adapter = _make_adapter()
|
|
notification = _signed_notification(Success=False)
|
|
|
|
adapter.verify_payment_notification(notification)
|
|
|
|
|
|
def test_verify_payment_notification_uses_deterministic_sha256_comparison() -> None:
|
|
payload = _base_payload()
|
|
expected_token = hashlib.sha256(
|
|
(
|
|
"125000"
|
|
"0"
|
|
"order-uuid-1"
|
|
"test-password"
|
|
"8347568144"
|
|
"CONFIRMED"
|
|
"true"
|
|
"TestTerminal"
|
|
).encode("utf-8")
|
|
).hexdigest()
|
|
|
|
assert _build_notification_token(payload) == expected_token
|