156 lines
5.1 KiB
Python
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"]
|