# SPDX-FileCopyrightText: 2025 Alexander Kalinovsky # # SPDX-License-Identifier: Apache-2.0 """Integration tests for the CLI functionality.""" import sys import tempfile from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch import pytest from quickbot_cli.cli import _init_project # Add src to path for imports sys.path.insert(0, str(Path(__file__).parent.parent / "src")) class TestCLIIntegration: """Integration tests for the CLI.""" def _create_template_spec(self) -> dict[str, Any]: """Create a comprehensive template specification.""" return { "variables": { "project_name": {"prompt": "Project name", "default": "test_bot"}, "description": {"prompt": "Description", "default": "A test bot"}, "author": {"prompt": "Author", "default": "Test Author"}, "license": {"prompt": "License", "default": "MIT"}, "include_alembic": {"prompt": "Include Alembic?", "choices": ["yes", "no"], "default": "yes"}, "include_i18n": {"prompt": "Include i18n?", "choices": ["yes", "no"], "default": "yes"}, }, "post_tasks": [ {"when": "{{ include_alembic }}", "run": ["echo", "alembic_initialized"]}, {"when": "{{ include_i18n }}", "run": ["echo", "babel_initialized"]}, ], } def _create_template_files(self, template_dir: Path) -> None: """Create template files in the template directory.""" # Create app files (template_dir / "app").mkdir() (template_dir / "app" / "main.py.j2").write_text( "from fastapi import FastAPI\n\n" "app = FastAPI(title='{{ project_name }}', description='{{ description }}')\n\n" "if __name__ == '__main__':\n" " import uvicorn\n" " uvicorn.run(app, host='0.0.0.0', port=8000)\n" ) (template_dir / "app" / "config.py.j2").write_text( "PROJECT_NAME = '{{ project_name }}'\n" "DESCRIPTION = '{{ description }}'\n" "AUTHOR = '{{ author }}'\n" "LICENSE = '{{ license }}'\n" ) # Create root files (template_dir / "README.md.j2").write_text( "# {{ project_name }}\n\n{{ description }}\n\n## Author\n{{ author }}\n\n## License\n{{ license }}\n" ) (template_dir / "pyproject.toml.j2").write_text( "[project]\n" "name = '{{ project_name }}'\n" "description = '{{ description }}'\n" "authors = [{name = '{{ author }}'}]\n" "license = {text = '{{ license }}'}\n" ) def _create_optional_modules(self, template_dir: Path) -> None: """Create optional module files.""" # Create Alembic files (template_dir / "alembic").mkdir() (template_dir / "alembic" / "alembic.ini.j2").write_text( "[alembic]\n" "script_location = alembic\n" "sqlalchemy.url = postgresql://user:pass@localhost/{{ project_name }}\n" ) # Create Babel files (template_dir / "locales").mkdir() (template_dir / "locales" / "en").mkdir() (template_dir / "locales" / "en" / "LC_MESSAGES").mkdir() (template_dir / "locales" / "en" / "LC_MESSAGES" / "messages.po.j2").write_text( 'msgid ""\n' 'msgstr ""\n' '"Project-Id-Version: {{ project_name }}\\n"\n' '"Report-Msgid-Bugs-To: \\n"\n' '"POT-Creation-Date: 2024-01-01 00:00+0000\\n"\n' '"PO-Revision-Date: 2024-01-01 00:00+0000\\n"\n' '"Last-Translator: {{ author }}\\n"\n' '"Language: en\\n"\n' '"MIME-Version: 1.0\\n"\n' '"Content-Type: text/plain; charset=UTF-8\\n"\n' '"Content-Transfer-Encoding: 8bit\\n"\n' ) # Create scripts (template_dir / "scripts").mkdir() (template_dir / "scripts" / "migrations_generate.sh.j2").write_text( "#!/bin/bash\n" "echo 'Generate migrations for {{ project_name }}'\n" "alembic revision --autogenerate -m 'Auto-generated migration'\n" ) (template_dir / "scripts" / "babel_init.sh.j2").write_text( "#!/bin/bash\n" "echo 'Initialize Babel for {{ project_name }}'\n" "pybabel init -i messages.pot -d locales -l en\n" ) def _verify_output_structure(self, output_dir: Path) -> None: """Verify the output directory structure and content.""" # Check main app files assert (output_dir / "app" / "main.py").exists() assert (output_dir / "app" / "config.py").exists() assert (output_dir / "README.md").exists() assert (output_dir / "pyproject.toml").exists() # Check rendered content main_py = (output_dir / "app" / "main.py").read_text() assert "app = FastAPI(title='my_awesome_bot'" in main_py assert "description='My awesome bot description'" in main_py config_py = (output_dir / "app" / "config.py").read_text() assert "PROJECT_NAME = 'my_awesome_bot'" in config_py assert "AUTHOR = 'John Doe'" in config_py assert "LICENSE = 'Apache-2.0'" in config_py readme = (output_dir / "README.md").read_text() assert "# my_awesome_bot" in readme assert "My awesome bot description" in readme assert "John Doe" in readme assert "Apache-2.0" in readme # Check optional modules (should be included) assert (output_dir / "alembic" / "alembic.ini").exists() assert (output_dir / "locales" / "en" / "LC_MESSAGES" / "messages.po").exists() assert (output_dir / "scripts" / "migrations_generate.sh").exists() assert (output_dir / "scripts" / "babel_init.sh").exists() def test_full_project_generation(self) -> None: """Test full project generation with all components.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_path = Path(tmp_dir) # Mock the templates directory with patch("quickbot_cli.cli.TEMPLATES_DIR", tmp_path / "templates"): template_dir = tmp_path / "templates" / "basic" template_dir.mkdir(parents=True) # Create template spec and files spec_content = self._create_template_spec() spec_file = template_dir / "__template__.yaml" spec_file.write_text(str(spec_content)) self._create_template_files(template_dir) self._create_optional_modules(template_dir) # Mock subprocess.run to avoid actual command execution with patch("quickbot_cli.cli.subprocess.run") as mock_run: mock_run.return_value = MagicMock(returncode=0) output_path = tmp_path / "output" # Call init function directly with options _init_project( output_path, "basic", project_name="my_awesome_bot", description="My awesome bot description", author="John Doe", license_name="Apache-2.0", include_alembic=True, include_i18n=True, ) # Verify output output_dir = tmp_path / "output" assert output_dir.exists() self._verify_output_structure(output_dir) # Check that post tasks were called expected_post_tasks_count = 2 assert mock_run.call_count == expected_post_tasks_count mock_run.assert_any_call(["echo", "alembic_initialized"], cwd=output_dir, check=True) mock_run.assert_any_call(["echo", "babel_initialized"], cwd=output_dir, check=True) def test_project_generation_with_disabled_modules(self) -> None: """Test project generation with optional modules disabled.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_path = Path(tmp_dir) # Mock the templates directory with patch("quickbot_cli.cli.TEMPLATES_DIR", tmp_path / "templates"): template_dir = tmp_path / "templates" / "basic" template_dir.mkdir(parents=True) # Create template spec spec_content = { "variables": { "project_name": {"prompt": "Project name", "default": "simple_bot"}, "include_alembic": {"prompt": "Include Alembic?", "choices": ["yes", "no"], "default": "no"}, "include_i18n": {"prompt": "Include i18n?", "choices": ["yes", "no"], "default": "no"}, } } spec_file = template_dir / "__template__.yaml" spec_file.write_text(str(spec_content)) # Create template files (template_dir / "app").mkdir() (template_dir / "app" / "main.py.j2").write_text( "from fastapi import FastAPI\n\napp = FastAPI(title='{{ project_name }}')\n" ) # Create optional modules (should be removed) (template_dir / "alembic").mkdir() (template_dir / "alembic" / "alembic.ini.j2").write_text("alembic config") (template_dir / "locales").mkdir() (template_dir / "locales" / "en").mkdir() (template_dir / "scripts").mkdir() (template_dir / "scripts" / "migrations_generate.sh.j2").write_text("migration script") (template_dir / "scripts" / "babel_init.sh.j2").write_text("babel script") output_path = tmp_path / "output" # Call init function directly with options _init_project( output_path, "basic", project_name="simple_bot", include_alembic=False, include_i18n=False ) # Verify output directory structure output_dir = tmp_path / "output" assert output_dir.exists() # Check main app files assert (output_dir / "app" / "main.py").exists() # Check that optional modules were removed assert not (output_dir / "alembic").exists() assert not (output_dir / "locales").exists() assert not (output_dir / "scripts" / "migrations_generate.sh").exists() assert not (output_dir / "scripts" / "babel_init.sh").exists() def test_project_generation_with_overwrite(self) -> None: """Test project generation with overwrite enabled.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_path = Path(tmp_dir) # Mock the templates directory with patch("quickbot_cli.cli.TEMPLATES_DIR", tmp_path / "templates"): template_dir = tmp_path / "templates" / "basic" template_dir.mkdir(parents=True) # Create template spec spec_content = {"variables": {"project_name": {"prompt": "Project name", "default": "overwrite_test"}}} spec_file = template_dir / "__template__.yaml" spec_file.write_text(str(spec_content)) # Create template file in app subdirectory (template_dir / "app").mkdir() (template_dir / "app" / "main.py.j2").write_text("app = FastAPI(title='{{ project_name }}')\n") # Create output directory with existing file output_dir = tmp_path / "output" output_dir.mkdir() existing_file = output_dir / "app" / "main.py" existing_file.parent.mkdir(parents=True, exist_ok=True) existing_file.write_text("existing content") # Call init function directly with overwrite _init_project(output_dir, "basic", project_name="overwrite_test", overwrite=True) # Check that file was overwritten assert (output_dir / "app" / "main.py").exists() assert "app = FastAPI(title='overwrite_test')" in (output_dir / "app" / "main.py").read_text() assert "existing content" not in (output_dir / "app" / "main.py").read_text() def test_project_generation_without_overwrite_fails(self) -> None: """Test that project generation fails without overwrite when files exist.""" with tempfile.TemporaryDirectory() as tmp_dir: tmp_path = Path(tmp_dir) # Mock the templates directory with patch("quickbot_cli.cli.TEMPLATES_DIR", tmp_path / "templates"): template_dir = tmp_path / "templates" / "basic" template_dir.mkdir(parents=True) # Create template spec spec_content = {"variables": {"project_name": {"prompt": "Project name", "default": "overwrite_test"}}} spec_file = template_dir / "__template__.yaml" spec_file.write_text(str(spec_content)) # Create template file in app subdirectory (template_dir / "app").mkdir() (template_dir / "app" / "main.py.j2").write_text("app = FastAPI(title='{{ project_name }}')\n") # Create output directory with existing file output_dir = tmp_path / "output" output_dir.mkdir() existing_file = output_dir / "app" / "main.py" existing_file.parent.mkdir(parents=True, exist_ok=True) existing_file.write_text("existing content") # Should fail with FileExistsError when overwrite is False with pytest.raises(FileExistsError): _init_project(output_dir, "basic", project_name="overwrite_test") # Check that file was not overwritten assert "existing content" in (output_dir / "app" / "main.py").read_text()