diff --git a/opencode_bridge/tests/test_client.py b/opencode_bridge/tests/test_client.py index c1d76da..a6dec27 100644 --- a/opencode_bridge/tests/test_client.py +++ b/opencode_bridge/tests/test_client.py @@ -2,6 +2,7 @@ import json from io import BytesIO +from unittest.mock import MagicMock import pytest import tornado.httpclient @@ -38,7 +39,9 @@ def base_config() -> OpenCodeConfig: ) -def _make_client(config: OpenCodeConfig, responses: list[tuple[int, object]]) -> tuple[OpenCodeClient, MockHTTPClient]: +def _make_client( + config: OpenCodeConfig, responses: list[tuple[int, object]] +) -> tuple[OpenCodeClient, MockHTTPClient]: mock = MockHTTPClient() mock.responses = responses return OpenCodeClient(config, http_client=mock), mock @@ -68,22 +71,30 @@ async def test_create_session_posts_title(base_config: OpenCodeConfig) -> None: @pytest.mark.asyncio -async def test_auth_in_fetch_kwargs_when_password_set(base_config: OpenCodeConfig) -> None: +async def test_auth_set_on_request_when_password_present( + base_config: OpenCodeConfig, +) -> None: + """Bug fix: auth must live on the HTTPRequest, not as fetch kwargs + (tornado rejects fetch(request, **kwargs) for request-construction kwargs).""" config = base_config._replace(password="secret") client, mock = _make_client(config, [(200, {"status": "ok"})]) await client.health() call = mock.calls[0] - assert call["kwargs"].get("auth_username") == "opencode" - assert call["kwargs"].get("auth_password") == "secret" + # No kwargs forwarded to fetch. + assert call["kwargs"] == {} + # Auth is on the HTTPRequest. + assert call["request"].auth_username == "opencode" + assert call["request"].auth_password == "secret" @pytest.mark.asyncio -async def test_auth_not_in_fetch_kwargs_when_password_empty(base_config: OpenCodeConfig) -> None: +async def test_auth_absent_when_no_password(base_config: OpenCodeConfig) -> None: client, mock = _make_client(base_config, [(200, {"status": "ok"})]) await client.health() call = mock.calls[0] - assert "auth_username" not in call["kwargs"] - assert "auth_password" not in call["kwargs"] + assert call["kwargs"] == {} + assert call["request"].auth_username is None + assert call["request"].auth_password is None @pytest.mark.asyncio @@ -149,3 +160,24 @@ async def test_non_2xx_response_raises_with_body(base_config: OpenCodeConfig) -> with pytest.raises(OpenCodeError) as exc_info: await client.health() assert "boom" in str(exc_info.value) + + +def test_close_is_idempotent() -> None: + """close() can be called multiple times safely.""" + client = OpenCodeClient(OpenCodeConfig( + url="http://x", user="u", password="", request_timeout_seconds=10 + )) + client.close() + client.close() # must not raise + assert client._http_client is None + + +def test_close_does_not_close_injected_client() -> None: + """close() must not call .close() on a client it didn't create.""" + injected = MagicMock() + client = OpenCodeClient(OpenCodeConfig( + url="http://x", user="u", password="", request_timeout_seconds=10 + ), http_client=injected) + client.close() + injected.close.assert_not_called() + assert client._http_client is None