Coverage for tests/test_new.py: 100%
114 statements
« prev ^ index » next coverage.py v7.11.2, created at 2026-11-04 19:57 +0000
« prev ^ index » next coverage.py v7.11.2, created at 2026-11-04 19:57 +0000
1import shutil 1abcdefgh
2import subprocess 1abcdefgh
3from pathlib import Path 1abcdefgh
4from typing import Any 1abcdefgh
6import pytest 1abcdefgh
7from typer.testing import CliRunner 1abcdefgh
9from fastapi_new.cli import app 1abcdefgh
11runner = CliRunner() 1abcdefgh
14@pytest.fixture 1abcdefgh
15def temp_project_dir(tmp_path: Path, monkeypatch: Any) -> Path: 1abcdefgh
16 """Create a temporary directory and cd into it."""
17 monkeypatch.chdir(tmp_path) 1abcdefgh
18 return tmp_path 1abcdefgh
21def _assert_project_created(project_path: Path) -> None: 1abcdefgh
22 assert (project_path / "main.py").exists() 1yiWXzYAjZ0B1Ck23D4El56F7Gm89H!In#$J%Ko'(L)Mp*+N,
23 assert (project_path / "README.md").exists() 1yiWXzYAjZ0B1Ck23D4El56F7Gm89H!In#$J%Ko'(L)Mp*+N,
24 assert (project_path / "pyproject.toml").exists() 1yiWXzYAjZ0B1Ck23D4El56F7Gm89H!In#$J%Ko'(L)Mp*+N,
27def test_creates_project_successfully(temp_project_dir: Path) -> None: 1abcdefgh
28 result = runner.invoke(app, ["my_fastapi_project"]) 1yACEGIKM
30 assert result.exit_code == 0 1yACEGIKM
31 project_path = temp_project_dir / "my_fastapi_project" 1yACEGIKM
32 _assert_project_created(project_path) 1yACEGIKM
33 assert "Success!" in result.output 1yACEGIKM
34 assert "my_fastapi_project" in result.output 1yACEGIKM
37def test_creates_project_with_python_version(temp_project_dir: Path) -> None: 1abcdefgh
38 # Test long form
39 result = runner.invoke(app, ["project_long", "--python", "3.12"]) 1ijklmnop
40 assert result.exit_code == 0 1ijklmnop
41 project_path = temp_project_dir / "project_long" 1ijklmnop
42 _assert_project_created(project_path) 1ijklmnop
43 assert "3.12" in (project_path / "pyproject.toml").read_text() 1ijklmnop
45 # Test short form
46 result = runner.invoke(app, ["project_short", "-p", "3.11"]) 1ijklmnop
47 assert result.exit_code == 0 1ijklmnop
48 project_path = temp_project_dir / "project_short" 1ijklmnop
49 assert "3.11" in (project_path / "pyproject.toml").read_text() 1ijklmnop
52def test_validates_template_file_contents(temp_project_dir: Path) -> None: 1abcdefgh
53 result = runner.invoke(app, ["sample_project"]) 1qrstuvwx
54 assert result.exit_code == 0 1qrstuvwx
56 project_path = temp_project_dir / "sample_project" 1qrstuvwx
58 main_py_content = (project_path / "main.py").read_text() 1qrstuvwx
59 assert "from fastapi import FastAPI" in main_py_content 1qrstuvwx
60 assert "app = FastAPI()" in main_py_content 1qrstuvwx
62 # Check README.md
63 readme_content = (project_path / "README.md").read_text() 1qrstuvwx
64 assert "# sample_project" in readme_content 1qrstuvwx
65 assert "A project created with FastAPI" in readme_content 1qrstuvwx
67 # Check pyproject.toml
68 pyproject_content = (project_path / "pyproject.toml").read_text() 1qrstuvwx
69 assert 'name = "sample-project"' in pyproject_content 1qrstuvwx
70 assert "fastapi[standard]" in pyproject_content 1qrstuvwx
73def test_initializes_in_current_directory(temp_project_dir: Path) -> None: 1abcdefgh
74 result = runner.invoke(app, []) 1X0369$(+
76 assert result.exit_code == 0 1X0369$(+
77 assert "Initializing in current directory" in result.output 1X0369$(+
78 _assert_project_created(temp_project_dir) 1X0369$(+
81def test_initializes_in_current_directory_with_dot(temp_project_dir: Path) -> None: 1abcdefgh
82 result = runner.invoke(app, ["."]) 1zBDFHJLN
84 assert result.exit_code == 0 1zBDFHJLN
85 assert "Initializing in current directory" in result.output 1zBDFHJLN
86 _assert_project_created(temp_project_dir) 1zBDFHJLN
88 readme_content = (temp_project_dir / "README.md").read_text() 1zBDFHJLN
89 assert f"# {temp_project_dir.name}" in readme_content 1zBDFHJLN
92def test_rejects_existing_directory(temp_project_dir: Path) -> None: 1abcdefgh
93 existing_dir = temp_project_dir / "existing_project" 2~ abbbcbdbebfbgb
94 existing_dir.mkdir() 2~ abbbcbdbebfbgb
96 result = runner.invoke(app, ["existing_project"]) 2~ abbbcbdbebfbgb
97 assert result.exit_code == 1 2~ abbbcbdbebfbgb
98 assert "Directory 'existing_project' already exists." in result.output 2~ abbbcbdbebfbgb
101def test_rejects_python_below_3_10(temp_project_dir: Path) -> None: 1abcdefgh
102 result = runner.invoke(app, ["test_project", "--python", "3.9"]) 2pbqbrbsbtbubvbwb
103 assert result.exit_code == 1 2pbqbrbsbtbubvbwb
104 assert "Python 3.9 is not supported" in result.output 2pbqbrbsbtbubvbwb
105 assert "FastAPI requires Python 3.10" in result.output 2pbqbrbsbtbubvbwb
108def test_passes_single_digit_python_version_to_uv(temp_project_dir: Path) -> None: 1abcdefgh
109 result = runner.invoke(app, ["test_project", "--python", "3"]) 1Y147!%),
110 assert result.exit_code == 0 1Y147!%),
111 project_path = temp_project_dir / "test_project" 1Y147!%),
112 _assert_project_created(project_path) 1Y147!%),
115def test_passes_malformed_python_version_to_uv(temp_project_dir: Path) -> None: 1abcdefgh
116 result = runner.invoke(app, ["test_project", "--python", "abc.def"]) 2xbybzbAbBbCbDbEb
117 # uv will reject this, we just verify we don't crash during validation
118 assert result.exit_code == 1 2xbybzbAbBbCbDbEb
121def test_creates_project_without_python_flag(temp_project_dir: Path) -> None: 1abcdefgh
122 result = runner.invoke(app, ["test_project"]) 1WZ258#'*
123 assert result.exit_code == 0 1WZ258#'*
124 project_path = temp_project_dir / "test_project" 1WZ258#'*
125 _assert_project_created(project_path) 1WZ258#'*
128def test_failed_to_initialize_with_uv(monkeypatch: Any) -> None: 1abcdefgh
129 def mock_run(*args: Any, **kwargs: Any) -> None: 1-./:;=?@
130 # Let the first check for 'uv' succeed, but fail on 'uv init'
131 if args[0][0] == "uv" and args[0][1] == "init": 1-./:;=?@
132 raise subprocess.CalledProcessError( 1-./:;=?@
133 1, args[0], stderr=b"uv init failed for some reason"
134 )
136 monkeypatch.setattr(subprocess, "run", mock_run) 1-./:;=?@
138 result = runner.invoke(app, ["failing_project"]) 1-./:;=?@
139 assert result.exit_code == 1 1-./:;=?@
140 assert "Failed to initialize project with uv" in result.output 1-./:;=?@
143def test_failed_to_add_dependencies(temp_project_dir: Path, monkeypatch: Any) -> None: 1abcdefgh
144 def mock_run(*args: Any, **kwargs: Any) -> None: 1[]^_`{|}
145 # Let 'uv init' succeed, but fail on 'uv add'
146 if args[0][0] == "uv" and args[0][1] == "add": 1[]^_`{|}
147 raise subprocess.CalledProcessError( 1[]^_`{|}
148 1, args[0], stderr=b"Failed to resolve dependencies"
149 )
151 monkeypatch.setattr(subprocess, "run", mock_run) 1[]^_`{|}
153 result = runner.invoke(app, ["failing_deps"]) 1[]^_`{|}
154 assert result.exit_code == 1 1[]^_`{|}
155 assert "Failed to install dependencies" in result.output 1[]^_`{|}
158def test_file_write_failure(temp_project_dir: Path, monkeypatch: Any) -> None: 1abcdefgh
159 original_write_text = Path.write_text 1OPQRSTUV
161 def mock_write_text(self: Path, *args: Any, **kwargs: Any) -> None: 1OPQRSTUV
162 # Fail when trying to write README.md (let main.py succeed first)
163 if self.name == "README.md": 1OPQRSTUV
164 raise PermissionError("Permission denied") 1OPQRSTUV
165 original_write_text(self, *args, **kwargs) 1OPQRSTUV
167 monkeypatch.setattr(Path, "write_text", mock_write_text) 1OPQRSTUV
169 result = runner.invoke(app, ["test_write_fail"]) 1OPQRSTUV
170 assert result.exit_code == 1 1OPQRSTUV
171 assert "Failed to write template files" in result.output 1OPQRSTUV
174def test_uv_not_installed(temp_project_dir: Path, monkeypatch: Any) -> None: 1abcdefgh
175 monkeypatch.setattr(shutil, "which", lambda _: None) 2hbibjbkblbmbnbob
177 result = runner.invoke(app, ["test_uv_missing_project"]) 2hbibjbkblbmbnbob
178 assert result.exit_code == 1 2hbibjbkblbmbnbob
179 assert "uv is required to create new projects" in result.output 2hbibjbkblbmbnbob
180 assert "https://docs.astral.sh/uv/" in result.output 2hbibjbkblbmbnbob