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:
Kyle Dammann
2026-10-30 22:06:35 +01:00
co-authored by Claude Opus 5.5
parent 74e11845de
commit 3b72436119
6 changed files with 84 additions and 1 deletions
+76
View File
@@ -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)