- Migrate SQLModel base classes, mixins, and database management to external sqlmodel-ext package; remove sqlmodels/base/, sqlmodels/mixin/, and sqlmodels/database.py - Add file viewer/editor system with WOPI protocol support for collaborative editing (OnlyOffice, Collabora) - Add enterprise edition license verification module (ee/) - Add Dockerfile multi-stage build with Cython compilation support - Add new dependencies: sqlmodel-ext, cryptography, whatthepatch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
306 lines
9.5 KiB
Python
306 lines
9.5 KiB
Python
"""
|
||
文件查看器集成测试
|
||
|
||
测试查看器查询、用户默认设置、用户组过滤等端点。
|
||
"""
|
||
from uuid import UUID
|
||
|
||
import pytest
|
||
import pytest_asyncio
|
||
from httpx import AsyncClient
|
||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||
|
||
from sqlmodels.file_app import (
|
||
FileApp,
|
||
FileAppExtension,
|
||
FileAppGroupLink,
|
||
FileAppType,
|
||
UserFileAppDefault,
|
||
)
|
||
from sqlmodels.user import User
|
||
|
||
|
||
# ==================== Fixtures ====================
|
||
|
||
@pytest_asyncio.fixture
|
||
async def setup_file_apps(
|
||
initialized_db: AsyncSession,
|
||
) -> dict[str, UUID]:
|
||
"""创建测试用文件查看器应用"""
|
||
# PDF 阅读器(不限制用户组)
|
||
pdf_app = FileApp(
|
||
name="PDF 阅读器",
|
||
app_key="pdfjs",
|
||
type=FileAppType.BUILTIN,
|
||
is_enabled=True,
|
||
is_restricted=False,
|
||
)
|
||
pdf_app = await pdf_app.save(initialized_db)
|
||
|
||
# Monaco 编辑器(不限制用户组)
|
||
monaco_app = FileApp(
|
||
name="代码编辑器",
|
||
app_key="monaco",
|
||
type=FileAppType.BUILTIN,
|
||
is_enabled=True,
|
||
is_restricted=False,
|
||
)
|
||
monaco_app = await monaco_app.save(initialized_db)
|
||
|
||
# Collabora(限制用户组)
|
||
collabora_app = FileApp(
|
||
name="Collabora",
|
||
app_key="collabora",
|
||
type=FileAppType.WOPI,
|
||
is_enabled=True,
|
||
is_restricted=True,
|
||
)
|
||
collabora_app = await collabora_app.save(initialized_db)
|
||
|
||
# 已禁用的应用
|
||
disabled_app = FileApp(
|
||
name="禁用的应用",
|
||
app_key="disabled_app",
|
||
type=FileAppType.BUILTIN,
|
||
is_enabled=False,
|
||
is_restricted=False,
|
||
)
|
||
disabled_app = await disabled_app.save(initialized_db)
|
||
|
||
# 创建扩展名
|
||
for ext in ["pdf"]:
|
||
await FileAppExtension(app_id=pdf_app.id, extension=ext, priority=0).save(initialized_db)
|
||
|
||
for ext in ["txt", "md", "json"]:
|
||
await FileAppExtension(app_id=monaco_app.id, extension=ext, priority=0).save(initialized_db)
|
||
|
||
for ext in ["docx", "xlsx", "pptx"]:
|
||
await FileAppExtension(app_id=collabora_app.id, extension=ext, priority=0).save(initialized_db)
|
||
|
||
for ext in ["pdf"]:
|
||
await FileAppExtension(app_id=disabled_app.id, extension=ext, priority=10).save(initialized_db)
|
||
|
||
return {
|
||
"pdf_app_id": pdf_app.id,
|
||
"monaco_app_id": monaco_app.id,
|
||
"collabora_app_id": collabora_app.id,
|
||
"disabled_app_id": disabled_app.id,
|
||
}
|
||
|
||
|
||
# ==================== GET /file/viewers ====================
|
||
|
||
class TestGetViewers:
|
||
"""查询可用查看器测试"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_viewers_for_pdf(
|
||
self,
|
||
async_client: AsyncClient,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""查询 PDF 查看器:返回已启用的,排除已禁用的"""
|
||
response = await async_client.get(
|
||
"/api/v1/file/viewers?ext=pdf",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
|
||
data = response.json()
|
||
assert "viewers" in data
|
||
viewer_keys = [v["app_key"] for v in data["viewers"]]
|
||
|
||
# pdfjs 应该在列表中
|
||
assert "pdfjs" in viewer_keys
|
||
# 禁用的应用不应出现
|
||
assert "disabled_app" not in viewer_keys
|
||
# 默认值应为 None
|
||
assert data["default_viewer_id"] is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_viewers_normalizes_extension(
|
||
self,
|
||
async_client: AsyncClient,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""扩展名规范化:.PDF → pdf"""
|
||
response = await async_client.get(
|
||
"/api/v1/file/viewers?ext=.PDF",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert len(data["viewers"]) >= 1
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_viewers_empty_for_unknown_ext(
|
||
self,
|
||
async_client: AsyncClient,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""未知扩展名返回空列表"""
|
||
response = await async_client.get(
|
||
"/api/v1/file/viewers?ext=xyz_unknown",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["viewers"] == []
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_group_restriction_filters_app(
|
||
self,
|
||
async_client: AsyncClient,
|
||
initialized_db: AsyncSession,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""用户组限制:collabora 限制了用户组,用户不在白名单内则不可见"""
|
||
# collabora 是受限的,用户组不在白名单中
|
||
response = await async_client.get(
|
||
"/api/v1/file/viewers?ext=docx",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
viewer_keys = [v["app_key"] for v in data["viewers"]]
|
||
assert "collabora" not in viewer_keys
|
||
|
||
# 将用户组加入白名单
|
||
test_user = await User.get(initialized_db, User.email == "testuser@test.local")
|
||
link = FileAppGroupLink(
|
||
app_id=setup_file_apps["collabora_app_id"],
|
||
group_id=test_user.group_id,
|
||
)
|
||
initialized_db.add(link)
|
||
await initialized_db.commit()
|
||
|
||
# 再次查询
|
||
response = await async_client.get(
|
||
"/api/v1/file/viewers?ext=docx",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
viewer_keys = [v["app_key"] for v in data["viewers"]]
|
||
assert "collabora" in viewer_keys
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_unauthorized_without_token(
|
||
self,
|
||
async_client: AsyncClient,
|
||
) -> None:
|
||
"""未认证请求返回 401"""
|
||
response = await async_client.get("/api/v1/file/viewers?ext=pdf")
|
||
assert response.status_code in (401, 403)
|
||
|
||
|
||
# ==================== User File Viewer Defaults ====================
|
||
|
||
class TestUserFileViewerDefaults:
|
||
"""用户默认查看器设置测试"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_set_default_viewer(
|
||
self,
|
||
async_client: AsyncClient,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""设置默认查看器"""
|
||
response = await async_client.put(
|
||
"/api/v1/user/settings/file-viewers/default",
|
||
headers=auth_headers,
|
||
json={
|
||
"extension": "pdf",
|
||
"app_id": str(setup_file_apps["pdf_app_id"]),
|
||
},
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["extension"] == "pdf"
|
||
assert data["app"]["app_key"] == "pdfjs"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_default_viewers(
|
||
self,
|
||
async_client: AsyncClient,
|
||
initialized_db: AsyncSession,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""列出默认查看器"""
|
||
# 先创建一个默认
|
||
test_user = await User.get(initialized_db, User.email == "testuser@test.local")
|
||
await UserFileAppDefault(
|
||
user_id=test_user.id,
|
||
extension="pdf",
|
||
app_id=setup_file_apps["pdf_app_id"],
|
||
).save(initialized_db)
|
||
|
||
response = await async_client.get(
|
||
"/api/v1/user/settings/file-viewers/defaults",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert isinstance(data, list)
|
||
assert len(data) >= 1
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_default_viewer(
|
||
self,
|
||
async_client: AsyncClient,
|
||
initialized_db: AsyncSession,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""撤销默认查看器"""
|
||
# 创建一个默认
|
||
test_user = await User.get(initialized_db, User.email == "testuser@test.local")
|
||
default = await UserFileAppDefault(
|
||
user_id=test_user.id,
|
||
extension="txt",
|
||
app_id=setup_file_apps["monaco_app_id"],
|
||
).save(initialized_db)
|
||
|
||
response = await async_client.delete(
|
||
f"/api/v1/user/settings/file-viewers/default/{default.id}",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 204
|
||
|
||
# 验证已删除
|
||
found = await UserFileAppDefault.get(
|
||
initialized_db, UserFileAppDefault.id == default.id
|
||
)
|
||
assert found is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_viewers_includes_default(
|
||
self,
|
||
async_client: AsyncClient,
|
||
initialized_db: AsyncSession,
|
||
auth_headers: dict[str, str],
|
||
setup_file_apps: dict[str, UUID],
|
||
) -> None:
|
||
"""查看器查询应包含用户默认选择"""
|
||
# 设置默认
|
||
test_user = await User.get(initialized_db, User.email == "testuser@test.local")
|
||
await UserFileAppDefault(
|
||
user_id=test_user.id,
|
||
extension="pdf",
|
||
app_id=setup_file_apps["pdf_app_id"],
|
||
).save(initialized_db)
|
||
|
||
response = await async_client.get(
|
||
"/api/v1/file/viewers?ext=pdf",
|
||
headers=auth_headers,
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["default_viewer_id"] == str(setup_file_apps["pdf_app_id"])
|