feat: add API key authentication and health endpoint
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
from fastapi import Header, HTTPException
|
||||||
|
|
||||||
|
from src.backend.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_api_key(x_api_key: str | None = Header(default=None)):
|
||||||
|
if x_api_key is None or x_api_key != settings.api_key:
|
||||||
|
raise HTTPException(status_code=403, detail="API key non valida")
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from src.backend.api.dependencies import verify_api_key
|
||||||
|
from src.backend.models.api import ok
|
||||||
|
|
||||||
|
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health", summary="Health check", tags=["system"])
|
||||||
|
async def health():
|
||||||
|
return ok({"status": "running"})
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from src.backend.api.routes.health import router as health_router
|
||||||
|
from src.backend.db.database import init_db
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
await init_db()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="OpusAgent", version="0.1.0", lifespan=lifespan)
|
||||||
|
|
||||||
|
app.include_router(health_router, prefix="/api")
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient, ASGITransport
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def app():
|
||||||
|
from src.backend.main import app
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client(app):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://test",
|
||||||
|
headers={"X-Api-Key": "test-key-12345"},
|
||||||
|
) as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def client_no_auth(app):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||||
|
yield ac
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_request_without_api_key_returns_403(client_no_auth):
|
||||||
|
resp = await client_no_auth.get("/api/health")
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_request_with_wrong_api_key_returns_403(app):
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(
|
||||||
|
transport=transport,
|
||||||
|
base_url="http://test",
|
||||||
|
headers={"X-Api-Key": "wrong-key"},
|
||||||
|
) as ac:
|
||||||
|
resp = await ac.get("/api/health")
|
||||||
|
assert resp.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_topic(client):
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/topics",
|
||||||
|
json={
|
||||||
|
"name": "coding",
|
||||||
|
"system_prompt": "You are an expert coder.",
|
||||||
|
"summary": "Coding topic",
|
||||||
|
"model": "claude-sonnet-4-6",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
data = resp.json()
|
||||||
|
assert data["success"] is True
|
||||||
|
assert data["data"]["name"] == "coding"
|
||||||
|
assert data["data"]["model"] == "claude-sonnet-4-6"
|
||||||
|
assert "id" in data["data"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_topic_minimal(client):
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/topics",
|
||||||
|
json={"name": "analysis", "system_prompt": "You analyze data."},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201
|
||||||
|
data = resp.json()
|
||||||
|
assert data["data"]["summary"] == ""
|
||||||
|
assert data["data"]["model"] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_topic_duplicate_name_fails(client):
|
||||||
|
await client.post(
|
||||||
|
"/api/topics",
|
||||||
|
json={"name": "coding", "system_prompt": "Prompt 1"},
|
||||||
|
)
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/topics",
|
||||||
|
json={"name": "coding", "system_prompt": "Prompt 2"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_topics(client):
|
||||||
|
await client.post(
|
||||||
|
"/api/topics", json={"name": "a", "system_prompt": "A"}
|
||||||
|
)
|
||||||
|
await client.post(
|
||||||
|
"/api/topics", json={"name": "b", "system_prompt": "B"}
|
||||||
|
)
|
||||||
|
resp = await client.get("/api/topics")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert len(data["data"]) == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_topic(client):
|
||||||
|
create_resp = await client.post(
|
||||||
|
"/api/topics", json={"name": "coding", "system_prompt": "Code"}
|
||||||
|
)
|
||||||
|
topic_id = create_resp.json()["data"]["id"]
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/topics/{topic_id}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["data"]["name"] == "coding"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_topic_not_found(client):
|
||||||
|
resp = await client.get("/api/topics/nonexistent-id")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_topic(client):
|
||||||
|
create_resp = await client.post(
|
||||||
|
"/api/topics", json={"name": "coding", "system_prompt": "Old"}
|
||||||
|
)
|
||||||
|
topic_id = create_resp.json()["data"]["id"]
|
||||||
|
|
||||||
|
resp = await client.put(
|
||||||
|
f"/api/topics/{topic_id}",
|
||||||
|
json={"system_prompt": "New prompt"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["data"]["system_prompt"] == "New prompt"
|
||||||
|
assert resp.json()["data"]["name"] == "coding"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_topic(client):
|
||||||
|
create_resp = await client.post(
|
||||||
|
"/api/topics", json={"name": "coding", "system_prompt": "Code"}
|
||||||
|
)
|
||||||
|
topic_id = create_resp.json()["data"]["id"]
|
||||||
|
|
||||||
|
resp = await client.delete(f"/api/topics/{topic_id}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
resp = await client.get(f"/api/topics/{topic_id}")
|
||||||
|
assert resp.status_code == 404
|
||||||
Reference in New Issue
Block a user