refactor(api): yun8_sync 并入 tests 分层框架(接口层 apis/yun8 + 用例层 api + Allure)

- tests/core: 新增 BaseHttpClient 统一 HTTP 封装(直连绕行/超时/日志/自定义编码);
  db_helper 追加 Db 长连接类(事务/mogrify/自动重连),既有函数不动
- tests/apis/yun8: 21 个 step 方法按域拆分为 5 个 Mixin(driver/accounting/
  audit_payment/receipt/third_party),Yun8Pipeline 24 步编排原样迁移,
  SQL 全部参数化并集中至 sql_templates
- tests/api: 5 阶段有序 P0 E2E 用例(业务失败由 print 改为真实报错)+
  README 迁移(删除明文密码段,修正步骤编号漂移)
- yun8_sync 目录退役:配置并入 test_config.yaml 的 yun8_api 段;
  造数 CLI 保留(python -m tests.apis.yun8.cli,兼容旧 config.yaml 结构)
- pytest.ini 注册 api/yun8_sync marker;requirements.txt 补 requests>=2.28
- 静态验证: compileall / 全仓 collect 24 条 / marker 过滤 / CLI --help /
  客户端与 pipeline 离线实例化 / SQL 占位符核对 / cross_end 兼容
This commit is contained in:
xst
2026-09-08 11:38:17 +08:00
parent e62e3224bf
commit 52b73f456c
26 changed files with 2147 additions and 1786 deletions
+95
View File
@@ -0,0 +1,95 @@
"""yun8 API 客户端门面。
组装司机端 / 管理端 / 第三方(滇运通)三套 BaseHttpClient 会话,
并挂载各业务域 Mixin 的 step01-21 方法。
请求头组装顺序与旧 yun8_sync/client.py._init_session 逐字一致:
1. 角色专属请求头(跳过空值)
2. common_headers 中的 host / accept-language / accept-encoding 无条件覆盖
Usage:
from tests.apis.yun8.client import Yun8ApiClient
client = Yun8ApiClient(config["yun8_api"])
client.set_driver_auth("Bearer xxx")
client.set_admin_auth("Bearer yyy")
data = client.step01_query_goods()
"""
from tests.core.http_client import BaseHttpClient
from tests.apis.yun8.driver_api import DriverApi
from tests.apis.yun8.accounting_api import AccountingApi
from tests.apis.yun8.audit_payment_api import AuditPaymentApi
from tests.apis.yun8.receipt_api import ReceiptApi
from tests.apis.yun8.third_party_api import ThirdPartyApi
class Yun8ApiClient(DriverApi, AccountingApi, AuditPaymentApi, ReceiptApi,
ThirdPartyApi):
"""封装 yun8 全部 API 请求(21 个 step 方法 + 3 套会话)。"""
def __init__(self, config: dict):
# config 为 test_config.yaml 的 yun8_api section
self.base_url = config["base_url"].rstrip("/")
self.verify_ssl = config.get("verify_ssl", False)
self.timeout = config.get("timeout_seconds", 60)
common = config.get("common_headers", {})
# ---- Driver Session ----
self.driver_http = BaseHttpClient(
self.base_url, headers=config.get("driver_headers", {}),
verify_ssl=self.verify_ssl, timeout=self.timeout, name="yun8-driver")
# ---- Admin Session ----
self.admin_http = BaseHttpClient(
self.base_url, headers=config.get("admin_headers", {}),
verify_ssl=self.verify_ssl, timeout=self.timeout, name="yun8-admin")
# 通用请求头无条件覆盖(与旧版一致,即使为空也写入)
for http in (self.driver_http, self.admin_http):
http.set_header("host", common.get("host", ""))
http.set_header("accept-language", common.get("accept-language", ""))
http.set_header("accept-encoding", common.get("accept-encoding", ""))
# ---- Third-party Session (滇运通,独立 token/host) ----
tp = config.get("dianyuntong", {})
self.tp_enabled = tp.get("enabled", False)
self.tp_http = None
self.tp_base = ""
if self.tp_enabled:
self.tp_base = tp.get("base_url", "").rstrip("/")
tp_headers = {
"host": "yndyt.cxtech.vip",
"authorization": tp.get("token", ""),
"deviceid": "0000000000",
"secret": "GouldAMap=d58bb5f7da1aa1d6b78741ded2450f2d",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 Edg/149.0.0.0",
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
"origin": tp.get("base_url", ""),
"referer": f"{tp.get('base_url', '')}/admin/",
"cookie": tp.get("cookie", ""),
}
self.tp_http = BaseHttpClient(
self.tp_base, headers=tp_headers,
verify_ssl=self.verify_ssl, timeout=self.timeout, name="yun8-tp")
# 快捷引用(各域 Mixin 使用)
self.dp = config.get("driver_params", {})
self.ac = config.get("accounting", {})
self.pm = config.get("payment", {})
# ------------------------------------------------------------------
def set_driver_auth(self, token: str):
self.driver_http.set_header("authorization", token)
def set_admin_auth(self, token: str):
self.admin_http.set_header("authorization", token)
def close(self):
"""关闭全部会话连接池。"""
self.driver_http.close()
self.admin_http.close()
if self.tp_http:
self.tp_http.close()