PMFlow/backend/tests/test_workspace_membership_a...

156 lines
5.1 KiB
Python

from __future__ import annotations
from datetime import timedelta
import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from sqlalchemy import JSON
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.api import workspaces_api
from app.core.database import get_db
from app.dependencies.auth import get_current_user, get_optional_current_user
from app.models import Project, Tenant, TenantInvitation, TenantMembership, User, UserProject
from app.models.base import Base
from app.services.workspace_service import hash_invitation_token
from app.utils.timezone import now_cn_naive
TABLES = [
Tenant.__table__,
User.__table__,
Project.__table__,
UserProject.__table__,
TenantMembership.__table__,
TenantInvitation.__table__,
]
def _use_sqlite_json_types() -> None:
for table in TABLES:
for column in table.columns:
if column.type.__class__.__name__.upper() in {"JSONB", "ARRAY"}:
column.type = JSON()
@pytest_asyncio.fixture
async def workspace_db_env():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
_use_sqlite_json_types()
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 _build_app(session, current_user: User | None = None) -> FastAPI:
app = FastAPI()
app.include_router(workspaces_api.router)
async def _fake_get_db():
yield session
app.dependency_overrides[get_db] = _fake_get_db
if current_user is not None:
app.dependency_overrides[get_current_user] = lambda: current_user
app.dependency_overrides[get_optional_current_user] = lambda: current_user
return app
@pytest.mark.asyncio
async def test_invited_user_keeps_personal_workspace_after_leaving_team(workspace_db_env) -> None:
session = workspace_db_env
team = Tenant(name="Alpha Team", workspace_type="team", status="active")
personal = Tenant(name="杨宇 的个人空间", workspace_type="personal", status="active")
owner = User(
tenant_id=1,
username="owner",
email="owner@example.com",
password_hash="hashed",
name="Owner",
role="OWNER",
assigned_domains=[],
workload=0,
status="active",
is_active=True,
)
builder = User(
tenant_id=2,
username="yangyu",
email="yangyu@example.com",
password_hash="hashed",
name="杨宇",
role="OWNER",
assigned_domains=[],
workload=0,
status="active",
is_active=True,
)
session.add_all([team, personal, owner, builder])
await session.flush()
team.owner_user_id = int(owner.id)
personal.owner_user_id = int(builder.id)
owner.tenant_id = int(team.id)
builder.tenant_id = int(personal.id)
session.add_all(
[
TenantMembership(tenant_id=int(team.id), user_id=int(owner.id), role="OWNER", status="active"),
TenantMembership(
tenant_id=int(personal.id),
user_id=int(builder.id),
role="OWNER",
status="active",
is_personal_owner=True,
),
]
)
project = Project(
tenant_id=int(team.id),
name="Team Project",
title="Team Project",
description="",
owner_id=int(owner.id),
status="active",
)
session.add(project)
await session.flush()
token = "invite-token"
session.add(
TenantInvitation(
tenant_id=int(team.id),
email="yangyu@example.com",
role="BUILDER",
project_ids=[int(project.id)],
token_hash=hash_invitation_token(token),
status="pending",
expires_at=now_cn_naive() + timedelta(days=1),
invited_by_user_id=int(owner.id),
)
)
await session.commit()
app = _build_app(session, builder)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
accepted = await client.post("/workspaces/invitations/accept", json={"token": token})
assert accepted.status_code == 200
assert accepted.json()["data"]["workspace_id"] == int(team.id)
spaces = await client.get("/workspaces/me")
assert spaces.status_code == 200
payload = spaces.json()["data"]
assert {item["workspace_type"] for item in payload["workspaces"]} == {"personal", "team"}
left = await client.post(f"/workspaces/{team.id}/leave")
assert left.status_code == 200
assert left.json()["data"]["active_workspace_id"] == int(personal.id)
spaces_after = await client.get("/workspaces/me")
assert spaces_after.status_code == 200
workspaces = spaces_after.json()["data"]["workspaces"]
assert [item["workspace_type"] for item in workspaces] == ["personal"]