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