159 lines
6.0 KiB
Python
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
|