Files
headquarter/apps/api/tests/integration/test_notifications_api.py
T
alex cbaebcf649 feat: notification center backend core (PR-1)
- Add notifications table with Alembic migration
- Notification model with user-scoped indexing and partial index on unread
- NotificationService singleton with create/list/count/mark-read/dismiss
- FastAPI router: GET /notifications, GET /unread, PATCH /{id}/read,
  POST /mark-all-read, DELETE /{id}
- Mute categories filtering from UserConfig
- 13 unit tests for NotificationService
- 10 integration tests for API endpoints
- Updated test_models.py with new table registration

Quality gates: pytest 23 new passed, ruff clean
2026-05-29 12:09:14 +02:00

327 lines
9.1 KiB
Python

"""Integration tests for notifications API."""
import uuid
from datetime import datetime, timedelta, timezone
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import AsyncSession
from src.models.user import User
from src.models.user_config import UserConfig
from src.services.notification_service import NotificationService
@pytest.fixture
def notification_service() -> NotificationService:
return NotificationService()
@pytest.fixture
async def user_a(db_session: AsyncSession) -> User:
user = User(
id=uuid.uuid4(),
email="user-a@headquarter.local",
name="User A",
authentik_id=f"authentik-{uuid.uuid4()}",
avatar_url=None,
)
db_session.add(user)
await db_session.commit()
return user
@pytest.fixture
async def user_b(db_session: AsyncSession) -> User:
user = User(
id=uuid.uuid4(),
email="user-b@headquarter.local",
name="User B",
authentik_id=f"authentik-{uuid.uuid4()}",
avatar_url=None,
)
db_session.add(user)
await db_session.commit()
return user
def _mint_cookie_for_user(test_client: TestClient, user_id: uuid.UUID) -> None:
from src.auth.session import create_session_cookie
from src.config import Settings
settings = Settings()
cookie = create_session_cookie(
settings=settings,
user_id=str(user_id),
)
test_client.cookies.set("session", cookie)
@pytest.mark.integration
def test_list_requires_auth(test_client: TestClient) -> None:
response = test_client.get("/notifications")
assert response.status_code == 401
@pytest.mark.integration
def test_list_returns_only_own_notifications(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
user_b: User,
) -> None:
async def create_notifications() -> None:
await notification_service.create_notification(
db_session, user_a.id, category="instance", severity="info", title="A"
)
await notification_service.create_notification(
db_session, user_b.id, category="instance", severity="info", title="B"
)
import asyncio
asyncio.run(create_notifications())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.get("/notifications")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["items"][0]["title"] == "A"
@pytest.mark.integration
def test_list_pagination(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
) -> None:
async def create_many() -> None:
for i in range(25):
n = await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title=f"Notification {i}",
)
n.created_at = datetime.now(timezone.utc) - timedelta(seconds=i)
await db_session.commit()
await db_session.refresh(n)
import asyncio
asyncio.run(create_many())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.get("/notifications?limit=10&offset=10")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 10
assert data["total"] == 25
assert data["limit"] == 10
assert data["offset"] == 10
@pytest.mark.integration
def test_unread_count_endpoint(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
) -> None:
async def create_unread() -> None:
for _ in range(3):
await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title="Unread",
)
import asyncio
asyncio.run(create_unread())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.get("/notifications/unread")
assert response.status_code == 200
data = response.json()
assert data["count"] == 3
@pytest.mark.integration
def test_mark_read_endpoint(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
) -> None:
async def create_and_get() -> uuid.UUID:
n = await notification_service.create_notification(
db_session, user_a.id, category="instance", severity="info", title="To read"
)
return n.id
import asyncio
nid = asyncio.run(create_and_get())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.patch(f"/notifications/{nid}/read")
assert response.status_code == 200
data = response.json()
assert data["read_at"] is not None
@pytest.mark.integration
def test_mark_read_404_for_other_user(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
user_b: User,
) -> None:
async def create_and_get() -> uuid.UUID:
n = await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title="Owned by A",
)
return n.id
import asyncio
nid = asyncio.run(create_and_get())
_mint_cookie_for_user(authenticated_client, user_b.id)
response = authenticated_client.patch(f"/notifications/{nid}/read")
assert response.status_code == 404
@pytest.mark.integration
def test_mark_all_read_endpoint(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
) -> None:
async def create_unread() -> None:
for _ in range(4):
await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title="Unread",
)
import asyncio
asyncio.run(create_unread())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.post("/notifications/mark-all-read")
assert response.status_code == 200
data = response.json()
assert data["marked_count"] == 4
@pytest.mark.integration
def test_dismiss_endpoint(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
) -> None:
async def create_and_get() -> uuid.UUID:
n = await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title="To dismiss",
)
return n.id
import asyncio
nid = asyncio.run(create_and_get())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.delete(f"/notifications/{nid}")
assert response.status_code == 204
response = authenticated_client.get("/notifications")
data = response.json()
assert len(data["items"]) == 0
@pytest.mark.integration
def test_dismiss_404_for_other_user(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
user_b: User,
) -> None:
async def create_and_get() -> uuid.UUID:
n = await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title="Owned by A",
)
return n.id
import asyncio
nid = asyncio.run(create_and_get())
_mint_cookie_for_user(authenticated_client, user_b.id)
response = authenticated_client.delete(f"/notifications/{nid}")
assert response.status_code == 404
@pytest.mark.integration
def test_mute_categories_filter_in_list(
authenticated_client: TestClient,
db_session: AsyncSession,
notification_service: NotificationService,
user_a: User,
) -> None:
async def setup() -> None:
config = UserConfig(
user_id=user_a.id, config={"notification_mute_categories": ["instance"]}
)
db_session.add(config)
await db_session.commit()
await notification_service.create_notification(
db_session,
user_a.id,
category="instance",
severity="info",
title="Instance",
)
await notification_service.create_notification(
db_session, user_a.id, category="system", severity="info", title="System"
)
import asyncio
asyncio.run(setup())
_mint_cookie_for_user(authenticated_client, user_a.id)
response = authenticated_client.get("/notifications")
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["items"][0]["title"] == "System"