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:
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user