11"""Tests the OpenAPI specification that is to be stored in docs/openapi.json."""
22
3+ from typing import Any
34import json
45from pathlib import Path
56
67import 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
1520OPENAPI_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