# -*- 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 asyncio 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.db, 'DATA_DIR', tmp_path) monkeypatch.setattr(auth.db, 'DB_PATH', tmp_path / 'site.db') 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', tmp_path / 'site.db') asyncio.run(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'))