mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-03 16:02:04 +00:00
restore test coverage to 100% in openbb_core
This commit is contained in:
@@ -496,8 +496,8 @@ def combine_certificates(cert: str, bundle: str | None = None) -> str:
|
||||
with open(cert, "rb") as cert_file:
|
||||
shutil.copyfileobj(cert_file, combined_cert_file)
|
||||
|
||||
# Register the combined certificate file for deletion
|
||||
atexit.register(os.remove, combined_cert)
|
||||
# Register the combined certificate file for deletion.
|
||||
atexit.register(Path(combined_cert).unlink, missing_ok=True)
|
||||
|
||||
return combined_cert
|
||||
except Exception as e:
|
||||
|
||||
@@ -534,3 +534,33 @@ def test_add_command_map_wraps_and_includes_plugin_routes(monkeypatch):
|
||||
getattr(route, "path", None) == "/plugin"
|
||||
for route in iter_api_routes(api_router)
|
||||
)
|
||||
|
||||
|
||||
def test_add_command_map_skips_non_apiroute_leaf(monkeypatch):
|
||||
"""A resolved route whose leaf is not an ``APIRoute`` is skipped, not wrapped."""
|
||||
from fastapi import APIRouter
|
||||
|
||||
plugins_api_router = APIRouter()
|
||||
plugins_router = type("PR", (), {"api_router": plugins_api_router})()
|
||||
|
||||
# Not an APIRoute -> `build_api_wrapper` must never be reached.
|
||||
mount = MagicMock(spec=[])
|
||||
|
||||
monkeypatch.setattr(
|
||||
"openbb_core.api.router.commands.RouterLoader.from_extensions",
|
||||
lambda: plugins_router,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"openbb_core.api.router.commands.iter_api_routes",
|
||||
lambda _router: iter([mount]),
|
||||
)
|
||||
|
||||
def _boom(command_runner, route):
|
||||
raise AssertionError("build_api_wrapper should not be called")
|
||||
|
||||
monkeypatch.setattr("openbb_core.api.router.commands.build_api_wrapper", _boom)
|
||||
|
||||
api_router = APIRouter()
|
||||
add_command_map(MagicMock(spec=CommandRunner), api_router)
|
||||
|
||||
assert plugins_api_router.routes == []
|
||||
|
||||
@@ -1644,3 +1644,56 @@ def test_generate_no_model_returns_str_type_name_and_any_fallback(monkeypatch):
|
||||
model_name=None,
|
||||
)
|
||||
assert "Any" in out2
|
||||
|
||||
|
||||
def test_get_field_type_union_renders_forward_ref_by_name():
|
||||
"""A ``ForwardRef`` inside a union renders as its target name."""
|
||||
from typing import ForwardRef
|
||||
|
||||
out = DocstringGenerator.get_field_type(
|
||||
list | ForwardRef("DataFrame"), is_required=True
|
||||
)
|
||||
assert "DataFrame" in out
|
||||
assert "ForwardRef" not in out
|
||||
|
||||
|
||||
def test_generate_non_model_strips_parameters_section_for_dependency_params(
|
||||
monkeypatch,
|
||||
):
|
||||
"""A dependency-backed param drops the hand-written ``Parameters`` section."""
|
||||
from fastapi import Depends
|
||||
|
||||
from openbb_core.app.static.package_builder import docstring_generator as dg
|
||||
|
||||
class _SS:
|
||||
class _PS:
|
||||
docstring_sections = ["description", "parameters"]
|
||||
docstring_max_length = None
|
||||
|
||||
python_settings = _PS()
|
||||
|
||||
class _Svc:
|
||||
system_settings = _SS()
|
||||
|
||||
monkeypatch.setattr(dg, "SystemService", _Svc)
|
||||
|
||||
def _dep() -> dict:
|
||||
return {}
|
||||
|
||||
def _func(principal: Annotated[dict, Depends(_dep)]):
|
||||
"""Do a thing.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
principal : dict
|
||||
The resolved principal.
|
||||
"""
|
||||
|
||||
out = DocstringGenerator.generate(
|
||||
path="/x/y",
|
||||
func=_func,
|
||||
formatted_params=OrderedDict(),
|
||||
model_name=None,
|
||||
)
|
||||
assert "Do a thing." in out
|
||||
assert "The resolved principal." not in out
|
||||
|
||||
@@ -1651,3 +1651,98 @@ def test_build_command_method_body_raw_query_params_fallback(monkeypatch):
|
||||
|
||||
code = MethodDefinition.build_command_method_body("/x/y", endpoint)
|
||||
assert '"symbol": symbol' in code
|
||||
|
||||
|
||||
def test_surfaced_param_spec_takes_description_from_metadata():
|
||||
from fastapi import Header
|
||||
|
||||
param = Parameter(
|
||||
"x_api_key",
|
||||
Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=Annotated[str, Header(description="The API key.")],
|
||||
)
|
||||
annotation, _default = MethodDefinition._surfaced_param_spec(param)
|
||||
field = annotation.__metadata__[0]
|
||||
assert isinstance(field, OpenBBField)
|
||||
assert field.description == "The API key."
|
||||
|
||||
|
||||
def test_collect_dependency_calls_surfaces_unsafe_but_resolvable_dependency():
|
||||
"""An unsafe dependency that is surfaceable is called with surfaced arguments."""
|
||||
from fastapi import Depends, Header
|
||||
|
||||
def get_principal(x_api_key: Annotated[str, Header()]) -> dict: ...
|
||||
|
||||
parameter_map = {
|
||||
"principal": Parameter(
|
||||
"principal",
|
||||
Parameter.POSITIONAL_OR_KEYWORD,
|
||||
annotation=Annotated[dict, Depends(get_principal)],
|
||||
)
|
||||
}
|
||||
|
||||
calls, names = MethodDefinition._collect_dependency_calls("/x/y", parameter_map)
|
||||
assert calls == [" principal = get_principal(x_api_key=x_api_key)"]
|
||||
assert names == {"principal"}
|
||||
|
||||
|
||||
def test_build_command_method_promotes_surfaced_dependency_params(monkeypatch):
|
||||
"""Parameters surfaced from a dependency land on the generated signature."""
|
||||
from fastapi import Depends, Header
|
||||
|
||||
monkeypatch.setattr(
|
||||
MethodDefinition, "is_deprecated_function", staticmethod(lambda p: False)
|
||||
)
|
||||
|
||||
def get_principal(x_api_key: Annotated[str, Header()]) -> dict: ...
|
||||
|
||||
def endpoint(principal: Annotated[dict, Depends(get_principal)]) -> dict:
|
||||
return principal
|
||||
|
||||
code = MethodDefinition.build_command_method("/x/y", endpoint)
|
||||
assert "x_api_key" in code
|
||||
assert "principal = get_principal(x_api_key=x_api_key)" in code
|
||||
|
||||
|
||||
def test_build_command_method_falls_back_to_response_model(monkeypatch):
|
||||
"""With no return annotation the passed ``response_model`` becomes the return type."""
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
monkeypatch.setattr(
|
||||
MethodDefinition, "is_deprecated_function", staticmethod(lambda p: False)
|
||||
)
|
||||
|
||||
def endpoint(symbol: str = "AAPL"):
|
||||
return symbol
|
||||
|
||||
code = MethodDefinition.build_command_method(
|
||||
"/x/y", endpoint, response_model=OBBject
|
||||
)
|
||||
assert "-> OBBject" in code
|
||||
|
||||
|
||||
def test_format_params_expands_query_params_model_field_types():
|
||||
"""Unpacking a bare ``QueryParams`` param expands ``TYPE_EXPANSION`` fields and
|
||||
leaves unexpanded fields at their declared type."""
|
||||
from typing import get_args
|
||||
|
||||
from openbb_core.provider.abstract.query_params import QueryParams
|
||||
|
||||
class _QP(QueryParams):
|
||||
data: list = []
|
||||
symbol: str = "AAPL"
|
||||
|
||||
parameter_map = {
|
||||
"params": Parameter("params", Parameter.POSITIONAL_OR_KEYWORD, annotation=_QP)
|
||||
}
|
||||
|
||||
out = MethodDefinition.format_params("/x/y", parameter_map)
|
||||
|
||||
# ``data`` is in TYPE_EXPANSION -> the TypeVar constraints are inlined.
|
||||
data_type = get_args(out["data"].annotation)[0]
|
||||
rendered = str(data_type)
|
||||
assert "DataFrame" in rendered
|
||||
assert "dict" in rendered
|
||||
|
||||
# ``symbol`` is not in TYPE_EXPANSION -> the declared type is kept as-is.
|
||||
assert get_args(out["symbol"].annotation)[0] is str
|
||||
|
||||
@@ -122,3 +122,44 @@ def test_get_router_description_returns_description(monkeypatch):
|
||||
)
|
||||
result = PathHandler.get_router_description("/equity")
|
||||
assert result == "My router description"
|
||||
|
||||
|
||||
def test_build_route_map_skips_route_without_path(monkeypatch):
|
||||
"""A resolved route that carries no ``path`` is skipped."""
|
||||
monkeypatch.setattr(
|
||||
"openbb_core.app.static.package_builder.path_handler.RouterLoader.from_extensions",
|
||||
MagicMock(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"openbb_core.app.static.package_builder.path_handler.iter_api_routes",
|
||||
lambda _router: iter([MagicMock(spec=[])]),
|
||||
)
|
||||
assert PathHandler.build_route_map() == {}
|
||||
|
||||
|
||||
def test_build_route_map_skips_route_excluded_from_schema(monkeypatch):
|
||||
"""A route flagged ``include_in_schema=False`` is left out of the route map."""
|
||||
from fastapi import APIRouter as FastAPIRouter
|
||||
|
||||
async def hidden_endpoint() -> None:
|
||||
pass
|
||||
|
||||
async def visible_endpoint() -> None:
|
||||
pass
|
||||
|
||||
inner = FastAPIRouter()
|
||||
inner.add_api_route(
|
||||
"/hidden", hidden_endpoint, methods=["GET"], include_in_schema=False
|
||||
)
|
||||
inner.add_api_route("/visible", visible_endpoint, methods=["GET"])
|
||||
|
||||
r = Router()
|
||||
r._api_router = inner
|
||||
monkeypatch.setattr(
|
||||
"openbb_core.app.static.package_builder.path_handler.RouterLoader.from_extensions",
|
||||
lambda: r,
|
||||
)
|
||||
|
||||
route_map = PathHandler.build_route_map()
|
||||
assert "/visible" in route_map
|
||||
assert "/hidden" not in route_map
|
||||
|
||||
@@ -234,10 +234,6 @@ def test_unwrap_generic_args_swallows_get_args_exception(monkeypatch):
|
||||
def test_unwrap_generic_args_handles_annotated_alias():
|
||||
"""Lines 76-79: ``_AnnotatedAlias`` branch pushes ``__origin__`` on stack."""
|
||||
out = import_def_module._unwrap_generic_args(Annotated[int, "tag"])
|
||||
# Origin (``int``) gets pushed and processed by the next loop iteration.
|
||||
# ``int`` itself isn't a generic, so it lands in ``out`` only if it appears
|
||||
# as a top-level type within get_args of the inner — which it doesn't here,
|
||||
# so we just confirm the function returns cleanly without raising.
|
||||
assert isinstance(out, list)
|
||||
|
||||
|
||||
@@ -280,3 +276,38 @@ def test_get_function_hint_type_list_extracts_depends_from_annotated_param():
|
||||
|
||||
out = ImportDefinition.get_function_hint_type_list(route)
|
||||
assert _dep in out
|
||||
|
||||
|
||||
def test_get_function_hint_type_list_falls_back_to_response_model():
|
||||
"""An endpoint with no return annotation falls back to ``route.response_model``."""
|
||||
from datetime import datetime
|
||||
|
||||
async def _endpoint(x: int):
|
||||
return x
|
||||
|
||||
route = SimpleNamespace(
|
||||
endpoint=_endpoint,
|
||||
openapi_extra={},
|
||||
response_model=datetime,
|
||||
)
|
||||
|
||||
out = ImportDefinition.get_function_hint_type_list(route)
|
||||
assert datetime in out
|
||||
|
||||
|
||||
def test_get_function_hint_type_list_event_stream_returns_obbstream():
|
||||
"""An ``text/event-stream`` response class forces an ``OBBStream`` return type."""
|
||||
from openbb_core.app.model.stream import OBBStream
|
||||
|
||||
async def _endpoint(x: int) -> int:
|
||||
return x
|
||||
|
||||
route = SimpleNamespace(
|
||||
endpoint=_endpoint,
|
||||
openapi_extra={},
|
||||
response_model=int,
|
||||
response_class=SimpleNamespace(media_type="text/event-stream"),
|
||||
)
|
||||
|
||||
out = ImportDefinition.get_function_hint_type_list(route)
|
||||
assert OBBStream in out
|
||||
|
||||
@@ -415,3 +415,23 @@ def test_router_loader_from_extensions_loaderror(monkeypatch):
|
||||
pass
|
||||
finally:
|
||||
RouterLoader.from_extensions.cache_clear()
|
||||
|
||||
|
||||
def test_command_with_stream_return_type():
|
||||
"""Test an ``OBBStream`` return annotation drops the response model and
|
||||
advertises an ``text/event-stream`` 200 response."""
|
||||
from openbb_core.app.model.stream import OBBStream
|
||||
|
||||
stream_router = Router()
|
||||
|
||||
@stream_router.command
|
||||
async def stream_function() -> OBBStream:
|
||||
"""Stream something."""
|
||||
|
||||
route = stream_router.api_router.routes[-1]
|
||||
assert route.path == "/stream_function"
|
||||
assert route.response_model is None
|
||||
assert route.responses[200] == {
|
||||
"description": "Event stream",
|
||||
"content": {"text/event-stream": {"schema": {"type": "string"}}},
|
||||
}
|
||||
|
||||
26
openbb_platform/core/tests/app/test_route_iter.py
Normal file
26
openbb_platform/core/tests/app/test_route_iter.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Tests for the ``route_iter`` helpers."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from openbb_core.app import route_iter
|
||||
from openbb_core.app.route_iter import iter_api_routes, iter_included_routers
|
||||
|
||||
|
||||
def test_iter_api_routes_object_without_routes():
|
||||
"""An object that exposes no ``routes`` attribute yields nothing."""
|
||||
assert list(iter_api_routes(object())) == []
|
||||
|
||||
|
||||
def test_iter_included_routers_object_without_routes():
|
||||
"""An object that exposes no ``routes`` attribute yields nothing."""
|
||||
assert list(iter_included_routers(object())) == []
|
||||
|
||||
|
||||
def test_iter_included_routers_without_included_router_support(monkeypatch):
|
||||
"""Without ``_IncludedRouter`` support there is nothing to walk into."""
|
||||
inner = APIRouter()
|
||||
outer = APIRouter()
|
||||
outer.include_router(inner, prefix="/inner")
|
||||
|
||||
monkeypatch.setattr(route_iter, "_IncludedRouter", None)
|
||||
assert list(iter_included_routers(outer)) == []
|
||||
@@ -7,6 +7,14 @@ providers built in ``tests/conftest.py``. The suite intentionally does
|
||||
not depend on any installed provider extension.
|
||||
"""
|
||||
|
||||
from collections.abc import (
|
||||
AsyncGenerator,
|
||||
AsyncIterable,
|
||||
AsyncIterator,
|
||||
Iterable,
|
||||
Iterator,
|
||||
)
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -188,6 +196,27 @@ def test_results_type_is_none_for_single_data_return(
|
||||
assert record["results_type"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"return_type",
|
||||
[
|
||||
AsyncIterator[Data],
|
||||
AsyncIterable[Data],
|
||||
AsyncGenerator[Data, None],
|
||||
Iterator[Data],
|
||||
Iterable[Data],
|
||||
],
|
||||
)
|
||||
def test_results_type_normalizes_streaming_containers(return_type: Any):
|
||||
"""A fetcher returning a streaming container normalizes to ``list``."""
|
||||
fetcher = SimpleNamespace(return_type=return_type)
|
||||
assert RegistryMap._get_results_type(fetcher) is list
|
||||
|
||||
|
||||
def test_results_type_is_none_when_fetcher_has_no_return_type():
|
||||
"""A fetcher with no ``return_type`` has no results container."""
|
||||
assert RegistryMap._get_results_type(SimpleNamespace()) is None
|
||||
|
||||
|
||||
def test_validate_rejects_non_data_subclass():
|
||||
"""``_validate`` raises ``ValueError`` for a non-``Data`` data type."""
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -654,6 +655,35 @@ def test_combine_certificates_returns_existing_combined_file(tmp_path):
|
||||
assert combine_certificates(str(cert)) == str(combined)
|
||||
|
||||
|
||||
def test_combine_certificates_cleanup_tolerates_missing_file(tmp_path, monkeypatch):
|
||||
"""The registered atexit cleanup must not raise if the file is already gone."""
|
||||
import atexit
|
||||
|
||||
import certifi
|
||||
|
||||
from openbb_core.provider.utils import helpers
|
||||
|
||||
cert = tmp_path / "ca.pem"
|
||||
cert.write_text("CERT")
|
||||
bundle = tmp_path / "ca-bundle.pem"
|
||||
bundle.write_text("BUNDLE")
|
||||
monkeypatch.setattr(certifi, "where", lambda: str(bundle))
|
||||
|
||||
registered: list = []
|
||||
monkeypatch.setattr(
|
||||
atexit, "register", lambda func, *a, **kw: registered.append((func, a, kw))
|
||||
)
|
||||
|
||||
out = Path(helpers.combine_certificates(str(cert)))
|
||||
assert out.exists()
|
||||
assert registered
|
||||
|
||||
# Something else removes it first - the cleanup must still be a no-op.
|
||||
out.unlink()
|
||||
for func, args, kwargs in registered:
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
def test_make_request_timeout_from_preferences(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user