restore test coverage to 100% in openbb_core

This commit is contained in:
Danglewood
2026-09-14 11:26:28 -07:00
parent 21ed5fe367
commit 90d427c5a5
10 changed files with 361 additions and 6 deletions

View File

@@ -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:

View File

@@ -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 == []

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"}}},
}

View 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)) == []

View File

@@ -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."""

View File

@@ -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 = {}