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 8446166120 refactor!: uv 管理环境 + 彻底删除技术债(Flask 与 requests 全部移除)
## uv 环境管理
- pyproject.toml 声明 dependencies + [dependency-groups].dev,uv.lock 精确锁定
- 删除 requirements.txt / requirements-fastapi.txt / requirements-dev.txt
- .python-version = 3.12;Dockerfile 改用 uv sync --frozen(可复现构建)
- 新增 jinja2(首页渲染)

## 删除 Flask 技术债
- 从 main.py 用 ast 精确剥离 56 个 Flask 节点(路由/中间件/错误处理/启动),
  框架无关的服务层落为 services/runtime.py(MusicAPIService、下载/打包 worker、
  任务监控、缓存清扫),零 Flask 依赖
- 删除 main.py(4527 行)与 auth_routes.py(1050 行)
- auth_system.py 移除会话/请求上下文层(login_session/current_user/login_required/
  admin_required/_client_ip 等),保留数据与业务层
- 新增 core/net.py(IP 解析)、web/auth_state.py(登录锁定/2FA 票据/发码限流)
- FastAPI 侧改直接依赖 services.runtime,不再 import main
- web/app.py 去掉 Flask fallback;入口默认根路径模式(API_PREFIX 可选)

## 删除 requests 技术债
- 新增 core/sync_http.py:requests 风格的同步门面(底层 aiohttp 连接池,
  流式读取经有界队列背压),含 Session/headers/(connect,read) 元组超时兼容
- core/utils、auth_system、podcast_api、music_downloader、services 全部切换
- 实测真实下载 5.2MB 音频完整成功

## 质量门禁归零
- ruff:0 错误(B904 全部修复,不再忽略)
- pyright:0 错误(类型收窄修复)
- pytest:140/140(含 golden 契约回归、root 模式 66 项路径覆盖)
- 真实 uvicorn 启动:/health、/、/static、受保护端鉴权全部正确

新增 tests/golden/responses.json:删除 Flask 前捕获的 32 例响应快照,
FastAPI 逐字段复现——删除巨石后行为回归仍有硬证据。
2026-09-26 11:10:17 +08:00

142 lines
4.8 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)
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