初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,161 @@
|
||||
"""角色头像上传 API 测试。
|
||||
|
||||
验证头像文件的上传、读取、替换、类型校验、大小限制与清理。
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
_AVATAR_PATH = "/api/v1/characters"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def admin_client(
|
||||
admin_headers: dict[str, str],
|
||||
) -> AsyncGenerator[AsyncClient, None]:
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://test",
|
||||
headers=admin_headers,
|
||||
) as http_client:
|
||||
yield http_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def active_char_id(admin_client: AsyncClient) -> str:
|
||||
"""获取当前激活角色 ID。"""
|
||||
resp = await admin_client.get(f"{_AVATAR_PATH}/active")
|
||||
assert resp.status_code == 200, resp.text
|
||||
return resp.json()["id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_and_get_avatar(
|
||||
admin_client: AsyncClient, active_char_id: str,
|
||||
) -> None:
|
||||
"""上传头像后可以 GET 获取到图片文件。"""
|
||||
png_bytes = _make_test_png()
|
||||
upload = await admin_client.post(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
files={"file": ("test.png", png_bytes, "image/png")},
|
||||
)
|
||||
assert upload.status_code == 200, upload.text
|
||||
body = upload.json()
|
||||
assert "avatar_url" in body
|
||||
assert active_char_id in body["avatar_url"]
|
||||
|
||||
# GET 获取图片
|
||||
get_resp = await admin_client.get(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
)
|
||||
assert get_resp.status_code == 200
|
||||
assert get_resp.headers["content-type"] == "image/png"
|
||||
assert len(await get_resp.aread()) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_avatar_replace(
|
||||
admin_client: AsyncClient, active_char_id: str,
|
||||
) -> None:
|
||||
"""上传新头像会替换旧头像(同角色只保留一张)。"""
|
||||
# 上传第一张
|
||||
first = _make_test_png(width=10, height=10)
|
||||
r1 = await admin_client.post(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
files={"file": ("first.png", first, "image/png")},
|
||||
)
|
||||
assert r1.status_code == 200
|
||||
|
||||
# 上传第二张
|
||||
second = _make_test_png(width=20, height=20)
|
||||
r2 = await admin_client.post(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
files={"file": ("second.png", second, "image/png")},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
|
||||
# 读取头像文件大小应与第二张匹配
|
||||
get_resp = await admin_client.get(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
)
|
||||
assert get_resp.status_code == 200
|
||||
body = await get_resp.aread()
|
||||
assert len(body) == len(second)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_avatar_rejects_invalid_type(
|
||||
admin_client: AsyncClient, active_char_id: str,
|
||||
) -> None:
|
||||
"""不支持的文件类型应返回 4xx。"""
|
||||
resp = await admin_client.post(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
files={"file": ("test.svg", b"<svg></svg>", "image/svg+xml")},
|
||||
)
|
||||
assert resp.status_code in (400, 422)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_avatar_requires_auth(active_char_id: str) -> None:
|
||||
"""未认证用户不能上传头像。"""
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(
|
||||
transport=transport,
|
||||
base_url="http://test",
|
||||
) as client:
|
||||
resp = await client.post(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
files={"file": ("test.png", _make_test_png(), "image/png")},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_avatar_not_found(admin_client: AsyncClient) -> None:
|
||||
"""不存在的角色上传头像应 404。"""
|
||||
resp = await admin_client.post(
|
||||
f"{_AVATAR_PATH}/character_nonexistent_id_00000/avatar",
|
||||
files={"file": ("test.png", _make_test_png(), "image/png")},
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_avatar_no_image(
|
||||
admin_client: AsyncClient, active_char_id: str,
|
||||
) -> None:
|
||||
"""无头像时 GET 应 404。"""
|
||||
from app.services.avatar import delete_avatar
|
||||
|
||||
await delete_avatar(active_char_id)
|
||||
|
||||
resp = await admin_client.get(
|
||||
f"{_AVATAR_PATH}/{active_char_id}/avatar",
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# ── 辅助:生成最小 PNG ──
|
||||
|
||||
def _make_test_png(width: int = 1, height: int = 1) -> bytes:
|
||||
"""生成一个最小的有效 PNG 文件。"""
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
sig = b"\x89PNG\r\n\x1a\n"
|
||||
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
||||
ihdr_crc = zlib.crc32(b"IHDR" + ihdr_data)
|
||||
ihdr = b"\x00\x00\x00\x0dIHDR" + ihdr_data + struct.pack(">I", ihdr_crc)
|
||||
raw = b"\x00" + b"\xff\x00\x00" * width
|
||||
raw *= height
|
||||
compressed = zlib.compress(raw)
|
||||
idat_crc = zlib.crc32(b"IDAT" + compressed)
|
||||
idat = struct.pack(">I", len(compressed)) + b"IDAT" + compressed + struct.pack(">I", idat_crc)
|
||||
iend_crc = zlib.crc32(b"IEND")
|
||||
iend = b"\x00\x00\x00\x00IEND" + struct.pack(">I", iend_crc)
|
||||
return sig + ihdr + idat + iend
|
||||
Reference in new issue
Block a user