We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/kpeacocke/souschef'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Tests for the CLI module."""
import json
from pathlib import Path
import pytest
from click.testing import CliRunner
from souschef.cli import cli
# Define the fixtures directory
FIXTURES_DIR = (
Path(__file__).parents[1] / "integration" / "fixtures" / "sample_cookbook"
)
@pytest.fixture
def runner():
"""Create a CLI test runner."""
return CliRunner()
# Recipe command tests
def test_recipe_command_text_format(runner):
"""Test recipe command with text output format."""
recipe_path = FIXTURES_DIR / "recipes" / "default.rb"
result = runner.invoke(cli, ["recipe", str(recipe_path), "--format", "text"])
assert result.exit_code == 0
assert "Resource 1:" in result.output or "package" in result.output.lower()
def test_recipe_command_json_format(runner):
"""Test recipe command with JSON output format."""
recipe_path = FIXTURES_DIR / "recipes" / "default.rb"
result = runner.invoke(cli, ["recipe", str(recipe_path), "--format", "json"])
assert result.exit_code == 0
# Should be valid JSON
try:
data = json.loads(result.output)
assert isinstance(data, (dict, list))
except json.JSONDecodeError:
# Some outputs might be plain text
assert len(result.output) > 0
def test_recipe_command_nonexistent_file(runner, tmp_path):
"""Test recipe command with nonexistent file."""
nonexistent = tmp_path / "nonexistent" / "file.rb"
result = runner.invoke(cli, ["recipe", str(nonexistent)])
assert result.exit_code != 0
assert "does not exist" in result.output.lower()
# Template command tests
def test_template_command(runner):
"""Test template command."""
template_path = FIXTURES_DIR / "templates" / "default" / "config.yml.erb"
result = runner.invoke(cli, ["template", str(template_path)])
assert result.exit_code == 0
# Should contain JSON with variables and jinja2_template
try:
data = json.loads(result.output)
assert "variables" in data or "jinja2_template" in data
except json.JSONDecodeError:
# Output might be error message
assert len(result.output) > 0
# Attributes command tests
def test_attributes_command(runner):
"""Test attributes command."""
attributes_path = FIXTURES_DIR / "attributes" / "default.rb"
result = runner.invoke(cli, ["attributes", str(attributes_path)])
assert result.exit_code == 0
assert "Attribute" in result.output or "default[" in result.output
# Resource command tests
def test_resource_command(runner):
"""Test custom resource parsing command."""
resource_path = FIXTURES_DIR / "resources" / "simple.rb"
result = runner.invoke(cli, ["resource", str(resource_path)])
assert result.exit_code == 0
try:
data = json.loads(result.output)
assert "resource_type" in data or "properties" in data
except json.JSONDecodeError:
assert len(result.output) > 0
# Metadata command tests
def test_metadata_command(runner):
"""Test metadata parsing command."""
metadata_path = FIXTURES_DIR / "metadata.rb"
result = runner.invoke(cli, ["metadata", str(metadata_path)])
assert result.exit_code == 0
assert "name" in result.output.lower() or "version" in result.output.lower()
# Structure command tests
def test_structure_command(runner):
"""Test cookbook structure listing command."""
result = runner.invoke(cli, ["structure", str(FIXTURES_DIR)])
assert result.exit_code == 0
assert "recipes" in result.output.lower() or "cookbook" in result.output.lower()
# List directory command tests
def test_ls_command(runner):
"""Test directory listing command."""
result = runner.invoke(cli, ["ls", str(FIXTURES_DIR / "recipes")])
assert result.exit_code == 0
assert "default.rb" in result.output
def test_ls_command_nonexistent_dir(runner, tmp_path):
"""Test ls command with nonexistent directory."""
nonexistent = tmp_path / "nonexistent" / "directory"
result = runner.invoke(cli, ["ls", str(nonexistent)])
assert result.exit_code != 0
# Cat command tests
def test_cat_command(runner):
"""Test file reading command."""
metadata_path = FIXTURES_DIR / "metadata.rb"
result = runner.invoke(cli, ["cat", str(metadata_path)])
assert result.exit_code == 0
assert "name" in result.output
# Convert command tests
def test_convert_command_default(runner):
"""Test resource conversion with defaults."""
result = runner.invoke(cli, ["convert", "package", "nginx"])
assert result.exit_code == 0
assert "ansible.builtin.package" in result.output or "name: nginx" in result.output
def test_convert_command_with_action(runner):
"""Test resource conversion with custom action."""
result = runner.invoke(cli, ["convert", "service", "nginx", "--action", "start"])
assert result.exit_code == 0
assert (
"ansible.builtin.service" in result.output or "state: started" in result.output
)
def test_convert_command_json_format(runner):
"""Test resource conversion with JSON output."""
result = runner.invoke(cli, ["convert", "package", "nginx", "--format", "json"])
assert result.exit_code == 0
# Should be valid JSON
try:
data = json.loads(result.output)
assert isinstance(data, (dict, list))
except json.JSONDecodeError:
# Might need PyYAML installed
assert "Warning:" in result.output or len(result.output) > 0
# Cookbook command tests
def test_cookbook_command(runner):
"""Test full cookbook analysis."""
result = runner.invoke(cli, ["cookbook", str(FIXTURES_DIR)])
assert result.exit_code == 0
assert "Analysing cookbook" in result.output
assert "Metadata" in result.output or "Structure" in result.output
def test_cookbook_command_with_dry_run(runner):
"""Test cookbook analysis with dry-run."""
result = runner.invoke(cli, ["cookbook", str(FIXTURES_DIR), "--dry-run"])
assert result.exit_code == 0
assert "Analysing cookbook" in result.output
def test_cookbook_command_with_output(runner, tmp_path):
"""Test cookbook analysis with output directory."""
output_dir = tmp_path / "output"
result = runner.invoke(
cli, ["cookbook", str(FIXTURES_DIR), "--output", str(output_dir)]
)
assert result.exit_code == 0
# Now actually converts and saves files
assert "Conversion complete" in result.output
assert output_dir.exists()
# Check that output directory contains converted files
assert (output_dir / "README.md").exists()
assert (output_dir / "conversion_summary.json").exists()
def test_v2_migrate_command(runner, tmp_path):
"""Test v2 migrate command with JSON output."""
from unittest.mock import MagicMock, patch
cookbook = tmp_path / "cookbook"
recipes = cookbook / "recipes"
recipes.mkdir(parents=True)
(recipes / "default.rb").write_text("package 'curl'")
env = {"SOUSCHEF_DB_PATH": str(tmp_path / "souschef.db")}
# Mock migration result
mock_result = MagicMock()
mock_result.to_dict.return_value = {
"migration_id": "test-mig-001",
"target_platform": "aap",
"target_version": "2.4.0",
"chef_version": "15.10.91",
"status": "converted",
"playbooks_generated": ["main.yml"],
}
mock_orchestrator = MagicMock()
mock_orchestrator.migrate_cookbook.return_value = mock_result
with patch(
"souschef.cli_v2_commands.MigrationOrchestrator", return_value=mock_orchestrator
):
result = runner.invoke(
cli,
[
"v2",
"migrate",
"--cookbook-path",
str(cookbook),
"--chef-version",
"15.10.91",
"--target-platform",
"aap",
"--target-version",
"2.4.0",
"--skip-validation",
"--format",
"json",
],
env=env,
)
assert result.exit_code == 0, f"Migration failed: {result.output}"
payload = json.loads(result.output)
assert "migration_id" in payload
assert payload["target_platform"] == "aap"
def test_v2_status_command(runner, tmp_path):
"""Test v2 status command loading stored state."""
from unittest.mock import MagicMock, patch
cookbook = tmp_path / "cookbook"
recipes = cookbook / "recipes"
recipes.mkdir(parents=True)
(recipes / "default.rb").write_text("package 'curl'")
env = {"SOUSCHEF_DB_PATH": str(tmp_path / "souschef.db")}
# Mock migration result - use a simple dict subclass that's JSON serializable
migration_data = {
"migration_id": "test-migration-123",
"target_platform": "aap",
"target_version": "2.4.0",
"chef_version": "15.10.91",
"status": "converted",
"playbooks_generated": ["main.yml"],
"metrics": {"recipes_converted": 1, "recipes_total": 1},
}
mock_result = MagicMock()
mock_result.to_dict.return_value = migration_data
mock_orchestrator = MagicMock()
mock_orchestrator.migrate_cookbook.return_value = mock_result
mock_orchestrator.save_state.return_value = "test-storage-id-123"
with patch(
"souschef.cli_v2_commands.MigrationOrchestrator", return_value=mock_orchestrator
):
migrate_result = runner.invoke(
cli,
[
"v2",
"migrate",
"--cookbook-path",
str(cookbook),
"--chef-version",
"15.10.91",
"--target-platform",
"aap",
"--target-version",
"2.4.0",
"--skip-validation",
"--save-state",
"--format",
"json",
],
env=env,
)
assert migrate_result.exit_code == 0, f"Migration failed: {migrate_result.output}"
migrate_payload = json.loads(migrate_result.output)
migration_id = migrate_payload["migration_id"]
assert migration_id == "test-migration-123"
# Version and help tests
def test_version_flag(runner):
"""Test --version flag."""
result = runner.invoke(cli, ["--version"])
assert result.exit_code == 0
assert "souschef" in result.output.lower()
def test_help_flag(runner):
"""Test --help flag."""
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "Usage:" in result.output
assert "recipe" in result.output
def test_command_help(runner):
"""Test help for specific command."""
result = runner.invoke(cli, ["recipe", "--help"])
assert result.exit_code == 0
assert "Parse a Chef recipe" in result.output
# Edge cases and error handling
def test_invalid_command(runner):
"""Test running invalid command."""
result = runner.invoke(cli, ["invalid_command"])
assert result.exit_code != 0
assert "Error:" in result.output or "No such command" in result.output
# Chef Server CLI Tests
def test_query_chef_nodes_command_missing_env(runner):
"""Test query-chef-nodes command without required environment."""
result = runner.invoke(
cli,
[
"query-chef-nodes",
"--search-query",
"*:*",
],
)
assert result.exit_code != 0
assert "CHEF_SERVER_URL" in result.output
# Template Conversion CLI Tests
def test_convert_template_ai_command_real(runner, tmp_path):
"""Test convert-template-ai command with real template input."""
erb_file = tmp_path / "config.erb"
erb_file.write_text("<%= @hostname %>")
result = runner.invoke(
cli,
[
"convert-template-ai",
str(erb_file),
"--no-ai",
],
)
assert result.exit_code == 0
assert "Conversion successful" in result.output
def test_convert_template_ai_command_with_output(runner, tmp_path):
"""Test convert-template-ai command writes output file."""
erb_file = tmp_path / "app.conf.erb"
erb_file.write_text("<%= @app_port %>")
output_file = tmp_path / "app.conf.j2"
result = runner.invoke(
cli,
[
"convert-template-ai",
str(erb_file),
"--no-ai",
"--output",
str(output_file),
],
)
assert result.exit_code == 0
assert output_file.exists()
def test_profile_command_error_handling(runner, monkeypatch):
"""Test profile command error handling."""
# Mock the profiling function to raise an exception
def mock_generate(*args, **kwargs):
raise RuntimeError("Mock profiling error")
monkeypatch.setattr(
"souschef.cli.generate_cookbook_performance_report", mock_generate
)
result = runner.invoke(
cli,
["profile", str(FIXTURES_DIR)],
)
assert result.exit_code != 0
assert "Error profiling cookbook" in result.output
def test_profile_operation_command_error_handling(runner, monkeypatch):
"""Test profile-operation command error handling."""
# Mock the profiling function to raise an exception
def mock_profile(*args, **kwargs):
raise RuntimeError("Mock profiling error")
import souschef.cli as cli_module
monkeypatch.setattr(cli_module, "profile_function", mock_profile)
recipe_path = FIXTURES_DIR / "recipes" / "default.rb"
result = runner.invoke(
cli,
["profile-operation", "recipe", str(recipe_path)],
)
assert result.exit_code != 0
assert "Error profiling operation" in result.output
# Migration configuration command tests
def test_configure_migration_with_args(runner):
"""Test configure-migration with CLI arguments (non-interactive mode)."""
result = runner.invoke(
cli,
[
"configure-migration",
"--deployment-target",
"awx",
],
)
assert result.exit_code == 0
# Should output JSON configuration
assert "deployment_target" in result.output
assert "awx" in result.output
def test_configure_migration_cli_args(runner):
"""Test configure-migration with CLI arguments."""
result = runner.invoke(
cli,
[
"configure-migration",
"--deployment-target",
"native",
"--migration-standard",
"flat",
"--python-version",
"3.11",
],
)
assert result.exit_code == 0
# Should contain JSON output
assert "deployment_target" in result.output
assert "native" in result.output
assert "flat" in result.output
assert "3.11" in result.output
def test_configure_migration_with_output_file(runner, tmp_path):
"""Test configure-migration with output file."""
output_file = tmp_path / "config.json"
result = runner.invoke(
cli,
[
"configure-migration",
"--deployment-target",
"awx",
"--output",
str(output_file),
],
)
assert result.exit_code == 0
assert "Configuration saved" in result.output
assert output_file.exists()
# Verify file content is valid JSON
config_data = json.loads(output_file.read_text())
assert config_data["deployment_target"] == "awx"
assert "migration_standard" in config_data
def test_configure_migration_multiple_validation_tools(runner):
"""Test configure-migration with multiple validation tools."""
result = runner.invoke(
cli,
[
"configure-migration",
"--deployment-target",
"native", # Add required arg
"--validation-tools",
"ansible-lint",
"--validation-tools",
"molecule",
"--validation-tools",
"tox-ansible",
],
)
assert result.exit_code == 0
assert "ansible-lint" in result.output
assert "molecule" in result.output
assert "tox-ansible" in result.output
def test_configure_migration_all_options(runner, tmp_path):
"""Test configure-migration with all CLI options."""
output_file = tmp_path / "full-config.json"
result = runner.invoke(
cli,
[
"configure-migration",
"--deployment-target",
"aap",
"--migration-standard",
"hybrid",
"--inventory-source",
"static-file",
"--validation-tools",
"molecule",
"--python-version",
"3.12",
"--ansible-version",
"2.15",
"--output",
str(output_file),
],
)
assert result.exit_code == 0
assert output_file.exists()
config_data = json.loads(output_file.read_text())
assert config_data["deployment_target"] == "aap"
assert config_data["migration_standard"] == "hybrid"
assert config_data["inventory_source"] == "static-file"
assert "molecule" in config_data["validation_tools"]
assert config_data["target_python_version"] == "3.12"
assert config_data["target_ansible_version"] == "2.15"
# Ansible Upgrade CLI Tests
def test_ansible_plan_command_missing_current_version(runner):
"""Test ansible plan requires current-version parameter."""
result = runner.invoke(cli, ["ansible", "plan", "--target-version", "2.16"])
assert result.exit_code != 0
assert "Missing option" in result.output or "required" in result.output.lower()
def test_ansible_plan_command_missing_target_version(runner):
"""Test ansible plan requires target-version parameter."""
result = runner.invoke(cli, ["ansible", "plan", "--current-version", "2.14"])
assert result.exit_code != 0
assert "Missing option" in result.output or "required" in result.output.lower()
def test_ansible_eol_command_missing_version(runner):
"""Test ansible eol requires version parameter."""
result = runner.invoke(cli, ["ansible", "eol"])
assert result.exit_code != 0
assert "Missing option" in result.output or "required" in result.output.lower()
def test_ansible_assess_command_real(runner, tmp_path, monkeypatch):
"""Test ansible assess command with real environment input."""
env_path = tmp_path / "ansible_env"
env_path.mkdir()
# Mock version detection in the ansible_upgrade module where it's used
def mock_detect_version(*args, **kwargs):
return "2.16.0"
import souschef.ansible_upgrade
monkeypatch.setattr(
souschef.ansible_upgrade, "detect_ansible_version", mock_detect_version
)
result = runner.invoke(
cli, ["ansible", "assess", "--environment-path", str(env_path)]
)
assert result.exit_code == 0
assert "Ansible Environment Assessment" in result.output
def test_ansible_plan_command_real(runner):
"""Test ansible plan command with real data."""
result = runner.invoke(
cli,
["ansible", "plan", "--current-version", "2.14", "--target-version", "2.16"],
)
assert result.exit_code == 0
assert "Upgrade Plan" in result.output
def test_ansible_eol_command_real(runner):
"""Test ansible eol command with real version data."""
result = runner.invoke(cli, ["ansible", "eol", "--version", "2.16"])
assert result.exit_code == 0
assert "EOL Status" in result.output
def test_ansible_validate_collections_command_real(runner, tmp_path):
"""Test ansible validate-collections command with a real file."""
requirements_file = tmp_path / "requirements.yml"
requirements_file.write_text(
"""
collections:
- name: community.general
version: ">=7.0.0"
- name: ansible.posix
"""
)
result = runner.invoke(
cli,
[
"ansible",
"validate-collections",
"--collections-file",
str(requirements_file),
"--target-version",
"2.16",
],
)
assert result.exit_code == 0
assert "Collection Compatibility Report" in result.output
def test_ansible_validate_collections_nonexistent_file(runner, tmp_path):
"""Test ansible validate-collections with nonexistent file."""
nonexistent = tmp_path / "nonexistent.yml"
result = runner.invoke(
cli,
[
"ansible",
"validate-collections",
"--collections-file",
str(nonexistent),
"--target-version",
"2.16",
],
)
assert result.exit_code != 0
def test_ansible_detect_python_command_real(runner):
"""Test ansible detect-python command with real interpreter."""
result = runner.invoke(cli, ["ansible", "detect-python"])
assert result.exit_code == 0
assert "Python Version" in result.output
def test_ansible_detect_python_with_environment_path(runner, tmp_path):
"""Test ansible detect-python with custom environment path."""
env_path = tmp_path / "ansible_venv"
env_path.mkdir()
result = runner.invoke(
cli, ["ansible", "detect-python", "--environment-path", str(env_path)]
)
assert result.exit_code == 0
assert "Python Version" in result.output
def test_ansible_group_help(runner):
"""Test ansible group help command."""
result = runner.invoke(cli, ["ansible", "--help"])
assert result.exit_code == 0
assert "assess" in result.output
assert "plan" in result.output
assert "eol" in result.output
assert "validate-collections" in result.output
assert "detect-python" in result.output