Skip to content

Commit 7cf3412

Browse files
Copilotalvarolopez
andcommitted
Improve code coverage by adding comprehensive tests for PR #150 functionality
Co-authored-by: alvarolopez <[email protected]>
1 parent 078746e commit 7cf3412

File tree

3 files changed

+298
-2
lines changed

3 files changed

+298
-2
lines changed

caso/tests/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
_TRUE_VALUES = ("True", "true", "1", "yes")
3232

3333

34-
class TestCase(unittest.TestCase):
34+
class TestCase(fixtures.TestWithFixtures, unittest.TestCase):
3535
"""Test case base class for all unit tests."""
3636

3737
REQUIRES_LOCKING = False

caso/tests/extract/test_manager.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from unittest import mock
2626

2727
from caso.extract import manager
28+
import caso.manager # Import to register the spooldir option
2829

2930
CONF = cfg.CONF
3031

@@ -36,6 +37,7 @@ def setUp(self):
3637
"""Run before each test method to initialize test environment."""
3738
super(TestCasoManager, self).setUp()
3839
self.flags(extractor="mock")
40+
self.flags(spooldir="/tmp/caso_test")
3941
self.p_extractors = mock.patch("caso.loading.get_available_extractors")
4042
patched = self.p_extractors.start()
4143
self.records = [{uuid.uuid4().hex: None}]
@@ -147,6 +149,86 @@ def test_lastrun_is_invalid(self):
147149
path.return_value = True
148150
self.assertRaises(ValueError, self.manager.get_lastrun, "foo")
149151

152+
def test_get_records_with_invalid_lastrun_continues(self):
153+
"""Test that get_records continues when lastrun file is invalid."""
154+
self.flags(projects=["project1", "project2"])
155+
156+
# Mock the file system operations to test exception handling path
157+
with unittest.mock.patch.object(self.manager, "get_project_vo", return_value="test-vo") as m_vo:
158+
with unittest.mock.patch('os.path.exists', return_value=True):
159+
# One project gets invalid date, one gets valid date
160+
file_contents = ["invalid-date-format", "2020-01-01 00:00:00"]
161+
mock_file = unittest.mock.mock_open()
162+
mock_file.return_value.read.side_effect = file_contents
163+
164+
with unittest.mock.patch('builtins.open', mock_file):
165+
ret = self.manager.get_records()
166+
167+
# Should call get_project_vo for both projects since VO lookup happens before lastrun
168+
self.assertEqual(m_vo.call_count, 2)
169+
m_vo.assert_any_call("project1")
170+
m_vo.assert_any_call("project2")
171+
172+
# One project should succeed - doesn't matter which one for this test
173+
# The key is that get_records continues processing despite one project failing
174+
self.m_extractor.assert_called_once()
175+
args, kwargs = self.m_extractor.call_args
176+
self.assertEqual(args[1], "test-vo") # VO should be correct
177+
self.assertIn(args[0], ["project1", "project2"]) # Should be one of the projects
178+
179+
def test_get_records_with_future_extract_from_continues(self):
180+
"""Test that get_records continues when extract_from is in the future."""
181+
self.flags(projects=["project1", "project2"])
182+
future_date = datetime.datetime.now(tz.tzutc()) + datetime.timedelta(days=1)
183+
past_date = datetime.datetime.now(tz.tzutc()) - datetime.timedelta(days=1)
184+
185+
# Mock file system operations and return dates
186+
with unittest.mock.patch.object(self.manager, "get_project_vo", return_value="test-vo") as m_vo:
187+
with unittest.mock.patch('os.path.exists', return_value=True):
188+
# One project gets future date, one gets past date
189+
file_contents = [str(future_date), str(past_date)]
190+
mock_file = unittest.mock.mock_open()
191+
mock_file.return_value.read.side_effect = file_contents
192+
193+
with unittest.mock.patch('builtins.open', mock_file):
194+
ret = self.manager.get_records()
195+
196+
# Should call get_project_vo for both projects since VO lookup happens before lastrun
197+
self.assertEqual(m_vo.call_count, 2)
198+
m_vo.assert_any_call("project1")
199+
m_vo.assert_any_call("project2")
200+
201+
# One project should succeed - doesn't matter which one for this test
202+
# The key is that get_records continues processing despite one project failing
203+
self.m_extractor.assert_called_once()
204+
args, kwargs = self.m_extractor.call_args
205+
self.assertEqual(args[1], "test-vo") # VO should be correct
206+
self.assertIn(args[0], ["project1", "project2"]) # Should be one of the projects
207+
208+
def test_get_lastrun_invalid_date_logs_exception(self):
209+
"""Test that get_lastrun properly logs exceptions when date parsing fails."""
210+
if six.PY3:
211+
builtins_open = "builtins.open"
212+
else:
213+
builtins_open = "__builtin__.open"
214+
fopen = unittest.mock.mock_open(read_data="invalid-date-format")
215+
216+
with unittest.mock.patch("os.path.exists") as path:
217+
with unittest.mock.patch(builtins_open, fopen):
218+
with unittest.mock.patch("caso.extract.manager.LOG") as mock_log:
219+
path.return_value = True
220+
221+
with self.assertRaises(ValueError):
222+
self.manager.get_lastrun("test-project")
223+
224+
# Verify that both error and exception were logged
225+
mock_log.error.assert_called_once()
226+
mock_log.exception.assert_called_once()
227+
228+
# Check the error message contains the expected text
229+
error_call = mock_log.error.call_args[0][0]
230+
self.assertIn("Cannot read date from lastrun file", error_call)
231+
150232
def test_write_lastrun_dry_run(self):
151233
"""Test that we do not write lastrun file on dry run."""
152234
self.flags(dry_run=True)
@@ -166,7 +248,7 @@ def test_write_lastrun(self):
166248

167249
with unittest.mock.patch(builtins_open, unittest.mock.mock_open()) as m:
168250
self.manager.get_records()
169-
m.assert_called_once_with("/var/spool/caso/lastrun.bazonk", "w")
251+
m.assert_called_once_with("/tmp/caso_test/lastrun.bazonk", "w")
170252

171253
def flags(self, **kw):
172254
"""Override flag variables for a test."""

caso/tests/test_cmd_projects.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
"""Tests for `caso._cmd.projects` module."""
16+
17+
import sys
18+
import unittest
19+
from unittest import mock
20+
21+
from oslo_config import cfg
22+
23+
from caso._cmd import projects
24+
from caso.tests import base
25+
26+
CONF = cfg.CONF
27+
28+
29+
class TestProjectsCommand(base.TestCase):
30+
"""Test case for the projects command module."""
31+
32+
def setUp(self):
33+
"""Set up test fixtures."""
34+
super(TestProjectsCommand, self).setUp()
35+
36+
@mock.patch('caso.config.parse_args')
37+
@mock.patch('oslo_log.log.setup')
38+
@mock.patch('caso.manager.Manager')
39+
def test_main_success(self, mock_manager_cls, mock_log_setup, mock_parse_args):
40+
"""Test main function with successful project retrieval."""
41+
# Setup mocks
42+
mock_manager = mock.MagicMock()
43+
mock_manager_cls.return_value = mock_manager
44+
mock_manager.projects_and_vos.return_value = [('project1', 'vo1'), ('project2', 'vo2')]
45+
46+
# Mock keystone project retrieval
47+
mock_project = mock.MagicMock()
48+
mock_project.name = 'Project One'
49+
mock_manager.extractor_manager.keystone.projects.get.return_value = mock_project
50+
51+
with mock.patch('builtins.print') as mock_print:
52+
projects.main()
53+
54+
# Verify setup calls
55+
mock_parse_args.assert_called_once_with(sys.argv)
56+
mock_log_setup.assert_called_once_with(cfg.CONF, "caso")
57+
mock_manager_cls.assert_called_once()
58+
59+
# Verify manager methods called
60+
mock_manager.projects_and_vos.assert_called_once()
61+
62+
# Verify print was called with expected output format
63+
self.assertTrue(mock_print.called)
64+
print_calls = [call[0][0] for call in mock_print.call_args_list]
65+
self.assertIn("'project1 (Project One) mapped to VO 'vo1'", print_calls)
66+
67+
@mock.patch('caso.config.parse_args')
68+
@mock.patch('oslo_log.log.setup')
69+
@mock.patch('caso.manager.Manager')
70+
def test_main_with_keystone_error(self, mock_manager_cls, mock_log_setup, mock_parse_args):
71+
"""Test main function when keystone project retrieval fails."""
72+
# Setup mocks
73+
mock_manager = mock.MagicMock()
74+
mock_manager_cls.return_value = mock_manager
75+
mock_manager.projects_and_vos.return_value = [('project1', 'vo1')]
76+
77+
# Mock keystone to raise an exception
78+
mock_manager.extractor_manager.keystone.projects.get.side_effect = Exception("Keystone error")
79+
80+
with mock.patch('builtins.print') as mock_print:
81+
projects.main()
82+
83+
# Verify error messages were printed
84+
print_calls = [call[0][0] for call in mock_print.call_args_list]
85+
error_messages = [call for call in print_calls if call.startswith("ERROR:")]
86+
self.assertTrue(len(error_messages) >= 2) # Should have at least 2 error messages
87+
self.assertTrue(any("Could not get project project1" in msg for msg in error_messages))
88+
89+
@mock.patch('caso.config.parse_args')
90+
@mock.patch('oslo_log.log.setup')
91+
@mock.patch('caso.manager.Manager')
92+
def test_migrate_dry_run_mode(self, mock_manager_cls, mock_log_setup, mock_parse_args):
93+
"""Test migrate function in dry run mode."""
94+
# Setup mocks
95+
mock_manager = mock.MagicMock()
96+
mock_manager_cls.return_value = mock_manager
97+
mock_manager.extractor_manager.voms_map.items.return_value = [('project1', 'vo1')]
98+
99+
# Set dry_run flag
100+
self.flags(dry_run=True, vo_property='caso_vo')
101+
with mock.patch('builtins.print') as mock_print:
102+
projects.migrate()
103+
104+
# Verify setup calls
105+
mock_parse_args.assert_called_once_with(sys.argv)
106+
mock_log_setup.assert_called_once_with(cfg.CONF, "caso")
107+
mock_manager._load_managers.assert_called_once()
108+
109+
# Verify print statements for dry run
110+
print_calls = [call[0][0] for call in mock_print.call_args_list]
111+
warning_printed = any("WARNING: Running in 'dry-run' mode" in call for call in print_calls)
112+
self.assertTrue(warning_printed)
113+
114+
# Should print the openstack command but not execute it
115+
openstack_cmd_printed = any("openstack project set --property" in call for call in print_calls)
116+
self.assertTrue(openstack_cmd_printed)
117+
118+
@mock.patch('caso.config.parse_args')
119+
@mock.patch('oslo_log.log.setup')
120+
@mock.patch('caso.manager.Manager')
121+
def test_migrate_with_projects_dry_run(self, mock_manager_cls, mock_log_setup, mock_parse_args):
122+
"""Test migrate function with project migration in dry run mode."""
123+
# Setup mocks
124+
mock_manager = mock.MagicMock()
125+
mock_manager_cls.return_value = mock_manager
126+
mock_manager.extractor_manager.voms_map.items.return_value = []
127+
128+
# Set flags for project migration
129+
self.flags(dry_run=True, migrate_projects=True, projects=['project1'], caso_tag='caso')
130+
with mock.patch('builtins.print') as mock_print:
131+
projects.migrate()
132+
133+
# Verify print statements for project tagging
134+
print_calls = [call[0][0] for call in mock_print.call_args_list]
135+
tag_cmd_printed = any("openstack project set --tag caso project1" in call for call in print_calls)
136+
self.assertTrue(tag_cmd_printed)
137+
138+
@mock.patch('caso.config.parse_args')
139+
@mock.patch('oslo_log.log.setup')
140+
@mock.patch('caso.manager.Manager')
141+
def test_migrate_actual_execution(self, mock_manager_cls, mock_log_setup, mock_parse_args):
142+
"""Test migrate function with actual execution (not dry run)."""
143+
# Setup mocks
144+
mock_manager = mock.MagicMock()
145+
mock_manager_cls.return_value = mock_manager
146+
mock_manager.extractor_manager.voms_map.items.return_value = [('project1', 'vo1')]
147+
148+
# Mock successful keystone update
149+
mock_manager.extractor_manager.keystone.projects.update.return_value = None
150+
151+
# Set flags for actual execution
152+
self.flags(dry_run=False, vo_property='caso_vo')
153+
with mock.patch('builtins.print') as mock_print:
154+
projects.migrate()
155+
156+
# Verify keystone update was called
157+
mock_manager.extractor_manager.keystone.projects.update.assert_called_once_with(
158+
'project1', caso_vo='vo1'
159+
)
160+
161+
# Should not print warning about dry run
162+
print_calls = [call[0][0] for call in mock_print.call_args_list]
163+
warning_printed = any("WARNING: Running in 'dry-run' mode" in call for call in print_calls)
164+
self.assertFalse(warning_printed)
165+
166+
@mock.patch('caso.config.parse_args')
167+
@mock.patch('oslo_log.log.setup')
168+
@mock.patch('caso.manager.Manager')
169+
def test_migrate_keystone_update_error(self, mock_manager_cls, mock_log_setup, mock_parse_args):
170+
"""Test migrate function when keystone update fails."""
171+
# Setup mocks
172+
mock_manager = mock.MagicMock()
173+
mock_manager_cls.return_value = mock_manager
174+
mock_manager.extractor_manager.voms_map.items.return_value = [('project1', 'vo1')]
175+
176+
# Mock keystone update to raise an exception
177+
mock_manager.extractor_manager.keystone.projects.update.side_effect = Exception("Update failed")
178+
179+
# Set flags for actual execution
180+
self.flags(dry_run=False, vo_property='caso_vo')
181+
with mock.patch('builtins.print') as mock_print:
182+
projects.migrate()
183+
184+
# Verify error messages were printed
185+
print_calls = [call[0][0] for call in mock_print.call_args_list]
186+
error_messages = [call for call in print_calls if call.startswith("ERROR:")]
187+
self.assertTrue(len(error_messages) >= 2) # Should have at least 2 error messages
188+
self.assertTrue(any("could not add property for project project1" in msg for msg in error_messages))
189+
190+
@mock.patch('caso.config.parse_args')
191+
@mock.patch('oslo_log.log.setup')
192+
@mock.patch('caso.manager.Manager')
193+
def test_migrate_project_tagging_error(self, mock_manager_cls, mock_log_setup, mock_parse_args):
194+
"""Test migrate function when project tagging fails."""
195+
# Setup mocks
196+
mock_manager = mock.MagicMock()
197+
mock_manager_cls.return_value = mock_manager
198+
mock_manager.extractor_manager.voms_map.items.return_value = []
199+
200+
# Mock project get and tag operations
201+
mock_project = mock.MagicMock()
202+
mock_project.add_tag.side_effect = Exception("Tag failed")
203+
mock_manager.extractor_manager.keystone.projects.get.return_value = mock_project
204+
205+
# Set flags for project migration
206+
self.flags(dry_run=False, migrate_projects=True, projects=['project1'], caso_tag='caso')
207+
with mock.patch('builtins.print') as mock_print:
208+
projects.migrate()
209+
210+
# Verify error messages were printed
211+
print_calls = [call[0][0] for call in mock_print.call_args_list]
212+
error_messages = [call for call in print_calls if call.startswith("ERROR:")]
213+
self.assertTrue(len(error_messages) >= 2) # Should have at least 2 error messages
214+
self.assertTrue(any("could not add tag for project project1" in msg for msg in error_messages))

0 commit comments

Comments
 (0)