feat: enhance organization management; add member registration and validation, update user registration flow, and improve enum handling
Test / test (push) Successful in 16s
Test / test (pull_request) Successful in 14s

This commit is contained in:
k1nq
2025-11-29 08:50:11 +05:00
parent 994b400221
commit e7e3752888
11 changed files with 462 additions and 20 deletions
+186
View File
@@ -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"