Skip to content

Commit 8aed0db

Browse files
committed
unit tests
1 parent 132fd48 commit 8aed0db

21 files changed

Lines changed: 4513 additions & 648 deletions

.coverage

52 KB
Binary file not shown.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ dev = [
6161
"importlib-metadata>=6.7.0",
6262
"rich>=13.7.1",
6363
"pytest-xdist>=3.6.1",
64-
"uuid-utils>=0.11.0"
64+
"uuid-utils>=0.11.0",
65+
"pytest-cov>=7.0.0",
6566
]
6667

6768
[tool.rye.scripts]

tests/sdk/conftest.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Shared fixtures and utilities for SDK tests."""
2+
3+
from __future__ import annotations
4+
5+
from types import SimpleNamespace
6+
from typing import Any
7+
from unittest.mock import Mock, AsyncMock
8+
9+
import httpx
10+
import pytest
11+
12+
from runloop_api_client import Runloop, AsyncRunloop
13+
14+
15+
def create_mock_httpx_client(methods: dict[str, Any] | None = None) -> AsyncMock:
16+
"""
17+
Create a mock httpx.AsyncClient with proper context manager setup.
18+
19+
Args:
20+
methods: Optional dict of method names to AsyncMock return values.
21+
Common keys: 'get', 'put'
22+
23+
Returns:
24+
Configured AsyncMock for httpx.AsyncClient
25+
"""
26+
mock_client = AsyncMock()
27+
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
28+
mock_client.__aexit__ = AsyncMock(return_value=None)
29+
30+
if methods:
31+
for method_name, return_value in methods.items():
32+
setattr(mock_client, method_name, AsyncMock(return_value=return_value))
33+
34+
return mock_client
35+
36+
37+
def create_mock_httpx_response(**attrs: Any) -> Mock:
38+
"""
39+
Create a mock httpx.Response with specified attributes.
40+
41+
Args:
42+
**attrs: Attributes to set on the mock response.
43+
Common: content, text, encoding
44+
45+
Returns:
46+
Mock configured with httpx.Response spec and attributes
47+
"""
48+
mock_response = Mock(spec=httpx.Response)
49+
for key, value in attrs.items():
50+
setattr(mock_response, key, value)
51+
return mock_response
52+
53+
54+
@pytest.fixture
55+
def mock_client() -> Mock:
56+
"""Create a mock Runloop client."""
57+
return Mock(spec=Runloop)
58+
59+
60+
@pytest.fixture
61+
def mock_async_client() -> AsyncMock:
62+
"""Create a mock AsyncRunloop client."""
63+
return AsyncMock(spec=AsyncRunloop)
64+
65+
66+
@pytest.fixture
67+
def devbox_view() -> SimpleNamespace:
68+
"""Create a mock DevboxView."""
69+
return SimpleNamespace(
70+
id="dev_123",
71+
status="running",
72+
name="test-devbox",
73+
)
74+
75+
76+
@pytest.fixture
77+
def execution_view() -> SimpleNamespace:
78+
"""Create a mock DevboxAsyncExecutionDetailView."""
79+
return SimpleNamespace(
80+
execution_id="exec_123",
81+
devbox_id="dev_123",
82+
status="completed",
83+
exit_status=0,
84+
stdout="output",
85+
stderr="",
86+
)
87+
88+
89+
@pytest.fixture
90+
def snapshot_view() -> SimpleNamespace:
91+
"""Create a mock DevboxSnapshotView."""
92+
return SimpleNamespace(
93+
id="snap_123",
94+
status="completed",
95+
name="test-snapshot",
96+
)
97+
98+
99+
@pytest.fixture
100+
def blueprint_view() -> SimpleNamespace:
101+
"""Create a mock BlueprintView."""
102+
return SimpleNamespace(
103+
id="bp_123",
104+
status="built",
105+
name="test-blueprint",
106+
)
107+
108+
109+
@pytest.fixture
110+
def object_view() -> SimpleNamespace:
111+
"""Create a mock ObjectView."""
112+
return SimpleNamespace(
113+
id="obj_123",
114+
upload_url="https://upload.example.com/obj_123",
115+
name="test-object",
116+
)
117+
118+
119+
@pytest.fixture
120+
def mock_httpx_response() -> Mock:
121+
"""Create a mock httpx.Response."""
122+
response = Mock(spec=httpx.Response)
123+
response.status_code = 200
124+
response.content = b"test content"
125+
response.text = "test content"
126+
response.encoding = "utf-8"
127+
response.raise_for_status = Mock()
128+
return response
129+
130+
131+
@pytest.fixture
132+
def mock_stream() -> Mock:
133+
"""Create a mock Stream for testing."""
134+
stream = Mock()
135+
stream.__iter__ = Mock(return_value=iter([]))
136+
stream.__enter__ = Mock(return_value=stream)
137+
stream.__exit__ = Mock(return_value=None)
138+
stream.close = Mock()
139+
return stream
140+
141+
142+
@pytest.fixture
143+
def mock_async_stream() -> AsyncMock:
144+
"""Create a mock AsyncStream for testing."""
145+
146+
async def async_iter():
147+
# Empty async iterator
148+
if False:
149+
yield
150+
151+
stream = AsyncMock()
152+
stream.__aiter__ = Mock(return_value=async_iter())
153+
stream.__aenter__ = AsyncMock(return_value=stream)
154+
stream.__aexit__ = AsyncMock(return_value=None)
155+
stream.close = AsyncMock()
156+
return stream

tests/sdk/test_async_blueprint.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Comprehensive tests for async Blueprint class."""
2+
3+
from __future__ import annotations
4+
5+
from types import SimpleNamespace
6+
from unittest.mock import AsyncMock
7+
8+
import pytest
9+
10+
from runloop_api_client.sdk import AsyncBlueprint
11+
12+
13+
class TestAsyncBlueprint:
14+
"""Tests for AsyncBlueprint class."""
15+
16+
def test_init(self, mock_async_client: AsyncMock) -> None:
17+
"""Test AsyncBlueprint initialization."""
18+
blueprint = AsyncBlueprint(mock_async_client, "bp_123")
19+
assert blueprint.id == "bp_123"
20+
21+
def test_repr(self, mock_async_client: AsyncMock) -> None:
22+
"""Test AsyncBlueprint string representation."""
23+
blueprint = AsyncBlueprint(mock_async_client, "bp_123")
24+
assert repr(blueprint) == "<AsyncBlueprint id='bp_123'>"
25+
26+
@pytest.mark.asyncio
27+
async def test_get_info(self, mock_async_client: AsyncMock, blueprint_view: SimpleNamespace) -> None:
28+
"""Test get_info method."""
29+
mock_async_client.blueprints.retrieve = AsyncMock(return_value=blueprint_view)
30+
31+
blueprint = AsyncBlueprint(mock_async_client, "bp_123")
32+
result = await blueprint.get_info(
33+
extra_headers={"X-Custom": "value"},
34+
extra_query={"param": "value"},
35+
extra_body={"key": "value"},
36+
timeout=30.0,
37+
)
38+
39+
assert result == blueprint_view
40+
mock_async_client.blueprints.retrieve.assert_called_once()
41+
42+
@pytest.mark.asyncio
43+
async def test_logs(self, mock_async_client: AsyncMock) -> None:
44+
"""Test logs method."""
45+
logs_view = SimpleNamespace(logs=[])
46+
mock_async_client.blueprints.logs = AsyncMock(return_value=logs_view)
47+
48+
blueprint = AsyncBlueprint(mock_async_client, "bp_123")
49+
result = await blueprint.logs(
50+
extra_headers={"X-Custom": "value"},
51+
extra_query={"param": "value"},
52+
extra_body={"key": "value"},
53+
timeout=30.0,
54+
)
55+
56+
assert result == logs_view
57+
mock_async_client.blueprints.logs.assert_called_once()
58+
59+
@pytest.mark.asyncio
60+
async def test_delete(self, mock_async_client: AsyncMock) -> None:
61+
"""Test delete method."""
62+
# Return value not used - testing side effect only
63+
mock_async_client.blueprints.delete = AsyncMock(return_value=object())
64+
65+
blueprint = AsyncBlueprint(mock_async_client, "bp_123")
66+
result = await blueprint.delete(
67+
extra_headers={"X-Custom": "value"},
68+
extra_query={"param": "value"},
69+
extra_body={"key": "value"},
70+
timeout=30.0,
71+
)
72+
73+
assert result is not None
74+
mock_async_client.blueprints.delete.assert_called_once()
75+
76+
@pytest.mark.asyncio
77+
async def test_create_devbox(self, mock_async_client: AsyncMock, devbox_view: SimpleNamespace) -> None:
78+
"""Test create_devbox method."""
79+
mock_async_client.devboxes.create_and_await_running = AsyncMock(return_value=devbox_view)
80+
81+
blueprint = AsyncBlueprint(mock_async_client, "bp_123")
82+
devbox = await blueprint.create_devbox(
83+
name="test-devbox",
84+
metadata={"key": "value"},
85+
polling_config=None,
86+
extra_headers={"X-Custom": "value"},
87+
)
88+
89+
assert devbox.id == "dev_123"
90+
mock_async_client.devboxes.create_and_await_running.assert_called_once()

0 commit comments

Comments
 (0)