11from pathlib import Path
2- from unittest .mock import call , patch
2+ from unittest .mock import Mock , call , patch
33
4+ import pytest
45from click .testing import CliRunner
56from twyn import cli
67from twyn .base .constants import AvailableLoggingLevels
78from twyn .base .exceptions import TwynError
89from twyn .trusted_packages .cache_handler import CacheEntry , CacheHandler
9- from twyn .trusted_packages .trusted_packages import TyposquatCheckResult
10+ from twyn .trusted_packages .trusted_packages import TyposquatCheckResult , TyposquatCheckResultList
1011
1112
1213class TestCli :
@@ -36,7 +37,7 @@ def test_cache_clear_removes_all_cache_files(self, tmp_path: Path) -> None:
3637 assert len (cache_files ) == 0
3738
3839 @patch ("twyn.cli.check_dependencies" )
39- def test_no_cache_option_disables_cache (self , mock_check_dependencies ) :
40+ def test_no_cache_option_disables_cache (self , mock_check_dependencies : Mock ) -> None :
4041 runner = CliRunner ()
4142 runner .invoke (
4243 cli .run ,
@@ -46,7 +47,7 @@ def test_no_cache_option_disables_cache(self, mock_check_dependencies):
4647 assert mock_check_dependencies .call_args [1 ]["dependencies" ] == {"requests" }
4748
4849 @patch ("twyn.config.config_handler.ConfigHandler.add_package_to_allowlist" )
49- def test_allowlist_add_package_to_allowlist (self , mock_allowlist_add ) :
50+ def test_allowlist_add_package_to_allowlist (self , mock_allowlist_add : Mock ) -> None :
5051 runner = CliRunner ()
5152 runner .invoke (
5253 cli .add ,
@@ -56,7 +57,7 @@ def test_allowlist_add_package_to_allowlist(self, mock_allowlist_add):
5657 assert mock_allowlist_add .call_args == call ("requests" )
5758
5859 @patch ("twyn.config.config_handler.ConfigHandler.remove_package_from_allowlist" )
59- def test_allowlist_remove (self , mock_allowlist_add ) :
60+ def test_allowlist_remove (self , mock_allowlist_add : Mock ) -> None :
6061 runner = CliRunner ()
6162 runner .invoke (
6263 cli .remove ,
@@ -66,7 +67,7 @@ def test_allowlist_remove(self, mock_allowlist_add):
6667 assert mock_allowlist_add .call_args == call ("requests" )
6768
6869 @patch ("twyn.cli.check_dependencies" )
69- def test_click_arguments_dependency_file (self , mock_check_dependencies ) :
70+ def test_click_arguments_dependency_file (self , mock_check_dependencies : Mock ) -> None :
7071 runner = CliRunner ()
7172 runner .invoke (
7273 cli .run ,
@@ -94,7 +95,7 @@ def test_click_arguments_dependency_file(self, mock_check_dependencies):
9495 ]
9596
9697 @patch ("twyn.cli.check_dependencies" )
97- def test_click_arguments_dependency_file_in_different_path (self , mock_check_dependencies ) :
98+ def test_click_arguments_dependency_file_in_different_path (self , mock_check_dependencies : Mock ) -> None :
9899 runner = CliRunner ()
99100 runner .invoke (
100101 cli .run ,
@@ -117,7 +118,7 @@ def test_click_arguments_dependency_file_in_different_path(self, mock_check_depe
117118 ]
118119
119120 @patch ("twyn.cli.check_dependencies" )
120- def test_click_arguments_single_dependency_cli (self , mock_check_dependencies ) :
121+ def test_click_arguments_single_dependency_cli (self , mock_check_dependencies : Mock ) -> None :
121122 runner = CliRunner ()
122123 runner .invoke (
123124 cli .run ,
@@ -148,7 +149,7 @@ def test_click_raises_error_dependency_and_dependency_file_set(self):
148149 assert "Only one of --dependency or --dependency-file can be set at a time." in result .output
149150
150151 @patch ("twyn.cli.check_dependencies" )
151- def test_click_arguments_multiple_dependencies (self , mock_check_dependencies ) :
152+ def test_click_arguments_multiple_dependencies (self , mock_check_dependencies : Mock ) -> None :
152153 runner = CliRunner ()
153154 runner .invoke (
154155 cli .run ,
@@ -173,7 +174,7 @@ def test_click_arguments_multiple_dependencies(self, mock_check_dependencies):
173174 ]
174175
175176 @patch ("twyn.cli.check_dependencies" )
176- def test_click_arguments_default (self , mock_check_dependencies ) :
177+ def test_click_arguments_default (self , mock_check_dependencies : Mock ) -> None :
177178 runner = CliRunner ()
178179 runner .invoke (cli .run )
179180
@@ -190,32 +191,60 @@ def test_click_arguments_default(self, mock_check_dependencies):
190191 ]
191192
192193 @patch ("twyn.cli.check_dependencies" )
193- def test_return_code_1 (self , mock_check_dependencies ) :
194+ def test_return_code_1 (self , mock_check_dependencies : Mock ) -> None :
194195 runner = CliRunner ()
195- mock_check_dependencies .return_value = [
196- TyposquatCheckResult (candidate_dependency = "my-package" , similar_dependencies = ["mypackage" ])
197- ]
196+ mock_check_dependencies .return_value = TyposquatCheckResultList (
197+ errors = [ TyposquatCheckResult (dependency = "my-package" , similarities = ["mypackage" ])]
198+ )
198199
199200 result = runner .invoke (cli .run )
200201 assert result .exit_code == 1
202+ assert (
203+ result .output
204+ == "\x1b [91mPossible typosquat detected: \x1b [0m`my-package`, did you mean any of [mypackage]?\n "
205+ )
201206
202207 @patch ("twyn.cli.check_dependencies" )
203- def test_return_code_0 (self , mock_check_dependencies ):
208+ def test_json_typo_detected (self , mock_check_dependencies : Mock ) -> None :
209+ mock_check_dependencies .return_value = TyposquatCheckResultList (
210+ errors = [TyposquatCheckResult (dependency = "my-package" , similarities = ["mypackage" ])]
211+ )
204212 runner = CliRunner ()
205- mock_check_dependencies .return_value = []
213+ result = runner .invoke (
214+ cli .run ,
215+ [
216+ "--json" ,
217+ ],
218+ )
219+
220+ assert result .exit_code == 1
221+ assert result .output == '{"errors":[{"dependency":"my-package","similarities":["mypackage"]}]}\n '
222+
223+ @patch ("twyn.cli.check_dependencies" )
224+ def test_json_no_typo (self , mock_check_dependencies : Mock ) -> None :
225+ mock_check_dependencies .return_value = TyposquatCheckResultList (errors = [])
226+ runner = CliRunner ()
227+ result = runner .invoke (
228+ cli .run ,
229+ [
230+ "--json" ,
231+ ],
232+ )
206233
207- result = runner .invoke (cli .run )
208234 assert result .exit_code == 0
235+ assert result .output == '{"errors":[]}\n '
209236
210- def test_only_one_verbosity_level_is_allowed (self ):
237+ @patch ("twyn.cli.check_dependencies" )
238+ def test_return_code_0 (self , mock_check_dependencies : Mock ) -> None :
211239 runner = CliRunner ()
212- result = runner . invoke ( cli . run , [ "-v" , "-vv" ], catch_exceptions = False )
240+ mock_check_dependencies . return_value = TyposquatCheckResultList ( )
213241
214- assert isinstance (result .exception , SystemExit )
215- assert result .exit_code == 2
216- assert "Only one verbosity level is allowed. Choose either -v or -vv." in result .output
242+ result = runner .invoke (cli .run )
243+
244+ assert result .exit_code == 0
245+ assert result .output == "\x1b [92mNo typosquats detected\x1b [0m\n "
217246
218- def test_dependency_file_name_has_to_be_recognized (self ):
247+ def test_dependency_file_name_has_to_be_recognized (self ) -> None :
219248 runner = CliRunner ()
220249 result = runner .invoke (cli .run , ["--dependency-file" , "requirements-dev.txt" ], catch_exceptions = False )
221250
@@ -241,7 +270,9 @@ def test_base_twyn_error_is_caught_and_wrapped_in_cli_error(self, mock_check_dep
241270 assert "Test base error message" in caplog .text
242271
243272 @patch ("twyn.cli.check_dependencies" )
244- def test_unhandled_exception_is_caught_and_wrapped_in_cli_error (self , mock_check_dependencies , caplog ):
273+ def test_unhandled_exception_is_caught_and_wrapped_in_cli_error (
274+ self , mock_check_dependencies : Mock , caplog : pytest .LogCaptureFixture
275+ ) -> None :
245276 """Test that unhandled exceptions are caught and wrapped in CliError."""
246277 runner = CliRunner ()
247278
0 commit comments