11import shutil
2+ import subprocess
23from pathlib import Path
34from typing import Any
45
@@ -25,104 +26,157 @@ def check_uv_installed() -> None:
2526
2627
2728class TestNewCommand :
29+ def _assert_project_created (
30+ self , project_path : Path , check_version_file : bool = False
31+ ) -> None :
32+ assert (project_path / "main.py" ).exists ()
33+ assert (project_path / "README.md" ).exists ()
34+ assert (project_path / "pyproject.toml" ).exists ()
35+ if check_version_file :
36+ assert (project_path / ".python-version" ).exists ()
37+
2838 def test_creates_project_successfully (self , temp_project_dir : Path ) -> None :
2939 result = runner .invoke (app , ["new" , "my_fastapi_project" ])
3040
3141 assert result .exit_code == 0
3242 project_path = temp_project_dir / "my_fastapi_project"
33- assert (project_path / "main.py" ).exists ()
34- assert (project_path / "README.md" ).exists ()
35- assert (project_path / "pyproject.toml" ).exists ()
43+ self ._assert_project_created (project_path )
3644 assert "Success!" in result .output
3745 assert "my_fastapi_project" in result .output
3846
39- def test_creates_project_with_python_flag (self , temp_project_dir : Path ) -> None :
40- result = runner . invoke ( app , [ "new" , "my_fastapi_project" , "--python" , "3.12" ])
41-
47+ def test_creates_project_with_python_version (self , temp_project_dir : Path ) -> None :
48+ # Test long form
49+ result = runner . invoke ( app , [ "new" , "project_long" , "--python" , "3.12" ])
4250 assert result .exit_code == 0
43- project_path = temp_project_dir / "my_fastapi_project"
44- assert (project_path / "main.py" ).exists ()
45- assert (project_path / "README.md" ).exists ()
46- assert (project_path / ".python-version" ).exists ()
47- python_version_file = (project_path / ".python-version" ).read_text ()
48- assert "3.12" in python_version_file
49- assert "Success!" in result .output
51+ project_path = temp_project_dir / "project_long"
52+ self ._assert_project_created (project_path , check_version_file = True )
53+ assert "3.12" in (project_path / ".python-version" ).read_text ()
5054
51- def test_creates_project_with_python_flag_short (
52- self , temp_project_dir : Path
53- ) -> None :
54- result = runner .invoke (app , ["new" , "another_project" , "-p" , "3.9" ])
55+ # Test short form
56+ result = runner .invoke (app , ["new" , "project_short" , "-p" , "3.9" ])
5557 assert result .exit_code == 0
56- project_path = temp_project_dir / "another_project"
57- assert (project_path / ".python-version" ).exists ()
58- python_version_file = (project_path / ".python-version" ).read_text ()
59- assert "3.9" in python_version_file
58+ project_path = temp_project_dir / "project_short"
59+ assert "3.9" in (project_path / ".python-version" ).read_text ()
6060
61- def test_creates_project_with_multiple_flags (self , temp_project_dir : Path ) -> None :
61+ def test_creates_project_with_extra_uv_flags (self , temp_project_dir : Path ) -> None :
62+ """Test that extra flags are passed through to uv."""
6263 result = runner .invoke (
6364 app , ["new" , "my_fastapi_project" , "--python" , "3.12" , "--lib" ]
6465 )
6566
6667 assert result .exit_code == 0
6768 project_path = temp_project_dir / "my_fastapi_project"
68- # With --lib flag, uv creates a library structure (no main.py by default)
69- assert (project_path / "pyproject.toml" ).exists ()
70- # Our template files should still be created
71- assert (project_path / "main.py" ).exists ()
72- assert (project_path / "README.md" ).exists ()
69+ self ._assert_project_created (project_path )
7370
74- def test_rejects_python_below_3_8 (self , temp_project_dir : Path ) -> None :
75- result = runner .invoke (app , ["new" , "my_fastapi_project" , "--python" , "3.7" ])
71+ def test_validates_template_file_contents (self , temp_project_dir : Path ) -> None :
72+ result = runner .invoke (app , ["new" , "sample_project" ])
73+ assert result .exit_code == 0
7674
77- assert result .exit_code == 1
78- assert "Python 3.7 is not supported" in result .output
79- assert "FastAPI requires Python 3.8" in result .output
75+ project_path = temp_project_dir / "sample_project"
76+
77+ main_py_content = (project_path / "main.py" ).read_text ()
78+ assert "from fastapi import FastAPI" in main_py_content
79+ assert "app = FastAPI()" in main_py_content
80+
81+ # Check README.md
82+ readme_content = (project_path / "README.md" ).read_text ()
83+ assert "# sample_project" in readme_content
84+ assert "A project created with FastAPI Cloud CLI." in readme_content
85+
86+ # Check pyproject.toml
87+ pyproject_content = (project_path / "pyproject.toml" ).read_text ()
88+ assert 'name = "sample-project"' in pyproject_content
89+ assert "fastapi[standard]" in pyproject_content
90+
91+ def test_initializes_in_current_directory (self , temp_project_dir : Path ) -> None :
92+ result = runner .invoke (app , ["new" ])
93+
94+ assert result .exit_code == 0
95+ assert "No project name provided" in result .output
96+ assert "Initializing in current directory" in result .output
97+ self ._assert_project_created (temp_project_dir )
8098
8199 def test_rejects_existing_directory (self , temp_project_dir : Path ) -> None :
82100 existing_dir = temp_project_dir / "existing_project"
83101 existing_dir .mkdir ()
84102
85103 result = runner .invoke (app , ["new" , "existing_project" ])
86-
87104 assert result .exit_code == 1
88105 assert "Directory 'existing_project' already exists." in result .output
89106
90- def test_initializes_in_current_directory_when_no_name_provided (
107+ def test_rejects_python_below_3_8 (self , temp_project_dir : Path ) -> None :
108+ result = runner .invoke (app , ["new" , "test_project" , "--python" , "3.7" ])
109+ assert result .exit_code == 1
110+ assert "Python 3.7 is not supported" in result .output
111+ assert "FastAPI requires Python 3.8" in result .output
112+
113+ def test_passes_single_digit_python_version_to_uv (
91114 self , temp_project_dir : Path
92115 ) -> None :
93- result = runner .invoke (app , ["new" ])
94-
116+ result = runner .invoke (app , ["new" , "test_project" , "--python" , "3" ])
95117 assert result .exit_code == 0
96- assert "No project name provided" in result . output
97- assert "Initializing in current directory" in result . output
118+ project_path = temp_project_dir / "test_project"
119+ self . _assert_project_created ( project_path )
98120
99- assert "Initialized FastAPI project in current directory" in result .output
100121
101- # Files should be created in current directory
102- assert (temp_project_dir / "main.py" ).exists ()
103- assert (temp_project_dir / "README.md" ).exists ()
104- assert (temp_project_dir / "pyproject.toml" ).exists ()
122+ class TestNewCommandUvFailures :
123+ """Tests for error handling in the new command when uv fails."""
105124
106- def test_validate_file_contents (self , temp_project_dir : Path ) -> None :
107- result = runner .invoke (app , ["new" , "sample_project" ])
125+ def test_failed_to_initialize_with_uv (self , monkeypatch : Any ) -> None :
126+ def mock_run (* args : Any , ** kwargs : Any ) -> None :
127+ # Let the first check for 'uv' succeed, but fail on 'uv init'
128+ if args [0 ][0 ] == "uv" and args [0 ][1 ] == "init" :
129+ raise subprocess .CalledProcessError (
130+ 1 , args [0 ], stderr = b"uv init failed for some reason"
131+ )
108132
109- assert result .exit_code == 0
110- project_path = temp_project_dir / "sample_project"
133+ monkeypatch .setattr (subprocess , "run" , mock_run )
111134
112- main_py_content = ( project_path / "main.py" ). read_text ( )
113- assert "from fastapi import FastAPI" in main_py_content
114- assert "app = FastAPI() " in main_py_content
135+ result = runner . invoke ( app , [ "new" , "failing_project" ] )
136+ assert result . exit_code == 1
137+ assert "Failed to initialize project with uv " in result . output
115138
116- readme_content = (project_path / "README.md" ).read_text ()
117- assert "# sample_project" in readme_content
118- assert "A project created with FastAPI Cloud CLI." in readme_content
139+ def test_failed_to_add_dependencies (
140+ self , temp_project_dir : Path , monkeypatch : Any
141+ ) -> None :
142+ """Test error handling when uv add fails."""
119143
120- def test_validate_pyproject_toml_contents (self , temp_project_dir : Path ) -> None :
121- result = runner .invoke (app , ["new" , "test_project" ])
144+ def mock_run (* args : Any , ** kwargs : Any ) -> None :
145+ # Let 'uv init' succeed, but fail on 'uv add'
146+ if args [0 ][0 ] == "uv" and args [0 ][1 ] == "add" :
147+ raise subprocess .CalledProcessError (
148+ 1 , args [0 ], stderr = b"Failed to resolve dependencies"
149+ )
122150
123- assert result .exit_code == 0
124- project_path = temp_project_dir / "test_project"
151+ monkeypatch .setattr (subprocess , "run" , mock_run )
125152
126- pyproject_content = (project_path / "pyproject.toml" ).read_text ()
127- assert 'name = "test-project"' in pyproject_content
128- assert "fastapi[standard]" in pyproject_content
153+ result = runner .invoke (app , ["new" , "failing_deps" ])
154+ assert result .exit_code == 1
155+ assert "Failed to install dependencies" in result .output
156+
157+ def test_file_write_failure (self , temp_project_dir : Path , monkeypatch : Any ) -> None :
158+ """Test error handling when template file writing fails."""
159+ original_write_text = Path .write_text
160+
161+ def mock_write_text (self : Path , * args : Any , ** kwargs : Any ) -> None :
162+ # Fail when trying to write main.py (our template file)
163+ if self .name == "main.py" :
164+ raise PermissionError ("Permission denied" )
165+ return original_write_text (self , * args , ** kwargs )
166+
167+ monkeypatch .setattr (Path , "write_text" , mock_write_text )
168+
169+ result = runner .invoke (app , ["new" , "test_write_fail" ])
170+ assert result .exit_code == 1
171+ assert "Failed to write template files" in result .output
172+
173+ def test_uv_not_installed (self , temp_project_dir : Path , monkeypatch : Any ) -> None :
174+ """Test error when uv is not installed."""
175+ # The check_uv_installed fixture runs before this, but we want to test
176+ # the case where uv disappears after that check
177+ monkeypatch .setattr (shutil , "which" , lambda _ : None )
178+
179+ result = runner .invoke (app , ["new" , "test_uv_missing_project" ])
180+ assert result .exit_code == 1
181+ assert "uv is required to create new projects" in result .output
182+ assert "https://uv.run/docs/installation/" in result .output
0 commit comments