"""Tests to improve cli.py coverage targeting error paths and edge cases."""
import json
from unittest.mock import MagicMock, patch
import pytest
from click.testing import CliRunner
from souschef.cli import cli
@pytest.fixture
def runner():
"""Create a CLI test runner."""
return CliRunner()
class TestListDirectoryCommand:
"""Test ls command error handling."""
def test_ls_with_error_result(self, runner, tmp_path):
"""Test ls command when list_directory returns error string."""
with patch("souschef.cli.list_directory") as mock_list:
mock_list.return_value = "Error: Permission denied"
result = runner.invoke(cli, ["ls", str(tmp_path)])
assert result.exit_code == 1
assert "Error: Permission denied" in result.output
class TestConvertCommand:
"""Test convert command variations."""
def test_convert_with_json_output_import_error(self, runner):
"""Test convert command with JSON output when PyYAML not available."""
with patch("souschef.cli.convert_resource_to_task") as mock_convert:
mock_convert.return_value = "- name: test"
with patch.dict("sys.modules", {"yaml": None}):
result = runner.invoke(
cli,
["convert", "package", "nginx", "--format", "json"],
)
assert result.exit_code == 0
def test_convert_with_json_output_parse_error(self, runner):
"""Test convert with JSON output when YAML parsing fails."""
with patch("souschef.cli.convert_resource_to_task") as mock_convert:
mock_convert.return_value = "invalid: yaml: ]["
result = runner.invoke(
cli,
["convert", "package", "nginx", "--format", "json"],
)
assert result.exit_code == 0
class TestJenkinsfileCommand:
"""Test generate-jenkinsfile command."""
def test_jenkinsfile_with_parallel_option(self, runner, tmp_path):
"""Test Jenkinsfile generation with parallel execution."""
cookbook_path = tmp_path / "cookbook"
cookbook_path.mkdir()
(cookbook_path / "metadata.rb").write_text("name 'test'\nversion '1.0.0'")
output_file = tmp_path / "Jenkinsfile"
with patch("souschef.cli.generate_jenkinsfile_from_chef") as mock_generate:
mock_generate.return_value = "pipeline { }"
result = runner.invoke(
cli,
[
"generate-jenkinsfile",
str(cookbook_path),
"--output",
str(output_file),
"--parallel",
],
)
assert result.exit_code == 0
# Check that the function was called with parallel enabled
mock_generate.assert_called_once()
def test_jenkinsfile_without_parallel_option(self, runner, tmp_path):
"""Test Jenkinsfile generation without parallel execution."""
cookbook_path = tmp_path / "cookbook"
cookbook_path.mkdir()
(cookbook_path / "metadata.rb").write_text("name 'test'\nversion '1.0.0'")
output_file = tmp_path / "Jenkinsfile"
with patch("souschef.cli.generate_jenkinsfile_from_chef") as mock_generate:
mock_generate.return_value = "pipeline { }"
result = runner.invoke(
cli,
[
"generate-jenkinsfile",
str(cookbook_path),
"--output",
str(output_file),
"--no-parallel",
],
)
assert result.exit_code == 0
# Check that function was called with parallel disabled
mock_generate.assert_called_once()
def test_jenkinsfile_generation_error(self, runner, tmp_path):
"""Test Jenkinsfile generation error handling."""
cookbook_path = tmp_path / "cookbook"
cookbook_path.mkdir()
output_file = tmp_path / "Jenkinsfile"
with patch("souschef.cli.generate_jenkinsfile_from_chef") as mock_generate:
mock_generate.side_effect = RuntimeError("CI generation failed")
result = runner.invoke(
cli,
[
"generate-jenkinsfile",
str(cookbook_path),
"--output",
str(output_file),
],
)
assert result.exit_code == 1
assert "Error" in result.output
class TestHabitatConversionCommand:
"""Test convert-habitat command."""
def test_habitat_not_a_file(self, runner, tmp_path):
"""Test Habitat conversion when path is not a file."""
plan_dir = tmp_path / "plans"
plan_dir.mkdir()
output_dir = tmp_path / "docker"
result = runner.invoke(
cli,
[
"convert-habitat",
"--plan-path",
str(plan_dir),
"--output-path",
str(output_dir),
],
)
assert result.exit_code == 1
assert "is not a file" in result.output
def test_habitat_invalid_output_parent(self, runner, tmp_path):
"""Test Habitat conversion with invalid output parent directory."""
plan_file = tmp_path / "plan.sh"
plan_file.write_text("pkg_name=nginx")
# Create an output path with non-existent parent
output_dir = tmp_path / "nonexistent" / "subdir" / "docker"
result = runner.invoke(
cli,
[
"convert-habitat",
"--plan-path",
str(plan_file),
"--output-path",
str(output_dir),
],
)
assert result.exit_code == 1
assert "Error converting Habitat plan" in result.output
def test_habitat_os_error_output_path(self, runner, tmp_path):
"""Test Habitat conversion with OS error on output path."""
plan_file = tmp_path / "plan.sh"
plan_file.write_text("pkg_name=nginx")
with patch("souschef.cli.Path.resolve") as mock_resolve:
mock_resolve.side_effect = OSError("Permission denied")
result = runner.invoke(
cli,
[
"convert-habitat",
"--plan-path",
str(plan_file),
"--output-path",
"/docker",
],
)
assert result.exit_code == 1
def test_habitat_conversion_success(self, runner, tmp_path):
"""Test successful Habitat conversion."""
plan_file = tmp_path / "plan.sh"
plan_file.write_text("pkg_name=nginx")
output_dir = tmp_path / "docker"
with patch("souschef.server.convert_habitat_to_dockerfile") as mock_convert:
mock_convert.return_value = "FROM ubuntu:22.04\nRUN echo 'hello'"
result = runner.invoke(
cli,
[
"convert-habitat",
"--plan-path",
str(plan_file),
"--output-path",
str(output_dir),
],
)
assert result.exit_code == 0
assert "Successfully converted" in result.output
assert "Dockerfile size:" in result.output
class TestInspecConversionCommand:
"""Test convert-inspec command."""
def test_inspec_format_options(self, runner, tmp_path):
"""Test InSpec conversion with different format options."""
profile_dir = tmp_path / "profile"
profile_dir.mkdir()
(profile_dir / "inspec.yml").write_text("name: test")
output_dir = tmp_path / "tests"
formats = ["testinfra", "serverspec", "goss", "ansible"]
for fmt in formats:
with patch("souschef.cli.convert_inspec_to_test") as mock_conv:
mock_conv.return_value = "Test content"
result = runner.invoke(
cli,
[
"convert-inspec",
"--profile-path",
str(profile_dir),
"--output-path",
str(output_dir),
"--format",
fmt,
],
)
# Should succeed
assert result.exit_code == 0
class TestHistoryCommands:
"""Test history commands."""
def test_history_list_analysis_only(self, runner):
"""Test history list for analysis only."""
with patch("souschef.storage.get_storage_manager") as mock_storage:
mock_manager = MagicMock()
mock_analysis = MagicMock()
mock_analysis.id = 1
mock_analysis.cookbook_name = "nginx"
mock_analysis.cookbook_version = "1.0.0"
mock_analysis.complexity = "medium"
mock_analysis.estimated_hours = 40.0
mock_analysis.estimated_hours_with_souschef = 8.0
mock_analysis.created_at = "2024-01-01"
mock_manager.get_analysis_history.return_value = [mock_analysis]
mock_storage.return_value = mock_manager
result = runner.invoke(
cli, ["history", "list", "--type", "analysis", "--limit", "10"]
)
assert result.exit_code == 0
assert "Analysis History" in result.output
assert "nginx" in result.output
def test_history_list_conversion_only(self, runner):
"""Test history list for conversions only."""
with patch("souschef.storage.get_storage_manager") as mock_storage:
mock_manager = MagicMock()
mock_conversion = MagicMock()
mock_conversion.id = 1
mock_conversion.cookbook_name = "nginx"
mock_conversion.output_type = "ansible"
mock_conversion.status = "completed"
mock_conversion.files_generated = 5
mock_conversion.created_at = "2024-01-01"
mock_manager.get_conversion_history.return_value = [mock_conversion]
mock_storage.return_value = mock_manager
result = runner.invoke(
cli, ["history", "list", "--type", "conversion", "--limit", "10"]
)
assert result.exit_code == 0
assert "Conversion History" in result.output
assert "nginx" in result.output
def test_history_list_both_types(self, runner):
"""Test history list for both analysis and conversion."""
with patch("souschef.storage.get_storage_manager") as mock_storage:
mock_manager = MagicMock()
mock_manager.get_analysis_history.return_value = []
mock_manager.get_conversion_history.return_value = []
mock_storage.return_value = mock_manager
result = runner.invoke(
cli, ["history", "list", "--type", "both", "--limit", "10"]
)
assert result.exit_code == 0
assert "Analysis History" in result.output
assert "Conversion History" in result.output
assert "No analysis history found" in result.output
assert "No conversion history found" in result.output
def test_history_list_with_cookbook_filter(self, runner):
"""Test history list with cookbook filter."""
with patch("souschef.storage.get_storage_manager") as mock_storage:
mock_manager = MagicMock()
mock_manager.get_analysis_history.return_value = []
mock_storage.return_value = mock_manager
result = runner.invoke(
cli,
[
"history",
"list",
"--type",
"analysis",
"--limit",
"5",
"--cookbook",
"nginx",
],
)
assert result.exit_code == 0
def test_history_delete_analysis(self, runner):
"""Test deleting analysis history record."""
with patch("souschef.storage.get_storage_manager") as mock_storage:
mock_manager = MagicMock()
mock_storage.return_value = mock_manager
result = runner.invoke(
cli, ["history", "delete", "--type", "analysis", "--id", "1"]
)
# Should succeed or fail gracefully
assert result.exit_code in [0, 1]
def test_history_delete_conversion(self, runner):
"""Test deleting conversion history record."""
with patch("souschef.storage.get_storage_manager") as mock_storage:
mock_manager = MagicMock()
mock_storage.return_value = mock_manager
result = runner.invoke(
cli, ["history", "delete", "--type", "conversion", "--id", "1"]
)
# Should succeed or fail gracefully
assert result.exit_code in [0, 1]
class TestSummaryDisplayFunctions:
"""Test summary display helper functions."""
def test_display_recipe_summary(self, runner, tmp_path):
"""Test recipe summary display."""
cookbook_dir = tmp_path / "cookbook"
recipes_dir = cookbook_dir / "recipes"
recipes_dir.mkdir(parents=True)
(recipes_dir / "default.rb").write_text("package 'nginx'")
with patch("souschef.cli.parse_recipe") as mock_parse:
mock_parse.return_value = "\n".join([f"Line {i}" for i in range(15)])
result = runner.invoke(cli, ["cookbook", str(cookbook_dir)])
# Function should be called during cookbook command
assert result.exit_code == 0
class TestEdgeCases:
"""Test edge cases and error conditions."""
def test_convert_resource_with_empty_properties(self, runner):
"""Test convert command with empty properties string."""
with patch("souschef.cli.convert_resource_to_task") as mock_convert:
mock_convert.return_value = "- name: test"
result = runner.invoke(
cli,
["convert", "package", "nginx", "--properties", ""],
)
assert result.exit_code == 0
def test_convert_resource_json_output_success(self, runner):
"""Test convert command with valid YAML->JSON conversion."""
with patch("souschef.cli.convert_resource_to_task") as mock_convert:
mock_convert.return_value = "name: test\nvalue: 123"
result = runner.invoke(
cli,
["convert", "package", "nginx", "--format", "json"],
)
assert result.exit_code == 0
# Should be valid JSON
if result.output.strip():
try:
data = json.loads(result.output)
assert isinstance(data, dict)
except json.JSONDecodeError:
# Output might be YAML if PyYAML import fails
pass