Skip to content

Commit 055ed90

Browse files
authored
Unit tests for operator: purge_openstack_resources, helper functions (#175)
1 parent da4a3c6 commit 055ed90

File tree

2 files changed

+322
-45
lines changed

2 files changed

+322
-45
lines changed

capi_janitor/tests/openstack/test_operator.py

Lines changed: 322 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,35 @@
44
import yaml
55

66
import easykube
7-
from easykube.rest.util import PropertyDict
7+
import httpx
88

9-
from capi_janitor.openstack import operator, openstack
9+
from capi_janitor.openstack import openstack
10+
from capi_janitor.openstack import operator
1011
from capi_janitor.openstack.operator import OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY
1112

1213

14+
# Helper to create an async iterable
15+
class AsyncIterList:
16+
def __init__(self, items):
17+
self.items = items
18+
self.kwargs = None
19+
20+
async def list(self, **kwargs):
21+
self.kwargs = kwargs
22+
for item in self.items:
23+
yield item
24+
25+
def __aiter__(self):
26+
self._iter = iter(self.items)
27+
return self
28+
29+
async def __anext__(self):
30+
try:
31+
return next(self._iter)
32+
except StopIteration:
33+
raise StopAsyncIteration
34+
35+
1336
class TestOperator(unittest.IsolatedAsyncioTestCase):
1437
async def test_operator(self):
1538
mock_easykube = mock.AsyncMock(spec=easykube.AsyncClient)
@@ -19,6 +42,183 @@ async def test_operator(self):
1942

2043
mock_easykube.aclose.assert_awaited_once_with()
2144

45+
async def test_fips_for_cluster(self):
46+
prefix = "Floating IP for Kubernetes external service from cluster"
47+
fips = [
48+
mock.Mock(description=f"{prefix} mycluster"),
49+
mock.Mock(description=f"{prefix} asdf"),
50+
mock.Mock(description="Some other description"),
51+
mock.Mock(description=f"{prefix} mycluster"),
52+
]
53+
resource_mock = AsyncIterList(fips)
54+
55+
result = [
56+
fip async for fip in operator.fips_for_cluster(resource_mock, "mycluster")
57+
]
58+
59+
self.assertEqual(len(result), 2)
60+
self.assertEqual(result[0], fips[0])
61+
self.assertEqual(result[1], fips[3])
62+
self.assertEqual(resource_mock.kwargs, {})
63+
64+
async def test_lbs_for_cluster(self):
65+
lbs = [
66+
mock.Mock(name="lb0"),
67+
mock.Mock(name="lb1"),
68+
mock.Mock(name="lb2"),
69+
mock.Mock(name="lb3"),
70+
]
71+
# can't pass name into mock.Mock() to do this
72+
lbs[0].name = "kube_service_mycluster_api"
73+
lbs[1].name = "kube_service_othercluster_api"
74+
lbs[2].name = "fake_service_mycluster_api"
75+
lbs[3].name = "kube_service_mycluster_ui"
76+
resource_mock = AsyncIterList(lbs)
77+
78+
result = [
79+
lb async for lb in operator.lbs_for_cluster(resource_mock, "mycluster")
80+
]
81+
82+
self.assertEqual(len(result), 2)
83+
self.assertEqual(result[0], lbs[0])
84+
self.assertEqual(result[1], lbs[3])
85+
86+
async def test_secgroups_for_cluster(self):
87+
prefix = "Security Group for Service LoadBalancer in cluster"
88+
secgroups = [
89+
mock.Mock(description=f"{prefix} mycluster"),
90+
mock.Mock(description=f"{prefix} othercluster"),
91+
mock.Mock(description="Other description"),
92+
mock.Mock(description=f"{prefix} mycluster"),
93+
]
94+
resource_mock = AsyncIterList(secgroups)
95+
96+
result = []
97+
async for sg in operator.secgroups_for_cluster(resource_mock, "mycluster"):
98+
result.append(sg)
99+
100+
self.assertEqual(len(result), 2)
101+
self.assertEqual(result[0], secgroups[0])
102+
self.assertEqual(result[1], secgroups[3])
103+
104+
async def test_filtered_volumes_for_cluster(self):
105+
volumes = [
106+
mock.Mock(
107+
metadata={
108+
"cinder.csi.openstack.org/cluster": "mycluster",
109+
OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY: "false",
110+
}
111+
),
112+
mock.Mock(
113+
metadata={
114+
"cinder.csi.openstack.org/cluster": "mycluster",
115+
OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY: "true",
116+
}
117+
),
118+
mock.Mock(
119+
metadata={
120+
"cinder.csi.openstack.org/cluster": "othercluster",
121+
OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY: "false",
122+
}
123+
),
124+
mock.Mock(
125+
metadata={
126+
"cinder.csi.openstack.org/cluster": "mycluster",
127+
}
128+
),
129+
mock.Mock(metadata={"other_key": "value"}),
130+
]
131+
resource_mock = AsyncIterList(volumes)
132+
133+
result = []
134+
async for vol in operator.filtered_volumes_for_cluster(
135+
resource_mock, "mycluster"
136+
):
137+
result.append(vol)
138+
139+
self.assertEqual(len(result), 2)
140+
self.assertEqual(result[0], volumes[0])
141+
self.assertEqual(result[1], volumes[3])
142+
143+
async def test_snapshots_for_cluster(self):
144+
snapshots = [
145+
mock.Mock(metadata={"cinder.csi.openstack.org/cluster": "mycluster"}),
146+
mock.Mock(metadata={"cinder.csi.openstack.org/cluster": "othercluster"}),
147+
mock.Mock(metadata={"cinder.csi.openstack.org/cluster": "mycluster"}),
148+
mock.Mock(metadata={"cinder.csi.openstack.org/cluster": "othercluster"}),
149+
# Volumes with invalid metadata
150+
mock.Mock(metadata={"another_key": "value"}),
151+
]
152+
resource_mock = AsyncIterList(snapshots)
153+
154+
result = []
155+
async for snap in operator.snapshots_for_cluster(resource_mock, "mycluster"):
156+
result.append(snap)
157+
158+
self.assertEqual(len(result), 2)
159+
self.assertEqual(result[0], snapshots[0])
160+
self.assertEqual(result[1], snapshots[2])
161+
162+
async def test_empty_iterator_returns_true(self):
163+
async_iter = AsyncIterList([]).__aiter__()
164+
result = await operator.empty(async_iter)
165+
self.assertTrue(result)
166+
167+
async def test_non_empty_iterator_returns_false(self):
168+
async_iter = AsyncIterList([1, 2, 3]).__aiter__()
169+
result = await operator.empty(async_iter)
170+
self.assertFalse(result)
171+
172+
async def test_try_delete_success(self):
173+
instances = [mock.Mock(id=1), mock.Mock(id=2)]
174+
resource = mock.Mock()
175+
resource.delete = mock.AsyncMock()
176+
resource.singular_name = "test_resource"
177+
logger = mock.Mock()
178+
179+
result = await operator.try_delete(logger, resource, AsyncIterList(instances))
180+
181+
self.assertTrue(result)
182+
resource.delete.assert_any_await(1)
183+
resource.delete.assert_any_await(2)
184+
logger.warn.assert_not_called()
185+
186+
async def test_try_delete_with_400_and_409_errors(self):
187+
instances = [mock.Mock(id=1), mock.Mock(id=2)]
188+
resource = mock.Mock()
189+
resource.singular_name = "test_resource"
190+
logger = mock.Mock()
191+
192+
resource.delete = mock.AsyncMock(
193+
side_effect=[
194+
httpx.HTTPStatusError(
195+
"error", request=mock.Mock(), response=mock.Mock(status_code=400)
196+
),
197+
httpx.HTTPStatusError(
198+
"error", request=mock.Mock(), response=mock.Mock(status_code=409)
199+
),
200+
]
201+
)
202+
result = await operator.try_delete(logger, resource, AsyncIterList(instances))
203+
204+
self.assertTrue(result)
205+
self.assertEqual(resource.delete.await_count, 2)
206+
self.assertEqual(logger.warn.call_count, 2)
207+
208+
async def test_delete_with_unexpected_error_raises(self):
209+
instances = [mock.Mock(id=1)]
210+
resource = mock.Mock()
211+
resource.singular_name = "test_resource"
212+
logger = mock.Mock()
213+
214+
resource.delete = mock.AsyncMock(
215+
side_effect=httpx.HTTPStatusError(
216+
"error", request=mock.Mock(), response=mock.Mock(status_code=500)
217+
)
218+
)
219+
with self.assertRaises(httpx.HTTPStatusError):
220+
await operator.try_delete(logger, resource, AsyncIterList(instances))
221+
22222
@mock.patch.object(operator, "patch_finalizers")
23223
@mock.patch.object(operator, "_get_os_cluster_client")
24224
async def test_on_openstackcluster_event_adds_finalizers(
@@ -147,47 +347,125 @@ async def test_on_openstackcluster_event_calls_purge(
147347
]
148348
)
149349

150-
@mock.patch.object(openstack, "Resource")
151-
async def test_user_keep_volumes_filter(self, mock_volumes_resource):
152-
# Arrange
153-
async def _list_volumes():
154-
test_volumes = [
155-
{
156-
"id": "123",
157-
"name": "volume-1",
158-
"metadata": {
159-
"cinder.csi.openstack.org/cluster": "cluster-1",
160-
OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY: "anything-but-true",
161-
},
162-
},
163-
{
164-
"id": "456",
165-
"name": "volume-2",
166-
"metadata": {
167-
"cinder.csi.openstack.org/cluster": "cluster-1",
168-
OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY: "true",
169-
},
170-
},
171-
{
172-
"id": "789",
173-
"name": "volume-3",
174-
"metadata": {
175-
"cinder.csi.openstack.org/cluster": "cluster-2",
176-
OPENSTACK_USER_VOLUMES_RECLAIM_PROPERTY: "true",
350+
@mock.patch.object(openstack.Cloud, "from_clouds")
351+
async def test_purge_openstack_resources_raises(self, mock_from_clouds):
352+
mock_networkapi = mock.AsyncMock()
353+
mock_networkapi.resource.side_effect = lambda resource: resource
354+
355+
mock_cloud = mock.AsyncMock()
356+
mock_cloud.__aenter__.return_value = mock_cloud
357+
mock_cloud.is_authenticated = False
358+
mock_cloud.current_user_id = "user"
359+
mock_cloud.api_client.return_value = mock_networkapi
360+
361+
mock_from_clouds.return_value = mock_cloud
362+
363+
logger = mock.Mock()
364+
clouds_yaml_data = {
365+
"clouds": {
366+
"openstack": {
367+
"auth": {
368+
"auth_url": "https://example.com:5000/v3",
369+
"application_credential_id": "user",
370+
"application_credential_secret": "pass",
177371
},
178-
},
179-
]
180-
for volume in map(PropertyDict, test_volumes):
181-
yield volume
182-
183-
mock_volumes_resource.list.return_value = _list_volumes()
184-
# Act
185-
filtered_volumes = [
186-
v
187-
async for v in operator.filtered_volumes_for_cluster(
188-
mock_volumes_resource, "cluster-1"
372+
"region_name": "RegionOne",
373+
"interface": "public",
374+
"identity_api_version": 3,
375+
"auth_type": "v3applicationcredential",
376+
}
377+
}
378+
}
379+
with self.assertRaises(openstack.AuthenticationError) as e:
380+
await operator.purge_openstack_resources(
381+
logger,
382+
clouds_yaml_data,
383+
"openstack",
384+
None,
385+
"mycluster",
386+
True,
387+
False,
189388
)
190-
]
191-
# Assert
192-
self.assertEqual(len(filtered_volumes), 1)
193-
self.assertEqual(filtered_volumes[0].get("name"), "volume-1")
389+
self.assertEqual(
390+
str(e.exception),
391+
"failed to authenticate as user: user",
392+
)
393+
394+
# @mock.patch.object(openstack.Cloud, "from_clouds")
395+
# async def test_purge_openstack_resources_success(self, mock_from_clouds):
396+
# # Mocking the cloud object and API clients
397+
# mock_cloud = mock.AsyncMock()
398+
# mock_networkapi = mock.AsyncMock()
399+
# mock_lbapi = mock.AsyncMock()
400+
# mock_volumeapi = mock.AsyncMock()
401+
# mock_identityapi = mock.AsyncMock()
402+
403+
# # Mock the __aenter__ to return the mock_cloud
404+
# mock_cloud.__aenter__.return_value = mock_cloud
405+
# mock_cloud.is_authenticated = True
406+
# mock_cloud.current_user_id = "user"
407+
# mock_cloud.api_client.return_value = mock_networkapi
408+
409+
# # Return mock clients for different services when requested
410+
# mock_from_clouds.return_value = mock_cloud
411+
412+
# # Mocking the resources for Network, Load Balancer, and Volume APIs
413+
# mock_networkapi.resource.side_effect = lambda resource: {
414+
# "floatingips": mock.AsyncMock(),
415+
# "security-groups": mock.AsyncMock(),
416+
# }.get(resource, None)
417+
418+
# mock_lbapi.resource.side_effect = lambda resource: {
419+
# "loadbalancers": mock.AsyncMock(),
420+
# }.get(resource, None)
421+
422+
# mock_volumeapi.resource.side_effect = lambda resource: {
423+
# "snapshots/detail": mock.AsyncMock(),
424+
# "snapshots": mock.AsyncMock(),
425+
# "volumes/detail": mock.AsyncMock(),
426+
# "volumes": mock.AsyncMock(),
427+
# }.get(resource, None)
428+
429+
# mock_identityapi.resource.side_effect = lambda resource: {
430+
# "application_credentials": mock.AsyncMock(),
431+
# }.get(resource, None)
432+
433+
# # Mock logger
434+
# logger = mock.Mock()
435+
436+
# clouds_yaml_data = {
437+
# "clouds": {
438+
# "openstack": {
439+
# "auth": {
440+
# "auth_url": "https://example.com:5000/v3",
441+
# "application_credential_id": "user",
442+
# "application_credential_secret": "pass",
443+
# },
444+
# "region_name": "RegionOne",
445+
# "interface": "public",
446+
# "identity_api_version": 3,
447+
# "auth_type": "v3applicationcredential",
448+
# }
449+
# }
450+
# }
451+
452+
# # Simulate the purge_openstack_resources method behavior
453+
# await operator.purge_openstack_resources(
454+
# logger,
455+
# clouds_yaml_data, # Pass the mock cloud config
456+
# "openstack",
457+
# None,
458+
# "mycluster",
459+
# True,
460+
# False,
461+
# )
462+
463+
# # Add assertions here based on expected behavior
464+
# # Example: Check that the resources were interacted with
465+
# mock_networkapi.resource.assert_any_call("floatingips")
466+
# mock_lbapi.resource.assert_any_call("loadbalancers")
467+
# mock_volumeapi.resource.assert_any_call("snapshots")
468+
# mock_volumeapi.resource.assert_any_call("volumes")
469+
470+
# # Example: Validate if appcred deletion was attempted
471+
# mock_identityapi.resource.assert_any_call("application_credentials")

tox.ini

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ commands = stestr run {posargs}
1717

1818
[testenv:pep8]
1919
commands =
20-
ruff check {tox_root}
2120
ruff format {tox_root}
2221
codespell {tox_root} -w
2322
flake8 {postargs}

0 commit comments

Comments
 (0)