"""Unit tests for docker service utilities.""" from unittest.mock import MagicMock, patch import logging from src.services.docker.container import ( get_container_id, get_container_name, ) from src.services.docker.compose import sort_volumes_by_specificity class TestGetContainerId: """Tests for get_container_id.""" @patch("subprocess.run") def test_lowercases_name_for_filter(self, mock_run) -> None: """Docker inspect is case-sensitive; we must lowercase the name.""" mock_run.return_value = MagicMock(returncode=0, stdout="abc123\n") result = get_container_id("MyContainer-ABC") assert result == "abc123" call_args = mock_run.call_args[0][0] # Exact inspect call uses lowercase assert call_args == ["docker", "inspect", "-f", "{{.Id}}", "mycontainer-abc"] @patch("subprocess.run") def test_returns_none_when_not_found(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="") result = get_container_id("missing") assert result is None class TestGetContainerName: """Tests for get_container_name.""" @patch("subprocess.run") def test_lowercases_name_for_filter(self, mock_run) -> None: """Docker inspect is case-sensitive; we must lowercase the name.""" mock_run.return_value = MagicMock(returncode=0, stdout="mycontainer-abc\n") result = get_container_name("MyContainer-ABC") assert result == "mycontainer-abc" call_args = mock_run.call_args[0][0] assert call_args == ["docker", "inspect", "-f", "{{.Name}}", "mycontainer-abc"] @patch("subprocess.run") def test_returns_none_when_not_found(self, mock_run) -> None: mock_run.return_value = MagicMock(returncode=0, stdout="") result = get_container_name("missing") assert result is None class TestSortVolumesBySpecificity: """Tests for sort_volumes_by_specificity.""" def test_parent_before_child(self) -> None: """A repo mount to /workspace/x should come before a file mount to /workspace/x/y/config.json.""" volumes = [ "/repo/x/y/config.json:/workspace/x/y/config.json", "/repo/x:/workspace/x", ] result = sort_volumes_by_specificity(volumes) assert result[0] == "/repo/x:/workspace/x" assert result[1] == "/repo/x/y/config.json:/workspace/x/y/config.json" def test_stable_sort_for_equal_depth(self) -> None: """Mounts at the same depth preserve input order.""" volumes = [ "/a:/workspace/a", "/b:/workspace/b", "/c:/workspace/c", ] result = sort_volumes_by_specificity(volumes) assert result == volumes def test_with_type_suffix(self) -> None: """Volume strings with :bind or :ro suffixes are parsed correctly.""" volumes = [ "/repo/x/y/config.json:/workspace/x/y/config.json:bind", "/repo/x:/workspace/x:bind", ] result = sort_volumes_by_specificity(volumes) assert result[0] == "/repo/x:/workspace/x:bind" assert result[1] == "/repo/x/y/config.json:/workspace/x/y/config.json:bind" def test_empty_list(self) -> None: """Empty list returns empty list.""" assert sort_volumes_by_specificity([]) == [] def test_single_volume(self) -> None: """Single volume returns unchanged.""" volumes = ["/repo:/workspace"] assert sort_volumes_by_specificity(volumes) == volumes def test_duplicate_target_warning(self, caplog) -> None: """Duplicate targets trigger a warning.""" with caplog.at_level(logging.WARNING, logger="src.services.docker"): volumes = [ "/a:/workspace/x", "/b:/workspace/x", ] sort_volumes_by_specificity(volumes) assert "Duplicate mount targets detected" in caplog.text assert "/workspace/x" in caplog.text