319 lines
14 KiB
Python
319 lines
14 KiB
Python
# SPDX-FileCopyrightText: 2025 Alexander Kalinovsky <a@k8y.ru>
|
|
#
|
|
# 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()
|