96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
import os
|
|
import tempfile
|
|
import unittest
|
|
from uuid import UUID
|
|
from unittest.mock import patch
|
|
|
|
from fastapi import HTTPException
|
|
|
|
# Importing the API initializes its SQLite schema. Keep this test process isolated
|
|
# from the real local conversation database.
|
|
_TEST_DATA_DIR = tempfile.TemporaryDirectory(prefix="sovereignai-api-tests-")
|
|
os.environ["SOVEREIGNAI_DATA_DIR"] = _TEST_DATA_DIR.name
|
|
|
|
from app.main import app
|
|
from tests.api_client import authenticated_client
|
|
|
|
|
|
class ApiErrorContractTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.client = authenticated_client(app)
|
|
|
|
def test_health_response_has_correlation_id(self) -> None:
|
|
response = self.client.get("/health")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
UUID(response.headers["x-request-id"])
|
|
self.assertEqual(response.json()["status"], "ok")
|
|
|
|
def test_not_found_uses_normalized_error_contract(self) -> None:
|
|
response = self.client.get("/no-such-route")
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
self.assertEqual(response.json()["error"]["code"], "not_found")
|
|
self.assertEqual(response.json()["request_id"], response.headers["x-request-id"])
|
|
|
|
def test_validation_error_is_normalized_and_does_not_echo_input(self) -> None:
|
|
sentinel = "sensitive-validation-input"
|
|
response = self.client.post(
|
|
"/v1/chat/completions", json={"messages": sentinel}
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 422)
|
|
payload = response.json()
|
|
self.assertEqual(payload["error"]["code"], "invalid_request")
|
|
self.assertEqual(payload["request_id"], response.headers["x-request-id"])
|
|
self.assertNotIn(sentinel, response.text)
|
|
self.assertNotIn("input", payload["detail"][0])
|
|
|
|
def test_upstream_timeout_uses_gateway_timeout_contract(self) -> None:
|
|
with patch(
|
|
"app.main.get_completion",
|
|
side_effect=HTTPException(
|
|
status_code=504, detail="انتهت مهلة انتظار خادم النموذج المحلي."
|
|
),
|
|
):
|
|
response = self.client.post(
|
|
"/v1/chat/completions",
|
|
json={"messages": [{"role": "user", "content": "مرحبا"}]},
|
|
)
|
|
|
|
self.assertEqual(response.status_code, 504)
|
|
self.assertEqual(response.json()["error"]["code"], "upstream_timeout")
|
|
self.assertEqual(response.json()["request_id"], response.headers["x-request-id"])
|
|
|
|
def test_model_list_exposes_verified_capabilities(self) -> None:
|
|
class MetadataProvider:
|
|
name = "test"
|
|
default_model = "vision-test"
|
|
base_url = "http://local"
|
|
|
|
async def list_models(self):
|
|
return ["vision-test", "unknown-test"]
|
|
|
|
async def describe_models(self, model_names):
|
|
return {
|
|
"vision-test": {
|
|
"verified": True,
|
|
"capabilities": ["completion", "vision"],
|
|
},
|
|
"unknown-test": {"verified": False, "capabilities": []},
|
|
}
|
|
|
|
with patch("app.main.get_model_provider", return_value=MetadataProvider()):
|
|
response = self.client.get("/v1/models")
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
models = {item["id"]: item for item in response.json()["data"]}
|
|
self.assertEqual(models["vision-test"]["capabilities"], ["completion", "vision"])
|
|
self.assertTrue(models["vision-test"]["capabilities_verified"])
|
|
self.assertFalse(models["unknown-test"]["capabilities_verified"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|