Files
huihuiSquare/digital-avatar-app/backend/tests/test_virtual_payment_flow.py
T

236 lines
10 KiB
Python

import hashlib
import hmac
import json
import os
import time
from unittest.mock import patch
from fastapi.testclient import TestClient
from database import SessionLocal
from main import app, seed
from models import InvoiceApplication, PaymentRefund, TokenAccount, TokenPaymentOrder, TokenPlan, User
from services.token_billing import DEFAULT_TOKEN_GRANT
client = TestClient(app)
def _huihui_virtual_headers(secret, body, timestamp=None):
timestamp = str(timestamp if timestamp is not None else int(time.time()))
signature = hmac.new(secret.encode(), timestamp.encode() + b"\n" + body, hashlib.sha256).hexdigest()
return {"X-Huihui-Timestamp": timestamp, "X-Huihui-Signature": signature}
def test_huihui_virtual_callback_requires_signature_and_exact_order_details(authorization_context):
seed()
context = authorization_context
db = SessionLocal()
try:
user = db.query(User).filter(User.id == context["owner"].id).one()
user.wechat_mp_openid = "openid-huihui-callback"
user.wechat_mp_session_key = "session-huihui-callback"
plan = db.query(TokenPlan).filter(TokenPlan.id == "Basic").one()
db.commit()
finally:
db.close()
secret = "huihui-virtual-callback-test-secret-123456"
env = {
"WECHAT_VIRTUAL_ENV": "sandbox",
"WECHAT_VIRTUAL_SANDBOX_APP_KEY": "sandbox-key",
"WECHAT_VIRTUAL_OFFER_ID": "offer-1",
"HUIHUI_VIRTUAL_CALLBACK_SECRET": secret,
}
with patch.dict(os.environ, env):
created = client.post(
"/api/token/charge",
headers=context["owner_headers"],
json={"planId": "Basic", "paymentMethod": "wechat", "payScene": "LITE"},
).json()["data"]
payload = {
"orderNo": created["orderNo"],
"paymentNo": f"huihui-virtual-payment-{context['suffix']}",
"status": "SUCCESS",
"amountCents": 1000,
"productId": "Basic",
"openId": "openid-huihui-callback",
"env": 1,
}
path = "/api/token/payment/huihui/virtual/notify"
body = json.dumps(payload, separators=(",", ":")).encode()
assert client.post(path, content=body, headers=_huihui_virtual_headers(secret, body, 1)).status_code == 401
assert client.post(path, content=body, headers={"X-Huihui-Timestamp": str(int(time.time())), "X-Huihui-Signature": "bad"}).status_code == 401
wrong = json.dumps({**payload, "amountCents": 999}, separators=(",", ":")).encode()
assert client.post(path, content=wrong, headers=_huihui_virtual_headers(secret, wrong)).status_code == 422
first = client.post(path, content=body, headers=_huihui_virtual_headers(secret, body))
second = client.post(path, content=body, headers=_huihui_virtual_headers(secret, body))
assert first.json()["data"] == {"received": True, "paid": True}
assert second.json()["data"] == {"received": True, "duplicate": True}
db = SessionLocal()
try:
order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == created["orderNo"]).one()
account = db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).one()
assert order.status == "paid"
assert account.balance == DEFAULT_TOKEN_GRANT + 2_000_000
finally:
db.close()
def test_huihui_org_callback_verifies_provider_before_granting_points(authorization_context):
seed()
context = authorization_context
provider_no = "26102010414409000001"
payment_no = f"WV-{context['suffix']}"
order_no = f"AV-{context['suffix']}"
db = SessionLocal()
try:
user = db.query(User).filter(User.id == context["owner"].id).one()
user.huihui_token = "test-huihui-token"
db.add(TokenPaymentOrder(
order_no=order_no, provider_order_no=provider_no,
user_id=user.id, plan_id="Basic", payment_method="wechat",
pay_type="WECHAT", pay_way="LITE", provider="huihui_org",
points_amount=2_000_000, price_cents=100, status="pending",
))
db.commit()
finally:
db.close()
secret = "abcdefghijklmnop"
path = "/api/token/payment/huihui/org/notify"
payload = {
"orderNo": provider_no, "paymentNo": payment_no,
"status": "SUCCESS", "amountCents": 100,
"productId": "Basic",
}
provider_data = {
"orderNo": provider_no, "paymentNo": payment_no,
"payStatus": 1, "chargeType": 9, "chargeAmount": 1,
"productId": "Basic", "userId": context["owner"].huihui_user_id,
}
def send(body_payload, *, signature=True):
body = json.dumps(body_payload, separators=(",", ":")).encode()
headers = _huihui_virtual_headers(secret, body) if signature else {"X-Huihui-Signature": "bad"}
return client.post(path, content=body, headers=headers)
with patch.dict(os.environ, {"HUIHUI_ORG_CALLBACK_SECRET": secret}), patch(
"routers.tokens.HuihuiOrgPaymentClient"
) as client_factory:
provider = client_factory.return_value
provider.get_order.return_value = provider_data
assert send(payload, signature=False).status_code == 401
assert send({**payload, "amountCents": 99}).status_code == 422
assert send(payload).status_code == 409
provider.get_order.return_value = {**provider_data, "payStatus": 2}
assert send({**payload, "paymentNo": "wrong"}).status_code == 422
provider.get_order.return_value = {**provider_data, "payStatus": 2, "chargeAmount": 2}
assert send(payload).status_code == 422
provider.get_order.return_value = {**provider_data, "payStatus": 2}
first = send(payload)
second = send(payload)
assert first.status_code == 200
assert first.json()["data"] == {"received": True, "paid": True}
assert second.json()["data"] == {"received": True, "duplicate": True}
db = SessionLocal()
try:
order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == order_no).one()
account = db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).one()
assert order.status == "paid"
assert account.balance == DEFAULT_TOKEN_GRANT + 2_000_000
assert provider.get_order.call_count >= 3
finally:
db.close()
def _signature(token, timestamp, nonce):
return hashlib.sha1("".join(sorted([token, timestamp, nonce])).encode()).hexdigest()
def test_virtual_payment_callback_and_refund_are_idempotent(authorization_context):
seed()
context = authorization_context
db = SessionLocal()
try:
user = db.query(User).filter(User.id == context["owner"].id).one()
user.wechat_mp_openid = "openid-flow"
user.wechat_mp_session_key = "session-flow"
plan = db.query(TokenPlan).filter(TokenPlan.id == "Basic").one()
db.commit()
finally:
db.close()
env = {
"WECHAT_VIRTUAL_ENV": "sandbox",
"WECHAT_VIRTUAL_SANDBOX_APP_KEY": "sandbox-key",
"WECHAT_VIRTUAL_OFFER_ID": "offer-1",
"WECHAT_VIRTUAL_CALLBACK_TOKEN": "callback-token",
"AVATAR_FINANCE_ADMIN_SECRET": "finance-admin-secret-123",
}
with patch.dict(os.environ, env):
created = client.post(
"/api/token/charge",
headers=context["owner_headers"],
json={"planId": "Basic", "paymentMethod": "wechat", "payScene": "LITE"},
).json()["data"]
assert created["provider"] == "wechat_virtual"
params = json.loads(created["payMessage"])
assert params["mode"] == "short_series_goods"
assert "session-flow" not in created["payMessage"]
notify = {
"Event": "xpay_goods_deliver_notify",
"OutTradeNo": created["orderNo"],
"OpenId": "openid-flow",
"Env": 1,
"GoodsInfo": json.dumps({"ProductId": "Basic", "ActualPrice": 1000}),
"WeChatPayInfo": json.dumps({"TransactionId": "wx-transaction-1"}),
}
query = {"timestamp": "100", "nonce": "nonce", "signature": _signature("callback-token", "100", "nonce")}
assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=notify).json()["ErrCode"] == 0
assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=notify).json()["ErrCode"] == 0
invoice = client.post(
f"/api/token/orders/{created['orderNo']}/invoice",
headers=context["owner_headers"],
json={"title": "测试用户", "invoiceType": "personal", "email": "test@example.com"},
).json()["data"]
assert invoice["status"] == "pending"
with patch("routers.tokens.request_wechat_virtual_refund", return_value={"errcode": 0}):
refund_response = client.post(
f"/api/token/admin/orders/{created['orderNo']}/refund",
headers={"X-Avatar-Finance-Key": "finance-admin-secret-123"},
json={"reason": "用户申请退款", "operator": "tester"},
)
assert refund_response.json()["data"]["status"] == "processing"
refund_no = refund_response.json()["data"]["refundNo"]
refund_notify = {
"Event": "xpay_refund_notify",
"MchOrderId": created["orderNo"],
"MchRefundId": refund_no,
"WxRefundId": "wx-refund-1",
"RefundFee": 1000,
"RetCode": 0,
}
assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=refund_notify).json()["ErrCode"] == 0
assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=refund_notify).json()["ErrCode"] == 0
assert client.post("/api/token/payment/wechat/virtual/notify", params=query, json=notify).json()["ErrCode"] == 0
db = SessionLocal()
try:
order = db.query(TokenPaymentOrder).filter(TokenPaymentOrder.order_no == created["orderNo"]).one()
account = db.query(TokenAccount).filter(TokenAccount.user_id == context["owner"].id).one()
refund = db.query(PaymentRefund).filter(PaymentRefund.refund_no == refund_no).one()
invoice = db.query(InvoiceApplication).filter(InvoiceApplication.order_no == created["orderNo"]).one()
assert order.status == "refunded"
assert refund.status == "succeeded"
assert invoice.status == "cancelled"
assert account.balance == DEFAULT_TOKEN_GRANT
finally:
db.close()