baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
"""电商平台签名与真实适配器测试。
覆盖:
1. 抖店 MD5 签名与 1688 HMAC-SHA256 签名的金标准向量
2. 抖店拉单真实调用(monkeypatch HTTP 缝隙)与分→元归一化
3. access_token 临期自动刷新
4. 未配置凭证时 Mock 回退
5. 1688 Ocean 协议签名参数构造
"""
import pytest
from datetime import datetime, timedelta, timezone as dt_timezone
from decimal import Decimal
from model_bakery import baker
from apps.channel import adapters as ch_adapters
from apps.channel.signing import douyin_sign, alibaba1688_sign
from apps.catalog.models import Product
from apps.inventory.models import Warehouse
from apps.partner.models import Customer
from apps.sales.models import SalesOrder
from apps.channel.models import ChannelAccount
@pytest.fixture
def customer(tenant, db):
return baker.make(Customer, tenant=tenant, code="C_FIX", name="渠道客户")
@pytest.fixture
def warehouse(tenant, db):
return baker.make(Warehouse, tenant=tenant, code="WH_FIX", name="渠道仓")
# ============================================================
# 1. 签名金标准向量
# ============================================================
def test_douyin_sign_golden_vector():
params = {"app_key": "k1", "timestamp": "123", "v": "2"}
sign = douyin_sign(params, "sec1")
assert sign == "1F269B821E839A3A2A28978AC04AFFF6"
assert len(sign) == 32 and sign.isupper()
def test_alibaba1688_sign_golden_vector():
params = {"a": "1", "b": "2"}
sign = alibaba1688_sign(params, "s")
assert sign == "AFD1ED70462CA57C91A26BEE586FE975C88C02AD0F1A52735712C4E8E897CED2"
assert len(sign) == 64 and sign.isupper()
# ============================================================
# 2. 抖店真实拉单(HTTP 缝隙 monkeypatch)
# ============================================================
@pytest.fixture
def douyin_account(tenant, customer, warehouse):
return ChannelAccount.objects.create(
tenant=tenant,
platform="douyin",
shop_id="DY_REAL_01",
shop_name="真实抖店",
app_key="test_app_key",
app_secret="test_app_secret",
access_token="test_access_token",
refresh_token="test_refresh_token",
token_expires_at=datetime.now(dt_timezone.utc) + timedelta(days=7),
default_warehouse=warehouse,
default_customer=customer,
)
def test_douyin_fetch_orders_real_call(douyin_account, monkeypatch):
captured = {}
def fake_post(self, url, params):
captured["url"] = url
captured["params"] = params
return {
"code": 10000,
"data": {
"shop_order_list": [
{
"order_id_str": "DY9876543210",
"order_status": 100,
"pay_amount": 8800, # 88.00 元(单位分)
"post_receiver": {
"user_name": "赵六",
"mobile": "13800001111",
"province": "浙江省",
"city": "杭州市",
"town": "西湖区",
"detail": "文一西路100号",
},
"sku_order_list": [
{
"sku_id": "SKU-1",
"external_sku_id": "P_EXT_01",
"sku_num": 3,
"sku_order_item_price": 2900, # 29.00 元
}
],
}
]
},
}
monkeypatch.setattr(ch_adapters.DouyinChannelAdapter, "_http_post", fake_post)
orders = ch_adapters.DouyinChannelAdapter().fetch_orders(douyin_account)
# 1. 签名请求参数已正确构造
assert captured["url"] == f"{ch_adapters.DOUYIN_API_BASE}/order/searchList"
p = captured["params"]
assert p["method"] == "order.searchList"
assert p["app_key"] == "test_app_key"
assert p["access_token"] == "test_access_token"
assert len(p["sign"]) == 32 and p["sign"].isupper()
# 2. 归一化结果:分→元
assert len(orders) == 1
o = orders[0]
assert o["external_order_id"] == "DY9876543210"
assert Decimal(o["order_amount"]) == Decimal("88.00")
assert o["buyer_name"] == "赵六"
assert "西湖区" in o["receiver_address"]
assert o["items"][0]["product_code"] == "P_EXT_01"
assert o["items"][0]["quantity"] == "3"
assert Decimal(o["items"][0]["unit_price"]) == Decimal("29.00")
def test_douyin_token_auto_refresh(douyin_account, monkeypatch):
# Token 只剩 2 小时 → 触发刷新
douyin_account.token_expires_at = datetime.now(dt_timezone.utc) + timedelta(hours=2)
douyin_account.save(update_fields=["token_expires_at"])
call_log = []
def fake_post(self, url, params):
call_log.append(url)
if "refresh_token" in url:
return {
"code": 10000,
"data": {
"access_token": "NEW_TOKEN",
"refresh_token": "NEW_REFRESH",
"expires_in": 604800,
},
}
return {"code": 10000, "data": {"shop_order_list": []}}
monkeypatch.setattr(ch_adapters.DouyinChannelAdapter, "_http_post", fake_post)
ch_adapters.DouyinChannelAdapter().fetch_orders(douyin_account)
assert any("oauth2/refresh_token" in u for u in call_log)
douyin_account.refresh_from_db()
assert douyin_account.access_token == "NEW_TOKEN"
assert douyin_account.refresh_token == "NEW_REFRESH"
assert douyin_account.token_expires_at > datetime.now(dt_timezone.utc) + timedelta(days=6)
def test_douyin_push_stock(douyin_account, monkeypatch):
def fake_post(self, url, params):
assert "product/stockNum/update" in url
return {"code": 10000, "data": {"update_result": True}}
monkeypatch.setattr(ch_adapters.DouyinChannelAdapter, "_http_post", fake_post)
ok = ch_adapters.DouyinChannelAdapter().push_stock(douyin_account, "P001", Decimal("99"))
assert ok is True
def test_mock_fallback_without_credentials(tenant):
# 无 app_key 的抖音店铺 → 自动回退 Mock(能产出标准订单)
account = baker.make(
ChannelAccount, tenant=tenant, platform="douyin",
shop_id="DY_EMPTY", shop_name="无凭证店铺",
app_key="", app_secret="", access_token="",
)
baker.make(Product, tenant=tenant, code="P_MOCK", status="active", sale_price=Decimal("25.00"))
orders = ch_adapters.DouyinChannelAdapter().fetch_orders(account)
assert len(orders) == 1
assert orders[0]["order_status"] == "PAID"
# ============================================================
# 3. 1688 Ocean 协议
# ============================================================
@pytest.fixture
def ali_account(tenant, customer, warehouse):
return ChannelAccount.objects.create(
tenant=tenant,
platform="1688",
shop_id="ALI_01",
shop_name="1688 分销店",
app_key="ali_key",
app_secret="ali_secret",
access_token="ali_token",
token_expires_at=datetime.now(dt_timezone.utc) + timedelta(days=7),
default_warehouse=warehouse,
default_customer=customer,
)
def test_1688_build_signed_params(ali_account):
params = ch_adapters.alibaba1688_build_url_params(
ali_account,
"com.alibaba.trade/alibaba.trade.getBuyerOrderList",
{"status": "waitsellerreceive"},
)
assert params["_aop_key"] == "ali_key"
assert params["access_token"] == "ali_token"
assert params["_aop_timestamp"].isdigit()
assert len(params["_aop_signature"]) == 64 and params["_aop_signature"].isupper()
def test_1688_fetch_orders_real_call(ali_account, monkeypatch):
captured = {}
def fake_get(self, url, params):
captured["url"] = url
captured["params"] = params
return {
"result": {
"result": [
{
"baseInfo": {
"idOfStr": "ALI10086",
"status": "waitsellerreceive",
"totalAmount": 16800, # 168.00 元
"buyerLoginID": "buyer_alibaba",
"receiverProvince": "广东省",
"receiverCity": "深圳市",
"receiverAddress": "南山区科技园",
},
"productItems": [
{
"productID": "P_ALI_EXT",
"quantity": 4,
"itemAmount": 16800, # 行金额分 → 单价 42 元
}
],
}
]
}
}
monkeypatch.setattr(ch_adapters.Alibaba1688Adapter, "_http_get", fake_get)
orders = ch_adapters.Alibaba1688Adapter().fetch_orders(ali_account)
assert captured["url"].startswith(f"{ch_adapters.ALI1688_API_BASE}/openapi/param2/1/")
assert len(orders) == 1
o = orders[0]
assert o["external_order_id"] == "ALI10086"
assert Decimal(o["order_amount"]) == Decimal("168.00")
assert o["items"][0]["product_code"] == "P_ALI_EXT"
assert o["items"][0]["quantity"] == "4"
assert Decimal(o["items"][0]["unit_price"]) == Decimal("42.0000")
# ============================================================
# 4. 真实归一化结构 → 自动转销售单(端到端)
# ============================================================
@pytest.mark.django_db
def test_real_normalized_order_converts_to_sales_order(tenant, douyin_account, monkeypatch):
baker.make(Product, tenant=tenant, code="P_EXT_01", status="active", sale_price=Decimal("29.00"))
def fake_post(self, url, params):
return {
"code": 10000,
"data": {
"shop_order_list": [
{
"order_id_str": "DY-E2E-001",
"order_status": 100,
"pay_amount": 8700,
"post_receiver": {"user_name": "测试", "mobile": "138"},
"sku_order_list": [
{"sku_id": "S1", "external_sku_id": "P_EXT_01", "sku_num": 3, "sku_order_item_price": 2900}
],
}
]
},
}
monkeypatch.setattr(ch_adapters.DouyinChannelAdapter, "_http_post", fake_post)
from apps.channel import services as channel_services
result = channel_services.sync_channel_orders(douyin_account, auto_convert=True)
assert result["pulled_count"] == 1
assert result["converted_count"] == 1
assert result["failed_count"] == 0
so = SalesOrder.objects.get(tenant=tenant, remark__contains="DY-E2E-001")
assert so.total_amount == Decimal("87.00")
assert so.state == "confirmed"