initial release
Some checks failed
CI / test (3.13) (push) Failing after 22s

This commit is contained in:
Alexander Kalinovsky
2025-08-25 19:45:21 +03:00
parent cc067ec78d
commit 2954913673
36 changed files with 2890 additions and 11 deletions

318
tests/test_integration.py Normal file
View 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()