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_admin_backup_upload.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

82 lines
3.0 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 -*-
"""备份上传端点回归:钉住「用哪个 UploadFile 做 isinstance」这个坑。
坑的由来:`fastapi.UploadFile` 是 `starlette.datastructures.UploadFile` 的**子类**
(MRO: fastapi.datastructures.UploadFile → starlette.datastructures.UploadFile),
而 `request.form()` 返回的是 **starlette 那个基类**的实例。所以
`isinstance(f, fastapi.UploadFile)` 对真实上传恒为 False ——端点会把每一次上传
都判成「请选择数据库文件」,功能静默失效且不报错。
本测试用真实 multipart 请求穿过端点,确保三种分支(接受 / 后缀拒 / 内容拒)行为正确。
"""
import io
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from web.deps import SiteUser
from web.routers import admin_site
UPLOAD_URL = '/admin/site/backup/upload'
SQLITE_MAGIC = b'SQLite format 3\x00'
@pytest.fixture()
def client(tmp_path, monkeypatch):
"""沙箱库 + 绕过管理员闸(本测试只关心 Form/UploadFile 处理,不测鉴权)。"""
import auth_system as auth
import core.db as db
monkeypatch.setattr(auth, 'DATA_DIR', tmp_path)
monkeypatch.setattr(auth, 'DB_PATH', tmp_path / 'site.db')
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', tmp_path / 'site.db')
auth.init_db()
app = FastAPI()
app.include_router(admin_site.router)
app.dependency_overrides[admin_site._require_admin] = lambda: SiteUser(
id=1, username='boss', role='admin', credits=-1)
return TestClient(app)
def _upload(client, filename, payload):
return client.post(
UPLOAD_URL,
files={'file': (filename, io.BytesIO(payload), 'application/octet-stream')},
)
def test_real_upload_is_accepted(client, tmp_path):
"""真实 SQLite 文件必须被接收并落盘(若 isinstance 用错类,这里会拿到「请选择数据库文件」)。"""
r = _upload(client, 'up.db', SQLITE_MAGIC + b'\x00' * 512)
assert r.status_code == 200
body = r.json()
assert body['success'] is True, body
assert body['data']['name'].endswith('.db')
assert (tmp_path / 'backups' / body['data']['name']).exists()
def test_missing_file_field_is_rejected(client):
r = client.post(UPLOAD_URL, data={'note': 'no file here'})
assert r.json()['success'] is False
def test_non_db_extension_is_rejected(client):
r = _upload(client, 'evil.txt', SQLITE_MAGIC + b'\x00' * 64)
body = r.json()
assert body['success'] is False
assert '.db' in body['message']
def test_valid_extension_but_bad_content_is_rejected(client, tmp_path):
"""后缀对但内容不是 SQLite:必须拒绝,且不得把半成品留在备份目录。"""
r = _upload(client, 'fake.db', b'this is definitely not a sqlite database')
body = r.json()
assert body['success'] is False
assert not list((tmp_path / 'backups').glob('*.db'))