chore: resolve WIP after merging internal/development

- .gitignore: keep api/uploads ignore and copyright_source_listing.pdf path

- auth: keep COS avatar upload URL; delete prior COS object when applying preset

- i18n: regenerate resources.ts (includes profile tapAwayToClose)

- Avatar/COS tests and personal-info remain from prior local work

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-18 15:34:50 +08:00
parent 98802240ac
commit eabda2c6a9
12 changed files with 350 additions and 97 deletions

View File

@@ -4,12 +4,14 @@ from __future__ import annotations
import uuid
from datetime import datetime, timezone
from io import BytesIO
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from PIL import Image
from app.core.dependencies import get_current_user
from app.features.auth.deps import get_auth_service
@@ -27,6 +29,7 @@ def _mock_current_user() -> User:
u.avatar_url = None
u.subscription_type = "free"
u.created_at = datetime.now(timezone.utc)
u.language_preference = "zh"
return u
@@ -91,7 +94,6 @@ async def test_get_avatar_preset_path_traversal(preset_auth_app: FastAPI) -> Non
@pytest.mark.asyncio
async def test_set_avatar_preset_ok(preset_auth_app: FastAPI) -> None:
uid = preset_auth_app.state._fixed_user.id
transport = ASGITransport(app=preset_auth_app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.put(
@@ -101,11 +103,13 @@ async def test_set_avatar_preset_ok(preset_auth_app: FastAPI) -> None:
)
assert r.status_code == 200
body = r.json()
assert body["avatar_url"] == "/api/auth/avatar-presets/02.png"
url = body["avatar_url"]
assert url.startswith("/api/auth/avatar-presets/02.png")
assert "?v=" in url
svc: MagicMock = preset_auth_app.state._mock_auth_service
svc.update_avatar_url.assert_awaited_once_with(
uid, "/api/auth/avatar-presets/02.png"
)
stored = svc.update_avatar_url.await_args[0][1]
assert stored.startswith("/api/auth/avatar-presets/02.png")
assert "?v=" in stored
@pytest.mark.asyncio
@@ -168,3 +172,71 @@ async def test_get_uploaded_avatar_ok_when_stem_has_underscore(
async with AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.get("/api/auth/avatars/user_abc_01.jpg")
assert r.status_code == 200
def _minimal_jpeg_bytes() -> bytes:
img = Image.new("RGB", (2, 2), color=(120, 80, 200))
buf = BytesIO()
img.save(buf, format="JPEG", quality=85)
return buf.getvalue()
@pytest.mark.asyncio
async def test_upload_avatar_cos_calls_storage_and_presigns(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
import app.core.dependencies as deps
from app.core.config import settings
from app.core.dependencies import get_current_user
from app.features.auth.router import router as auth_router
bucket, region = "life-test-bucket", "ap-shanghai"
uid = str(uuid.uuid4())
public = f"https://{bucket}.cos.{region}.myqcloud.com/avatars/{uid}.jpg"
for attr, val in (
("tencent_cos_secret_id", "sid"),
("tencent_cos_secret_key", "sk"),
("tencent_cos_bucket", bucket),
("tencent_cos_region", region),
("tencent_cos_base_url", f"https://{bucket}.cos.{region}.myqcloud.com"),
):
monkeypatch.setattr(settings, attr, val, raising=False)
mock_storage = MagicMock()
mock_storage.upload = MagicMock(return_value=public)
mock_storage.get_url = MagicMock(return_value="https://example.com/signed-avatar")
monkeypatch.setattr(deps, "get_object_storage", lambda: mock_storage)
fixed_user = _mock_current_user()
fixed_user.id = uid
fixed_user.avatar_url = None
async def _fake_update_avatar(u: str, url: str):
fixed_user.avatar_url = url
return fixed_user
mock_service = MagicMock(spec=AuthService)
mock_service.update_avatar_url = AsyncMock(side_effect=_fake_update_avatar)
app = FastAPI()
app.include_router(auth_router)
app.dependency_overrides[get_auth_service] = lambda: mock_service
app.dependency_overrides[get_current_user] = lambda: fixed_user
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
r = await ac.post(
"/api/auth/me/avatar",
files={"file": ("a.jpg", BytesIO(_minimal_jpeg_bytes()), "image/jpeg")},
headers={"Authorization": "Bearer x"},
)
assert r.status_code == 200
assert r.json()["avatar_url"] == "https://example.com/signed-avatar"
mock_storage.upload.assert_called_once()
assert mock_storage.upload.call_args[0][0] == f"avatars/{uid}.jpg"
mock_storage.get_url.assert_called_once()
assert mock_storage.get_url.call_args[0][0] == f"avatars/{uid}.jpg"

View File

@@ -50,8 +50,30 @@ async def test_avatar_upload_500_detail_sanitized(
) -> None:
from fastapi import FastAPI
from app.core.config import settings
import app.core.dependencies as deps
fake_user = MagicMock()
fake_user.id = "user-contract-test"
fake_user.avatar_url = None
mock_storage = MagicMock()
mock_storage.upload = MagicMock(
return_value="https://test-bucket.cos.ap-shanghai.myqcloud.com/avatars/user-contract-test/abc.jpg"
)
monkeypatch.setattr(deps, "get_object_storage", lambda: mock_storage)
monkeypatch.setattr(settings, "tencent_cos_secret_id", "sid", raising=False)
monkeypatch.setattr(settings, "tencent_cos_secret_key", "sk", raising=False)
monkeypatch.setattr(settings, "tencent_cos_bucket", "test-bucket", raising=False)
monkeypatch.setattr(
settings, "tencent_cos_region", "ap-shanghai", raising=False
)
monkeypatch.setattr(
settings,
"tencent_cos_base_url",
"https://test-bucket.cos.ap-shanghai.myqcloud.com",
raising=False,
)
class BoomAuth:
async def update_avatar_url(self, user_id: str, avatar_url: str):