239 lines
7.0 KiB
Python
239 lines
7.0 KiB
Python
"""Integration tests for org endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID, uuid4
|
|
|
|
import asyncpg
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from app.core import security
|
|
from tests.api import helpers
|
|
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
API_PREFIX = "/v1/org"
|
|
|
|
|
|
async def _create_user_in_org(
|
|
conn: asyncpg.Connection,
|
|
*,
|
|
org_id: UUID,
|
|
email: str,
|
|
password: str,
|
|
role: str,
|
|
) -> UUID:
|
|
user_id = uuid4()
|
|
await conn.execute(
|
|
"INSERT INTO users (id, email, password_hash) VALUES ($1, $2, $3)",
|
|
user_id,
|
|
email,
|
|
security.hash_password(password),
|
|
)
|
|
await conn.execute(
|
|
"INSERT INTO org_members (id, user_id, org_id, role) VALUES ($1, $2, $3, $4)",
|
|
uuid4(),
|
|
user_id,
|
|
org_id,
|
|
role,
|
|
)
|
|
return user_id
|
|
|
|
|
|
async def _login(client: AsyncClient, *, email: str, password: str) -> dict:
|
|
response = await client.post(
|
|
"/v1/auth/login",
|
|
json={"email": email, "password": password},
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
|
|
async def test_get_org_returns_active_org(api_client: AsyncClient) -> None:
|
|
tokens = await helpers.register_user(
|
|
api_client,
|
|
email="org-owner@example.com",
|
|
password="OrgOwner1!",
|
|
org_name="Org Owner Inc",
|
|
)
|
|
|
|
response = await api_client.get(
|
|
API_PREFIX,
|
|
headers={"Authorization": f"Bearer {tokens['access_token']}",},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
payload = security.decode_access_token(tokens["access_token"])
|
|
assert data["id"] == payload["org_id"]
|
|
assert data["name"] == "Org Owner Inc"
|
|
|
|
|
|
async def test_get_members_requires_admin(
|
|
api_client: AsyncClient,
|
|
db_admin: asyncpg.Connection,
|
|
) -> None:
|
|
owner_tokens = await helpers.register_user(
|
|
api_client,
|
|
email="owner@example.com",
|
|
password="OwnerPass1!",
|
|
org_name="Members Co",
|
|
)
|
|
payload = security.decode_access_token(owner_tokens["access_token"])
|
|
org_id = UUID(payload["org_id"])
|
|
|
|
member_password = "MemberPass1!"
|
|
await _create_user_in_org(
|
|
db_admin,
|
|
org_id=org_id,
|
|
email="member@example.com",
|
|
password=member_password,
|
|
role="member",
|
|
)
|
|
member_tokens = await _login(api_client, email="member@example.com", password=member_password)
|
|
|
|
admin_response = await api_client.get(
|
|
f"{API_PREFIX}/members",
|
|
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
|
|
)
|
|
assert admin_response.status_code == 200
|
|
emails = {item["email"] for item in admin_response.json()}
|
|
assert emails == {"owner@example.com", "member@example.com"}
|
|
|
|
member_response = await api_client.get(
|
|
f"{API_PREFIX}/members",
|
|
headers={"Authorization": f"Bearer {member_tokens['access_token']}"},
|
|
)
|
|
assert member_response.status_code == 403
|
|
|
|
|
|
async def test_create_service_allows_member_and_persists(
|
|
api_client: AsyncClient,
|
|
db_admin: asyncpg.Connection,
|
|
) -> None:
|
|
owner_tokens = await helpers.register_user(
|
|
api_client,
|
|
email="service-owner@example.com",
|
|
password="ServiceOwner1!",
|
|
org_name="Service Org",
|
|
)
|
|
payload = security.decode_access_token(owner_tokens["access_token"])
|
|
org_id = UUID(payload["org_id"])
|
|
|
|
member_password = "CreateSvc1!"
|
|
await _create_user_in_org(
|
|
db_admin,
|
|
org_id=org_id,
|
|
email="svc-member@example.com",
|
|
password=member_password,
|
|
role="member",
|
|
)
|
|
member_tokens = await _login(api_client, email="svc-member@example.com", password=member_password)
|
|
|
|
response = await api_client.post(
|
|
f"{API_PREFIX}/services",
|
|
json={"name": "API Gateway", "slug": "api-gateway"},
|
|
headers={"Authorization": f"Bearer {member_tokens['access_token']}"},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
body = response.json()
|
|
row = await db_admin.fetchrow(
|
|
"SELECT org_id, slug FROM services WHERE id = $1",
|
|
UUID(body["id"]),
|
|
)
|
|
assert row is not None and row["org_id"] == org_id and row["slug"] == "api-gateway"
|
|
|
|
|
|
async def test_create_service_rejects_duplicate_slug(
|
|
api_client: AsyncClient,
|
|
db_admin: asyncpg.Connection,
|
|
) -> None:
|
|
tokens = await helpers.register_user(
|
|
api_client,
|
|
email="dup-owner@example.com",
|
|
password="DupOwner1!",
|
|
org_name="Dup Org",
|
|
)
|
|
payload = security.decode_access_token(tokens["access_token"])
|
|
org_id = UUID(payload["org_id"])
|
|
|
|
await db_admin.execute(
|
|
"INSERT INTO services (id, org_id, name, slug) VALUES ($1, $2, $3, $4)",
|
|
uuid4(),
|
|
org_id,
|
|
"Existing",
|
|
"duplicate",
|
|
)
|
|
|
|
response = await api_client.post(
|
|
f"{API_PREFIX}/services",
|
|
json={"name": "New", "slug": "duplicate"},
|
|
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
async def test_notification_targets_admin_only_and_validation(
|
|
api_client: AsyncClient,
|
|
db_admin: asyncpg.Connection,
|
|
) -> None:
|
|
owner_tokens = await helpers.register_user(
|
|
api_client,
|
|
email="notify-owner@example.com",
|
|
password="NotifyOwner1!",
|
|
org_name="Notify Org",
|
|
)
|
|
payload = security.decode_access_token(owner_tokens["access_token"])
|
|
org_id = UUID(payload["org_id"])
|
|
|
|
member_password = "NotifyMember1!"
|
|
await _create_user_in_org(
|
|
db_admin,
|
|
org_id=org_id,
|
|
email="notify-member@example.com",
|
|
password=member_password,
|
|
role="member",
|
|
)
|
|
member_tokens = await _login(api_client, email="notify-member@example.com", password=member_password)
|
|
|
|
forbidden = await api_client.post(
|
|
f"{API_PREFIX}/notification-targets",
|
|
json={"name": "Webhook", "target_type": "webhook", "webhook_url": "https://example.com"},
|
|
headers={"Authorization": f"Bearer {member_tokens['access_token']}"},
|
|
)
|
|
assert forbidden.status_code == 403
|
|
|
|
missing_url = await api_client.post(
|
|
f"{API_PREFIX}/notification-targets",
|
|
json={"name": "Bad", "target_type": "webhook"},
|
|
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
|
|
)
|
|
assert missing_url.status_code == 400
|
|
|
|
created = await api_client.post(
|
|
f"{API_PREFIX}/notification-targets",
|
|
json={"name": "Pager", "target_type": "webhook", "webhook_url": "https://example.com/hook"},
|
|
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
|
|
)
|
|
|
|
assert created.status_code == 201
|
|
target_id = UUID(created.json()["id"])
|
|
|
|
row = await db_admin.fetchrow(
|
|
"SELECT org_id, name FROM notification_targets WHERE id = $1",
|
|
target_id,
|
|
)
|
|
assert row is not None and row["org_id"] == org_id
|
|
|
|
listing = await api_client.get(
|
|
f"{API_PREFIX}/notification-targets",
|
|
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
|
|
)
|
|
assert listing.status_code == 200
|
|
names = [item["name"] for item in listing.json()]
|
|
assert names == ["Pager"]
|