Skip to content
113 changes: 113 additions & 0 deletions tests/test_async_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#
# Copyright (c) 2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Unit tests for th_cli/async_cmd.py."""

import asyncio

import pytest

from th_cli.async_cmd import async_cmd


# ---------------------------------------------------------------------------
# @async_cmd decorator
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAsyncCmd:
def test_wrapped_async_function_runs_synchronously(self):
@async_cmd
async def my_async_func():
return "result"

result = my_async_func()
assert result == "result"

def test_return_value_is_propagated(self):
@async_cmd
async def compute():
return 42

assert compute() == 42

def test_positional_arguments_forwarded(self):
@async_cmd
async def add(a, b):
return a + b

assert add(3, 4) == 7

def test_keyword_arguments_forwarded(self):
@async_cmd
async def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

result = greet(name="World", greeting="Hi")
assert result == "Hi, World!"

def test_wraps_preserves_function_name(self):
@async_cmd
async def original_name():
pass

assert original_name.__name__ == "original_name"

def test_wraps_preserves_docstring(self):
@async_cmd
async def documented():
"""My docstring."""
pass

assert documented.__doc__ == "My docstring."

def test_async_operations_are_executed(self):
executed = []

@async_cmd
async def side_effects():
await asyncio.sleep(0) # real async operation
executed.append("done")

side_effects()
assert executed == ["done"]

def test_exception_propagates_from_async_body(self):
@async_cmd
async def raises():
raise ValueError("async error")

with pytest.raises(ValueError, match="async error"):
raises()

def test_none_return_value(self):
@async_cmd
async def returns_none():
return None

assert returns_none() is None

def test_can_decorate_multiple_functions_independently(self):
@async_cmd
async def func_a():
return "a"

@async_cmd
async def func_b():
return "b"

assert func_a() == "a"
assert func_b() == "b"
102 changes: 102 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#
# Copyright (c) 2026 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Unit tests for th_cli/client.py."""

from unittest.mock import MagicMock, patch

import pytest

from th_cli.exceptions import ConfigurationError


# ---------------------------------------------------------------------------
# get_client()
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetClient:
def test_returns_api_client_instance(self):
from th_cli.client import get_client
from th_cli.api_lib_autogen.api_client import ApiClient

with patch("th_cli.client.ApiClient") as mock_cls:
mock_instance = MagicMock(spec=ApiClient)
mock_cls.return_value = mock_instance
result = get_client()

assert result is mock_instance

def test_passes_correct_host_url(self):
from th_cli.client import get_client

with patch("th_cli.client.ApiClient") as mock_cls:
with patch("th_cli.client.config") as mock_config:
mock_config.hostname = "myserver"
mock_cls.return_value = MagicMock()
get_client()

call_kwargs = mock_cls.call_args
host_value = call_kwargs[1].get("host") or call_kwargs[0][0]
assert "myserver" in host_value

def test_raises_configuration_error_on_exception(self):
from th_cli.client import get_client

with patch("th_cli.client.ApiClient", side_effect=Exception("connection refused")):
with pytest.raises(ConfigurationError):
get_client()

def test_configuration_error_message_mentions_hostname(self):
from th_cli.client import get_client

with patch("th_cli.client.ApiClient", side_effect=Exception("boom")):
with patch("th_cli.client.config") as mock_config:
mock_config.hostname = "badhost"
with pytest.raises(ConfigurationError) as exc_info:
get_client()
assert "badhost" in exc_info.value.format_message()

def test_host_url_has_http_scheme(self):
from th_cli.client import get_client

captured_host = []

def capture(**kwargs):
captured_host.append(kwargs.get("host", ""))
return MagicMock()

with patch("th_cli.client.ApiClient", side_effect=capture):
with patch("th_cli.client.config") as mock_config:
mock_config.hostname = "somehost"
get_client()

assert captured_host[0].startswith("http://")


# ---------------------------------------------------------------------------
# Module-level client fallback
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestModuleLevelClient:
def test_client_is_none_or_api_client(self):
"""The module-level client should be either an ApiClient instance or None."""
import th_cli.client as client_mod
from th_cli.api_lib_autogen.api_client import ApiClient

assert client_mod.client is None or isinstance(client_mod.client, ApiClient)
Loading
Loading