This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
"""推广归因测试:点击埋点 → 首启模糊匹配 → 注册一次性消费(全链路离线可验证)"""
import pytest
from app.services import invite_service
class FakeRedis:
"""内存版 Redis 桩:仅实现归因模块用到的命令子集"""
def __init__(self):
self.kv: dict[str, str] = {}
self.zsets: dict[str, dict[str, float]] = {}
self.ttls: dict[str, int] = {}
async def set(self, key, value, nx=False, ex=None):
if nx and key in self.kv:
return None
self.kv[key] = str(value)
if ex is not None:
self.ttls[key] = ex
return True
async def get(self, key):
return self.kv.get(key)
async def delete(self, key):
self.kv.pop(key, None)
self.zsets.pop(key, None)
async def expire(self, key, ttl):
self.ttls[key] = ttl
return True
async def zadd(self, key, mapping):
self.zsets.setdefault(key, {}).update({m: float(s) for m, s in mapping.items()})
return len(mapping)
async def zrevrange(self, key, start, stop):
items = sorted(self.zsets.get(key, {}).items(), key=lambda kv: kv[1], reverse=True)
members = [m for m, _ in items]
if stop < 0:
stop = len(members) + stop # Redis 负索引语义:-1 为最后一个元素
return members[start:stop + 1]
@pytest.fixture
def redis():
return FakeRedis()
IP = "203.0.113.8"
UA = "Mozilla/5.0 (Linux; Android 14)"
CODE_A, CODE_B = "AAAA2222", "BBBB3333"
DEVICE = "dev-uuid-0001"
# ---------- 点击埋点 ----------
@pytest.mark.asyncio
async def test_click_writes_ip_zset_and_detail(redis):
r = await invite_service.record_click(redis, CODE_A, IP, UA)
assert r == {"recorded": True}
assert await redis.zrevrange(invite_service._ip_key(IP), 0, -1) == [CODE_A]
detail = await redis.get(invite_service._clk_key(CODE_A, IP))
assert detail and UA in detail
# 两个 key 都设置了 24h TTL
assert redis.ttls[invite_service._ip_key(IP)] == invite_service.CLICK_TTL
assert redis.ttls[invite_service._clk_key(CODE_A, IP)] == invite_service.CLICK_TTL
@pytest.mark.asyncio
async def test_click_dedup_within_60s(redis):
await invite_service.record_click(redis, CODE_A, IP, UA)
r2 = await invite_service.record_click(redis, CODE_A, IP, UA)
assert r2 == {"recorded": True, "dedup": True}
assert len(redis.zsets[invite_service._ip_key(IP)]) == 1
@pytest.mark.asyncio
async def test_click_without_ip_not_recorded(redis):
assert (await invite_service.record_click(redis, CODE_A, "", UA)) == {"recorded": False}
# ---------- 归因匹配 ----------
@pytest.mark.asyncio
async def test_match_returns_clicked_code(redis):
await invite_service.record_click(redis, CODE_A, IP, UA)
r = await invite_service.match_and_cache(redis, DEVICE, IP, UA)
assert r == {"matched": True, "invite_code": CODE_A}
@pytest.mark.asyncio
async def test_match_most_recent_click_wins(redis):
await invite_service.record_click(redis, CODE_A, IP, UA)
await invite_service.record_click(redis, CODE_B, IP, UA) # 时间更晚
r = await invite_service.match_and_cache(redis, DEVICE, IP, UA)
assert r["invite_code"] == CODE_B
@pytest.mark.asyncio
async def test_match_no_click_unmatched(redis):
assert (await invite_service.match_and_cache(redis, DEVICE, IP, UA)) == {"matched": False}
@pytest.mark.asyncio
async def test_match_skips_candidate_missing_detail(redis):
"""zset 中有 code 但点击详情已过期 → 跳到下一候选"""
await invite_service.record_click(redis, CODE_A, IP, UA)
await invite_service.record_click(redis, CODE_B, IP, UA)
await redis.delete(invite_service._clk_key(CODE_B, IP)) # 详情丢失
r = await invite_service.match_and_cache(redis, DEVICE, IP, UA)
assert r["invite_code"] == CODE_A
@pytest.mark.asyncio
async def test_match_idempotent_via_cache(redis):
await invite_service.record_click(redis, CODE_A, IP, UA)
await invite_service.match_and_cache(redis, DEVICE, IP, UA)
# 点击记录清除后,归因缓存仍命中(同设备重复首启不重复计算)
await redis.delete(invite_service._ip_key(IP))
r = await invite_service.match_and_cache(redis, DEVICE, IP, UA)
assert r == {"matched": True, "invite_code": CODE_A}
# ---------- 注册消费 ----------
@pytest.mark.asyncio
async def test_pop_attribution_one_time(redis):
await invite_service.record_click(redis, CODE_A, IP, UA)
await invite_service.match_and_cache(redis, DEVICE, IP, UA)
assert await invite_service.pop_attribution(redis, DEVICE) == CODE_A
# 一次性消费:第二次取不到
assert await invite_service.pop_attribution(redis, DEVICE) is None
@pytest.mark.asyncio
async def test_pop_attribution_empty(redis):
assert await invite_service.pop_attribution(redis, "never-matched") is None