More tests

This commit is contained in:
Alex committed 2026-03-31 00:07:19 +01:00
1 parent d5c0322e2a
commit a3b08a5b44
37 files changed
+21904 -6

No files matched your search

+357
View File
@@ -333,3 +333,360 @@ paths:
metadata, actions = parse_spec(yaml_spec)
assert metadata["title"] == "YAML API"
assert actions[0]["name"] == "healthCheck"
def test_non_dict_path_item_skipped(self):
"""Cover line 117: non-dict path item is skipped."""
spec = json.dumps(
{
"openapi": "3.0.0",
"info": {"title": "T", "version": "1"},
"paths": {
"/valid": {
"get": {
"operationId": "validOp",
"responses": {"200": {"description": "OK"}},
}
},
"/invalid": "not_a_dict",
},
}
)
_, actions = parse_spec(spec)
assert len(actions) == 1
assert actions[0]["name"] == "validOp"
def test_non_dict_operation_skipped(self):
"""Cover line 122: non-dict operation for a method is skipped."""
spec = json.dumps(
{
"openapi": "3.0.0",
"info": {"title": "T", "version": "1"},
"paths": {
"/items": {
"get": "not_a_dict",
"post": {
"operationId": "createItem",
"responses": {},
},
}
},
}
)
_, actions = parse_spec(spec)
assert len(actions) == 1
assert actions[0]["name"] == "createItem"
def test_operation_parse_failure_logged(self):
"""Cover lines 137: exception parsing operation is caught."""
spec = json.dumps(
{
"openapi": "3.0.0",
"info": {"title": "T", "version": "1"},
"paths": {
"/items": {
"get": {
"operationId": "getItems",
"responses": {},
},
"post": {
"operationId": "createItem",
"requestBody": {
"$ref": "#/components/schemas/Missing"
},
"responses": {},
},
}
},
}
)
_, actions = parse_spec(spec)
# At least the GET should succeed
assert any(a["name"] == "getItems" for a in actions)
def test_path_level_params_merged(self):
"""Cover lines 129-130, 148, 159: path-level parameters merged."""
spec = json.dumps(
{
"openapi": "3.0.0",
"info": {"title": "T", "version": "1"},
"paths": {
"/items/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"get": {
"operationId": "getItem",
"responses": {},
},
}
},
}
)
_, actions = parse_spec(spec)
assert "id" in actions[0]["query_params"]["properties"]
def test_swagger_body_param_extraction(self):
"""Cover lines 145, 148, 152-153: Swagger 2.0 body parameter extraction."""
spec = json.dumps(
{
"swagger": "2.0",
"info": {"title": "T", "version": "1"},
"host": "api.test.com",
"basePath": "/v1",
"schemes": ["https"],
"paths": {
"/items": {
"post": {
"operationId": "createItem",
"consumes": ["application/json"],
"parameters": [
{
"name": "body",
"in": "body",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Item name",
}
},
"required": ["name"],
},
}
],
"responses": {"201": {"description": "Created"}},
}
}
},
}
)
_, actions = parse_spec(spec)
assert len(actions) == 1
assert "name" in actions[0]["body"]["properties"]
assert actions[0]["body_content_type"] == "application/json"
def test_traverse_path_key_error(self):
"""Cover lines 173-176: _traverse_path returns None on KeyError."""
from application.agents.tools.spec_parser import _traverse_path
result = _traverse_path({"a": {"b": 1}}, ["a", "c"])
assert result is None
def test_traverse_path_non_dict_result(self):
"""Cover line 175-176: _traverse_path returns None for non-dict result."""
from application.agents.tools.spec_parser import _traverse_path
result = _traverse_path({"a": "string_value"}, ["a"])
assert result is None
def test_openapi_request_body_form_urlencoded(self):
"""Cover lines 152-153: OpenAPI 3.x request body with form-urlencoded."""
spec = json.dumps(
{
"openapi": "3.0.0",
"info": {"title": "T", "version": "1"},
"paths": {
"/login": {
"post": {
"operationId": "login",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"username": {"type": "string"},
"password": {"type": "string"},
},
}
}
}
},
"responses": {},
}
}
},
}
)
_, actions = parse_spec(spec)
assert actions[0]["body_content_type"] == "application/x-www-form-urlencoded"
assert "username" in actions[0]["body"]["properties"]
# ---------------------------------------------------------------------------
# Coverage — additional uncovered lines: 205, 209, 213, 216-217, 222, 228
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSpecParserAdditionalCoverage:
def test_categorize_params_query_and_header(self):
"""Cover lines 205, 209, 213: parameters categorized into query and header."""
from application.agents.tools.spec_parser import _categorize_parameters
parameters = [
{"name": "q", "in": "query", "required": True, "description": "Query param"},
{"name": "X-Auth", "in": "header", "required": False, "description": "Auth header"},
{"name": "id", "in": "path", "required": True, "description": "Path param"},
]
query_params, headers = _categorize_parameters(parameters, {}, {})
assert "q" in query_params
assert "X-Auth" in headers
assert "id" in query_params # path params go to query_params
def test_categorize_params_skips_no_name(self):
"""Cover line 205: parameters without name are skipped."""
from application.agents.tools.spec_parser import _categorize_parameters
parameters = [
{"in": "query"}, # no name
]
query_params, headers = _categorize_parameters(parameters, {}, {})
assert len(query_params) == 0
assert len(headers) == 0
def test_param_to_property_integer_type(self):
"""Cover lines 216-217, 222, 228: _param_to_property with integer type."""
from application.agents.tools.spec_parser import _param_to_property
param = {
"name": "count",
"schema": {"type": "integer"},
"description": "Count of items",
"required": True,
}
prop = _param_to_property(param)
assert prop["type"] == "integer"
assert prop["required"] is True
assert prop["filled_by_llm"] is True
def test_param_to_property_number_type(self):
"""Cover line 222: number type mapped to integer."""
from application.agents.tools.spec_parser import _param_to_property
param = {
"schema": {"type": "number"},
"description": "A number",
"required": False,
}
prop = _param_to_property(param)
assert prop["type"] == "integer"
def test_param_to_property_string_default(self):
"""Cover line 222: unknown type defaults to string."""
from application.agents.tools.spec_parser import _param_to_property
param = {"description": "Desc", "required": False}
prop = _param_to_property(param)
assert prop["type"] == "string"
def test_param_to_property_description_truncated(self):
"""Cover line 228: description truncated to 200 chars."""
from application.agents.tools.spec_parser import _param_to_property
param = {"description": "x" * 300, "required": False}
prop = _param_to_property(param)
assert len(prop["description"]) == 200
# ---------------------------------------------------------------------------
# Additional coverage for spec_parser.py
# Lines: 57-59 (YAML error), 116-117 (non-dict path_item), 136-140
# (action parse exception), 156 (full_url with no base_url),
# 184-190 (generate_action_name from path), 99 (swagger base URL)
# ---------------------------------------------------------------------------
from application.agents.tools.spec_parser import _extract_actions # noqa: E402
@pytest.mark.unit
class TestLoadSpecYAMLError:
"""Cover lines 58-59: YAML parse error."""
def test_invalid_yaml_raises(self):
with pytest.raises(ValueError, match="Invalid YAML"):
_load_spec(" \ttabs: [invalid: yaml: {{")
@pytest.mark.unit
class TestExtractActionsNonDictPathItem:
"""Cover lines 116-117: non-dict path_item is skipped."""
def test_non_dict_path_skipped(self):
spec = {
"openapi": "3.0.0",
"paths": {
"/valid": {"get": {"operationId": "getValid"}},
"/invalid": "not-a-dict",
},
}
actions = _extract_actions(spec, False)
assert len(actions) == 1
assert actions[0]["name"] == "getValid"
@pytest.mark.unit
class TestExtractActionsParseException:
"""Cover lines 136-140: exception in _build_action is caught."""
def test_bad_operation_skipped(self):
spec = {
"openapi": "3.0.0",
"paths": {
"/test": {
"get": {
"operationId": "good",
},
"post": {
"operationId": "bad",
"parameters": [{"$ref": "#/invalid/ref"}],
},
},
},
}
# Should not raise, bad operation is skipped
actions = _extract_actions(spec, False)
assert len(actions) >= 1
@pytest.mark.unit
class TestGenerateActionNameFromPath:
"""Cover lines 184-190: operationId missing, generate from path."""
def test_name_from_path(self):
name = _generate_action_name({}, "get", "/users/{id}/posts")
assert name.startswith("get_")
assert "users" in name
assert "{" not in name
def test_name_truncated(self):
long_path = "/a" * 100
name = _generate_action_name({}, "post", long_path)
assert len(name) <= 64
@pytest.mark.unit
class TestGetBaseUrlSwagger:
"""Cover line 99: swagger base URL with host and scheme."""
def test_swagger_base_url(self):
spec = {
"swagger": "2.0",
"host": "api.example.com",
"basePath": "/v2",
"schemes": ["https"],
}
url = _get_base_url(spec, True)
assert url == "https://api.example.com/v2"
def test_swagger_no_host(self):
spec = {"swagger": "2.0"}
url = _get_base_url(spec, True)
assert url == ""