#!/usr/bin/env python3 """ Unit tests for ContainerManager (api/container_manager.py). """ import sys import unittest from pathlib import Path from unittest.mock import patch, MagicMock, PropertyMock api_dir = Path(__file__).parent.parent / 'api' sys.path.insert(0, str(api_dir)) from container_manager import ContainerManager # --------------------------------------------------------------------------- # Helper to build a ContainerManager with a pre-wired mock Docker client # --------------------------------------------------------------------------- def _make_manager(mock_from_env): """Return a ContainerManager whose Docker client is mock_from_env's return.""" mock_client = MagicMock() mock_from_env.return_value = mock_client return ContainerManager(), mock_client class TestListContainers(unittest.TestCase): @patch('docker.from_env') def test_list_containers(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_container.id = 'abc' mock_container.name = 'test' mock_container.status = 'running' mock_container.image.tags = ['img'] mock_container.labels = {} mock_client.containers.list.return_value = [mock_container] result = mgr.list_containers() self.assertEqual(result[0]['name'], 'test') class TestStartStopRestart(unittest.TestCase): @patch('docker.from_env') def test_start_stop_restart_container(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_client.containers.get.return_value = mock_container # Start self.assertTrue(mgr.start_container('test')) mock_container.start.assert_called_once() # Stop self.assertTrue(mgr.stop_container('test')) mock_container.stop.assert_called_once() # Restart self.assertTrue(mgr.restart_container('test')) mock_container.restart.assert_called_once() # Exception cases mock_client.containers.get.side_effect = Exception('fail') self.assertFalse(mgr.start_container('bad')) self.assertFalse(mgr.stop_container('bad')) self.assertFalse(mgr.restart_container('bad')) class TestGetContainerLogs(unittest.TestCase): @patch('docker.from_env') def test_get_container_logs_returns_string(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_container.logs.return_value = b'log line 1\nlog line 2\n' mock_client.containers.get.return_value = mock_container result = mgr.get_container_logs('mycontainer') self.assertIsInstance(result, str) self.assertIn('log line 1', result) @patch('docker.from_env') def test_get_container_logs_uses_tail_parameter(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_container.logs.return_value = b'' mock_client.containers.get.return_value = mock_container mgr.get_container_logs('mycontainer', tail=50) mock_container.logs.assert_called_once_with(tail=50) @patch('docker.from_env') def test_get_container_logs_raises_when_docker_unavailable(self, mock_from_env): mock_from_env.side_effect = Exception('docker not found') with self.assertRaises(Exception): mgr = ContainerManager() mgr.get_container_logs('test') class TestGetContainerStats(unittest.TestCase): @patch('docker.from_env') def test_get_container_stats_returns_dict(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_container.stats.return_value = { 'cpu_stats': {'cpu_usage': {'total_usage': 123}}, 'memory_stats': {'usage': 4096}, } mock_client.containers.get.return_value = mock_container result = mgr.get_container_stats('mycontainer') self.assertIsInstance(result, dict) self.assertIn('cpu_stats', result) self.assertIn('memory_stats', result) @patch('docker.from_env') def test_get_container_stats_returns_error_dict_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.containers.get.side_effect = Exception('not found') result = mgr.get_container_stats('nonexistent') self.assertIsInstance(result, dict) self.assertIn('error', result) class TestCreateContainer(unittest.TestCase): @patch('docker.from_env') def test_create_container_returns_id_and_name(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_container.id = 'cid123' mock_container.name = 'myapp' mock_client.containers.create.return_value = mock_container result = mgr.create_container( image='nginx:latest', name='myapp', env={'ENV_VAR': 'value'}, volumes={'/host/path': '/container/path'}, command='nginx -g "daemon off;"', ports={'80/tcp': 8080}, ) self.assertEqual(result['id'], 'cid123') self.assertEqual(result['name'], 'myapp') @patch('docker.from_env') def test_create_container_returns_error_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.containers.create.side_effect = Exception('image not found') result = mgr.create_container(image='nonexistent:latest', name='test') self.assertIn('error', result) @patch('docker.from_env') def test_create_container_passes_env_to_docker(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_container.id = 'x' mock_container.name = 'y' mock_client.containers.create.return_value = mock_container mgr.create_container(image='alpine', name='test', env={'KEY': 'VAL'}) _, kwargs = mock_client.containers.create.call_args self.assertEqual(kwargs['environment'], {'KEY': 'VAL'}) class TestRemoveContainer(unittest.TestCase): @patch('docker.from_env') def test_remove_container_returns_true_on_success(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_container = MagicMock() mock_client.containers.get.return_value = mock_container result = mgr.remove_container('mycontainer') self.assertTrue(result) mock_container.remove.assert_called_once() @patch('docker.from_env') def test_remove_container_returns_false_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.containers.get.side_effect = Exception('not found') result = mgr.remove_container('ghost') self.assertFalse(result) class TestPullImage(unittest.TestCase): @patch('docker.from_env') def test_pull_image_returns_id_and_tags(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_image = MagicMock() mock_image.id = 'sha256:abc' mock_image.tags = ['nginx:latest'] mock_client.images.pull.return_value = mock_image result = mgr.pull_image('nginx:latest') self.assertEqual(result['id'], 'sha256:abc') self.assertEqual(result['tags'], ['nginx:latest']) @patch('docker.from_env') def test_pull_image_returns_error_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.images.pull.side_effect = Exception('pull access denied') result = mgr.pull_image('private/image:latest') self.assertIn('error', result) class TestRemoveImage(unittest.TestCase): @patch('docker.from_env') def test_remove_image_returns_true_on_success(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) result = mgr.remove_image('nginx:latest') self.assertTrue(result) mock_client.images.remove.assert_called_once_with(image='nginx:latest', force=False) @patch('docker.from_env') def test_remove_image_returns_false_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.images.remove.side_effect = Exception('image in use') result = mgr.remove_image('nginx:latest') self.assertFalse(result) class TestCreateVolume(unittest.TestCase): @patch('docker.from_env') def test_create_volume_returns_name_and_mountpoint(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_vol = MagicMock() mock_vol.name = 'myvolume' mock_vol.attrs = {'Mountpoint': '/var/lib/docker/volumes/myvolume/_data'} mock_client.volumes.create.return_value = mock_vol result = mgr.create_volume('myvolume') self.assertEqual(result['name'], 'myvolume') self.assertIn('mountpoint', result) self.assertIn('myvolume', result['mountpoint']) @patch('docker.from_env') def test_create_volume_returns_error_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.volumes.create.side_effect = Exception('no space left') result = mgr.create_volume('bigvolume') self.assertIn('error', result) class TestRemoveVolume(unittest.TestCase): @patch('docker.from_env') def test_remove_volume_returns_true_on_success(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_vol = MagicMock() mock_client.volumes.get.return_value = mock_vol result = mgr.remove_volume('myvolume') self.assertTrue(result) mock_vol.remove.assert_called_once() @patch('docker.from_env') def test_remove_volume_returns_false_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.volumes.get.side_effect = Exception('volume not found') result = mgr.remove_volume('ghostvolume') self.assertFalse(result) class TestListImages(unittest.TestCase): @patch('docker.from_env') def test_list_images_returns_list(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_img = MagicMock() mock_img.id = 'sha256:abc' mock_img.tags = ['nginx:latest'] mock_img.short_id = 'abc123' mock_client.images.list.return_value = [mock_img] result = mgr.list_images() self.assertIsInstance(result, list) self.assertEqual(len(result), 1) self.assertEqual(result[0]['id'], 'sha256:abc') @patch('docker.from_env') def test_list_images_returns_empty_list_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.images.list.side_effect = Exception('daemon unreachable') result = mgr.list_images() self.assertEqual(result, []) class TestListVolumes(unittest.TestCase): @patch('docker.from_env') def test_list_volumes_returns_list(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_vol = MagicMock() mock_vol.name = 'vol1' mock_vol.attrs = {'Mountpoint': '/mnt/vol1'} mock_client.volumes.list.return_value = [mock_vol] result = mgr.list_volumes() self.assertIsInstance(result, list) self.assertEqual(result[0]['name'], 'vol1') self.assertEqual(result[0]['mountpoint'], '/mnt/vol1') @patch('docker.from_env') def test_list_volumes_returns_empty_list_on_exception(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.volumes.list.side_effect = Exception('daemon unreachable') result = mgr.list_volumes() self.assertEqual(result, []) class TestGetStatusWhenDockerUnavailable(unittest.TestCase): @patch('docker.from_env') def test_get_status_offline_when_docker_init_fails(self, mock_from_env): """ContainerManager.get_status() returns {running: False, status: 'offline'} when Docker client could not be initialised.""" mock_from_env.side_effect = Exception('Cannot connect to Docker daemon') mgr = ContainerManager() self.assertFalse(mgr.docker_available) status = mgr.get_status() self.assertFalse(status['running']) self.assertEqual(status['status'], 'offline') @patch('docker.from_env') def test_get_status_online_when_docker_available(self, mock_from_env): mgr, mock_client = _make_manager(mock_from_env) mock_client.containers.list.return_value = [] mock_client.images.list.return_value = [] mock_client.volumes.list.return_value = [] mock_client.info.return_value = { 'ServerVersion': '24.0.0', 'Containers': 0, 'Images': 0, 'Driver': 'overlay2', 'KernelVersion': '6.1.0', 'OperatingSystem': 'Linux', } status = mgr.get_status() self.assertTrue(status['running']) self.assertEqual(status['status'], 'online') if __name__ == '__main__': unittest.main()