This commit is contained in:
318
tests/test_integration.py
Normal file
318
tests/test_integration.py
Normal file
@@ -0,0 +1,318 @@
|
||||
# SPDX-FileCopyrightText: 2025 Alexander Kalinovsky <a@k8y.ru>
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
"""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()
|
||||
Reference in New Issue
Block a user