diff --git a/app/api/routers/auth.py b/app/api/routers/auth.py index 7906d55..be25619 100644 --- a/app/api/routers/auth.py +++ b/app/api/routers/auth.py @@ -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"]) diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index b001643..7e60f10 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -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: diff --git a/app/infrastructure/seed.py b/app/infrastructure/seed.py index f62e7e9..9ad2935 100644 --- a/app/infrastructure/seed.py +++ b/app/infrastructure/seed.py @@ -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() diff --git a/tests/conftest.py b/tests/conftest.py index e147e59..2d7c18c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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())