fischer-agentkit/tests/unit/calendar/test_routes.py

271 lines
8.6 KiB
Python

"""Tests for calendar REST API routes (U2)."""
from __future__ import annotations
import asyncio
import uuid
from pathlib import Path
from typing import Any
import aiosqlite
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from agentkit.calendar.db import init_calendar_db
from agentkit.calendar.service import CalendarService
from agentkit.server.auth.dependencies import require_authenticated
from agentkit.server.auth.models import init_auth_db
from agentkit.server.routes import calendar as calendar_routes
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
TEST_USER_ID = "test-user-id"
TEST_USER_EMAIL = "testuser@example.com"
def _make_test_user() -> dict[str, Any]:
return {
"user_id": TEST_USER_ID,
"username": "testuser",
"role": "member",
}
async def _seed_auth_user(auth_db_path: Path) -> None:
"""Seed the test user into the auth DB so get_user_email works."""
async with aiosqlite.connect(str(auth_db_path)) as db:
await db.execute(
"INSERT INTO users (id, username, email, password_hash, role, is_active, "
"is_terminal_authorized, is_server_terminal_authorized, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
TEST_USER_ID,
"testuser",
TEST_USER_EMAIL,
"fake-hash",
"member",
1,
0,
0,
"2026-01-01T00:00:00+00:00",
"2026-01-01T00:00:00+00:00",
),
)
await db.commit()
async def _seed_searchable_users(auth_db_path: Path) -> None:
"""Seed extra users for search tests."""
for name in ("alice", "bob", "charlie"):
async with aiosqlite.connect(str(auth_db_path)) as db:
await db.execute(
"INSERT INTO users (id, username, email, password_hash, role, is_active, "
"is_terminal_authorized, is_server_terminal_authorized, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
uuid.uuid4().hex,
name,
f"{name}@example.com",
"fake-hash",
"member",
1,
0,
0,
"2026-01-01T00:00:00+00:00",
"2026-01-01T00:00:00+00:00",
),
)
await db.commit()
@pytest.fixture
def calendar_db_path(tmp_path: Path) -> Path:
path = tmp_path / "test_calendar.db"
asyncio.run(init_calendar_db(path))
return path
@pytest.fixture
def auth_db_path(tmp_path: Path) -> Path:
path = tmp_path / "test_auth.db"
asyncio.run(init_auth_db(path))
asyncio.run(_seed_auth_user(path))
return path
@pytest.fixture
def app(calendar_db_path: Path, auth_db_path: Path) -> FastAPI:
"""Create a test app with CalendarService and mock auth."""
service = CalendarService(db_path=calendar_db_path, auth_db_path=auth_db_path)
app = FastAPI()
app.state.calendar_service = service
app.state.auth_db_path = str(auth_db_path)
app.include_router(calendar_routes.router, prefix="/api/v1")
# Override auth dependency to return a test user
app.dependency_overrides[require_authenticated] = lambda: _make_test_user()
return app
@pytest.fixture
def client(app: FastAPI) -> TestClient:
return TestClient(app)
@pytest.fixture
def unauth_app(calendar_db_path: Path, auth_db_path: Path) -> FastAPI:
"""App without auth override — simulates unauthenticated requests."""
service = CalendarService(db_path=calendar_db_path, auth_db_path=auth_db_path)
app = FastAPI()
app.state.calendar_service = service
app.state.auth_db_path = str(auth_db_path)
app.include_router(calendar_routes.router, prefix="/api/v1")
# No dependency override → require_authenticated will see no current_user
return app
@pytest.fixture
def unauth_client(unauth_app: FastAPI) -> TestClient:
return TestClient(unauth_app)
# ---------------------------------------------------------------------------
# Auth requirement
# ---------------------------------------------------------------------------
def test_route_create_event_requires_auth(unauth_client: TestClient) -> None:
"""No auth → 401."""
resp = unauth_client.post(
"/api/v1/calendar/events",
json={
"title": "Test",
"start_time": "2026-07-01T10:00:00+00:00",
"end_time": "2026-07-01T11:00:00+00:00",
},
)
assert resp.status_code == 401
# ---------------------------------------------------------------------------
# Create event
# ---------------------------------------------------------------------------
def test_route_create_event_success(client: TestClient) -> None:
"""Create event via API returns 200 with event data."""
resp = client.post(
"/api/v1/calendar/events",
json={
"title": "Sprint Planning",
"start_time": "2026-07-01T10:00:00+00:00",
"end_time": "2026-07-01T11:00:00+00:00",
"description": "Bi-weekly sprint planning",
"location": "Room A",
},
)
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
event = data["event"]
assert event["title"] == "Sprint Planning"
assert event["start_time"] == "2026-07-01T10:00:00+00:00"
assert event["description"] == "Bi-weekly sprint planning"
assert event["location"] == "Room A"
assert event["source"] == "manual"
assert "id" in event
# ---------------------------------------------------------------------------
# List events
# ---------------------------------------------------------------------------
def test_route_list_events_returns_events(client: TestClient) -> None:
"""Create events, list via API returns them."""
for i in range(3):
client.post(
"/api/v1/calendar/events",
json={
"title": f"Event {i}",
"start_time": f"2026-07-{i + 1:02d}T10:00:00+00:00",
"end_time": f"2026-07-{i + 1:02d}T11:00:00+00:00",
},
)
resp = client.get("/api/v1/calendar/events")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["count"] == 3
titles = {e["title"] for e in data["events"]}
assert titles == {"Event 0", "Event 1", "Event 2"}
# ---------------------------------------------------------------------------
# Tag filter via API (G2)
# ---------------------------------------------------------------------------
def test_route_list_events_filters_by_tag(client: TestClient) -> None:
"""G2 tag filter via API — only tagged events returned."""
# Create a tag first
tag_resp = client.post("/api/v1/calendar/tags", json={"name": "important"})
assert tag_resp.status_code == 200
tag_id = tag_resp.json()["tag"]["id"]
# Event 1: with tag
client.post(
"/api/v1/calendar/events",
json={
"title": "Tagged Event",
"start_time": "2026-07-01T10:00:00+00:00",
"end_time": "2026-07-01T11:00:00+00:00",
"tag_ids": [tag_id],
},
)
# Event 2: without tag
client.post(
"/api/v1/calendar/events",
json={
"title": "Untagged Event",
"start_time": "2026-07-02T10:00:00+00:00",
"end_time": "2026-07-02T11:00:00+00:00",
},
)
# List with tag filter
resp = client.get("/api/v1/calendar/events", params={"tag_id": tag_id})
assert resp.status_code == 200
data = resp.json()
assert data["count"] == 1
assert data["events"][0]["title"] == "Tagged Event"
# ---------------------------------------------------------------------------
# User search via API (G5)
# ---------------------------------------------------------------------------
def test_route_search_users(client: TestClient, auth_db_path: Path) -> None:
"""G5 user search via API returns matching users."""
asyncio.run(_seed_searchable_users(auth_db_path))
resp = client.get("/api/v1/calendar/users/search", params={"q": "ali"})
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["count"] == 1
assert data["users"][0]["username"] == "alice"
assert data["users"][0]["email"] == "alice@example.com"
# Must not expose sensitive fields
assert "id" not in data["users"][0]
assert "password_hash" not in data["users"][0]