mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
233 lines
7.6 KiB
Python
233 lines
7.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from decimal import Decimal
|
||
|
|
from types import SimpleNamespace
|
||
|
|
from unittest.mock import MagicMock
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from fastapi import FastAPI
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
from src.api.admin.payments.routes import AdminPaymentOrderCreditAdapter
|
||
|
|
from src.api.payment.routes import router as payment_router
|
||
|
|
from src.config import config
|
||
|
|
from src.database import get_db
|
||
|
|
from src.models.database import PaymentOrder
|
||
|
|
from src.services.payment.gateway import get_payment_gateway
|
||
|
|
|
||
|
|
CALLBACK_SECRET = "test-callback-secret"
|
||
|
|
|
||
|
|
|
||
|
|
def _build_payment_app(db: MagicMock) -> TestClient:
|
||
|
|
app = FastAPI()
|
||
|
|
app.include_router(payment_router)
|
||
|
|
app.dependency_overrides[get_db] = lambda: db
|
||
|
|
return TestClient(app)
|
||
|
|
|
||
|
|
|
||
|
|
def _sign_payload(payload: dict[str, object]) -> str:
|
||
|
|
gateway = get_payment_gateway("alipay")
|
||
|
|
signature = gateway.build_callback_signature(payload=payload, callback_secret=CALLBACK_SECRET)
|
||
|
|
assert signature is not None
|
||
|
|
return signature
|
||
|
|
|
||
|
|
|
||
|
|
def test_specific_wechat_callback_route_is_not_shadowed(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
client = _build_payment_app(db)
|
||
|
|
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
|
||
|
|
|
||
|
|
captured_kwargs: dict[str, object] = {}
|
||
|
|
|
||
|
|
def _fake_handle_callback(*args: object, **kwargs: object) -> dict[str, object]:
|
||
|
|
captured_kwargs.update(kwargs)
|
||
|
|
return {
|
||
|
|
"ok": True,
|
||
|
|
"credited": True,
|
||
|
|
"duplicate": False,
|
||
|
|
"payment_method_seen": kwargs["payment_method"],
|
||
|
|
}
|
||
|
|
|
||
|
|
monkeypatch.setattr("src.api.payment.routes.PaymentService.handle_callback", _fake_handle_callback)
|
||
|
|
|
||
|
|
callback_payload = {"callback_key": "cb-wechat", "amount_usd": 1.0}
|
||
|
|
response = client.post(
|
||
|
|
"/api/payment/callback/wechat",
|
||
|
|
json=callback_payload,
|
||
|
|
headers={
|
||
|
|
"x-payment-callback-token": CALLBACK_SECRET,
|
||
|
|
"x-payment-callback-signature": _sign_payload(callback_payload),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
payload = response.json()
|
||
|
|
assert payload["payment_method"] == "wechat"
|
||
|
|
assert payload["payment_method_seen"] == "wechat"
|
||
|
|
assert payload["request_path"] == "/api/payment/callback/wechat"
|
||
|
|
assert captured_kwargs["callback_signature"] == _sign_payload(callback_payload)
|
||
|
|
assert captured_kwargs["callback_secret"] == CALLBACK_SECRET
|
||
|
|
assert "signature_valid" not in captured_kwargs
|
||
|
|
db.commit.assert_called_once()
|
||
|
|
|
||
|
|
|
||
|
|
def test_generic_payment_callback_route_still_handles_custom_methods(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
client = _build_payment_app(db)
|
||
|
|
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
|
||
|
|
|
||
|
|
captured_kwargs: dict[str, object] = {}
|
||
|
|
|
||
|
|
def _fake_handle_callback(*args: object, **kwargs: object) -> dict[str, object]:
|
||
|
|
captured_kwargs.update(kwargs)
|
||
|
|
return {
|
||
|
|
"ok": True,
|
||
|
|
"credited": False,
|
||
|
|
"duplicate": False,
|
||
|
|
"payment_method_seen": kwargs["payment_method"],
|
||
|
|
}
|
||
|
|
|
||
|
|
monkeypatch.setattr("src.api.payment.routes.PaymentService.handle_callback", _fake_handle_callback)
|
||
|
|
|
||
|
|
callback_payload = {"callback_key": "cb-generic", "amount_usd": 1.0}
|
||
|
|
response = client.post(
|
||
|
|
"/api/payment/callback/mockpay",
|
||
|
|
json=callback_payload,
|
||
|
|
headers={
|
||
|
|
"x-payment-callback-token": CALLBACK_SECRET,
|
||
|
|
"x-payment-callback-signature": _sign_payload(callback_payload),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
payload = response.json()
|
||
|
|
assert payload["payment_method"] == "mockpay"
|
||
|
|
assert payload["payment_method_seen"] == "mockpay"
|
||
|
|
assert captured_kwargs["callback_signature"] == _sign_payload(callback_payload)
|
||
|
|
assert captured_kwargs["callback_secret"] == CALLBACK_SECRET
|
||
|
|
assert "signature_valid" not in captured_kwargs
|
||
|
|
|
||
|
|
|
||
|
|
def test_callback_requires_shared_token(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
client = _build_payment_app(db)
|
||
|
|
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/payment/callback/alipay",
|
||
|
|
json={"callback_key": "cb-missing-token", "amount_usd": 1.0},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 401
|
||
|
|
db.commit.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
def test_callback_rejects_invalid_shared_token(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
client = _build_payment_app(db)
|
||
|
|
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
|
||
|
|
|
||
|
|
callback_payload = {"callback_key": "cb-invalid-token", "amount_usd": 1.0}
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/payment/callback/alipay",
|
||
|
|
json=callback_payload,
|
||
|
|
headers={
|
||
|
|
"x-payment-callback-token": "wrong-token",
|
||
|
|
"x-payment-callback-signature": _sign_payload(callback_payload),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 401
|
||
|
|
db.commit.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
def test_callback_rejects_missing_signature(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
client = _build_payment_app(db)
|
||
|
|
monkeypatch.setattr(config, "payment_callback_secret", CALLBACK_SECRET)
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/payment/callback/alipay",
|
||
|
|
json={"callback_key": "cb-missing-signature", "amount_usd": 1.0},
|
||
|
|
headers={"x-payment-callback-token": CALLBACK_SECRET},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 401
|
||
|
|
db.commit.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
def test_callback_disabled_when_secret_not_configured(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
client = _build_payment_app(db)
|
||
|
|
monkeypatch.setattr(config, "payment_callback_secret", "")
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/payment/callback/alipay",
|
||
|
|
json={"callback_key": "cb-secret-missing", "amount_usd": 1.0},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 503
|
||
|
|
db.commit.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_admin_payment_credit_adapter_marks_manual_credit(
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
) -> None:
|
||
|
|
db = MagicMock()
|
||
|
|
order = PaymentOrder(
|
||
|
|
id="po-credit",
|
||
|
|
order_no="order-credit",
|
||
|
|
wallet_id="w1",
|
||
|
|
user_id="u1",
|
||
|
|
amount_usd=Decimal("8.00000000"),
|
||
|
|
refunded_amount_usd=Decimal("0"),
|
||
|
|
refundable_amount_usd=Decimal("8.00000000"),
|
||
|
|
payment_method="alipay",
|
||
|
|
status="pending",
|
||
|
|
gateway_response={"existing": True},
|
||
|
|
)
|
||
|
|
adapter = AdminPaymentOrderCreditAdapter(order_id=order.id)
|
||
|
|
context = SimpleNamespace(
|
||
|
|
db=db,
|
||
|
|
raw_body=b"{}",
|
||
|
|
ensure_json_body=lambda: {
|
||
|
|
"pay_amount": 58.0,
|
||
|
|
"pay_currency": "CNY",
|
||
|
|
"exchange_rate": 7.25,
|
||
|
|
},
|
||
|
|
user=SimpleNamespace(id="admin-1"),
|
||
|
|
)
|
||
|
|
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"src.api.admin.payments.routes.PaymentService.get_order",
|
||
|
|
lambda _db, order_id: order if order_id == "po-credit" else None,
|
||
|
|
)
|
||
|
|
|
||
|
|
captured: dict[str, object] = {}
|
||
|
|
|
||
|
|
def _fake_credit_order(_db: MagicMock, **kwargs: object) -> tuple[PaymentOrder, bool]:
|
||
|
|
captured.update(kwargs)
|
||
|
|
return order, True
|
||
|
|
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"src.api.admin.payments.routes.PaymentService.credit_order",
|
||
|
|
_fake_credit_order,
|
||
|
|
)
|
||
|
|
|
||
|
|
result = await adapter.handle(context)
|
||
|
|
|
||
|
|
assert result["credited"] is True
|
||
|
|
assert result["order"]["id"] == "po-credit"
|
||
|
|
gateway_response = captured["gateway_response"]
|
||
|
|
assert isinstance(gateway_response, dict)
|
||
|
|
assert gateway_response["existing"] is True
|
||
|
|
assert gateway_response["manual_credit"] is True
|
||
|
|
assert gateway_response["credited_by"] == "admin-1"
|
||
|
|
db.commit.assert_called_once()
|