Files
shaguabijia-app-server/tests/test_pangle_report.py
T
zhuzihao b622f76a02 feat(ad-revenue): 接入穿山甲 GroMore 数据 API 拉取后台收益(预估+收益API) (#92)
- 新增穿山甲 GroMore「聚合数据报告 API」对接,按天 T+1 拉取后台 revenue(预估)与
  api_revenue(收益API,更接近结算),在广告收益报表大盘 + 按天趋势级展示,与客户端自报
  eCPM 折算的预估并列对照;逐条广告事件行不动(仍是客户端预估)。
- 链路:integrations/pangle_report.py(MD5 签名,与官方文档两个测试向量逐字节一致)→
  scripts/sync_pangle_revenue.py(拉昨天/--days 回补)→ 新表 ad_pangle_daily_revenue
  (repositories/ad_pangle_revenue.py:upsert + 按日聚合)→ admin/repositories/ad_revenue.py
  汇总,新增 total_pangle_revenue_yuan / total_pangle_api_revenue_yuan / daily[].pangle_*。
- 穿山甲无用户/类型/场景维度:仅全量视图(未按 user/ad_type/feed_scene 过滤)给值,否则置
  None;join key 用 ad_unit_id(=客户端配的 104xxx),非 code_id。
- 新增配置 PANGLE_REPORT_USER_ID/ROLE_ID/SECURITY_KEY(≠发奖 m-key)+ site_id→应用映射;
  含单测(签名向量+分页解析);已真连穿山甲验证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: zzhyyyyy <2685922758@qq.com>
Reviewed-on: #92
Co-authored-by: zhuzihao <zhuzihao@wonderable.ai>
Co-committed-by: zhuzihao <zhuzihao@wonderable.ai>
2026-06-29 09:54:13 +08:00

135 lines
4.8 KiB
Python

"""穿山甲 GroMore 数据 API 客户端测试。
重点锚定**签名算法**:用官方文档 Python / Java 示例里给出的「参数 + secure_key → sign」两个
测试向量做断言,保证我们的 build_sign 与穿山甲服务端逐字节一致(签错=接口直接 101 验签失败)。
另测拉取的翻页、业务码非 100 抛错、响应解析。
"""
from __future__ import annotations
import pytest
from app.core.config import settings
from app.integrations import pangle_report
from app.integrations.pangle_report import PangleReportError, build_sign
# ---- 签名:官方文档测试向量(完整 key 与期望 sign 来自文档代码注释)----
def test_build_sign_matches_doc_python_vector():
params = {
"current_time": "2022-01-06 16:10:54",
"sign_type": "MD5",
"version": "2.0",
"user_id": 459,
"role_id": 459,
}
sign = build_sign(params, "7f2ddb34d999ea61c00246ea3971529c")
assert sign == "57b9fed9b6c09cefdced7fa770fe341d"
def test_build_sign_matches_doc_java_vector():
params = {
"user_id": 459,
"role_id": 459,
"version": "2.0",
"sign_type": "MD5",
"current_time": "2022-01-06 16:05:49",
}
sign = build_sign(params, "1c589c70f7bb2746027ce90c33d2544c")
assert sign == "4d47b3a7679448c3da5c1d05e5291533"
def test_build_sign_drops_sign_and_empty_fields():
"""sign 字段与空值字段不参与签名:带上它们应得到与不带时相同的 sign。"""
base = {"version": "2.0", "user_id": 459, "role_id": 459, "sign_type": "MD5"}
with_noise = {**base, "sign": "deadbeef", "code_ids": "", "os": None}
assert build_sign(with_noise, "key123") == build_sign(base, "key123")
# ---- 拉取:伪造 httpx 客户端,验证翻页 / 解析 / 业务码 ----
class _FakeResp:
def __init__(self, payload: dict):
self._payload = payload
def raise_for_status(self) -> None:
pass
def json(self) -> dict:
return self._payload
class _FakeClient:
"""按 offset 返回分页响应;记录每次请求的 params 供断言。"""
def __init__(self, pages: list[dict]):
self._pages = pages
self.calls: list[dict] = []
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get(self, url: str, params: dict):
self.calls.append(params)
# 第 N 次调用取第 N 页(has_next 驱动翻页);超出用最后一页兜底
page = self._pages[min(len(self.calls) - 1, len(self._pages) - 1)]
return _FakeResp(page)
@pytest.fixture
def _configured(monkeypatch):
monkeypatch.setattr(settings, "PANGLE_REPORT_USER_ID", 459)
monkeypatch.setattr(settings, "PANGLE_REPORT_ROLE_ID", 459)
monkeypatch.setattr(settings, "PANGLE_REPORT_SECURITY_KEY", "key123")
def test_fetch_daily_report_paginates_and_parses(monkeypatch, _configured):
page1 = {
"code": "100", "message": "",
"data": {
"has_next": "1", "currency": "cny", "total": 3,
"report_list": [
{"start_date": "2026-06-27", "site_id": "5830519", "ad_unit_id": "104142227",
"revenue": "1.23", "api_revenue": "1.05", "ecpm": "0.80", "imp_cnt": "1537"},
],
},
}
page2 = {
"code": "100", "message": "",
"data": {
"has_next": "0", "currency": "cny", "total": 3,
"report_list": [
{"start_date": "2026-06-27", "site_id": "5832303", "ad_unit_id": "104137445",
"revenue": "0.00", "api_revenue": "", "ecpm": "0", "imp_cnt": "12"},
],
},
}
fake = _FakeClient([page1, page2])
monkeypatch.setattr(pangle_report.httpx, "Client", lambda *a, **k: fake)
rows = pangle_report.fetch_daily_report(start_date="2026-06-27", end_date="2026-06-27")
assert len(rows) == 2 # 两页都被取到(has_next 驱动翻页)
assert rows[0]["ad_unit_id"] == "104142227"
assert rows[1]["api_revenue"] == ""
# 每次请求都带上签名与必填鉴权参数
assert "sign" in fake.calls[0]
assert fake.calls[0]["user_id"] == 459 and fake.calls[0]["version"] == "2.0"
def test_fetch_daily_report_raises_on_business_error(monkeypatch, _configured):
fail = {"code": "118", "message": "no revenue permission", "data": {}}
monkeypatch.setattr(
pangle_report.httpx, "Client", lambda *a, **k: _FakeClient([fail])
)
with pytest.raises(PangleReportError):
pangle_report.fetch_daily_report(start_date="2026-06-27", end_date="2026-06-27")
def test_fetch_daily_report_no_config_raises(monkeypatch):
monkeypatch.setattr(settings, "PANGLE_REPORT_USER_ID", 0)
with pytest.raises(PangleReportError):
pangle_report.fetch_daily_report(start_date="2026-06-27", end_date="2026-06-27")