Add OPENAI_BASE_URL and end-to-end AI tests through the SDK
- Optional OPENAI_BASE_URL for OpenAI-compatible endpoints or proxies - Tests run the real openai SDK against a local stub server: request shape (model, auth, strict json_schema), text cleanup, photo import and an authentication error mapped to the UI message Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
74e11845de
commit
3b72436119
@@ -313,3 +313,79 @@ def test_refusal_and_truncation(monkeypatch):
|
||||
monkeypatch.setattr(ai, "_client", lambda: StubClient(content='{"is_re', finish_reason="length"))
|
||||
with pytest.raises(ai.AIError, match="zu lang"):
|
||||
ai._create_completion([])
|
||||
|
||||
|
||||
# --- End to end through the real OpenAI SDK against a local stub server --------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_openai_server(settings):
|
||||
"""A local HTTP server that speaks enough of the Chat Completions API for one request."""
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
received = {}
|
||||
reply = {"status": 200, "body": None}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
received["path"] = self.path
|
||||
received["auth"] = self.headers.get("Authorization")
|
||||
received["body"] = json.loads(self.rfile.read(length))
|
||||
body = reply["body"] or {
|
||||
"id": "chatcmpl-test", "object": "chat.completion", "created": 0, "model": received["body"]["model"],
|
||||
"choices": [{"index": 0, "finish_reason": "stop",
|
||||
"message": {"role": "assistant", "content": ai_answer(), "refusal": None}}],
|
||||
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
}
|
||||
data = json.dumps(body).encode()
|
||||
self.send_response(reply["status"])
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 0), Handler)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
settings.OPENAI_API_KEY = "sk-test-local"
|
||||
settings.OPENAI_BASE_URL = f"http://127.0.0.1:{server.server_port}/v1"
|
||||
settings.OPENAI_MODEL = "stub-model"
|
||||
yield received, reply
|
||||
server.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_end_to_end_ai_cleanup_through_sdk(stub_openai_server, user_client):
|
||||
received, _ = stub_openai_server
|
||||
response = user_client.post(reverse("recipes:create"), form_data(ai="1"))
|
||||
assert response.status_code == 200
|
||||
assert response.context["form"].initial["title"] == "Linsen-Dal"
|
||||
assert received["path"] == "/v1/chat/completions"
|
||||
assert received["auth"] == "Bearer sk-test-local"
|
||||
body = received["body"]
|
||||
assert body["model"] == "stub-model"
|
||||
assert body["response_format"]["json_schema"]["strict"] is True
|
||||
assert body["messages"][0]["role"] == "system"
|
||||
assert not Recipe.objects.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_end_to_end_api_error_through_sdk(stub_openai_server, user_client):
|
||||
_, reply = stub_openai_server
|
||||
reply["status"] = 401
|
||||
reply["body"] = {"error": {"message": "Incorrect API key", "type": "invalid_request_error", "code": "invalid_api_key"}}
|
||||
response = user_client.post(reverse("recipes:create"), form_data(ai="1"))
|
||||
assert "API-Key ist ungültig" in response.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_end_to_end_photo_import_through_sdk(stub_openai_server, user_client):
|
||||
received, _ = stub_openai_server
|
||||
response = user_client.post(reverse("recipes:import_photo"), {"photos": [image_upload()]})
|
||||
assert response.context["form"].initial["title"] == "Linsen-Dal"
|
||||
parts = received["body"]["messages"][1]["content"]
|
||||
assert any(p["type"] == "image_url" and p["image_url"]["url"].startswith("data:image/jpeg;base64,") for p in parts)
|
||||
|
||||
Reference in New Issue
Block a user