PMFlow/backend/tests/test_account_settings_api.py

159 lines
6.0 KiB
Python

from __future__ import annotations
import os
from types import SimpleNamespace
import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy import JSON, select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test")
from app.api import account_settings_api # noqa: E402
from app.core.database import get_db # noqa: E402
from app.dependencies.auth import get_current_user # noqa: E402
from app.models.base import Base # noqa: E402
from app.models.integration import Integration # noqa: E402
from app.services.account_settings_service import INTEGRATION_TYPE # noqa: E402
def _use_sqlite_json_types(*tables) -> None:
for table in tables:
for column in table.columns:
if column.type.__class__.__name__ in {"JSONB", "ARRAY", "Vector"}:
column.type = JSON()
@pytest_asyncio.fixture
async def db_session():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
tables = [Integration.__table__]
_use_sqlite_json_types(*tables)
async with engine.begin() as conn:
await conn.run_sync(lambda sync_conn: Base.metadata.create_all(sync_conn, tables=tables))
maker = async_sessionmaker(engine, expire_on_commit=False)
async with maker() as session:
yield session
await engine.dispose()
def _current_user(*, user_id: int = 7, tenant_id: int = 1):
return SimpleNamespace(
id=user_id,
tenant_id=tenant_id,
role="MEMBER",
username=f"tester-{tenant_id}-{user_id}",
email=f"tester-{tenant_id}-{user_id}@example.com",
name="陈雨",
)
def _build_app(db_session, current_user) -> FastAPI:
app = FastAPI()
app.include_router(account_settings_api.router)
async def _fake_get_db():
yield db_session
app.dependency_overrides[get_db] = _fake_get_db
app.dependency_overrides[get_current_user] = lambda: current_user
return app
def _settings_payload(**overrides):
payload = {
"profile": {
"displayName": "陈雨",
"roleTitle": "独立开发者",
"email": "yu@example.com",
"defaultLanguage": "en-US",
},
"aiPreferences": {
"answerDepth": "deep",
"evidenceDisplay": "full",
},
"notifications": {
"pendingConfirmation": {"enabled": True, "desktop": True, "emailDigest": False},
"executionWriteback": {"enabled": True, "desktop": False, "emailDigest": False},
"releaseGate": {"enabled": True, "desktop": True, "emailDigest": True},
"failureAlert": {"enabled": False, "desktop": False, "emailDigest": False},
},
}
payload.update(overrides)
return payload
@pytest.mark.asyncio
async def test_get_account_settings_returns_defaults_from_current_user(db_session) -> None:
app = _build_app(db_session, _current_user())
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/account-settings/me")
assert response.status_code == 200
body = response.json()
assert body["code"] == 0
assert body["data"]["profile"]["displayName"] == "陈雨"
assert body["data"]["profile"]["defaultLanguage"] == "zh-CN"
assert body["data"]["aiPreferences"]["answerDepth"] == "balanced"
assert body["data"]["notifications"]["pendingConfirmation"]["enabled"] is True
assert body["data"]["saveScope"] == "account"
@pytest.mark.asyncio
async def test_put_account_settings_persists_user_preferences(db_session) -> None:
app = _build_app(db_session, _current_user())
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
update = await client.put("/account-settings/me", json=_settings_payload())
restored = await client.get("/account-settings/me")
assert update.status_code == 200
assert update.json()["data"]["saveScope"] == "account"
assert restored.status_code == 200
assert restored.json()["data"]["profile"]["defaultLanguage"] == "en-US"
assert restored.json()["data"]["aiPreferences"]["answerDepth"] == "deep"
assert restored.json()["data"]["aiPreferences"]["evidenceDisplay"] == "full"
assert restored.json()["data"]["notifications"]["failureAlert"]["enabled"] is False
row = (
await db_session.execute(
select(Integration).where(
Integration.tenant_id == 1,
Integration.type == INTEGRATION_TYPE,
)
)
).scalar_one()
assert row.config["users"]["7"]["profile"]["displayName"] == "陈雨"
@pytest.mark.asyncio
async def test_account_settings_are_user_scoped(db_session) -> None:
user7_app = _build_app(db_session, _current_user(user_id=7))
user8_app = _build_app(db_session, _current_user(user_id=8))
async with AsyncClient(transport=ASGITransport(app=user7_app), base_url="http://test") as client:
update = await client.put("/account-settings/me", json=_settings_payload())
async with AsyncClient(transport=ASGITransport(app=user8_app), base_url="http://test") as client:
restored = await client.get("/account-settings/me")
assert update.status_code == 200
assert restored.status_code == 200
assert restored.json()["data"]["profile"]["defaultLanguage"] == "zh-CN"
assert restored.json()["data"]["aiPreferences"]["answerDepth"] == "balanced"
@pytest.mark.asyncio
async def test_account_settings_reject_invalid_notification_key(db_session) -> None:
app = _build_app(db_session, _current_user())
payload = _settings_payload(notifications={"pendingConfirmation": {"enabled": True, "desktop": True, "emailDigest": False}})
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.put("/account-settings/me", json=payload)
assert response.status_code == 422