feat(api): Pydantic schemas + Data Repositories

This commit is contained in:
2025-12-07 03:58:02 -05:00
parent fbe9fbba6e
commit a8fbce09c4
23 changed files with 3549 additions and 3 deletions

View File

@@ -0,0 +1,250 @@
"""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")