251 lines
10 KiB
Python
251 lines
10 KiB
Python
|
|
"""Tests for OrgRepository."""
|
||
|
|
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
import asyncpg
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.repositories.org import OrgRepository
|
||
|
|
from app.repositories.user import UserRepository
|
||
|
|
|
||
|
|
|
||
|
|
class TestOrgRepository:
|
||
|
|
"""Tests for OrgRepository conforming to SPECS.md."""
|
||
|
|
|
||
|
|
async def test_create_org_returns_org_data(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Creating an org returns the org data."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
org_id = uuid4()
|
||
|
|
name = "Test Organization"
|
||
|
|
slug = "test-org"
|
||
|
|
|
||
|
|
result = await repo.create(org_id, name, slug)
|
||
|
|
|
||
|
|
assert result["id"] == org_id
|
||
|
|
assert result["name"] == name
|
||
|
|
assert result["slug"] == slug
|
||
|
|
assert result["created_at"] is not None
|
||
|
|
|
||
|
|
async def test_create_org_slug_must_be_unique(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Org slug uniqueness constraint per SPECS.md orgs table."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
slug = "unique-slug"
|
||
|
|
|
||
|
|
await repo.create(uuid4(), "Org One", slug)
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.UniqueViolationError):
|
||
|
|
await repo.create(uuid4(), "Org Two", slug)
|
||
|
|
|
||
|
|
async def test_get_by_id_returns_org(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_id returns the correct organization."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
org_id = uuid4()
|
||
|
|
|
||
|
|
await repo.create(org_id, "My Org", "my-org")
|
||
|
|
result = await repo.get_by_id(org_id)
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["id"] == org_id
|
||
|
|
assert result["name"] == "My Org"
|
||
|
|
|
||
|
|
async def test_get_by_id_returns_none_for_nonexistent(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_id returns None for non-existent org."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_id(uuid4())
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_get_by_slug_returns_org(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_slug returns the correct organization."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
org_id = uuid4()
|
||
|
|
slug = "slug-lookup"
|
||
|
|
|
||
|
|
await repo.create(org_id, "Slug Test", slug)
|
||
|
|
result = await repo.get_by_slug(slug)
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["id"] == org_id
|
||
|
|
|
||
|
|
async def test_get_by_slug_returns_none_for_nonexistent(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_slug returns None for non-existent slug."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_slug("nonexistent-slug")
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_slug_exists_returns_true_when_exists(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""slug_exists returns True when slug exists."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
slug = "existing-slug"
|
||
|
|
|
||
|
|
await repo.create(uuid4(), "Existing Org", slug)
|
||
|
|
result = await repo.slug_exists(slug)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
|
||
|
|
async def test_slug_exists_returns_false_when_not_exists(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""slug_exists returns False when slug doesn't exist."""
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.slug_exists("no-such-slug")
|
||
|
|
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
|
||
|
|
class TestOrgMembership:
|
||
|
|
"""Tests for org membership operations per SPECS.md org_members table."""
|
||
|
|
|
||
|
|
async def _create_user(self, conn: asyncpg.Connection, email: str) -> uuid4:
|
||
|
|
"""Helper to create a user."""
|
||
|
|
user_repo = UserRepository(conn)
|
||
|
|
user_id = uuid4()
|
||
|
|
await user_repo.create(user_id, email, "hash")
|
||
|
|
return user_id
|
||
|
|
|
||
|
|
async def _create_org(self, conn: asyncpg.Connection, slug: str) -> uuid4:
|
||
|
|
"""Helper to create an org."""
|
||
|
|
org_repo = OrgRepository(conn)
|
||
|
|
org_id = uuid4()
|
||
|
|
await org_repo.create(org_id, f"Org {slug}", slug)
|
||
|
|
return org_id
|
||
|
|
|
||
|
|
async def test_add_member_creates_membership(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""add_member creates a membership record."""
|
||
|
|
user_id = await self._create_user(db_conn, "member@example.com")
|
||
|
|
org_id = await self._create_org(db_conn, "member-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.add_member(uuid4(), user_id, org_id, "member")
|
||
|
|
|
||
|
|
assert result["user_id"] == user_id
|
||
|
|
assert result["org_id"] == org_id
|
||
|
|
assert result["role"] == "member"
|
||
|
|
assert result["created_at"] is not None
|
||
|
|
|
||
|
|
async def test_add_member_role_must_be_valid(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Role must be admin, member, or viewer per SPECS.md."""
|
||
|
|
org_id = await self._create_org(db_conn, "role-test-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
# Valid roles should work
|
||
|
|
for role in ["admin", "member", "viewer"]:
|
||
|
|
member_id = uuid4()
|
||
|
|
# Need a new user for each since user+org must be unique
|
||
|
|
new_user_id = await self._create_user(db_conn, f"{role}@example.com")
|
||
|
|
result = await repo.add_member(member_id, new_user_id, org_id, role)
|
||
|
|
assert result["role"] == role
|
||
|
|
|
||
|
|
# Invalid role should fail
|
||
|
|
another_user = await self._create_user(db_conn, "invalid_role@example.com")
|
||
|
|
with pytest.raises(asyncpg.CheckViolationError):
|
||
|
|
await repo.add_member(uuid4(), another_user, org_id, "superuser")
|
||
|
|
|
||
|
|
async def test_add_member_user_org_must_be_unique(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""User can only be member of an org once (unique constraint)."""
|
||
|
|
user_id = await self._create_user(db_conn, "unique_member@example.com")
|
||
|
|
org_id = await self._create_org(db_conn, "unique-member-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.add_member(uuid4(), user_id, org_id, "member")
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.UniqueViolationError):
|
||
|
|
await repo.add_member(uuid4(), user_id, org_id, "admin")
|
||
|
|
|
||
|
|
async def test_get_member_returns_membership(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_member returns the membership for user and org."""
|
||
|
|
user_id = await self._create_user(db_conn, "get_member@example.com")
|
||
|
|
org_id = await self._create_org(db_conn, "get-member-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.add_member(uuid4(), user_id, org_id, "admin")
|
||
|
|
result = await repo.get_member(user_id, org_id)
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["user_id"] == user_id
|
||
|
|
assert result["org_id"] == org_id
|
||
|
|
assert result["role"] == "admin"
|
||
|
|
|
||
|
|
async def test_get_member_returns_none_for_nonmember(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_member returns None if user is not a member."""
|
||
|
|
user_id = await self._create_user(db_conn, "nonmember@example.com")
|
||
|
|
org_id = await self._create_org(db_conn, "nonmember-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_member(user_id, org_id)
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_get_members_returns_all_org_members(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_members returns all members with their emails."""
|
||
|
|
org_id = await self._create_org(db_conn, "all-members-org")
|
||
|
|
user1 = await self._create_user(db_conn, "user1@example.com")
|
||
|
|
user2 = await self._create_user(db_conn, "user2@example.com")
|
||
|
|
user3 = await self._create_user(db_conn, "user3@example.com")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.add_member(uuid4(), user1, org_id, "admin")
|
||
|
|
await repo.add_member(uuid4(), user2, org_id, "member")
|
||
|
|
await repo.add_member(uuid4(), user3, org_id, "viewer")
|
||
|
|
|
||
|
|
result = await repo.get_members(org_id)
|
||
|
|
|
||
|
|
assert len(result) == 3
|
||
|
|
emails = {m["email"] for m in result}
|
||
|
|
assert emails == {"user1@example.com", "user2@example.com", "user3@example.com"}
|
||
|
|
|
||
|
|
async def test_get_members_returns_empty_list_for_no_members(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_members returns empty list for org with no members."""
|
||
|
|
org_id = await self._create_org(db_conn, "empty-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_members(org_id)
|
||
|
|
|
||
|
|
assert result == []
|
||
|
|
|
||
|
|
async def test_get_user_orgs_returns_all_user_memberships(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_user_orgs returns all orgs a user belongs to with their role."""
|
||
|
|
user_id = await self._create_user(db_conn, "multi_org@example.com")
|
||
|
|
org1 = await self._create_org(db_conn, "user-org-1")
|
||
|
|
org2 = await self._create_org(db_conn, "user-org-2")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.add_member(uuid4(), user_id, org1, "admin")
|
||
|
|
await repo.add_member(uuid4(), user_id, org2, "member")
|
||
|
|
|
||
|
|
result = await repo.get_user_orgs(user_id)
|
||
|
|
|
||
|
|
assert len(result) == 2
|
||
|
|
slugs = {o["slug"] for o in result}
|
||
|
|
assert slugs == {"user-org-1", "user-org-2"}
|
||
|
|
# Check role is included
|
||
|
|
roles = {o["role"] for o in result}
|
||
|
|
assert roles == {"admin", "member"}
|
||
|
|
|
||
|
|
async def test_get_user_orgs_returns_empty_for_no_memberships(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_user_orgs returns empty list for user with no memberships."""
|
||
|
|
user_id = await self._create_user(db_conn, "no_orgs@example.com")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_user_orgs(user_id)
|
||
|
|
|
||
|
|
assert result == []
|
||
|
|
|
||
|
|
async def test_member_requires_valid_user_foreign_key(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""org_members.user_id must reference existing user."""
|
||
|
|
org_id = await self._create_org(db_conn, "fk-test-org")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.ForeignKeyViolationError):
|
||
|
|
await repo.add_member(uuid4(), uuid4(), org_id, "member")
|
||
|
|
|
||
|
|
async def test_member_requires_valid_org_foreign_key(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""org_members.org_id must reference existing org."""
|
||
|
|
user_id = await self._create_user(db_conn, "fk_user@example.com")
|
||
|
|
repo = OrgRepository(db_conn)
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.ForeignKeyViolationError):
|
||
|
|
await repo.add_member(uuid4(), user_id, uuid4(), "member")
|