Skip to content

Commit f933cc3

Browse files
authored
test: generate actions tests for each resource that have actions (hetznercloud#545)
- Reduce the amount of duplicate tests for the actions calls, - Ensure consistency across all resources, - Have few good tests, instead of many with few coverage.
1 parent 8de4107 commit f933cc3

File tree

10 files changed

+250
-1161
lines changed

10 files changed

+250
-1161
lines changed

tests/unit/actions/test_client.py

Lines changed: 250 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import inspect
34
from unittest import mock
45

56
import pytest
@@ -12,9 +13,48 @@
1213
BoundAction,
1314
ResourceActionsClient,
1415
)
16+
from hcloud.certificates import BoundCertificate, CertificatesClient
17+
from hcloud.core import BoundModelBase, ResourceClientBase
18+
from hcloud.firewalls import BoundFirewall, FirewallsClient
19+
from hcloud.floating_ips import BoundFloatingIP, FloatingIPsClient
20+
from hcloud.images import BoundImage, ImagesClient
21+
from hcloud.load_balancers import BoundLoadBalancer, LoadBalancersClient
22+
from hcloud.networks import BoundNetwork, NetworksClient
23+
from hcloud.primary_ips import BoundPrimaryIP, PrimaryIPsClient
24+
from hcloud.servers import BoundServer, ServersClient
25+
from hcloud.volumes import BoundVolume, VolumesClient
1526

1627
from ..conftest import assert_bound_action1, assert_bound_action2
1728

29+
resources_with_actions: dict[str, tuple[ResourceClientBase, BoundModelBase]] = {
30+
"certificates": (CertificatesClient, BoundCertificate),
31+
"firewalls": (FirewallsClient, BoundFirewall),
32+
"floating_ips": (FloatingIPsClient, BoundFloatingIP),
33+
"images": (ImagesClient, BoundImage),
34+
"load_balancers": (LoadBalancersClient, BoundLoadBalancer),
35+
"networks": (NetworksClient, BoundNetwork),
36+
"primary_ips": (PrimaryIPsClient, BoundPrimaryIP),
37+
"servers": (ServersClient, BoundServer),
38+
"volumes": (VolumesClient, BoundVolume),
39+
}
40+
41+
42+
def test_resources_with_actions(client: Client):
43+
"""
44+
Ensure that the list of resource clients above is up to date.
45+
"""
46+
members = inspect.getmembers(
47+
client,
48+
predicate=lambda p: isinstance(p, ResourceClientBase) and hasattr(p, "actions"),
49+
)
50+
for name, member in members:
51+
assert name in resources_with_actions
52+
53+
resource_client_class, _ = resources_with_actions[name]
54+
assert member.__class__ is resource_client_class
55+
56+
assert len(members) == len(resources_with_actions)
57+
1858

1959
class TestBoundAction:
2060
@pytest.fixture()
@@ -90,85 +130,262 @@ def test_wait_until_finished_max_retries(
90130

91131

92132
class TestResourceActionsClient:
133+
"""
134+
/<resource>/actions
135+
/<resource>/actions/<id>
136+
"""
137+
138+
@pytest.fixture(params=resources_with_actions.keys())
139+
def resource(self, request) -> str:
140+
return request.param
141+
93142
@pytest.fixture()
94-
def actions_client(self, client: Client):
95-
return ResourceActionsClient(client, resource="/resource")
143+
def resource_client(self, client: Client, resource: str) -> ResourceActionsClient:
144+
"""
145+
Extract the resource actions client from the client.
146+
"""
147+
return getattr(client, resource).actions
96148

97149
def test_get_by_id(
98150
self,
99151
request_mock: mock.MagicMock,
100-
actions_client: ActionsClient,
152+
resource_client: ResourceActionsClient,
153+
resource: str,
101154
action_response,
102155
):
103156
request_mock.return_value = action_response
104157

105-
action = actions_client.get_by_id(1)
158+
action = resource_client.get_by_id(1)
106159

107160
request_mock.assert_called_with(
108161
method="GET",
109-
url="/resource/actions/1",
162+
url=f"/{resource}/actions/1",
110163
)
111164

112-
assert_bound_action1(action, actions_client._parent.actions)
165+
assert_bound_action1(action, resource_client._parent.actions)
113166

114167
@pytest.mark.parametrize(
115168
"params",
116169
[
117170
{},
118-
{"status": ["active"], "sort": ["status"], "page": 2, "per_page": 10},
171+
{"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10},
119172
],
120173
)
121174
def test_get_list(
122175
self,
123176
request_mock: mock.MagicMock,
124-
actions_client: ActionsClient,
177+
resource_client: ResourceActionsClient,
178+
resource: str,
125179
action_list_response,
126180
params,
127181
):
128182
request_mock.return_value = action_list_response
129183

130-
result = actions_client.get_list(**params)
184+
result = resource_client.get_list(**params)
131185

132186
request_mock.assert_called_with(
133187
method="GET",
134-
url="/resource/actions",
188+
url=f"/{resource}/actions",
135189
params=params,
136190
)
137191

138192
assert result.meta is not None
139193

140194
actions = result.actions
141195
assert len(actions) == 2
142-
assert_bound_action1(actions[0], actions_client._parent.actions)
143-
assert_bound_action2(actions[1], actions_client._parent.actions)
196+
assert_bound_action1(actions[0], resource_client._parent.actions)
197+
assert_bound_action2(actions[1], resource_client._parent.actions)
144198

145-
@pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}])
199+
@pytest.mark.parametrize(
200+
"params",
201+
[
202+
{},
203+
{"status": ["running"], "sort": ["status"]},
204+
],
205+
)
146206
def test_get_all(
147207
self,
148208
request_mock: mock.MagicMock,
149-
actions_client: ActionsClient,
209+
resource_client: ResourceActionsClient,
210+
resource: str,
211+
action_list_response,
212+
params,
213+
):
214+
request_mock.return_value = action_list_response
215+
216+
actions = resource_client.get_all(**params)
217+
218+
request_mock.assert_called_with(
219+
method="GET",
220+
url=f"/{resource}/actions",
221+
params={**params, "page": 1, "per_page": 50},
222+
)
223+
224+
assert len(actions) == 2
225+
assert_bound_action1(actions[0], resource_client._parent.actions)
226+
assert_bound_action2(actions[1], resource_client._parent.actions)
227+
228+
229+
class TestResourceObjectActionsClient:
230+
"""
231+
/<resource>/<id>/actions
232+
"""
233+
234+
@pytest.fixture(params=resources_with_actions.keys())
235+
def resource(self, request):
236+
if request.param == "primary_ips":
237+
pytest.skip("not implemented yet")
238+
return request.param
239+
240+
@pytest.fixture()
241+
def resource_client(self, client: Client, resource: str) -> ResourceClientBase:
242+
return getattr(client, resource)
243+
244+
@pytest.mark.parametrize(
245+
"params",
246+
[
247+
{},
248+
{"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10},
249+
],
250+
)
251+
def test_get_actions_list(
252+
self,
253+
request_mock: mock.MagicMock,
254+
resource_client: ResourceClientBase,
255+
resource: str,
256+
action_list_response,
257+
params,
258+
):
259+
request_mock.return_value = action_list_response
260+
261+
result = resource_client.get_actions_list(mock.MagicMock(id=1), **params)
262+
263+
request_mock.assert_called_with(
264+
method="GET",
265+
url=f"/{resource}/1/actions",
266+
params=params,
267+
)
268+
269+
assert result.meta is not None
270+
271+
actions = result.actions
272+
assert len(actions) == 2
273+
assert_bound_action1(actions[0], resource_client._parent.actions)
274+
assert_bound_action2(actions[1], resource_client._parent.actions)
275+
276+
@pytest.mark.parametrize(
277+
"params",
278+
[
279+
{},
280+
{"status": ["running"], "sort": ["status"]},
281+
],
282+
)
283+
def test_get_actions(
284+
self,
285+
request_mock: mock.MagicMock,
286+
resource_client: ResourceClientBase,
287+
resource: str,
150288
action_list_response,
151289
params,
152290
):
153291
request_mock.return_value = action_list_response
154292

155-
actions = actions_client.get_all(**params)
293+
actions = resource_client.get_actions(mock.MagicMock(id=1), **params)
156294

157295
request_mock.assert_called_with(
158296
method="GET",
159-
url="/resource/actions",
297+
url=f"/{resource}/1/actions",
160298
params={**params, "page": 1, "per_page": 50},
161299
)
162300

163301
assert len(actions) == 2
164-
assert_bound_action1(actions[0], actions_client._parent.actions)
165-
assert_bound_action2(actions[1], actions_client._parent.actions)
302+
assert_bound_action1(actions[0], resource_client._parent.actions)
303+
assert_bound_action2(actions[1], resource_client._parent.actions)
304+
305+
306+
class TestBoundModelActions:
307+
"""
308+
/<resource>/<id>/actions
309+
"""
310+
311+
@pytest.fixture(params=resources_with_actions.keys())
312+
def resource(self, request):
313+
if request.param == "primary_ips":
314+
pytest.skip("not implemented yet")
315+
return request.param
316+
317+
@pytest.fixture()
318+
def bound_model(self, client: Client, resource: str) -> ResourceClientBase:
319+
_, bound_model_class = resources_with_actions[resource]
320+
resource_client = getattr(client, resource)
321+
return bound_model_class(resource_client, data={"id": 1})
322+
323+
@pytest.mark.parametrize(
324+
"params",
325+
[
326+
{},
327+
{"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10},
328+
],
329+
)
330+
def test_get_actions_list(
331+
self,
332+
request_mock: mock.MagicMock,
333+
bound_model: BoundModelBase,
334+
resource: str,
335+
action_list_response,
336+
params,
337+
):
338+
request_mock.return_value = action_list_response
339+
340+
result = bound_model.get_actions_list(**params)
341+
342+
request_mock.assert_called_with(
343+
method="GET",
344+
url=f"/{resource}/1/actions",
345+
params=params,
346+
)
347+
348+
assert result.meta is not None
349+
350+
actions = result.actions
351+
assert len(actions) == 2
352+
assert_bound_action1(actions[0], bound_model._client._parent.actions)
353+
assert_bound_action2(actions[1], bound_model._client._parent.actions)
354+
355+
@pytest.mark.parametrize(
356+
"params",
357+
[
358+
{},
359+
{"status": ["running"], "sort": ["status"]},
360+
],
361+
)
362+
def test_get_actions(
363+
self,
364+
request_mock: mock.MagicMock,
365+
bound_model: BoundModelBase,
366+
resource: str,
367+
action_list_response,
368+
params,
369+
):
370+
request_mock.return_value = action_list_response
371+
372+
actions = bound_model.get_actions(**params)
373+
374+
request_mock.assert_called_with(
375+
method="GET",
376+
url=f"/{resource}/1/actions",
377+
params={**params, "page": 1, "per_page": 50},
378+
)
379+
380+
assert len(actions) == 2
381+
assert_bound_action1(actions[0], bound_model._client._parent.actions)
382+
assert_bound_action2(actions[1], bound_model._client._parent.actions)
166383

167384

168385
class TestActionsClient:
169386
@pytest.fixture()
170-
def actions_client(self, client: Client):
171-
return ActionsClient(client)
387+
def actions_client(self, client: Client) -> ActionsClient:
388+
return client.actions
172389

173390
def test_get_by_id(
174391
self,
@@ -184,13 +401,13 @@ def test_get_by_id(
184401
method="GET",
185402
url="/actions/1",
186403
)
187-
assert_bound_action1(action, actions_client._parent.actions)
404+
assert_bound_action1(action, actions_client)
188405

189406
@pytest.mark.parametrize(
190407
"params",
191408
[
192409
{},
193-
{"status": ["active"], "sort": ["status"], "page": 2, "per_page": 10},
410+
{"status": ["running"], "sort": ["status"], "page": 2, "per_page": 10},
194411
],
195412
)
196413
def test_get_list(
@@ -215,10 +432,16 @@ def test_get_list(
215432

216433
actions = result.actions
217434
assert len(actions) == 2
218-
assert_bound_action1(actions[0], actions_client._parent.actions)
219-
assert_bound_action2(actions[1], actions_client._parent.actions)
435+
assert_bound_action1(actions[0], actions_client)
436+
assert_bound_action2(actions[1], actions_client)
220437

221-
@pytest.mark.parametrize("params", [{}, {"status": ["active"], "sort": ["status"]}])
438+
@pytest.mark.parametrize(
439+
"params",
440+
[
441+
{},
442+
{"status": ["running"], "sort": ["status"]},
443+
],
444+
)
222445
def test_get_all(
223446
self,
224447
request_mock: mock.MagicMock,
@@ -238,5 +461,5 @@ def test_get_all(
238461
)
239462

240463
assert len(actions) == 2
241-
assert_bound_action1(actions[0], actions_client._parent.actions)
242-
assert_bound_action2(actions[1], actions_client._parent.actions)
464+
assert_bound_action1(actions[0], actions_client)
465+
assert_bound_action2(actions[1], actions_client)

0 commit comments

Comments
 (0)