fix: require api auth in server factory
This commit is contained in:
@@ -414,11 +414,11 @@ def create_app(
|
|||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def auth_middleware(request: web.Request, handler) -> web.StreamResponse:
|
async def auth_middleware(request: web.Request, handler) -> web.StreamResponse:
|
||||||
if not api_key:
|
|
||||||
return await handler(request)
|
|
||||||
# Allow unauthenticated health checks.
|
# Allow unauthenticated health checks.
|
||||||
if request.path == "/health":
|
if request.path == "/health":
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
if not api_key:
|
||||||
|
return _error_json(401, "API key is not configured")
|
||||||
auth = request.headers.get("Authorization", "")
|
auth = request.headers.get("Authorization", "")
|
||||||
if not auth.startswith("Bearer "):
|
if not auth.startswith("Bearer "):
|
||||||
return _error_json(401, "Missing Authorization header. Use: Bearer <api_key>")
|
return _error_json(401, "Missing Authorization header. Use: Bearer <api_key>")
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ except ImportError:
|
|||||||
|
|
||||||
pytest_plugins = ("pytest_asyncio",)
|
pytest_plugins = ("pytest_asyncio",)
|
||||||
|
|
||||||
|
API_KEY = "secret"
|
||||||
|
AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}
|
||||||
|
|
||||||
|
|
||||||
def _make_mock_agent(response_text: str = "mock response") -> MagicMock:
|
def _make_mock_agent(response_text: str = "mock response") -> MagicMock:
|
||||||
agent = MagicMock()
|
agent = MagicMock()
|
||||||
@@ -43,7 +46,7 @@ def mock_agent():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app(mock_agent):
|
def app(mock_agent):
|
||||||
return create_app(mock_agent, model_name="test-model", request_timeout=10.0)
|
return create_app(mock_agent, model_name="test-model", request_timeout=10.0, api_key=API_KEY)
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
@@ -192,7 +195,7 @@ async def test_multipart_upload_saves_file(aiohttp_client, mock_agent, tmp_path)
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
file_data = b"test file content"
|
file_data = b"test file content"
|
||||||
@@ -200,6 +203,7 @@ async def test_multipart_upload_saves_file(aiohttp_client, mock_agent, tmp_path)
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
data={"message": "analyze this", "files": data},
|
data={"message": "analyze this", "files": data},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -219,7 +223,7 @@ async def test_multipart_multiple_files(aiohttp_client, mock_agent, tmp_path) ->
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
# Note: aiohttp test client has limited multipart support
|
# Note: aiohttp test client has limited multipart support
|
||||||
@@ -229,6 +233,7 @@ async def test_multipart_multiple_files(aiohttp_client, mock_agent, tmp_path) ->
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
data={"message": "analyze", "files": data},
|
data={"message": "analyze", "files": data},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -245,7 +250,7 @@ async def test_multipart_file_size_limit(aiohttp_client, mock_agent, tmp_path) -
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
# Create a file larger than 10MB
|
# Create a file larger than 10MB
|
||||||
@@ -254,6 +259,7 @@ async def test_multipart_file_size_limit(aiohttp_client, mock_agent, tmp_path) -
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
data={"message": "analyze", "files": data},
|
data={"message": "analyze", "files": data},
|
||||||
)
|
)
|
||||||
assert resp.status == 413
|
assert resp.status == 413
|
||||||
@@ -270,7 +276,7 @@ async def test_multipart_defaults_text_when_missing(aiohttp_client, mock_agent,
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
file_data = b"content"
|
file_data = b"content"
|
||||||
@@ -278,6 +284,7 @@ async def test_multipart_defaults_text_when_missing(aiohttp_client, mock_agent,
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
data={"files": data},
|
data={"files": data},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -296,7 +303,7 @@ async def test_multipart_with_session_id(aiohttp_client, mock_agent, tmp_path) -
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
file_data = b"content"
|
file_data = b"content"
|
||||||
@@ -304,6 +311,7 @@ async def test_multipart_with_session_id(aiohttp_client, mock_agent, tmp_path) -
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
data={"message": "hello", "session_id": "my-session", "files": data},
|
data={"message": "hello", "session_id": "my-session", "files": data},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -321,10 +329,11 @@ async def test_multipart_with_session_id(aiohttp_client, mock_agent, tmp_path) -
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_plain_text_backward_compat(aiohttp_client, mock_agent) -> None:
|
async def test_plain_text_backward_compat(aiohttp_client, mock_agent) -> None:
|
||||||
"""Plain text JSON request (no media) works as before."""
|
"""Plain text JSON request (no media) works as before."""
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hello world"}]},
|
json={"messages": [{"role": "user", "content": "hello world"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -344,7 +353,7 @@ async def test_json_base64_image_upload(aiohttp_client, mock_agent, tmp_path) ->
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
# Use valid base64 for a tiny PNG (1x1 transparent pixel)
|
# Use valid base64 for a tiny PNG (1x1 transparent pixel)
|
||||||
@@ -352,6 +361,7 @@ async def test_json_base64_image_upload(aiohttp_client, mock_agent, tmp_path) ->
|
|||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={
|
json={
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
@@ -471,7 +481,7 @@ async def test_docx_upload_passes_media_path(aiohttp_client, tmp_path) -> None:
|
|||||||
os.chdir(tmp_path)
|
os.chdir(tmp_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
from docx import Document
|
from docx import Document
|
||||||
@@ -486,7 +496,7 @@ async def test_docx_upload_passes_media_path(aiohttp_client, tmp_path) -> None:
|
|||||||
data.add_field("files", buf.getvalue(), filename="report.docx",
|
data.add_field("files", buf.getvalue(), filename="report.docx",
|
||||||
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
||||||
|
|
||||||
resp = await client.post("/v1/chat/completions", data=data)
|
resp = await client.post("/v1/chat/completions", headers=AUTH_HEADERS, data=data)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
call_kwargs = agent.process_direct.call_args.kwargs
|
call_kwargs = agent.process_direct.call_args.kwargs
|
||||||
assert call_kwargs["content"] == "summarize the report"
|
assert call_kwargs["content"] == "summarize the report"
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ except ImportError:
|
|||||||
|
|
||||||
pytest_plugins = ("pytest_asyncio",)
|
pytest_plugins = ("pytest_asyncio",)
|
||||||
|
|
||||||
|
API_KEY = "secret"
|
||||||
|
AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Unit tests for SSE helpers
|
# Unit tests for SSE helpers
|
||||||
@@ -101,11 +104,12 @@ async def aiohttp_client():
|
|||||||
async def test_stream_true_returns_sse(aiohttp_client) -> None:
|
async def test_stream_true_returns_sse(aiohttp_client) -> None:
|
||||||
"""stream=true should return text/event-stream with SSE chunks."""
|
"""stream=true should return text/event-stream with SSE chunks."""
|
||||||
agent = _make_streaming_agent(["Hello", " world"])
|
agent = _make_streaming_agent(["Hello", " world"])
|
||||||
app = create_app(agent, model_name="test-model")
|
app = create_app(agent, model_name="test-model", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -136,11 +140,12 @@ async def test_stream_false_returns_json(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": False},
|
json={"messages": [{"role": "user", "content": "hi"}], "stream": False},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -159,11 +164,12 @@ async def test_stream_default_is_false(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hi"}]},
|
json={"messages": [{"role": "user", "content": "hi"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -176,11 +182,12 @@ async def test_stream_default_is_false(aiohttp_client) -> None:
|
|||||||
async def test_stream_sse_chunk_ids_are_consistent(aiohttp_client) -> None:
|
async def test_stream_sse_chunk_ids_are_consistent(aiohttp_client) -> None:
|
||||||
"""All SSE chunks in a single stream should share the same id."""
|
"""All SSE chunks in a single stream should share the same id."""
|
||||||
agent = _make_streaming_agent(["A", "B", "C"])
|
agent = _make_streaming_agent(["A", "B", "C"])
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "go"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "go"}], "stream": True},
|
||||||
)
|
)
|
||||||
body = await resp.text()
|
body = await resp.text()
|
||||||
@@ -214,11 +221,12 @@ async def test_stream_passes_on_stream_callbacks(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -247,11 +255,12 @@ async def test_stream_segment_end_does_not_close_sse(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "use a tool"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "use a tool"}], "stream": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -286,11 +295,12 @@ async def test_stream_uses_final_response_when_no_deltas(aiohttp_client) -> None
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -328,11 +338,12 @@ async def test_stream_with_session_id(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={
|
json={
|
||||||
"messages": [{"role": "user", "content": "hi"}],
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
"stream": True,
|
"stream": True,
|
||||||
@@ -357,11 +368,12 @@ async def test_streaming_backend_failure_does_not_emit_success_terminator(aiohtt
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+47
-13
@@ -27,6 +27,9 @@ except ImportError:
|
|||||||
|
|
||||||
pytest_plugins = ("pytest_asyncio",)
|
pytest_plugins = ("pytest_asyncio",)
|
||||||
|
|
||||||
|
API_KEY = "secret"
|
||||||
|
AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}
|
||||||
|
|
||||||
|
|
||||||
def _make_mock_agent(response_text: str = "mock response") -> MagicMock:
|
def _make_mock_agent(response_text: str = "mock response") -> MagicMock:
|
||||||
agent = MagicMock()
|
agent = MagicMock()
|
||||||
@@ -44,7 +47,7 @@ def mock_agent():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app(mock_agent):
|
def app(mock_agent):
|
||||||
return create_app(mock_agent, model_name="test-model", request_timeout=10.0)
|
return create_app(mock_agent, model_name="test-model", request_timeout=10.0, api_key=API_KEY)
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
@@ -104,20 +107,20 @@ def test_chat_completion_response_preserves_provider_total_usage() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_missing_messages_returns_400(aiohttp_client, app) -> None:
|
async def test_missing_messages_returns_400(aiohttp_client, app) -> None:
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post("/v1/chat/completions", json={"model": "test"})
|
resp = await client.post("/v1/chat/completions", headers=AUTH_HEADERS, json={"model": "test"})
|
||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_api_key_protects_api_routes_but_not_health(aiohttp_client, mock_agent) -> None:
|
async def test_api_key_protects_api_routes_but_not_health(aiohttp_client, mock_agent) -> None:
|
||||||
app = create_app(mock_agent, model_name="test-model", api_key="secret")
|
app = create_app(mock_agent, model_name="test-model", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
health = await client.get("/health")
|
health = await client.get("/health")
|
||||||
missing = await client.get("/v1/models")
|
missing = await client.get("/v1/models")
|
||||||
wrong = await client.get("/v1/models", headers={"Authorization": "Bearer wrong"})
|
wrong = await client.get("/v1/models", headers={"Authorization": "Bearer wrong"})
|
||||||
ok = await client.get("/v1/models", headers={"Authorization": "Bearer secret"})
|
ok = await client.get("/v1/models", headers=AUTH_HEADERS)
|
||||||
|
|
||||||
assert health.status == 200
|
assert health.status == 200
|
||||||
assert missing.status == 401
|
assert missing.status == 401
|
||||||
@@ -127,12 +130,33 @@ async def test_api_key_protects_api_routes_but_not_health(aiohttp_client, mock_a
|
|||||||
assert (await wrong.json())["error"]["message"] == "Invalid API key"
|
assert (await wrong.json())["error"]["message"] == "Invalid API key"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_routes_fail_closed_without_configured_api_key(aiohttp_client, mock_agent) -> None:
|
||||||
|
app = create_app(mock_agent, model_name="test-model")
|
||||||
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
|
health = await client.get("/health")
|
||||||
|
models = await client.get("/v1/models")
|
||||||
|
chat = await client.post(
|
||||||
|
"/v1/chat/completions",
|
||||||
|
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert health.status == 200
|
||||||
|
assert models.status == 401
|
||||||
|
assert chat.status == 401
|
||||||
|
assert (await models.json())["error"]["message"] == "API key is not configured"
|
||||||
|
mock_agent.process_direct.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_no_user_message_returns_400(aiohttp_client, app) -> None:
|
async def test_no_user_message_returns_400(aiohttp_client, app) -> None:
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "system", "content": "you are a bot"}]},
|
json={"messages": [{"role": "system", "content": "you are a bot"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 400
|
assert resp.status == 400
|
||||||
@@ -144,6 +168,7 @@ async def test_stream_true_returns_sse(aiohttp_client, app) -> None:
|
|||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hello"}], "stream": True},
|
json={"messages": [{"role": "user", "content": "hello"}], "stream": True},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -220,10 +245,11 @@ async def test_single_user_message_must_have_user_role() -> None:
|
|||||||
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_agent) -> None:
|
async def test_successful_request_uses_fixed_api_session(aiohttp_client, mock_agent) -> None:
|
||||||
app = create_app(mock_agent, model_name="test-model")
|
app = create_app(mock_agent, model_name="test-model", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -254,15 +280,17 @@ async def test_followup_requests_share_same_session_key(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
r1 = await client.post(
|
r1 = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "first"}]},
|
json={"messages": [{"role": "user", "content": "first"}]},
|
||||||
)
|
)
|
||||||
r2 = await client.post(
|
r2 = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "second"}]},
|
json={"messages": [{"role": "user", "content": "second"}]},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,12 +320,13 @@ async def test_fixed_session_requests_are_serialized(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
|
|
||||||
async def send(msg: str):
|
async def send(msg: str):
|
||||||
return await client.post(
|
return await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": msg}]},
|
json={"messages": [{"role": "user", "content": msg}]},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -318,7 +347,7 @@ async def test_fixed_session_requests_are_serialized(aiohttp_client) -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_models_endpoint(aiohttp_client, app) -> None:
|
async def test_models_endpoint(aiohttp_client, app) -> None:
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.get("/v1/models")
|
resp = await client.get("/v1/models", headers=AUTH_HEADERS)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
body = await resp.json()
|
body = await resp.json()
|
||||||
assert body["object"] == "list"
|
assert body["object"] == "list"
|
||||||
@@ -338,10 +367,11 @@ async def test_health_endpoint(aiohttp_client, app) -> None:
|
|||||||
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_multimodal_content_extracts_text(aiohttp_client, mock_agent) -> None:
|
async def test_multimodal_content_extracts_text(aiohttp_client, mock_agent) -> None:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={
|
json={
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
@@ -366,10 +396,11 @@ async def test_multimodal_content_extracts_text(aiohttp_client, mock_agent) -> N
|
|||||||
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_multimodal_remote_image_url_returns_400(aiohttp_client, mock_agent) -> None:
|
async def test_multimodal_remote_image_url_returns_400(aiohttp_client, mock_agent) -> None:
|
||||||
app = create_app(mock_agent, model_name="m")
|
app = create_app(mock_agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={
|
json={
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
@@ -407,10 +438,11 @@ async def test_empty_response_retry_then_success(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -434,10 +466,11 @@ async def test_empty_response_retry_does_not_duplicate_user_turn(aiohttp_client)
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
@@ -463,10 +496,11 @@ async def test_empty_response_falls_back(aiohttp_client) -> None:
|
|||||||
agent.close_mcp = AsyncMock()
|
agent.close_mcp = AsyncMock()
|
||||||
agent._last_usage = {}
|
agent._last_usage = {}
|
||||||
|
|
||||||
app = create_app(agent, model_name="m")
|
app = create_app(agent, model_name="m", api_key=API_KEY)
|
||||||
client = await aiohttp_client(app)
|
client = await aiohttp_client(app)
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
"/v1/chat/completions",
|
"/v1/chat/completions",
|
||||||
|
headers=AUTH_HEADERS,
|
||||||
json={"messages": [{"role": "user", "content": "hello"}]},
|
json={"messages": [{"role": "user", "content": "hello"}]},
|
||||||
)
|
)
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
|
|||||||
Reference in New Issue
Block a user