Skip to content

Commit ab35ff1

Browse files
committed
nova: add tests for IronicUnderstackDriver and related classes
1 parent bba9037 commit ab35ff1

File tree

4 files changed

+1287
-0
lines changed

4 files changed

+1287
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Test package for nova-understack
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
"""Unit tests for ArgoClient."""
2+
3+
from unittest.mock import Mock
4+
from unittest.mock import mock_open
5+
from unittest.mock import patch
6+
7+
import pytest
8+
import requests
9+
10+
from ironic_understack.argo_client import ArgoClient
11+
12+
13+
class TestArgoClient:
14+
"""Test cases for ArgoClient class."""
15+
16+
def setup_method(self):
17+
"""Set up test fixtures."""
18+
self.base_url = "https://argo.example.com"
19+
self.token = "test-token"
20+
self.namespace = "test-namespace"
21+
22+
@patch("ironic_understack.argo_client.urllib3.disable_warnings")
23+
def test_init_with_token(self, mock_disable_warnings):
24+
"""Test ArgoClient initialization with provided token."""
25+
client = ArgoClient(
26+
url=self.base_url,
27+
token=self.token,
28+
namespace=self.namespace,
29+
ssl_verify=False,
30+
)
31+
32+
assert client.url == self.base_url
33+
assert client.token == self.token
34+
assert client.namespace == self.namespace
35+
assert not client.session.verify
36+
assert client.session.headers["Authorization"] == f"Bearer {self.token}"
37+
assert client.session.headers["Content-Type"] == "application/json"
38+
mock_disable_warnings.assert_called_once()
39+
40+
@patch("builtins.open", mock_open(read_data="k8s-token"))
41+
@patch("ironic_understack.argo_client.urllib3.disable_warnings")
42+
def test_init_without_token(self, mock_disable_warnings):
43+
"""Test ArgoClient initialization without token (uses k8s token)."""
44+
client = ArgoClient(
45+
url=self.base_url, token=None, namespace=self.namespace, ssl_verify=False
46+
)
47+
48+
assert client.token == "k8s-token"
49+
assert client.session.headers["Authorization"] == "Bearer k8s-token"
50+
51+
def test_init_with_ssl_verify(self):
52+
"""Test ArgoClient initialization with SSL verification enabled."""
53+
client = ArgoClient(url=self.base_url, token=self.token, ssl_verify=True)
54+
55+
assert client.session.verify is True
56+
57+
def test_url_stripping(self):
58+
"""Test that trailing slashes are stripped from URL."""
59+
client = ArgoClient(url="https://argo.example.com/", token=self.token)
60+
61+
assert client.url == "https://argo.example.com"
62+
63+
def test_generate_workflow_name(self):
64+
"""Test workflow name generation from playbook names."""
65+
client = ArgoClient(self.base_url, self.token)
66+
67+
# Test with .yml extension
68+
result = client._generate_workflow_name("storage_on_server_create.yml")
69+
assert result == "ansible-storage-on-server-create-"
70+
71+
# Test with .yaml extension
72+
result = client._generate_workflow_name("network_setup.yaml")
73+
assert result == "ansible-network-setup-"
74+
75+
# Test without extension
76+
result = client._generate_workflow_name("deploy_app")
77+
assert result == "ansible-deploy-app-"
78+
79+
# Test underscore replacement
80+
result = client._generate_workflow_name("test_playbook_name")
81+
assert result == "ansible-test-playbook-name-"
82+
83+
@patch("builtins.open", mock_open(read_data="k8s-service-token"))
84+
def test_kubernetes_token_property(self):
85+
"""Test reading Kubernetes service account token."""
86+
client = ArgoClient(self.base_url, None)
87+
88+
token = client._kubernetes_token
89+
assert token == "k8s-service-token"
90+
91+
@patch("requests.Session.post")
92+
@patch("requests.Session.get")
93+
def test_run_playbook_success(self, mock_get, mock_post):
94+
"""Test successful playbook execution."""
95+
client = ArgoClient(self.base_url, self.token)
96+
97+
# Mock workflow creation response
98+
workflow_response = {"metadata": {"name": "ansible-test-playbook-abc123"}}
99+
mock_post.return_value.json.return_value = workflow_response
100+
mock_post.return_value.raise_for_status = Mock()
101+
102+
# Mock workflow completion response
103+
completed_workflow = {
104+
"status": {"phase": "Succeeded"},
105+
"metadata": {"name": "ansible-test-playbook-abc123"},
106+
}
107+
mock_get.return_value.json.return_value = completed_workflow
108+
mock_get.return_value.raise_for_status = Mock()
109+
110+
result = client.run_playbook(
111+
"test_playbook.yml", device_id="device-123", project_id="project-456"
112+
)
113+
114+
# Verify workflow creation request
115+
expected_workflow_request = {
116+
"workflow": {
117+
"metadata": {"generateName": "ansible-test-playbook-"},
118+
"spec": {
119+
"workflowTemplateRef": {"name": "ansible-workflow-template"},
120+
"entrypoint": "ansible-run",
121+
"arguments": {
122+
"parameters": [
123+
{"name": "playbook", "value": "test_playbook.yml"},
124+
{
125+
"name": "extra_vars",
126+
"value": "device_id=device-123 project_id=project-456",
127+
},
128+
]
129+
},
130+
},
131+
}
132+
}
133+
134+
mock_post.assert_called_once_with(
135+
f"{self.base_url}/api/v1/workflows/{client.namespace}",
136+
json=expected_workflow_request,
137+
)
138+
139+
# Verify workflow monitoring
140+
mock_get.assert_called_with(
141+
f"{self.base_url}/api/v1/workflows/{client.namespace}/ansible-test-playbook-abc123"
142+
)
143+
144+
assert result == completed_workflow
145+
146+
@patch("requests.Session.post")
147+
def test_run_playbook_creation_failure(self, mock_post):
148+
"""Test playbook execution when workflow creation fails."""
149+
client = ArgoClient(self.base_url, self.token)
150+
151+
mock_post.return_value.raise_for_status.side_effect = requests.RequestException(
152+
"API Error"
153+
)
154+
155+
with pytest.raises(requests.RequestException):
156+
client.run_playbook("test_playbook.yml")
157+
158+
@patch("requests.Session.post")
159+
@patch("requests.Session.get")
160+
def test_run_playbook_with_empty_extra_vars(self, mock_get, mock_post):
161+
"""Test playbook execution with no extra variables."""
162+
client = ArgoClient(self.base_url, self.token)
163+
164+
workflow_response = {"metadata": {"name": "ansible-test-abc123"}}
165+
mock_post.return_value.json.return_value = workflow_response
166+
mock_post.return_value.raise_for_status = Mock()
167+
168+
completed_workflow = {"status": {"phase": "Succeeded"}}
169+
mock_get.return_value.json.return_value = completed_workflow
170+
mock_get.return_value.raise_for_status = Mock()
171+
172+
client.run_playbook("test_playbook.yml")
173+
174+
# Verify empty extra_vars string
175+
call_args = mock_post.call_args[1]["json"]
176+
extra_vars_param = call_args["workflow"]["spec"]["arguments"]["parameters"][1]
177+
assert extra_vars_param["value"] == ""
178+
179+
@patch("time.sleep")
180+
@patch("requests.Session.get")
181+
def test_wait_for_completion_success(self, mock_get, mock_sleep):
182+
"""Test successful workflow completion monitoring."""
183+
client = ArgoClient(self.base_url, self.token)
184+
185+
# Mock workflow status progression
186+
responses = [
187+
{"status": {"phase": "Running"}},
188+
{"status": {"phase": "Running"}},
189+
{"status": {"phase": "Succeeded"}},
190+
]
191+
mock_get.return_value.json.side_effect = responses
192+
mock_get.return_value.raise_for_status = Mock()
193+
194+
result = client._wait_for_completion("test-workflow")
195+
196+
assert result == responses[-1]
197+
assert mock_get.call_count == 3
198+
assert mock_sleep.call_count == 2
199+
200+
@patch("time.sleep")
201+
@patch("requests.Session.get")
202+
def test_wait_for_completion_failure(self, mock_get, mock_sleep):
203+
"""Test workflow failure during monitoring."""
204+
client = ArgoClient(self.base_url, self.token)
205+
206+
failed_workflow = {
207+
"status": {"phase": "Failed", "message": "Workflow execution failed"}
208+
}
209+
mock_get.return_value.json.return_value = failed_workflow
210+
mock_get.return_value.raise_for_status = Mock()
211+
212+
with pytest.raises(RuntimeError, match="Workflow test-workflow failed"):
213+
client._wait_for_completion("test-workflow")
214+
215+
@patch("time.sleep")
216+
@patch("requests.Session.get")
217+
def test_wait_for_completion_error(self, mock_get, mock_sleep):
218+
"""Test workflow error during monitoring."""
219+
client = ArgoClient(self.base_url, self.token)
220+
221+
error_workflow = {
222+
"status": {"phase": "Error", "message": "Workflow encountered an error"}
223+
}
224+
mock_get.return_value.json.return_value = error_workflow
225+
mock_get.return_value.raise_for_status = Mock()
226+
227+
with pytest.raises(
228+
RuntimeError, match="Workflow test-workflow encountered an error"
229+
):
230+
client._wait_for_completion("test-workflow")
231+
232+
@patch("time.time")
233+
@patch("time.sleep")
234+
@patch("requests.Session.get")
235+
def test_wait_for_completion_timeout(self, mock_get, mock_sleep, mock_time):
236+
"""Test workflow timeout during monitoring."""
237+
client = ArgoClient(self.base_url, self.token)
238+
239+
# Mock time progression to simulate timeout
240+
mock_time.side_effect = [0, 300, 700] # Start, middle, timeout
241+
242+
running_workflow = {"status": {"phase": "Running"}}
243+
mock_get.return_value.json.return_value = running_workflow
244+
mock_get.return_value.raise_for_status = Mock()
245+
246+
with pytest.raises(RuntimeError, match="Workflow test-workflow timed out"):
247+
client._wait_for_completion("test-workflow", timeout=600)
248+
249+
@patch("requests.Session.get")
250+
def test_wait_for_completion_api_error(self, mock_get):
251+
"""Test API error during workflow monitoring."""
252+
client = ArgoClient(self.base_url, self.token)
253+
254+
mock_get.return_value.raise_for_status.side_effect = requests.RequestException(
255+
"API Error"
256+
)
257+
258+
with pytest.raises(requests.RequestException):
259+
client._wait_for_completion("test-workflow")
260+
261+
@patch("time.sleep")
262+
@patch("requests.Session.get")
263+
def test_wait_for_completion_missing_status(self, mock_get, mock_sleep):
264+
"""Test workflow monitoring with missing status information."""
265+
client = ArgoClient(self.base_url, self.token)
266+
267+
# Mock workflow without status
268+
workflow_no_status = {"metadata": {"name": "test-workflow"}}
269+
mock_get.return_value.json.return_value = workflow_no_status
270+
mock_get.return_value.raise_for_status = Mock()
271+
272+
# Should continue polling when status is missing
273+
with patch("time.time", side_effect=[0, 700]): # Simulate timeout
274+
with pytest.raises(RuntimeError, match="timed out"):
275+
client._wait_for_completion(
276+
"test-workflow", timeout=600, poll_interval=1
277+
)

0 commit comments

Comments
 (0)