202 lines
8.2 KiB
Python
202 lines
8.2 KiB
Python
|
|
"""Tests for ServiceRepository."""
|
||
|
|
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
import asyncpg
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from app.repositories.org import OrgRepository
|
||
|
|
from app.repositories.service import ServiceRepository
|
||
|
|
|
||
|
|
|
||
|
|
class TestServiceRepository:
|
||
|
|
"""Tests for ServiceRepository conforming to SPECS.md."""
|
||
|
|
|
||
|
|
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_create_service_returns_service_data(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Creating a service returns the service data."""
|
||
|
|
org_id = await self._create_org(db_conn, "service-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
service_id = uuid4()
|
||
|
|
|
||
|
|
result = await repo.create(service_id, org_id, "API Gateway", "api-gateway")
|
||
|
|
|
||
|
|
assert result["id"] == service_id
|
||
|
|
assert result["org_id"] == org_id
|
||
|
|
assert result["name"] == "API Gateway"
|
||
|
|
assert result["slug"] == "api-gateway"
|
||
|
|
assert result["created_at"] is not None
|
||
|
|
|
||
|
|
async def test_create_service_slug_unique_per_org(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Service slug must be unique within an org per SPECS.md."""
|
||
|
|
org_id = await self._create_org(db_conn, "unique-slug-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org_id, "Service One", "my-service")
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.UniqueViolationError):
|
||
|
|
await repo.create(uuid4(), org_id, "Service Two", "my-service")
|
||
|
|
|
||
|
|
async def test_same_slug_allowed_in_different_orgs(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""Same slug can exist in different orgs."""
|
||
|
|
org1 = await self._create_org(db_conn, "org-one")
|
||
|
|
org2 = await self._create_org(db_conn, "org-two")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
slug = "shared-slug"
|
||
|
|
|
||
|
|
# Both should succeed
|
||
|
|
result1 = await repo.create(uuid4(), org1, "Service Org1", slug)
|
||
|
|
result2 = await repo.create(uuid4(), org2, "Service Org2", slug)
|
||
|
|
|
||
|
|
assert result1["slug"] == slug
|
||
|
|
assert result2["slug"] == slug
|
||
|
|
assert result1["org_id"] != result2["org_id"]
|
||
|
|
|
||
|
|
async def test_get_by_id_returns_service(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_id returns the correct service."""
|
||
|
|
org_id = await self._create_org(db_conn, "get-service-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
service_id = uuid4()
|
||
|
|
|
||
|
|
await repo.create(service_id, org_id, "My Service", "my-service")
|
||
|
|
result = await repo.get_by_id(service_id)
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["id"] == service_id
|
||
|
|
assert result["name"] == "My Service"
|
||
|
|
|
||
|
|
async def test_get_by_id_returns_none_for_nonexistent(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_id returns None for non-existent service."""
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_id(uuid4())
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
async def test_get_by_org_returns_all_org_services(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_org returns all services for an organization."""
|
||
|
|
org_id = await self._create_org(db_conn, "multi-service-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org_id, "Service A", "service-a")
|
||
|
|
await repo.create(uuid4(), org_id, "Service B", "service-b")
|
||
|
|
await repo.create(uuid4(), org_id, "Service C", "service-c")
|
||
|
|
|
||
|
|
result = await repo.get_by_org(org_id)
|
||
|
|
|
||
|
|
assert len(result) == 3
|
||
|
|
names = {s["name"] for s in result}
|
||
|
|
assert names == {"Service A", "Service B", "Service C"}
|
||
|
|
|
||
|
|
async def test_get_by_org_returns_empty_for_no_services(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_org returns empty list for org with no services."""
|
||
|
|
org_id = await self._create_org(db_conn, "empty-service-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_org(org_id)
|
||
|
|
|
||
|
|
assert result == []
|
||
|
|
|
||
|
|
async def test_get_by_org_only_returns_own_services(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_org doesn't return services from other orgs (tenant isolation)."""
|
||
|
|
org1 = await self._create_org(db_conn, "isolated-org-1")
|
||
|
|
org2 = await self._create_org(db_conn, "isolated-org-2")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org1, "Org1 Service", "org1-service")
|
||
|
|
await repo.create(uuid4(), org2, "Org2 Service", "org2-service")
|
||
|
|
|
||
|
|
result = await repo.get_by_org(org1)
|
||
|
|
|
||
|
|
assert len(result) == 1
|
||
|
|
assert result[0]["name"] == "Org1 Service"
|
||
|
|
|
||
|
|
async def test_get_by_slug_returns_service(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_slug returns service by org and slug."""
|
||
|
|
org_id = await self._create_org(db_conn, "slug-lookup-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
service_id = uuid4()
|
||
|
|
|
||
|
|
await repo.create(service_id, org_id, "Slug Service", "slug-service")
|
||
|
|
result = await repo.get_by_slug(org_id, "slug-service")
|
||
|
|
|
||
|
|
assert result is not None
|
||
|
|
assert result["id"] == service_id
|
||
|
|
|
||
|
|
async def test_get_by_slug_returns_none_for_wrong_org(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_slug returns None if slug exists but in different org."""
|
||
|
|
org1 = await self._create_org(db_conn, "slug-org-1")
|
||
|
|
org2 = await self._create_org(db_conn, "slug-org-2")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org1, "Service", "the-slug")
|
||
|
|
result = await repo.get_by_slug(org2, "the-slug")
|
||
|
|
|
||
|
|
assert result is None
|
||
|
|
|
||
|
|
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."""
|
||
|
|
org_id = await self._create_org(db_conn, "no-slug-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.get_by_slug(org_id, "nonexistent")
|
||
|
|
|
||
|
|
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 in org."""
|
||
|
|
org_id = await self._create_org(db_conn, "exists-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org_id, "Exists Service", "exists-slug")
|
||
|
|
result = await repo.slug_exists(org_id, "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 in org."""
|
||
|
|
org_id = await self._create_org(db_conn, "not-exists-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
result = await repo.slug_exists(org_id, "no-such-slug")
|
||
|
|
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
async def test_slug_exists_returns_false_for_other_org(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""slug_exists returns False for slug in different org."""
|
||
|
|
org1 = await self._create_org(db_conn, "other-org-1")
|
||
|
|
org2 = await self._create_org(db_conn, "other-org-2")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org1, "Service", "cross-org-slug")
|
||
|
|
result = await repo.slug_exists(org2, "cross-org-slug")
|
||
|
|
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
async def test_service_requires_valid_org_foreign_key(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""services.org_id must reference existing org."""
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
with pytest.raises(asyncpg.ForeignKeyViolationError):
|
||
|
|
await repo.create(uuid4(), uuid4(), "Orphan Service", "orphan")
|
||
|
|
|
||
|
|
async def test_get_by_org_orders_by_name(self, db_conn: asyncpg.Connection) -> None:
|
||
|
|
"""get_by_org returns services ordered by name."""
|
||
|
|
org_id = await self._create_org(db_conn, "ordered-org")
|
||
|
|
repo = ServiceRepository(db_conn)
|
||
|
|
|
||
|
|
await repo.create(uuid4(), org_id, "Zebra", "zebra")
|
||
|
|
await repo.create(uuid4(), org_id, "Alpha", "alpha")
|
||
|
|
await repo.create(uuid4(), org_id, "Middle", "middle")
|
||
|
|
|
||
|
|
result = await repo.get_by_org(org_id)
|
||
|
|
|
||
|
|
names = [s["name"] for s in result]
|
||
|
|
assert names == ["Alpha", "Middle", "Zebra"]
|