feat(yamlreaders): Added OpenAPIReader for OpenAPI 3.0/3.1
Parses OpenAPI specifications: - Extracts base_url from servers - Parses paths and operations - Generates request body from schema - Auto-generates asserts from responses
This commit is contained in:
1 parent
d6b97e8b09
commit
f28c1d6d7c
1 file changed
+215
@@ -0,0 +1,215 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from models import (
|
||||||
|
AuthModel,
|
||||||
|
ExtractModel,
|
||||||
|
RequestModel,
|
||||||
|
StepModel,
|
||||||
|
TestModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .base import BaseReader, ParseError
|
||||||
|
|
||||||
|
|
||||||
|
class OpenAPIReader(BaseReader):
|
||||||
|
def validate(self, source: str | Path) -> bool:
|
||||||
|
try:
|
||||||
|
data = self.load_yaml(source)
|
||||||
|
|
||||||
|
if "openapi" not in data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
version = data["openapi"]
|
||||||
|
if not version.startswith("3."):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "paths" not in data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse(self, source: str | Path) -> TestModel:
|
||||||
|
data = self.load_yaml(source)
|
||||||
|
|
||||||
|
info = data.get("info", {})
|
||||||
|
name = info.get("title", "Unnamed API")
|
||||||
|
|
||||||
|
base_url = self._extract_base_url(data)
|
||||||
|
auth = self._extract_auth(data)
|
||||||
|
steps = self._parse_paths(data.get("paths", {}))
|
||||||
|
|
||||||
|
return TestModel(
|
||||||
|
name=name,
|
||||||
|
base_url=base_url,
|
||||||
|
auth=auth,
|
||||||
|
steps=steps,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extract_base_url(self, data: dict) -> str:
|
||||||
|
servers = data.get("servers", [])
|
||||||
|
|
||||||
|
if servers:
|
||||||
|
url = servers[0].get("url", "")
|
||||||
|
|
||||||
|
if not url.startswith("http"):
|
||||||
|
url = f"http://localhost{url}" if url.startswith("/") else f"http://localhost/{url}"
|
||||||
|
|
||||||
|
return url.rstrip("/")
|
||||||
|
|
||||||
|
return "http://localhost"
|
||||||
|
|
||||||
|
def _extract_auth(self, data: dict) -> AuthModel | None:
|
||||||
|
security_schemes = data.get("components", {}).get("securitySchemes", {})
|
||||||
|
|
||||||
|
if not security_schemes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for scheme_name, scheme in security_schemes.items():
|
||||||
|
scheme_type = scheme.get("type", "")
|
||||||
|
|
||||||
|
if scheme_type == "http":
|
||||||
|
auth_type = scheme.get("scheme", "bearer")
|
||||||
|
return AuthModel(
|
||||||
|
type=auth_type,
|
||||||
|
credentials={"scheme": auth_type},
|
||||||
|
)
|
||||||
|
|
||||||
|
elif scheme_type == "apiKey":
|
||||||
|
return AuthModel(
|
||||||
|
type="api_key",
|
||||||
|
credentials={
|
||||||
|
"in": scheme.get("in", "header"),
|
||||||
|
"name": scheme.get("name", ""),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
elif scheme_type == "oauth2":
|
||||||
|
return AuthModel(
|
||||||
|
type="oauth2",
|
||||||
|
credentials={},
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_paths(self, paths: dict) -> list[StepModel]:
|
||||||
|
steps = []
|
||||||
|
|
||||||
|
for path, path_item in paths.items():
|
||||||
|
for method in ["get", "post", "put", "patch", "delete", "head", "options"]:
|
||||||
|
operation = path_item.get(method)
|
||||||
|
if operation:
|
||||||
|
step = self._parse_operation(path, method, operation)
|
||||||
|
if step:
|
||||||
|
steps.append(step)
|
||||||
|
|
||||||
|
return steps
|
||||||
|
|
||||||
|
def _parse_operation(self, path: str, method: str, operation: dict) -> StepModel | None:
|
||||||
|
name = operation.get("operationId", f"{method.upper()} {path}")
|
||||||
|
|
||||||
|
request = self._build_request(path, method, operation)
|
||||||
|
extract = self._generate_extracts(operation)
|
||||||
|
|
||||||
|
return StepModel(
|
||||||
|
name=name,
|
||||||
|
request=request,
|
||||||
|
extract=extract,
|
||||||
|
assert_=self._generate_asserts(operation),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_request(self, path: str, method: str, operation: dict) -> RequestModel:
|
||||||
|
headers = {}
|
||||||
|
query = {}
|
||||||
|
body = None
|
||||||
|
|
||||||
|
for param in operation.get("parameters", []):
|
||||||
|
param_in = param.get("in", "")
|
||||||
|
param_name = param.get("name", "")
|
||||||
|
|
||||||
|
if param_in == "header":
|
||||||
|
headers[param_name] = ""
|
||||||
|
elif param_in == "query":
|
||||||
|
query[param_name] = ""
|
||||||
|
|
||||||
|
request_body = operation.get("requestBody", {})
|
||||||
|
if request_body:
|
||||||
|
content = request_body.get("content", {})
|
||||||
|
json_content = content.get("application/json", {})
|
||||||
|
schema = json_content.get("schema", {})
|
||||||
|
|
||||||
|
if schema:
|
||||||
|
body = self._schema_to_example(schema)
|
||||||
|
|
||||||
|
return RequestModel(
|
||||||
|
method=method.upper(),
|
||||||
|
path=path,
|
||||||
|
headers=headers,
|
||||||
|
query=query,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _schema_to_example(self, schema: dict) -> dict:
|
||||||
|
if "example" in schema:
|
||||||
|
return schema["example"]
|
||||||
|
|
||||||
|
if schema.get("type") == "object":
|
||||||
|
properties = schema.get("properties", {})
|
||||||
|
example = {}
|
||||||
|
|
||||||
|
for prop_name, prop_schema in properties.items():
|
||||||
|
example[prop_name] = self._schema_to_example(prop_schema)
|
||||||
|
|
||||||
|
return example
|
||||||
|
|
||||||
|
if schema.get("type") == "string":
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if schema.get("type") == "integer":
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if schema.get("type") == "boolean":
|
||||||
|
return False
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _generate_extracts(self, operation: dict) -> list[ExtractModel]:
|
||||||
|
extracts = []
|
||||||
|
|
||||||
|
responses = operation.get("responses", {})
|
||||||
|
for status_code, response in responses.items():
|
||||||
|
if status_code.startswith("2"):
|
||||||
|
content = response.get("content", {})
|
||||||
|
json_content = content.get("application/json", {})
|
||||||
|
schema = json_content.get("schema", {})
|
||||||
|
|
||||||
|
if schema and schema.get("type") == "object":
|
||||||
|
for prop_name in schema.get("properties", {}).keys():
|
||||||
|
extracts.append(ExtractModel(
|
||||||
|
name=f"{operation.get('operationId', 'response')}_{prop_name}",
|
||||||
|
json_path=f"$.{prop_name}",
|
||||||
|
source="body",
|
||||||
|
))
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
return extracts
|
||||||
|
|
||||||
|
def _generate_asserts(self, operation: dict) -> list:
|
||||||
|
from models import AssertModel
|
||||||
|
|
||||||
|
asserts = []
|
||||||
|
|
||||||
|
responses = operation.get("responses", {})
|
||||||
|
for status_code in responses.keys():
|
||||||
|
if status_code.startswith("2"):
|
||||||
|
asserts.append(AssertModel(
|
||||||
|
type="status",
|
||||||
|
operator="equals",
|
||||||
|
value=int(status_code) if status_code.isdigit() else 200,
|
||||||
|
))
|
||||||
|
break
|
||||||
|
|
||||||
|
return asserts
|
||||||
Reference in new issue
Block a user