This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/tests/test_download_billing.py
T
snowy-yang 70ca21974b refactor: 拆掉 auth_system 包内循环依赖,路径常量收敛到 db 单一来源
为什么:auth_system 的 11 个子模块此前构成一个强连通分量——db.py 与 backup.py
为了「动态读取路径常量」而在模块级 `import auth_system`(包自身),而包 __init__
又再导出这两个子模块的全部符号,互相指成环。

这个环当前并不崩(实测 8/8 种导入姿势全通过),它的代价是**双绑定**:
`auth_system.DB_PATH is auth_system.db.DB_PATH` 初始为 True,但只 patch 其一
另一处不跟随。于是测试不得不把 4 个路径常量**两处都 patch**
(setattr(auth,...) + setattr(db,...))——而漏 patch 的后果是静默写生产库,
commit fc7fc21 修的那次「黄金测试写进生产库」正是这么来的。

改法:让路径常量回归**模块属性单一来源**。
- db.py:删掉 `import auth_system`,内部改用本模块常量(DATA_DIR / DB_PATH /
  SECRET_KEY_PATH)。这些常量本就定义在本模块,绕道包属性纯属多余。
- backup.py:改用 `import auth_system.db as db` 做模块限定访问,路径走
  db.BACKUP_DIR / db.DB_PATH——保留动态读取能力,来源从「包属性」换成
  「db 模块」,与唯一来源一致。
  注意**不能**写成 `from . import db`:该形式在静态图里同时指向包本身与子模块,
  会把这个环重新连上(实测确实如此,SCC 从 11 降到 2 就卡住了)。
- admin_site.py:5 处 `auth.DB_PATH` / `auth.BACKUP_DIR`(auth 是包别名)同样
  改为经 db 模块访问。**这处不修就会出事**——它读的是包属性快照,而测试已改为
  patch db 模块,于是备份上传会静默落到生产 data/backups/(本次实测真的发生了,
  2 个 upload_*.db 已清理,副本留在临时目录)。
- 测试:7 个文件的 patch 目标从包属性迁到 auth_system.db.*。
- __init__.py:补注释说明这些常量在此只是 import 快照,禁止经包属性重定向。

验证:
- Tarjan 重算:非平凡强连通分量 11 → **0**(import 图已是 DAG)
- AST 全仓扫描:经包属性访问路径常量的点 **0 处残留**
- 行为:子进程内 patch auth_system.db.* 后,get_conn 连的库 / create_backup
  落盘目录 / get_secret_key 目标路径**三者全部跟随**
- 导入姿势回归 8/8 通过
- 门禁:ruff 0 · pyright 0 errors 0 warnings · pytest 175 passed
2026-09-26 14:37:58 +08:00

143 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""下载/计费端点沙箱测试(不触碰生产 data/site.db)。
重点验证账本守恒:
- 未登录 401
- 无效歌曲 ID:400 且**预扣已退**(账本不留 open 行、余额不变)
- 次数不足:401/expired 且不建任务
- 批量超 500:400 且退款
- 参数校验失败各分支均按「只退 open 行」退费
"""
import pytest
from fastapi.testclient import TestClient
import auth_system as auth
import core.db as db
SECRET = 'download-test-secret'
PW = 'Passw0rd12'
@pytest.fixture()
def env(tmp_path, monkeypatch):
db_path = tmp_path / 'site.db'
monkeypatch.setattr(auth.db, 'DATA_DIR', tmp_path)
monkeypatch.setattr(auth.db, 'DB_PATH', db_path)
monkeypatch.setattr(auth.db, 'BACKUP_DIR', tmp_path / 'backups')
monkeypatch.setattr(auth.db, 'SECRET_KEY_PATH', tmp_path / 'secret.key')
monkeypatch.setattr(db, 'DATA_DIR', tmp_path)
monkeypatch.setattr(db, 'DB_PATH', db_path)
auth.init_db()
# 单一密钥源:web 层统一经 web.deps.secret_key 取密钥(调用期解析),
# patch 它即让登录签发与依赖校验同源
monkeypatch.setattr('web.deps.secret_key', lambda: SECRET)
# 造 3 次余额用户 + 0 次余额用户
auth.create_user('payer', PW, email='p@example.com', credits=3)
auth.create_user('broke', PW, email='b@example.com', credits=0)
auth.create_user('rich', PW, email='r@example.com', credits=600)
from web.app import create_app
return TestClient(create_app())
def _login(env, username):
"""登录并返回 (cookie_value, body)。
直接取 Set-Cookie 原始值并用 Cookie 头携带——httpx 的 cookie jar 会对含
base64 padding('=') 的值加双引号,导致服务端签名解析失败(真实浏览器不会),
故测试避开 jar。
"""
r = env.post('/auth/login', json={'username': username, 'password': PW})
assert r.status_code == 200, r.text
raw = r.headers.get('set-cookie', '')
cookie = ''
for part in raw.split(';'):
if part.strip().startswith('session='):
cookie = part.strip()[len('session='):]
break
assert cookie, f'no session cookie in: {raw!r}'
return cookie, r.json()
def _balance(username):
u = auth.get_user_by_name(username)
assert u is not None, f'用户 {username} 不存在,余额无从校验'
return int(u['duration_seconds'] or 0)
def _open_ledger_rows():
conn = auth.get_conn()
rows = conn.execute("SELECT task_id, reserved, status FROM billing_ledger WHERE status='open'").fetchall()
conn.close()
return [dict(r) for r in rows]
def test_download_requires_login(env):
r = env.post('/download', json={'id': '347230'})
assert r.status_code == 401
def test_download_bad_id_refunds(env):
ck, _ = _login(env, 'payer')
H = {'Cookie': f'session={ck}'}
before = _balance('payer')
r = env.post('/download', json={'id': 'not-a-number'}, headers=H)
assert r.status_code == 400
assert '无效' in r.json()['message']
# 预扣已退回,且无 open 账本行残留
assert _balance('payer') == before
assert _open_ledger_rows() == []
def test_download_bad_quality_refunds(env):
ck, _ = _login(env, 'payer')
H = {'Cookie': f'session={ck}'}
before = _balance('payer')
r = env.post('/download', json={'id': '347230', 'quality': 'nope'}, headers=H)
assert r.status_code == 400
assert _balance('payer') == before
assert _open_ledger_rows() == []
def test_download_bad_format_refunds(env):
ck, _ = _login(env, 'payer')
H = {'Cookie': f'session={ck}'}
before = _balance('payer')
r = env.post('/download', json={'id': '347230', 'format': 'xml'}, headers=H)
assert r.status_code == 400
assert _balance('payer') == before
def test_download_insufficient_credits(env):
ck, _ = _login(env, 'broke')
H = {'Cookie': f'session={ck}'}
r = env.post('/download', json={'id': '347230'}, headers=H)
assert r.status_code == 401
assert r.json().get('error_code') == 'expired'
# 未开账
assert _open_ledger_rows() == []
def test_batch_zip_too_many_refunds(env):
# 需足够余额才能为「封顶 500」预扣成功,再验证 400 时把 500 全额退回
ck, _ = _login(env, 'rich')
H = {'Cookie': f'session={ck}'}
before = _balance('rich')
ids = ','.join(str(1000 + i) for i in range(501))
r = env.post('/download/batch_zip', json={'ids': ids}, headers=H)
assert r.status_code == 400
assert '500' in r.json()['message']
# 预扣(封顶 500)已全额退回,余额回到 600
assert _balance('rich') == before
assert _open_ledger_rows() == []
def test_batch_zip_missing_ids_refunds(env):
ck, _ = _login(env, 'payer')
H = {'Cookie': f'session={ck}'}
before = _balance('payer')
r = env.post('/download/batch_zip', json={}, headers=H)
assert r.status_code == 400
assert _balance('payer') == before