77import subprocess
88import tempfile
99from pathlib import Path
10- from unittest .mock import MagicMock , patch
10+ from unittest .mock import MagicMock , mock_open , patch
1111
1212import pytest
1313
1414from fastapi_fastkit .backend .main import (
15+ _parse_setup_dependencies ,
16+ _process_config_file ,
17+ _process_setup_file ,
1518 add_new_route ,
1619 create_venv ,
1720 find_template_core_modules ,
@@ -63,6 +66,16 @@ def test_create_venv_failure(self, mock_subprocess: MagicMock) -> None:
6366 with pytest .raises (BackendExceptions , match = "Failed to create venv" ):
6467 create_venv (str (self .project_path ))
6568
69+ @patch ("subprocess.run" )
70+ def test_create_venv_os_error (self , mock_subprocess : MagicMock ) -> None :
71+ """Test create_venv function with OSError."""
72+ # given
73+ mock_subprocess .side_effect = OSError ("Permission denied" )
74+
75+ # when & then
76+ with pytest .raises (BackendExceptions , match = "Failed to create venv" ):
77+ create_venv (str (self .project_path ))
78+
6679 def test_find_template_core_modules (self ) -> None :
6780 """Test find_template_core_modules function."""
6881 # given
@@ -122,6 +135,47 @@ def test_install_dependencies_success(self, mock_subprocess: MagicMock) -> None:
122135 # Should be called twice: pip upgrade and install requirements
123136 assert mock_subprocess .call_count == 2
124137
138+ @patch ("subprocess.run" )
139+ def test_install_dependencies_pip_upgrade_failure (
140+ self , mock_subprocess : MagicMock
141+ ) -> None :
142+ """Test install_dependencies function with pip upgrade failure."""
143+ # given
144+ requirements_txt = self .project_path / "requirements.txt"
145+ requirements_txt .write_text ("fastapi==0.104.1" )
146+ venv_path = str (self .project_path / ".venv" )
147+ (self .project_path / ".venv" ).mkdir ()
148+ mock_subprocess .side_effect = subprocess .CalledProcessError (
149+ 1 , ["pip" , "install" , "--upgrade" , "pip" ]
150+ )
151+
152+ # when & then
153+ with pytest .raises (BackendExceptions , match = "Failed to install dependencies" ):
154+ install_dependencies (str (self .project_path ), venv_path )
155+
156+ @patch ("subprocess.run" )
157+ def test_install_dependencies_requirements_failure (
158+ self , mock_subprocess : MagicMock
159+ ) -> None :
160+ """Test install_dependencies function with requirements installation failure."""
161+ # given
162+ requirements_txt = self .project_path / "requirements.txt"
163+ requirements_txt .write_text ("fastapi==0.104.1" )
164+ venv_path = str (self .project_path / ".venv" )
165+ (self .project_path / ".venv" ).mkdir ()
166+
167+ # Mock successful pip upgrade but failed requirements install
168+ mock_subprocess .side_effect = [
169+ MagicMock (returncode = 0 ), # successful pip upgrade
170+ subprocess .CalledProcessError (
171+ 1 , ["pip" , "install" , "-r" , "requirements.txt" ]
172+ ), # failed requirements install
173+ ]
174+
175+ # when & then
176+ with pytest .raises (BackendExceptions , match = "Failed to install dependencies" ):
177+ install_dependencies (str (self .project_path ), venv_path )
178+
125179 def test_inject_project_metadata (self ) -> None :
126180 """Test inject_project_metadata function."""
127181 # given
@@ -163,6 +217,26 @@ def test_inject_project_metadata(self) -> None:
163217 config_content = config_py .read_text ()
164218 assert 'PROJECT_NAME = "test-project"' in config_content
165219
220+ @patch ("fastapi_fastkit.backend.main.find_template_core_modules" )
221+ def test_inject_project_metadata_with_exception (
222+ self , mock_find_modules : MagicMock
223+ ) -> None :
224+ """Test inject_project_metadata function with exception handling."""
225+ # given
226+ mock_find_modules .side_effect = Exception ("Mock error" )
227+
228+ # when & then
229+ with pytest .raises (
230+ BackendExceptions , match = "Failed to inject project metadata"
231+ ):
232+ inject_project_metadata (
233+ str (self .project_path ),
234+ "test-project" ,
235+ "Test Author" ,
236+ 237+ "Test description" ,
238+ )
239+
166240 def test_read_template_stack (self ) -> None :
167241 """Test read_template_stack function."""
168242 # given
@@ -199,7 +273,212 @@ def test_read_template_stack(self) -> None:
199273 finally :
200274 import shutil
201275
202- shutil .rmtree (template_path )
276+ shutil .rmtree (str (template_path ))
277+
278+ def test_read_template_stack_requirements_file (self ) -> None :
279+ """Test read_template_stack function with requirements.txt file."""
280+ # given
281+ template_path = Path (tempfile .mkdtemp ())
282+ try :
283+ requirements_txt = template_path / "requirements.txt-tpl"
284+ requirements_txt .write_text (
285+ "fastapi>=0.100.0\n uvicorn[standard]>=0.23.0\n pydantic>=2.0.0"
286+ )
287+
288+ # when
289+ result = read_template_stack (str (template_path ))
290+
291+ # then
292+ assert len (result ) == 3
293+ assert "fastapi>=0.100.0" in result
294+ assert "uvicorn[standard]>=0.23.0" in result
295+ assert "pydantic>=2.0.0" in result
296+
297+ finally :
298+ import shutil
299+
300+ shutil .rmtree (str (template_path ))
301+
302+ @patch ("builtins.open" , mock_open (read_data = "fastapi>=0.100.0" ))
303+ @patch ("os.path.exists" , return_value = True )
304+ def test_read_template_stack_file_read_error (self , mock_exists : MagicMock ) -> None :
305+ """Test read_template_stack function with file read error."""
306+ # given
307+ template_path = "/fake/path"
308+
309+ # Mock file read error
310+ with patch ("builtins.open" , side_effect = OSError ("Permission denied" )):
311+ # when
312+ result = read_template_stack (template_path )
313+
314+ # then
315+ assert result == []
316+
317+ @patch ("builtins.open" , mock_open (read_data = "fastapi>=0.100.0" ))
318+ @patch ("os.path.exists" , return_value = True )
319+ def test_read_template_stack_unicode_error (self , mock_exists : MagicMock ) -> None :
320+ """Test read_template_stack function with unicode decode error."""
321+ # given
322+ template_path = "/fake/path"
323+
324+ # Mock unicode decode error
325+ with patch (
326+ "builtins.open" ,
327+ side_effect = UnicodeDecodeError ("utf-8" , b"" , 0 , 1 , "invalid start byte" ),
328+ ):
329+ # when
330+ result = read_template_stack (template_path )
331+
332+ # then
333+ assert result == []
334+
335+ def test_parse_setup_dependencies_list_format (self ) -> None :
336+ """Test _parse_setup_dependencies function with list format."""
337+ # given
338+ content = """
339+ install_requires: list[str] = [
340+ "fastapi>=0.100.0",
341+ "uvicorn>=0.23.0",
342+ # "commented-out-package",
343+ "pydantic>=2.0.0",
344+ ]
345+ """
346+
347+ # when
348+ result = _parse_setup_dependencies (content )
349+
350+ # then
351+ assert len (result ) == 3
352+ assert "fastapi>=0.100.0" in result
353+ assert "uvicorn>=0.23.0" in result
354+ assert "pydantic>=2.0.0" in result
355+
356+ def test_parse_setup_dependencies_traditional_format (self ) -> None :
357+ """Test _parse_setup_dependencies function with traditional format."""
358+ # given
359+ content = """
360+ install_requires = [
361+ 'fastapi>=0.100.0',
362+ 'uvicorn>=0.23.0',
363+ 'pydantic>=2.0.0',
364+ ]
365+ """
366+
367+ # when
368+ result = _parse_setup_dependencies (content )
369+
370+ # then
371+ assert len (result ) == 3
372+ assert "fastapi>=0.100.0" in result
373+ assert "uvicorn>=0.23.0" in result
374+ assert "pydantic>=2.0.0" in result
375+
376+ def test_parse_setup_dependencies_empty_content (self ) -> None :
377+ """Test _parse_setup_dependencies function with empty content."""
378+ # given
379+ content = ""
380+
381+ # when
382+ result = _parse_setup_dependencies (content )
383+
384+ # then
385+ assert result == []
386+
387+ def test_process_setup_file_success (self ) -> None :
388+ """Test _process_setup_file function with successful processing."""
389+ # given
390+ setup_py = self .project_path / "setup.py"
391+ setup_py .write_text (
392+ """
393+ setup(
394+ name="<project_name>",
395+ author="<author>",
396+ author_email="<author_email>",
397+ description="<description>",
398+ )
399+ """
400+ )
401+
402+ # when
403+ _process_setup_file (
404+ str (setup_py ),
405+ "test-project" ,
406+ "Test Author" ,
407+ 408+ "Test description" ,
409+ )
410+
411+ # then
412+ content = setup_py .read_text ()
413+ assert "test-project" in content
414+ assert "Test Author" in content
415+ assert "[email protected] " in content 416+ assert "Test description" in content
417+
418+ def test_process_setup_file_missing_file (self ) -> None :
419+ """Test _process_setup_file function with missing file."""
420+ # given
421+ setup_py = str (self .project_path / "nonexistent.py" )
422+
423+ # when & then (should not raise exception)
424+ _process_setup_file (
425+ setup_py ,
426+ "test-project" ,
427+ "Test Author" ,
428+ 429+ "Test description" ,
430+ )
431+
432+ def test_process_setup_file_read_error (self ) -> None :
433+ """Test _process_setup_file function with file read error."""
434+ # given
435+ setup_py = self .project_path / "setup.py"
436+ setup_py .write_text ("content" )
437+
438+ # when & then
439+ with patch ("builtins.open" , side_effect = OSError ("Permission denied" )):
440+ with pytest .raises (BackendExceptions , match = "Failed to process setup.py" ):
441+ _process_setup_file (
442+ str (setup_py ),
443+ "test-project" ,
444+ "Test Author" ,
445+ 446+ "Test description" ,
447+ )
448+
449+ def test_process_config_file_success (self ) -> None :
450+ """Test _process_config_file function with successful processing."""
451+ # given
452+ config_py = self .project_path / "config.py"
453+ config_py .write_text ('PROJECT_NAME = "<project_name>"' )
454+
455+ # when
456+ _process_config_file (str (config_py ), "test-project" )
457+
458+ # then
459+ content = config_py .read_text ()
460+ assert 'PROJECT_NAME = "test-project"' in content
461+
462+ def test_process_config_file_missing_file (self ) -> None :
463+ """Test _process_config_file function with missing file."""
464+ # given
465+ config_py = str (self .project_path / "nonexistent.py" )
466+
467+ # when & then (should not raise exception)
468+ _process_config_file (config_py , "test-project" )
469+
470+ def test_process_config_file_read_error (self ) -> None :
471+ """Test _process_config_file function with file read error."""
472+ # given
473+ config_py = self .project_path / "config.py"
474+ config_py .write_text ("content" )
475+
476+ # when & then
477+ with patch ("builtins.open" , side_effect = OSError ("Permission denied" )):
478+ with pytest .raises (
479+ BackendExceptions , match = "Failed to process config file"
480+ ):
481+ _process_config_file (str (config_py ), "test-project" )
203482
204483 @patch ("fastapi_fastkit.backend.main._ensure_project_structure" )
205484 @patch ("fastapi_fastkit.backend.main._create_route_files" )
@@ -214,15 +493,14 @@ def test_add_new_route(
214493 ) -> None :
215494 """Test add_new_route function."""
216495 # given
217- route_name = "user"
218496 mock_ensure_structure .return_value = {
219- "api " : "/fake/api" ,
497+ "api_routes " : "/fake/api/routes " ,
220498 "crud" : "/fake/crud" ,
221499 "schemas" : "/fake/schemas" ,
222500 }
223501
224502 # when
225- add_new_route (str (self .project_path ), route_name )
503+ add_new_route (str (self .project_path ), "test_route" )
226504
227505 # then
228506 mock_ensure_structure .assert_called_once ()
0 commit comments