139 lines
5.5 KiB
Python
139 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def _write_keypair(tmp_path: Path) -> tuple[Path, Path]:
|
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
private_path = tmp_path / "alipay_private.pem"
|
|
public_path = tmp_path / "alipay_public.pem"
|
|
private_path.write_bytes(
|
|
private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
)
|
|
public_path.write_bytes(
|
|
private_key.public_key().public_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
)
|
|
return private_path, public_path
|
|
|
|
|
|
def test_alipay_notify_marks_order_paid_and_is_idempotent(tmp_path, monkeypatch):
|
|
admin_db = tmp_path / "admin.db"
|
|
orders_db = tmp_path / "orders.db"
|
|
share_db = tmp_path / "share.db"
|
|
share_reports = tmp_path / "share_reports"
|
|
share_reports.mkdir()
|
|
private_path, public_path = _write_keypair(tmp_path)
|
|
|
|
monkeypatch.setenv("GAOKAO_ENV", "dev")
|
|
monkeypatch.setenv("GAOKAO_DB_PATH", str(admin_db))
|
|
monkeypatch.setenv("GAOKAO_ORDERS_DB_PATH", str(orders_db))
|
|
monkeypatch.setenv("GAOKAO_SHARE_DB_PATH", str(share_db))
|
|
monkeypatch.setenv("GAOKAO_SHARE_REPORT_DIR", str(share_reports))
|
|
monkeypatch.setenv("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-web-self-service")
|
|
monkeypatch.setenv("GAOKAO_JWT_SECRET", "x" * 64)
|
|
monkeypatch.setenv("GAOKAO_JWT_EXP_MIN", "5")
|
|
monkeypatch.setenv("GAOKAO_ADMIN_USER", "admin")
|
|
monkeypatch.setenv("GAOKAO_ADMIN_PASS", "test-pass-123")
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_PROVIDER", "alipay")
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_BASE_URL", "http://testserver")
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_WEBHOOK_SECRET", "alipay-independent-secret")
|
|
monkeypatch.setenv(
|
|
"GAOKAO_PAYMENT_NOTIFY_URL",
|
|
"https://example.com/api/public/payments/alipay/notify",
|
|
)
|
|
monkeypatch.setenv(
|
|
"GAOKAO_PAYMENT_RETURN_URL",
|
|
"https://example.com/portal/payment-return",
|
|
)
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_APP_ID", "20260001")
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_MERCHANT_ID", "2088123412341234")
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_PRIVATE_KEY_PATH", str(private_path))
|
|
monkeypatch.setenv("GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH", str(public_path))
|
|
|
|
from admin.app import create_app
|
|
from admin.config import load_settings
|
|
from data.orders.dao import OrdersDAO
|
|
from data.payments.providers.alipay import AlipayProvider
|
|
from data.payments.service import PaymentService
|
|
|
|
settings = load_settings()
|
|
app = create_app(settings)
|
|
|
|
with TestClient(app) as client:
|
|
create_resp = client.post(
|
|
"/api/public/orders",
|
|
json={
|
|
"service_version": "standard",
|
|
"amount_cents": 9900,
|
|
"customer_name": "张家长",
|
|
"customer_phone": "13800138000",
|
|
"candidate_name": "张三",
|
|
"candidate_province": "湖南",
|
|
},
|
|
)
|
|
assert create_resp.status_code == 201, create_resp.text
|
|
created = create_resp.json()
|
|
assert created["checkout_url"].startswith(
|
|
"https://openapi.alipay.com/gateway.do?"
|
|
)
|
|
|
|
service = PaymentService.for_db(
|
|
settings.orders_db_path,
|
|
base_url=settings.payment_base_url,
|
|
webhook_secret=settings.payment_webhook_secret,
|
|
provider_name="alipay",
|
|
notify_url=settings.payment_notify_url,
|
|
return_url=settings.payment_return_url,
|
|
app_id=settings.payment_app_id,
|
|
merchant_id=settings.payment_merchant_id,
|
|
private_key_path=settings.payment_private_key_path,
|
|
alipay_public_key_path=settings.payment_alipay_public_key_path,
|
|
)
|
|
payment = service.get_payment_by_order(created["order_id"])
|
|
assert payment is not None
|
|
|
|
provider = AlipayProvider(
|
|
app_id=settings.payment_app_id,
|
|
merchant_id=settings.payment_merchant_id,
|
|
private_key_path=settings.payment_private_key_path,
|
|
alipay_public_key_path=settings.payment_alipay_public_key_path,
|
|
notify_url=settings.payment_notify_url,
|
|
return_url=settings.payment_return_url,
|
|
)
|
|
payload, signature = provider.build_webhook_request(
|
|
payment_id=payment.id,
|
|
amount_cents=payment.amount_cents,
|
|
provider_trade_no="ALI-ORDER-001",
|
|
)
|
|
|
|
first = client.post(
|
|
"/api/public/payments/alipay/notify",
|
|
data={**payload, "sign": signature, "sign_type": "RSA2"},
|
|
)
|
|
assert first.status_code == 200, first.text
|
|
assert first.text == "success"
|
|
|
|
second = client.post(
|
|
"/api/public/payments/alipay/notify",
|
|
data={**payload, "sign": signature, "sign_type": "RSA2"},
|
|
)
|
|
assert second.status_code == 200, second.text
|
|
assert second.text == "success"
|
|
|
|
with OrdersDAO.connect(settings.orders_db_path) as dao:
|
|
updated = dao.get(created["order_id"])
|
|
history = dao.get_status_history(created["order_id"])
|
|
assert updated.status == "paid"
|
|
assert [item.to_status for item in history] == ["pending", "paid"]
|