feat: enhance organization management; add member registration and validation, update user registration flow, and improve enum handling
This commit is contained in:
@@ -7,6 +7,7 @@ from typing import AsyncGenerator, Sequence, cast
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.schema import Table
|
||||
|
||||
@@ -101,3 +102,188 @@ async def test_list_user_organizations_returns_memberships(
|
||||
async def test_list_user_organizations_requires_token(client: AsyncClient) -> None:
|
||||
response = await client.get("/api/v1/organizations/me")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_owner_can_add_member_to_organization(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
async with session_factory() as session:
|
||||
owner = User(email="owner-add@example.com", hashed_password="hashed", name="Owner", is_active=True)
|
||||
invitee = User(email="new-member@example.com", hashed_password="hashed", name="Member", is_active=True)
|
||||
session.add_all([owner, invitee])
|
||||
await session.flush()
|
||||
|
||||
organization = Organization(name="Membership LLC")
|
||||
session.add(organization)
|
||||
await session.flush()
|
||||
|
||||
membership = OrganizationMember(
|
||||
organization_id=organization.id,
|
||||
user_id=owner.id,
|
||||
role=OrganizationRole.OWNER,
|
||||
)
|
||||
session.add(membership)
|
||||
await session.commit()
|
||||
|
||||
token = jwt_service.create_access_token(
|
||||
subject=str(owner.id),
|
||||
expires_delta=timedelta(minutes=30),
|
||||
claims={"email": owner.email},
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/organizations/members",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"X-Organization-Id": str(organization.id),
|
||||
},
|
||||
json={"email": invitee.email, "role": OrganizationRole.MANAGER.value},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
assert payload["organization_id"] == organization.id
|
||||
assert payload["user_id"] == invitee.id
|
||||
assert payload["role"] == OrganizationRole.MANAGER.value
|
||||
|
||||
async with session_factory() as session:
|
||||
new_membership = await session.scalar(
|
||||
select(OrganizationMember).where(
|
||||
OrganizationMember.organization_id == organization.id,
|
||||
OrganizationMember.user_id == invitee.id,
|
||||
)
|
||||
)
|
||||
assert new_membership is not None
|
||||
assert new_membership.role == OrganizationRole.MANAGER
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_member_requires_existing_user(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
async with session_factory() as session:
|
||||
owner = User(email="owner-missing@example.com", hashed_password="hashed", name="Owner", is_active=True)
|
||||
session.add(owner)
|
||||
await session.flush()
|
||||
|
||||
organization = Organization(name="Missing LLC")
|
||||
session.add(organization)
|
||||
await session.flush()
|
||||
|
||||
membership = OrganizationMember(
|
||||
organization_id=organization.id,
|
||||
user_id=owner.id,
|
||||
role=OrganizationRole.OWNER,
|
||||
)
|
||||
session.add(membership)
|
||||
await session.commit()
|
||||
|
||||
token = jwt_service.create_access_token(
|
||||
subject=str(owner.id),
|
||||
expires_delta=timedelta(minutes=30),
|
||||
claims={"email": owner.email},
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/organizations/members",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"X-Organization-Id": str(organization.id),
|
||||
},
|
||||
json={"email": "ghost@example.com"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.json()["detail"] == "User not found"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_member_role_cannot_add_users(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
async with session_factory() as session:
|
||||
member_user = User(email="member@example.com", hashed_password="hashed", name="Member", is_active=True)
|
||||
invitee = User(email="invitee@example.com", hashed_password="hashed", name="Invitee", is_active=True)
|
||||
session.add_all([member_user, invitee])
|
||||
await session.flush()
|
||||
|
||||
organization = Organization(name="Members Only LLC")
|
||||
session.add(organization)
|
||||
await session.flush()
|
||||
|
||||
membership = OrganizationMember(
|
||||
organization_id=organization.id,
|
||||
user_id=member_user.id,
|
||||
role=OrganizationRole.MEMBER,
|
||||
)
|
||||
session.add(membership)
|
||||
await session.commit()
|
||||
|
||||
token = jwt_service.create_access_token(
|
||||
subject=str(member_user.id),
|
||||
expires_delta=timedelta(minutes=30),
|
||||
claims={"email": member_user.email},
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/organizations/members",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"X-Organization-Id": str(organization.id),
|
||||
},
|
||||
json={"email": invitee.email},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "Only owner/admin can modify organization settings"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cannot_add_duplicate_member(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
client: AsyncClient,
|
||||
) -> None:
|
||||
async with session_factory() as session:
|
||||
owner = User(email="dup-owner@example.com", hashed_password="hashed", name="Owner", is_active=True)
|
||||
invitee = User(email="dup-member@example.com", hashed_password="hashed", name="Invitee", is_active=True)
|
||||
session.add_all([owner, invitee])
|
||||
await session.flush()
|
||||
|
||||
organization = Organization(name="Duplicate LLC")
|
||||
session.add(organization)
|
||||
await session.flush()
|
||||
|
||||
owner_membership = OrganizationMember(
|
||||
organization_id=organization.id,
|
||||
user_id=owner.id,
|
||||
role=OrganizationRole.OWNER,
|
||||
)
|
||||
invitee_membership = OrganizationMember(
|
||||
organization_id=organization.id,
|
||||
user_id=invitee.id,
|
||||
role=OrganizationRole.MEMBER,
|
||||
)
|
||||
session.add_all([owner_membership, invitee_membership])
|
||||
await session.commit()
|
||||
|
||||
token = jwt_service.create_access_token(
|
||||
subject=str(owner.id),
|
||||
expires_delta=timedelta(minutes=30),
|
||||
claims={"email": owner.email},
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/organizations/members",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"X-Organization-Id": str(organization.id),
|
||||
},
|
||||
json={"email": invitee.email},
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
assert response.json()["detail"] == "User already belongs to this organization"
|
||||
|
||||
Reference in New Issue
Block a user