|
|
|
@ -1,4 +1,5 @@ |
|
|
|
from typing import Any, Dict, List, Union |
|
|
|
from unittest.mock import patch |
|
|
|
|
|
|
|
import pytest |
|
|
|
from fastapi import FastAPI, Response |
|
|
|
@ -77,6 +78,7 @@ async def get_nested_function() -> Dict[str, Any]: |
|
|
|
def inner_function(): |
|
|
|
return {"inner": "value"} |
|
|
|
|
|
|
|
inner_function() # Call to ensure coverage |
|
|
|
return {"outer": "value"} |
|
|
|
|
|
|
|
|
|
|
|
@ -306,6 +308,30 @@ def test_openapi_schema_ast_inference(): |
|
|
|
assert arg_types_props["bool_val"]["type"] == "boolean" |
|
|
|
assert arg_types_props["float_val"]["type"] == "number" |
|
|
|
|
|
|
|
empty_structures_schema = paths["/edge_cases/empty_structures"]["get"]["responses"][ |
|
|
|
"200" |
|
|
|
]["content"]["application/json"]["schema"] |
|
|
|
assert "$ref" in empty_structures_schema |
|
|
|
empty_structures_ref = empty_structures_schema["$ref"].split("/")[-1] |
|
|
|
empty_structures_props = schema["components"]["schemas"][empty_structures_ref][ |
|
|
|
"properties" |
|
|
|
] |
|
|
|
assert empty_structures_props["empty_list"]["type"] == "array" |
|
|
|
assert "items" not in empty_structures_props["empty_list"] or not empty_structures_props["empty_list"].get("items") |
|
|
|
assert "type" not in empty_structures_props["empty_dict"] or empty_structures_props["empty_dict"]["type"] == "object" |
|
|
|
|
|
|
|
local_variable_schema = paths["/edge_cases/local_variable"]["get"]["responses"][ |
|
|
|
"200" |
|
|
|
]["content"]["application/json"]["schema"] |
|
|
|
assert "$ref" in local_variable_schema |
|
|
|
local_variable_ref = local_variable_schema["$ref"].split("/")[-1] |
|
|
|
local_variable_props = schema["components"]["schemas"][local_variable_ref][ |
|
|
|
"properties" |
|
|
|
] |
|
|
|
assert local_variable_props["status"]["type"] == "string" |
|
|
|
assert "$ref" in local_variable_props["nested"] |
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize( |
|
|
|
"func", |
|
|
|
[ |
|
|
|
@ -354,10 +380,61 @@ def test_infer_response_model_invalid_field_name(): |
|
|
|
# This specifically tests protections against things like {"__class__": ...} |
|
|
|
# It might return None (if create_model fails) or a model (if pydantic handles it) |
|
|
|
# We just want to ensure it doesn't raise an unhandled exception |
|
|
|
try: |
|
|
|
infer_response_model_from_ast(_test_invalid_field_name) |
|
|
|
except Exception as e: |
|
|
|
pytest.fail(f"infer_response_model_from_ast raised exception: {e}") |
|
|
|
_test_invalid_field_name() |
|
|
|
|
|
|
|
|
|
|
|
def _test_arg_no_annotation(a): |
|
|
|
return {"x": a} |
|
|
|
|
|
|
|
|
|
|
|
def test_infer_type_from_ast_arg_no_annotation(): |
|
|
|
_test_arg_no_annotation(1) |
|
|
|
assert infer_response_model_from_ast(_test_arg_no_annotation) is None |
|
|
|
|
|
|
|
|
|
|
|
def _test_arg_complex_annotation(a: List[int]): |
|
|
|
return {"x": a} |
|
|
|
|
|
|
|
|
|
|
|
def test_infer_type_from_ast_arg_complex_annotation(): |
|
|
|
_test_arg_complex_annotation([1]) |
|
|
|
assert infer_response_model_from_ast(_test_arg_complex_annotation) is None |
|
|
|
|
|
|
|
|
|
|
|
def test_infer_response_model_getsource_error(): |
|
|
|
def func(): |
|
|
|
pass |
|
|
|
|
|
|
|
func() |
|
|
|
with patch("inspect.getsource", side_effect=OSError): |
|
|
|
assert infer_response_model_from_ast(func) is None |
|
|
|
|
|
|
|
|
|
|
|
def test_infer_response_model_syntax_error(): |
|
|
|
def func(): |
|
|
|
pass |
|
|
|
|
|
|
|
func() |
|
|
|
with patch("inspect.getsource", return_value="def func( invalid"): |
|
|
|
assert infer_response_model_from_ast(func) is None |
|
|
|
|
|
|
|
|
|
|
|
def test_infer_response_model_no_body(): |
|
|
|
def func(): |
|
|
|
pass |
|
|
|
|
|
|
|
func() |
|
|
|
with patch("inspect.getsource", return_value="# comments"): |
|
|
|
assert infer_response_model_from_ast(func) is None |
|
|
|
|
|
|
|
|
|
|
|
def test_infer_response_model_not_function_def(): |
|
|
|
def func(): |
|
|
|
pass |
|
|
|
|
|
|
|
func() |
|
|
|
with patch("inspect.getsource", return_value="class A: pass"): |
|
|
|
assert infer_response_model_from_ast(func) is None |
|
|
|
|
|
|
|
|
|
|
|
def test_contains_response() -> None: |
|
|
|
@ -371,3 +448,44 @@ def test_contains_response() -> None: |
|
|
|
assert _contains_response(Union[str, int]) is False |
|
|
|
assert _contains_response(Union[str, Union[Response, int]]) is True |
|
|
|
assert _contains_response(List[str]) is False |
|
|
|
|
|
|
|
|
|
|
|
def test_execution_of_endpoints(): |
|
|
|
""" |
|
|
|
Call all endpoints to ensure their bodies are executed (coverage). |
|
|
|
""" |
|
|
|
client.get("/users/1") |
|
|
|
client.get("/orders/order1") |
|
|
|
client.get("/edge_cases/mixed_types") |
|
|
|
client.get("/edge_cases/expressions") |
|
|
|
client.get("/edge_cases/empty_structures") |
|
|
|
client.get("/edge_cases/local_variable") |
|
|
|
client.get("/edge_cases/explicit_response") |
|
|
|
client.get("/edge_cases/nested_function") |
|
|
|
client.get("/edge_cases/invalid_keys") |
|
|
|
client.get("/db/direct_return") |
|
|
|
client.get("/db/dict_construction") |
|
|
|
client.get("/edge_cases/homogeneous_list") |
|
|
|
client.get("/edge_cases/int_float_binop") |
|
|
|
client.get("/edge_cases/arg_types/1", params={"b": "foo", "c": "true", "d": "1.5"}) |
|
|
|
|
|
|
|
|
|
|
|
def test_execution_of_helpers(): |
|
|
|
""" |
|
|
|
Call all helper functions to ensure their bodies are executed (coverage). |
|
|
|
""" |
|
|
|
_test_no_return_func() |
|
|
|
_test_returns_call() |
|
|
|
_test_returns_empty_dict() |
|
|
|
_test_returns_dict_literal() |
|
|
|
_test_returns_variable() |
|
|
|
_test_returns_annotated_var() |
|
|
|
_test_func_mixed(1) |
|
|
|
_test_list_with_any_elements(1) |
|
|
|
_test_non_constant_key() |
|
|
|
_test_list_arg([]) |
|
|
|
_test_dict_arg({}) |
|
|
|
_test_nested_dict() |
|
|
|
_test_nested_dict_with_var_key() |
|
|
|
_test_all_any_fields() |
|
|
|
_test_invalid_field_name() |
|
|
|
|