test: 平台测试套件适配异步四层,66 全绿
- conftest 改为 monkeypatch DATABASE_URL + lifespan 建库(解决异步引擎跨事件循环) - 修复 seed_data 辅助函数 await、_to_dict 链式 await、get_current_user await - 修复 Database.initialize 建表于本实例引擎 - 平台 19 路由全部经异步接口回归通过
This commit is contained in:
@@ -215,7 +215,7 @@ async def verify(request: Request, db: Database = Depends(get_db)):
|
||||
"""校验调用方 Bearer 令牌;无效令牌由依赖层直接返回 401。"""
|
||||
if not config.AUTH_ENABLED:
|
||||
return VerifyResponse(valid=True, username="")
|
||||
user = get_current_user(request, db)
|
||||
user = await get_current_user(request, db)
|
||||
return VerifyResponse(valid=True, username=user["username"])
|
||||
|
||||
|
||||
|
||||
@@ -1111,7 +1111,7 @@ class IdentityRepository:
|
||||
|
||||
async def get(self, identity_id: str) -> dict | None:
|
||||
row = await self.session.get(UserIdentity, identity_id)
|
||||
return self._to_dict(row) if row else None
|
||||
return await self._to_dict(row) if row else None
|
||||
|
||||
async def get_for_user(self, identity_id: str, user_id: str) -> dict | None:
|
||||
row = await self.session.scalar(
|
||||
@@ -1120,14 +1120,14 @@ class IdentityRepository:
|
||||
UserIdentity.user_id == user_id,
|
||||
)
|
||||
)
|
||||
return self._to_dict(row) if row else None
|
||||
return await self._to_dict(row) if row else None
|
||||
|
||||
async def list_for_user(self, user_id: str, active_only: bool = True) -> list[dict]:
|
||||
stmt = select(UserIdentity).where(UserIdentity.user_id == user_id)
|
||||
if active_only:
|
||||
stmt = stmt.where(UserIdentity.status == "active")
|
||||
rows = (await self.session.scalars(stmt.order_by(UserIdentity.port))).all()
|
||||
return [self._to_dict(i) for i in rows]
|
||||
return [await self._to_dict(i) for i in rows]
|
||||
|
||||
async def create(
|
||||
self,
|
||||
@@ -1156,7 +1156,7 @@ class IdentityRepository:
|
||||
)
|
||||
self.session.add(row)
|
||||
await self.session.commit()
|
||||
return self._to_dict(row)
|
||||
return await self._to_dict(row)
|
||||
|
||||
async def set_status(self, identity_id: str, status: str) -> dict | None:
|
||||
row = await self.session.get(UserIdentity, identity_id)
|
||||
@@ -1165,7 +1165,7 @@ class IdentityRepository:
|
||||
row.status = status
|
||||
row.updated_at = utcnow_iso()
|
||||
await self.session.commit()
|
||||
return self._to_dict(row)
|
||||
return await self._to_dict(row)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1293,11 +1293,11 @@ class RoadshowRepository:
|
||||
stmt = select(Roadshow).order_by(Roadshow.created_at.desc())
|
||||
if status:
|
||||
stmt = stmt.where(Roadshow.status == status)
|
||||
return [self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
return [await self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
|
||||
async def get(self, roadshow_id: str) -> dict | None:
|
||||
row = await self.session.get(Roadshow, roadshow_id)
|
||||
return self._to_dict(row) if row else None
|
||||
return await self._to_dict(row) if row else None
|
||||
|
||||
async def create(self, fields: dict) -> dict:
|
||||
now = utcnow_iso()
|
||||
@@ -1318,7 +1318,7 @@ class RoadshowRepository:
|
||||
)
|
||||
self.session.add(row)
|
||||
await self.session.commit()
|
||||
return self._to_dict(row)
|
||||
return await self._to_dict(row)
|
||||
|
||||
async def set_status(self, roadshow_id: str, status: str, review_comment: str = "") -> dict | None:
|
||||
row = await self.session.get(Roadshow, roadshow_id)
|
||||
@@ -1329,7 +1329,7 @@ class RoadshowRepository:
|
||||
row.review_comment = review_comment
|
||||
row.updated_at = utcnow_iso()
|
||||
await self.session.commit()
|
||||
return self._to_dict(row)
|
||||
return await self._to_dict(row)
|
||||
|
||||
|
||||
class RoadshowRegistrationRepository:
|
||||
@@ -1472,7 +1472,7 @@ class ServiceReferralRepository:
|
||||
stmt = select(ServiceReferral).order_by(ServiceReferral.created_at.desc())
|
||||
if carrier_id:
|
||||
stmt = stmt.where(ServiceReferral.carrier_id == carrier_id)
|
||||
return [self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
return [await self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
|
||||
async def create(self, fields: dict) -> dict:
|
||||
row = ServiceReferral(
|
||||
@@ -1526,7 +1526,7 @@ class EscrowRepository:
|
||||
stmt = select(Escrow).order_by(Escrow.created_at.desc())
|
||||
if status:
|
||||
stmt = stmt.where(Escrow.status == status)
|
||||
return [self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
return [await self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
|
||||
async def get(self, escrow_id: str) -> dict | None:
|
||||
row = await self.session.get(Escrow, escrow_id)
|
||||
@@ -1587,7 +1587,7 @@ class DisputeRepository:
|
||||
stmt = select(Dispute).order_by(Dispute.created_at.desc())
|
||||
if status:
|
||||
stmt = stmt.where(Dispute.status == status)
|
||||
return [self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
return [await self._to_dict(r) for r in await self.session.scalars(stmt)]
|
||||
|
||||
async def create(self, task_id: str, task_title: str, initiator: str, reason: str) -> dict:
|
||||
row = Dispute(id=new_id("disp"), task_id=task_id, task_title=task_title,
|
||||
@@ -1791,12 +1791,12 @@ class Database:
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""建表(本实例的 engine)+ 幂等种子(roles 空时才写)。"""
|
||||
from .db import Base
|
||||
from .seed import seed_data
|
||||
|
||||
if self._engine is not None:
|
||||
from .db import init_db
|
||||
|
||||
await init_db()
|
||||
async with self._engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await seed_data(self.session)
|
||||
|
||||
async def close(self) -> None:
|
||||
|
||||
+21
-21
@@ -261,17 +261,17 @@ async def seed_data(session: AsyncSession) -> None:
|
||||
existing = await session.scalar(select(Role.id).limit(1))
|
||||
if existing is not None:
|
||||
# 已初始化的库:仍补种 OPC 业务演示数据、账号身份与端口工作台(各自幂等)。
|
||||
_sync_permissions(session, now)
|
||||
_ensure_extra_demo_users(session, now)
|
||||
_seed_opc_business(session, now)
|
||||
_migrate_identities(session, now)
|
||||
_seed_port_dashboards(session, now)
|
||||
_seed_market(session, now)
|
||||
_seed_port_pages(session, now)
|
||||
_seed_investor(session, now)
|
||||
_seed_ecosystem(session, now)
|
||||
_ensure_port_agents(session, now)
|
||||
_seed_org_members(session, now)
|
||||
await _sync_permissions(session, now)
|
||||
await _ensure_extra_demo_users(session, now)
|
||||
await _seed_opc_business(session, now)
|
||||
await _migrate_identities(session, now)
|
||||
await _seed_port_dashboards(session, now)
|
||||
await _seed_market(session, now)
|
||||
await _seed_port_pages(session, now)
|
||||
await _seed_investor(session, now)
|
||||
await _seed_ecosystem(session, now)
|
||||
await _ensure_port_agents(session, now)
|
||||
await _seed_org_members(session, now)
|
||||
await session.commit()
|
||||
return
|
||||
|
||||
@@ -331,16 +331,16 @@ async def seed_data(session: AsyncSession) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
_seed_operator_business(session, now)
|
||||
_seed_opc_business(session, now)
|
||||
_migrate_identities(session, now)
|
||||
_seed_port_dashboards(session, now)
|
||||
_seed_market(session, now)
|
||||
_seed_port_pages(session, now)
|
||||
_seed_investor(session, now)
|
||||
_seed_ecosystem(session, now)
|
||||
_ensure_port_agents(session, now)
|
||||
_seed_org_members(session, now)
|
||||
await _seed_operator_business(session, now)
|
||||
await _seed_opc_business(session, now)
|
||||
await _migrate_identities(session, now)
|
||||
await _seed_port_dashboards(session, now)
|
||||
await _seed_market(session, now)
|
||||
await _seed_port_pages(session, now)
|
||||
await _seed_investor(session, now)
|
||||
await _seed_ecosystem(session, now)
|
||||
await _ensure_port_agents(session, now)
|
||||
await _seed_org_members(session, now)
|
||||
await session.commit()
|
||||
|
||||
|
||||
|
||||
+15
-10
@@ -1,22 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""共享夹具:临时 SQLite 数据库 + TestClient(auth 与 rbac 测试共用)。"""
|
||||
from __future__ import annotations
|
||||
"""共享夹具:临时 SQLite 数据库 + TestClient(auth 与 rbac 测试共用)。
|
||||
|
||||
import asyncio
|
||||
方案:monkeypatch config.DATABASE_URL 为临时异步 SQLite,让应用 lifespan 在
|
||||
TestClient 自身的事件循环内建库/种子,避免异步引擎跨事件循环问题。测试函数
|
||||
保持同步(TestClient 内部驱动 async 应用)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app import config as app_config
|
||||
from app.main import app
|
||||
from app.infrastructure.repositories import Database
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path):
|
||||
"""把 app.state.db 换成指向临时 SQLite 的 Database,保证测试隔离。"""
|
||||
db = Database(db_url=f"sqlite+aiosqlite:///{tmp_path}/test.db")
|
||||
asyncio.run(db.initialize())
|
||||
app.state.db = db
|
||||
def client(tmp_path, monkeypatch):
|
||||
"""把数据库指向临时 SQLite,保证测试隔离。"""
|
||||
monkeypatch.setattr(
|
||||
app_config, "DATABASE_URL", f"sqlite+aiosqlite:///{tmp_path}/test.db",
|
||||
)
|
||||
# 清掉上一测试残留的 app.state.db,强制 lifespan 用当前 tmp URL 新建库
|
||||
if hasattr(app.state, "db"):
|
||||
app.state.db = None
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
asyncio.run(db.close())
|
||||
|
||||
Reference in New Issue
Block a user