feat: redesign metadata as KV store, add custom properties and WOPI Discovery
Some checks failed
Test / test (push) Failing after 2m32s
Some checks failed
Test / test (push) Failing after 2m32s
Replace one-to-one FileMetadata table with flexible ObjectMetadata KV pairs, add custom property definitions, WOPI Discovery auto-configuration, and per-extension action URL support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
239
tests/integration/api/test_object_metadata.py
Normal file
239
tests/integration/api/test_object_metadata.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
对象元数据端点集成测试
|
||||
"""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlmodels import ObjectMetadata
|
||||
|
||||
|
||||
# ==================== 获取元数据测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_metadata_requires_auth(async_client: AsyncClient):
|
||||
"""测试获取元数据需要认证"""
|
||||
fake_id = str(uuid4())
|
||||
response = await async_client.get(f"/api/v1/object/{fake_id}/metadata")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_metadata_empty(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
):
|
||||
"""测试获取无元数据的对象"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
response = await async_client.get(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["metadatas"] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_metadata_with_entries(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
initialized_db,
|
||||
):
|
||||
"""测试获取有元数据的对象"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
# 直接写入元数据
|
||||
entries = [
|
||||
ObjectMetadata(object_id=file_id, name="exif:width", value="1920", is_public=True),
|
||||
ObjectMetadata(object_id=file_id, name="exif:height", value="1080", is_public=True),
|
||||
ObjectMetadata(object_id=file_id, name="sys:extract_status", value="done", is_public=False),
|
||||
]
|
||||
for entry in entries:
|
||||
initialized_db.add(entry)
|
||||
await initialized_db.commit()
|
||||
|
||||
response = await async_client.get(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# sys: 命名空间应被过滤
|
||||
assert "exif:width" in data["metadatas"]
|
||||
assert "exif:height" in data["metadatas"]
|
||||
assert "sys:extract_status" not in data["metadatas"]
|
||||
assert data["metadatas"]["exif:width"] == "1920"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_metadata_ns_filter(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
initialized_db,
|
||||
):
|
||||
"""测试按命名空间过滤元数据"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
entries = [
|
||||
ObjectMetadata(object_id=file_id, name="exif:width", value="1920", is_public=True),
|
||||
ObjectMetadata(object_id=file_id, name="music:title", value="Test Song", is_public=True),
|
||||
]
|
||||
for entry in entries:
|
||||
initialized_db.add(entry)
|
||||
await initialized_db.commit()
|
||||
|
||||
# 只获取 exif 命名空间
|
||||
response = await async_client.get(
|
||||
f"/api/v1/object/{file_id}/metadata?ns=exif",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "exif:width" in data["metadatas"]
|
||||
assert "music:title" not in data["metadatas"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_metadata_nonexistent_object(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
):
|
||||
"""测试获取不存在对象的元数据"""
|
||||
fake_id = str(uuid4())
|
||||
response = await async_client.get(
|
||||
f"/api/v1/object/{fake_id}/metadata",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ==================== 更新元数据测试 ====================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_metadata_requires_auth(async_client: AsyncClient):
|
||||
"""测试更新元数据需要认证"""
|
||||
fake_id = str(uuid4())
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/object/{fake_id}/metadata",
|
||||
json={"patches": [{"key": "custom:tag", "value": "test"}]},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_metadata_set_custom(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
):
|
||||
"""测试设置自定义元数据"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"patches": [
|
||||
{"key": "custom:tag1", "value": "旅游"},
|
||||
{"key": "custom:tag2", "value": "风景"},
|
||||
]
|
||||
},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# 验证已写入
|
||||
get_response = await async_client.get(
|
||||
f"/api/v1/object/{file_id}/metadata?ns=custom",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert get_response.status_code == 200
|
||||
data = get_response.json()
|
||||
assert data["metadatas"]["custom:tag1"] == "旅游"
|
||||
assert data["metadatas"]["custom:tag2"] == "风景"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_metadata_update_existing(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
):
|
||||
"""测试更新已有的元数据"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
# 先创建
|
||||
await async_client.patch(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
json={"patches": [{"key": "custom:note", "value": "旧值"}]},
|
||||
)
|
||||
|
||||
# 再更新
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
json={"patches": [{"key": "custom:note", "value": "新值"}]},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# 验证已更新
|
||||
get_response = await async_client.get(
|
||||
f"/api/v1/object/{file_id}/metadata?ns=custom",
|
||||
headers=auth_headers,
|
||||
)
|
||||
data = get_response.json()
|
||||
assert data["metadatas"]["custom:note"] == "新值"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_metadata_delete(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
):
|
||||
"""测试删除元数据条目"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
# 先创建
|
||||
await async_client.patch(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
json={"patches": [{"key": "custom:to_delete", "value": "temp"}]},
|
||||
)
|
||||
|
||||
# 删除(value 为 null)
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
json={"patches": [{"key": "custom:to_delete", "value": None}]},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# 验证已删除
|
||||
get_response = await async_client.get(
|
||||
f"/api/v1/object/{file_id}/metadata?ns=custom",
|
||||
headers=auth_headers,
|
||||
)
|
||||
data = get_response.json()
|
||||
assert "custom:to_delete" not in data["metadatas"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_metadata_reject_non_custom_namespace(
|
||||
async_client: AsyncClient,
|
||||
auth_headers: dict[str, str],
|
||||
test_directory_structure: dict[str, UUID],
|
||||
):
|
||||
"""测试拒绝修改非 custom: 命名空间"""
|
||||
file_id = test_directory_structure["file_id"]
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/object/{file_id}/metadata",
|
||||
headers=auth_headers,
|
||||
json={"patches": [{"key": "exif:width", "value": "1920"}]},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
Reference in New Issue
Block a user