Skip to content

Commit 3768324

Browse files
committed
LCORE-628: OpenAPI integration tests
1 parent 36d0a94 commit 3768324

File tree

2 files changed

+182
-22
lines changed

2 files changed

+182
-22
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Lightspeed Core Service (LCS)
2+
service:
3+
host: localhost
4+
port: 8080
5+
auth_enabled: false
6+
workers: 1
7+
color_log: true
8+
access_log: true
9+
cors:
10+
allow_origins:
11+
- foo_origin
12+
- bar_origin
13+
- baz_origin
14+
allow_credentials: false
15+
allow_methods:
16+
- foo_method
17+
- bar_method
18+
- baz_method
19+
allow_headers:
20+
- foo_header
21+
- bar_header
22+
- baz_header
23+
llama_stack:
24+
# Uses a remote llama-stack service
25+
# The instance would have already been started with a llama-stack-run.yaml file
26+
use_as_library_client: false
27+
# Alternative for "as library use"
28+
# use_as_library_client: true
29+
# library_client_config_path: <path-to-llama-stack-run.yaml-file>
30+
url: http://localhost:8321
31+
api_key: xyzzy
32+
user_data_collection:
33+
feedback_enabled: true
34+
feedback_storage: "/tmp/data/feedback"
35+
mcp_servers:
36+
- name: "server1"
37+
provider_id: "provider1"
38+
url: "http://url.com:1"
39+
- name: "server2"
40+
provider_id: "provider2"
41+
url: "http://url.com:2"
42+
- name: "server3"
43+
provider_id: "provider3"
44+
url: "http://url.com:3"

tests/integration/test_openapi_json.py

Lines changed: 138 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
"""Tests the OpenAPI specification that is to be stored in docs/openapi.json."""
22

3+
from typing import Any
34
import json
45
from pathlib import Path
56

67
import pytest
8+
import requests
9+
10+
from fastapi.testclient import TestClient
11+
from configuration import configuration
712

813
# Strategy:
9-
# - Load the OpenAPI document from docs/openapi.json
14+
# - Load the OpenAPI document from docs/openapi.json and from endpoint handler
1015
# - Validate critical structure based on the PR diff:
1116
# * openapi version, info, servers
1217
# * presence of paths/methods and key response codes
1318
# * presence and key attributes of important component schemas (enums, required fields)
1419

1520
OPENAPI_FILE = "docs/openapi.json"
21+
URL = "/openapi.json"
1622

1723

18-
def _load_openapi_spec() -> dict:
24+
def _load_openapi_spec_from_file() -> dict[str, Any]:
1925
"""Load OpenAPI specification from configured path."""
2026
path = Path(OPENAPI_FILE)
2127
if path.is_file():
@@ -26,14 +32,38 @@ def _load_openapi_spec() -> dict:
2632
return {}
2733

2834

29-
@pytest.fixture(scope="module", name="spec")
30-
def open_api_spec() -> dict:
35+
def _load_openapi_spec_from_url() -> dict[str, Any]:
36+
"""Load OpenAPI specification from URL."""
37+
configuration_filename = "tests/configuration/lightspeed-stack-proper-name.yaml"
38+
cfg = configuration
39+
cfg.load_configuration(configuration_filename)
40+
from app.main import app # pylint: disable=C0415
41+
42+
client = TestClient(app)
43+
response = client.get("/openapi.json")
44+
assert response.status_code == requests.codes.ok # pylint: disable=no-member
45+
46+
# this line ensures that response payload contains proper JSON
47+
payload = response.json()
48+
assert payload is not None, "Incorrect response"
49+
50+
return payload
51+
52+
53+
@pytest.fixture(scope="module", name="spec_from_file")
54+
def open_api_spec_from_file() -> dict[str, Any]:
3155
"""Fixture containing OpenAPI specification represented as a dictionary."""
32-
return _load_openapi_spec()
56+
return _load_openapi_spec_from_file()
3357

3458

35-
def test_openapi_top_level_info(spec: dict) -> None:
36-
"""Test all top level informations stored in OpenAPI specification."""
59+
@pytest.fixture(scope="module", name="spec_from_url")
60+
def open_api_spec_from_url() -> dict[str, Any]:
61+
"""Fixture containing OpenAPI specification represented as a dictionary."""
62+
return _load_openapi_spec_from_url()
63+
64+
65+
def _check_openapi_top_level_info(spec: dict[str, Any]) -> None:
66+
"""Check all top level informations stored in OpenAPI specification."""
3767
assert spec.get("openapi") == "3.1.0"
3868

3969
info = spec.get("info") or {}
@@ -48,20 +78,59 @@ def test_openapi_top_level_info(spec: dict) -> None:
4878
assert "apache.org/licenses" in (license_info.get("url") or "")
4979

5080

51-
def test_servers_section_present(spec: dict) -> None:
52-
"""Test the servers section stored in OpenAPI specification."""
81+
def _check_server_section_present(spec: dict[str, Any]) -> None:
82+
"""Check if the servers section stored in OpenAPI specification."""
5383
servers = spec.get("servers")
5484
assert isinstance(servers, list) and servers, "servers must be a non-empty list"
5585

5686

87+
def _check_paths_and_responses_exist(
88+
spec: dict, path: str, method: str, expected_codes: set[str]
89+
) -> None:
90+
paths = spec.get("paths") or {}
91+
assert path in paths, f"Missing path: {path}"
92+
op = (paths[path] or {}).get(method)
93+
assert isinstance(op, dict), f"Missing method {method.upper()} for path {path}"
94+
responses = op.get("responses") or {}
95+
got_codes = set(responses.keys())
96+
for code in expected_codes:
97+
assert (
98+
code in got_codes
99+
), f"Missing response code {code} for {method.upper()} {path}"
100+
101+
102+
def test_openapi_top_level_info_from_file(spec_from_file: dict[str, Any]) -> None:
103+
"""Test all top level informations stored in OpenAPI specification."""
104+
_check_openapi_top_level_info(spec_from_file)
105+
106+
107+
def test_openapi_top_level_info_from_url(spec_from_url: dict[str, Any]) -> None:
108+
"""Test all top level informations stored in OpenAPI specification."""
109+
_check_openapi_top_level_info(spec_from_url)
110+
111+
112+
def test_servers_section_present_from_file(spec_from_file: dict[str, Any]) -> None:
113+
"""Test the servers section stored in OpenAPI specification."""
114+
_check_server_section_present(spec_from_file)
115+
116+
117+
def test_servers_section_present_from_url(spec_from_url: dict[str, Any]) -> None:
118+
"""Test the servers section stored in OpenAPI specification."""
119+
_check_server_section_present(spec_from_url)
120+
121+
57122
@pytest.mark.parametrize(
58123
"path,method,expected_codes",
59124
[
60125
("/", "get", {"200"}),
61126
("/v1/info", "get", {"200", "500"}),
62127
("/v1/models", "get", {"200", "500"}),
128+
("/v1/tools", "get", {"200", "500"}),
129+
("/v1/shields", "get", {"200", "500"}),
130+
("/v1/providers", "get", {"200", "500"}),
131+
("/v1/providers/{provider_id}", "get", {"200", "404", "422", "500"}),
63132
("/v1/query", "post", {"200", "400", "403", "500", "422"}),
64-
("/v1/streaming_query", "post", {"200", "422"}),
133+
("/v1/streaming_query", "post", {"200", "400", "401", "403", "422", "500"}),
65134
("/v1/config", "get", {"200", "503"}),
66135
("/v1/feedback", "post", {"200", "401", "403", "500", "422"}),
67136
("/v1/feedback/status", "get", {"200"}),
@@ -99,17 +168,64 @@ def test_servers_section_present(spec: dict) -> None:
99168
("/metrics", "get", {"200"}),
100169
],
101170
)
102-
def test_paths_and_responses_exist(
103-
spec: dict, path: str, method: str, expected_codes: set[str]
171+
def test_paths_and_responses_exist_from_file(
172+
spec_from_file: dict, path: str, method: str, expected_codes: set[str]
104173
) -> None:
105174
"""Tests all paths defined in OpenAPI specification."""
106-
paths = spec.get("paths") or {}
107-
assert path in paths, f"Missing path: {path}"
108-
op = (paths[path] or {}).get(method)
109-
assert isinstance(op, dict), f"Missing method {method.upper()} for path {path}"
110-
responses = op.get("responses") or {}
111-
got_codes = set(responses.keys())
112-
for code in expected_codes:
113-
assert (
114-
code in got_codes
115-
), f"Missing response code {code} for {method.upper()} {path}"
175+
_check_paths_and_responses_exist(spec_from_file, path, method, expected_codes)
176+
177+
178+
@pytest.mark.parametrize(
179+
"path,method,expected_codes",
180+
[
181+
("/", "get", {"200"}),
182+
("/v1/info", "get", {"200", "500"}),
183+
("/v1/models", "get", {"200", "500"}),
184+
("/v1/tools", "get", {"200", "500"}),
185+
("/v1/shields", "get", {"200", "500"}),
186+
("/v1/providers", "get", {"200", "500"}),
187+
("/v1/providers/{provider_id}", "get", {"200", "404", "422", "500"}),
188+
("/v1/query", "post", {"200", "400", "403", "500", "422"}),
189+
("/v1/streaming_query", "post", {"200", "400", "401", "403", "422", "500"}),
190+
("/v1/config", "get", {"200", "503"}),
191+
("/v1/feedback", "post", {"200", "401", "403", "500", "422"}),
192+
("/v1/feedback/status", "get", {"200"}),
193+
("/v1/feedback/status", "put", {"200", "422"}),
194+
("/v1/conversations", "get", {"200", "401", "503"}),
195+
(
196+
"/v1/conversations/{conversation_id}",
197+
"get",
198+
{"200", "400", "401", "404", "503", "422"},
199+
),
200+
(
201+
"/v1/conversations/{conversation_id}",
202+
"delete",
203+
{"200", "400", "401", "404", "503", "422"},
204+
),
205+
("/v2/conversations", "get", {"200"}),
206+
(
207+
"/v2/conversations/{conversation_id}",
208+
"get",
209+
{"200", "400", "401", "404", "422"},
210+
),
211+
(
212+
"/v2/conversations/{conversation_id}",
213+
"delete",
214+
{"200", "400", "401", "404", "422"},
215+
),
216+
(
217+
"/v2/conversations/{conversation_id}",
218+
"put",
219+
{"200", "400", "401", "404", "422"},
220+
),
221+
("/readiness", "get", {"200", "503"}),
222+
("/liveness", "get", {"200"}),
223+
("/authorized", "post", {"200", "400", "401", "403"}),
224+
("/metrics", "get", {"200"}),
225+
],
226+
)
227+
def test_paths_and_responses_exist_from_url(
228+
spec_from_url: dict, path: str, method: str, expected_codes: set[str]
229+
) -> None:
230+
"""Tests all paths defined in OpenAPI specification."""
231+
_check_paths_and_responses_exist(spec_from_url, path, method, expected_codes)

0 commit comments

Comments
 (0)