diff --git a/tests/pycharting/api/test_interface.py b/tests/pycharting/api/test_interface.py index 25445ce..f6693a3 100644 --- a/tests/pycharting/api/test_interface.py +++ b/tests/pycharting/api/test_interface.py @@ -530,3 +530,74 @@ def test_dependencies_available(): assert np is not None assert fastapi is not None assert uvicorn is not None + + +class TestPlotResult: + """Tests for the PlotResult TypedDict returned by ``plot``.""" + + def test_declares_the_documented_keys(self): + """The declared keys are the full documented result contract.""" + from pycharting.api.interface import PlotResult + + assert set(PlotResult.__annotations__) == { + "status", + "session_id", + "url", + "server_url", + "data_points", + "server_running", + "stage", + "error", + } + + def test_every_key_is_optional(self): + """``total=False`` means success and failure shapes share one type.""" + from pycharting.api.interface import PlotResult + + assert PlotResult.__total__ is False + assert PlotResult.__required_keys__ == frozenset() + + def test_success_result_only_uses_declared_keys(self): + """A real successful plot() result introduces no undeclared key.""" + from pycharting.api.interface import PlotResult + + n = 10 + result = plot(np.arange(n), close=np.random.randn(n) + 100, open_browser=False, block=False) + + assert result["status"] == "success" + assert set(result) <= set(PlotResult.__annotations__) + + def test_validation_error_result_only_uses_declared_keys(self): + """A validation failure also stays within the declared keys.""" + from pycharting.api.interface import PlotResult + + result = plot(np.arange(10), close=np.random.randn(5), open_browser=False, block=False) + + assert result["stage"] == "validation" + assert set(result) <= set(PlotResult.__annotations__) + + +class TestServerStatus: + """Tests for the ServerStatus TypedDict returned by ``get_server_status``.""" + + def test_declares_the_documented_keys(self): + """The declared keys are the full documented status contract.""" + from pycharting.api.interface import ServerStatus + + assert set(ServerStatus.__annotations__) == {"running", "server_info", "active_sessions"} + + def test_every_key_is_required(self): + """Unlike PlotResult, the status shape is total.""" + from pycharting.api.interface import ServerStatus + + assert ServerStatus.__total__ is True + + def test_payload_matches_declaration_when_stopped(self): + """get_server_status() returns exactly the declared keys with no server.""" + from pycharting.api.interface import ServerStatus + + status = get_server_status() + + assert set(status) == set(ServerStatus.__annotations__) + assert status["running"] is False + assert status["server_info"] is None diff --git a/tests/pycharting/api/test_routes.py b/tests/pycharting/api/test_routes.py index 4fc5bad..5cc3999 100644 --- a/tests/pycharting/api/test_routes.py +++ b/tests/pycharting/api/test_routes.py @@ -280,3 +280,121 @@ def test_detail_defaults_to_none(self): from pycharting.api.routes import ErrorResponse assert ErrorResponse(error="boom").detail is None + + +class TestInitDataResponse: + """Tests for the InitDataResponse pydantic model.""" + + def test_construction(self): + """InitDataResponse carries the session id, status, size and message.""" + from pycharting.api.routes import InitDataResponse + + resp = InitDataResponse(session_id="s1", status="success", data_points=100, message="ready") + assert resp.session_id == "s1" + assert resp.status == "success" + assert resp.data_points == 100 + assert resp.message == "ready" + + def test_all_fields_required(self): + """Every field is required — none carries a default.""" + from pydantic import ValidationError + + from pycharting.api.routes import InitDataResponse + + with pytest.raises(ValidationError): + InitDataResponse(session_id="s1") + + +class TestSessionInfo: + """Tests for the SessionInfo pydantic model.""" + + def test_construction(self): + """SessionInfo describes one session's size and overlay/subplot presence.""" + from pycharting.api.routes import SessionInfo + + info = SessionInfo(session_id="s1", data_points=50, has_overlays=True, has_subplots=False) + assert info.session_id == "s1" + assert info.data_points == 50 + assert info.has_overlays is True + assert info.has_subplots is False + + def test_matches_list_sessions_payload(self, client): + """The /api/sessions payload validates against SessionInfo.""" + from pycharting.api.routes import SessionInfo + + client.post("/api/data/init?session_id=s1") + entry = client.get("/api/sessions").json()["sessions"][0] + + assert SessionInfo(**entry).session_id == "s1" + + +class TestSessionListResponse: + """Tests for the SessionListResponse pydantic model.""" + + def test_construction_with_nested_sessions(self): + """SessionListResponse nests SessionInfo entries alongside a count.""" + from pycharting.api.routes import SessionInfo, SessionListResponse + + info = SessionInfo(session_id="s1", data_points=10, has_overlays=False, has_subplots=False) + resp = SessionListResponse(sessions=[info], count=1) + assert resp.count == 1 + assert resp.sessions[0].session_id == "s1" + + def test_coerces_nested_dicts(self): + """Nested session dicts are coerced into SessionInfo instances.""" + from pycharting.api.routes import SessionInfo, SessionListResponse + + resp = SessionListResponse( + sessions=[{"session_id": "s1", "data_points": 10, "has_overlays": False, "has_subplots": False}], + count=1, + ) + assert isinstance(resp.sessions[0], SessionInfo) + + def test_empty_session_list_allowed(self): + """An empty listing is representable.""" + from pycharting.api.routes import SessionListResponse + + assert SessionListResponse(sessions=[], count=0).sessions == [] + + +class TestDeleteSessionResponse: + """Tests for the DeleteSessionResponse pydantic model.""" + + def test_construction(self): + """DeleteSessionResponse reports which session was removed.""" + from pycharting.api.routes import DeleteSessionResponse + + resp = DeleteSessionResponse(session_id="s1", status="deleted", message="gone") + assert resp.session_id == "s1" + assert resp.status == "deleted" + assert resp.message == "gone" + + def test_all_fields_required(self): + """Every field is required — none carries a default.""" + from pydantic import ValidationError + + from pycharting.api.routes import DeleteSessionResponse + + with pytest.raises(ValidationError): + DeleteSessionResponse(session_id="s1") + + +class TestStatusResponse: + """Tests for the StatusResponse pydantic model.""" + + def test_construction(self): + """StatusResponse carries the status, session count and endpoint map.""" + from pycharting.api.routes import StatusResponse + + resp = StatusResponse(status="ok", active_sessions=2, endpoints={"data": "/api/data"}) + assert resp.status == "ok" + assert resp.active_sessions == 2 + assert resp.endpoints == {"data": "/api/data"} + + def test_matches_api_status_payload(self, client): + """The /api/status payload validates against StatusResponse.""" + from pycharting.api.routes import StatusResponse + + resp = StatusResponse(**client.get("/api/status").json()) + assert resp.active_sessions == 0 + assert resp.endpoints