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 838d635bc5 fix: 修复重构遗留缺陷,补齐 pyright 检查盲区
上一次重构(8446166)声称「pyright 0 错误」,但 pyproject 里 pyright 的 include
只覆盖 core/web/services —— 根目录 6 个应用模块与整个 tests/ 从未被检查,而
requests→sync_http 迁移的遗留恰好全部藏在那里。补齐 include 后立刻暴露
7 errors + 9 warnings,其中数处在运行时静默失效。

修复:
- services/runtime.py:删除 _podcast_billing_count/_billing_ids_count 两个
  无调用方的死函数。它们仍调用 api_service._safe_get_request_data(),而该方法
  在剥离 Flask 时已被删除,异常又被 except 吞掉 —— 一旦被调用,播客/歌单计费
  恒为 1 次且不报任何错。路由层已各自实现同口径版本,故整体删除。
- web/routers/admin_site.py:备份上传的类型判定必须用 starlette 的 UploadFile。
  fastapi.UploadFile 是它的子类,而 request.form() 返回基类实例,用错会让
  isinstance 恒为 False,把每一次真实上传都判成「请选择数据库文件」。
  已补 4 例真实 multipart 回归测试,并用变异测试确认能抓住该缺陷。
- auth_system.py:修正 create_user(实返 sqlite3.Row|None)与 list_cards
  (实返 (cards,total) 元组)与实现不符的返回注解;注解一改正,立刻暴露出被它
  掩盖的 register.py 中 int(user['id']) 未做 None 检查的 500 崩溃路径,已补守卫。
- music_downloader.py:修正注解引用的 core.sync_http.Response(正确名应为
  SyncResponse);封面缓存淘汰不再读类型为 Optional 的 deque.maxlen,改用命名常量。
- services/runtime.py:zip worker 的 result.music_info 可能为 None,原样使用会
  炸掉整批任务,改为按单曲失败处理。
- music_api.py:三处 isinstance 三元表达式收敛为 _as_dict() 帮助函数。
- web/main.py:文档串移除已删除的 Flask 兜底描述,补充 data/ 相对 CWD 解析的提醒。

验证:ruff check 通过;pyright 0 errors / 0 warnings(filesAnalyzed=67);
pytest 144 passed;真实 uvicorn 启动冒烟通过(/health、/、静态资源 200,
未鉴权管理端点 403)。生产库仅做只读 integrity_check,未被写入。
2026-09-26 11:52:06 +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, 'DATA_DIR', tmp_path)
monkeypatch.setattr(auth, 'DB_PATH', db_path)
monkeypatch.setattr(auth, 'BACKUP_DIR', tmp_path / 'backups')
monkeypatch.setattr(auth, '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