271 lines
8.6 KiB
Python
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]
|