/ tests / unit / cli / test_utils.py
test_utils.py
  1  from pathlib import Path
  2  from unittest.mock import MagicMock
  3  import click
  4  import pytest
  5  
  6  
  7  @pytest.fixture
  8  def mock_click_confirm(mocker):
  9      """Mock click.confirm for yes/no questions"""
 10      return mocker.patch("cli.utils.click.confirm")
 11  
 12  
 13  @pytest.fixture
 14  def mock_click_prompt(mocker):
 15      """Mock click.prompt for general questions"""
 16      return mocker.patch("cli.utils.click.prompt")
 17  
 18  
 19  @pytest.fixture
 20  def mock_click_echo(mocker):
 21      """Mock click.echo for output"""
 22      return mocker.patch("cli.utils.click.echo")
 23  
 24  
 25  class TestAskYesNoQuestion:
 26      """Tests for ask_yes_no_question function"""
 27  
 28      def test_ask_yes_no_question_default_false(self, mock_click_confirm):
 29          """Test yes/no question with default False"""
 30          from cli.utils import ask_yes_no_question
 31  
 32          mock_click_confirm.return_value = False
 33          result = ask_yes_no_question("Continue?", default=False)
 34  
 35          assert result is False
 36          mock_click_confirm.assert_called_once_with("Continue?", default=False)
 37  
 38      def test_ask_yes_no_question_default_true(self, mock_click_confirm):
 39          """Test yes/no question with default True"""
 40          from cli.utils import ask_yes_no_question
 41  
 42          mock_click_confirm.return_value = True
 43          result = ask_yes_no_question("Proceed?", default=True)
 44  
 45          assert result is True
 46          mock_click_confirm.assert_called_once_with("Proceed?", default=True)
 47  
 48      def test_ask_yes_no_question_user_response(self, mock_click_confirm):
 49          """Test yes/no question with user response"""
 50          from cli.utils import ask_yes_no_question
 51  
 52          mock_click_confirm.return_value = True
 53          result = ask_yes_no_question("Are you sure?")
 54  
 55          assert result is True
 56          mock_click_confirm.assert_called_once_with("Are you sure?", default=False)
 57  
 58  
 59  class TestAskQuestion:
 60      """Tests for ask_question function"""
 61  
 62      def test_ask_question_basic(self, mock_click_prompt):
 63          """Test basic question asking"""
 64          from cli.utils import ask_question
 65  
 66          mock_click_prompt.return_value = "test_answer"
 67          result = ask_question("What is your name?")
 68  
 69          assert result == "test_answer"
 70          mock_click_prompt.assert_called_once_with(
 71              "What is your name?",
 72              default=None,
 73              hide_input=False,
 74              type=None,
 75              show_choices=None,
 76          )
 77  
 78      def test_ask_question_with_default(self, mock_click_prompt):
 79          """Test question with default value"""
 80          from cli.utils import ask_question
 81  
 82          mock_click_prompt.return_value = "default_value"
 83          result = ask_question("Enter value:", default="default_value")
 84  
 85          assert result == "default_value"
 86          mock_click_prompt.assert_called_once_with(
 87              "Enter value:",
 88              default="default_value",
 89              hide_input=False,
 90              type=None,
 91              show_choices=None,
 92          )
 93  
 94      def test_ask_question_hide_input(self, mock_click_prompt):
 95          """Test question with hidden input (password)"""
 96          from cli.utils import ask_question
 97  
 98          mock_click_prompt.return_value = "secret"
 99          result = ask_question("Enter password:", hide_input=True)
100  
101          assert result == "secret"
102          mock_click_prompt.assert_called_once_with(
103              "Enter password:",
104              default=None,
105              hide_input=True,
106              type=None,
107              show_choices=None,
108          )
109  
110      def test_ask_question_with_type(self, mock_click_prompt):
111          """Test question with type parameter"""
112          from cli.utils import ask_question
113  
114          mock_click_prompt.return_value = 42
115          result = ask_question("Enter number:", type=int)
116  
117          assert result == 42
118          mock_click_prompt.assert_called_once_with(
119              "Enter number:",
120              default=None,
121              hide_input=False,
122              type=int,
123              show_choices=None,
124          )
125  
126      def test_ask_question_with_show_choices(self, mock_click_prompt):
127          """Test question with show_choices parameter"""
128          from cli.utils import ask_question
129  
130          mock_click_prompt.return_value = "option1"
131          result = ask_question("Select option:", show_choices=True)
132  
133          assert result == "option1"
134          mock_click_prompt.assert_called_once_with(
135              "Select option:",
136              default=None,
137              hide_input=False,
138              type=None,
139              show_choices=True,
140          )
141  
142  
143  class TestAskIfNotProvided:
144      """Tests for ask_if_not_provided function"""
145  
146      def test_key_exists_returns_value(self, mock_click_prompt):
147          """Test when key exists in options, returns existing value"""
148          from cli.utils import ask_if_not_provided
149  
150          options = {"name": "existing_value"}
151          result = ask_if_not_provided(options, "name", "Enter name:")
152  
153          assert result == "existing_value"
154          mock_click_prompt.assert_not_called()
155  
156      def test_key_missing_interactive_regular_question(self, mock_click_prompt):
157          """Test when key is missing, asks question in interactive mode"""
158          from cli.utils import ask_if_not_provided
159  
160          mock_click_prompt.return_value = "new_value"
161          options = {}
162          result = ask_if_not_provided(options, "name", "Enter name:")
163  
164          assert result == "new_value"
165          assert options["name"] == "new_value"
166          mock_click_prompt.assert_called_once()
167  
168      def test_key_missing_non_interactive_uses_default(self, mock_click_prompt):
169          """Test when key is missing in non-interactive mode, uses default"""
170          from cli.utils import ask_if_not_provided
171  
172          options = {}
173          result = ask_if_not_provided(
174              options, "name", "Enter name:", default="default_name", none_interactive=True
175          )
176  
177          assert result == "default_name"
178          assert options["name"] == "default_name"
179          mock_click_prompt.assert_not_called()
180  
181      def test_key_missing_bool_question(self, mock_click_confirm):
182          """Test when key is missing and is_bool=True"""
183          from cli.utils import ask_if_not_provided
184  
185          mock_click_confirm.return_value = True
186          options = {}
187          result = ask_if_not_provided(
188              options, "enabled", "Enable feature?", default=True, is_bool=True
189          )
190  
191          assert result is True
192          assert options["enabled"] is True
193          mock_click_confirm.assert_called_once_with("Enable feature?", default=True)
194  
195      def test_key_missing_bool_question_default_false(self, mock_click_confirm):
196          """Test bool question with non-bool default falls back to False"""
197          from cli.utils import ask_if_not_provided
198  
199          mock_click_confirm.return_value = False
200          options = {}
201          result = ask_if_not_provided(
202              options, "enabled", "Enable feature?", default="not_bool", is_bool=True
203          )
204  
205          assert result is False
206          assert options["enabled"] is False
207          mock_click_confirm.assert_called_once_with("Enable feature?", default=False)
208  
209      def test_key_missing_with_choices(self, mock_click_prompt):
210          """Test when key is missing with choices parameter"""
211          from cli.utils import ask_if_not_provided
212  
213          mock_click_prompt.return_value = "option2"
214          options = {}
215          choices = ["option1", "option2", "option3"]
216          result = ask_if_not_provided(
217              options, "choice", "Select option:", choices=choices
218          )
219  
220          assert result == "option2"
221          assert options["choice"] == "option2"
222          mock_click_prompt.assert_called_once()
223          # Verify that click.Choice was used
224          call_args = mock_click_prompt.call_args
225          assert call_args[1]["show_choices"] is True
226  
227      def test_key_missing_with_hide_input(self, mock_click_prompt):
228          """Test when key is missing with hide_input parameter"""
229          from cli.utils import ask_if_not_provided
230  
231          mock_click_prompt.return_value = "secret"
232          options = {}
233          result = ask_if_not_provided(
234              options, "password", "Enter password:", hide_input=True
235          )
236  
237          assert result == "secret"
238          assert options["password"] == "secret"
239          mock_click_prompt.assert_called_once()
240          call_args = mock_click_prompt.call_args
241          assert call_args[1]["hide_input"] is True
242  
243      def test_key_none_value_asks_question(self, mock_click_prompt):
244          """Test when key exists but value is None, asks question"""
245          from cli.utils import ask_if_not_provided
246  
247          mock_click_prompt.return_value = "new_value"
248          options = {"name": None}
249          result = ask_if_not_provided(options, "name", "Enter name:")
250  
251          assert result == "new_value"
252          assert options["name"] == "new_value"
253          mock_click_prompt.assert_called_once()
254  
255  
256  class TestGetCliRootDir:
257      """Tests for get_cli_root_dir function"""
258  
259      def test_get_cli_root_dir_returns_path(self):
260          """Test that get_cli_root_dir returns a valid Path"""
261          from cli.utils import get_cli_root_dir
262  
263          result = get_cli_root_dir()
264  
265          assert isinstance(result, Path)
266          assert result.exists()
267  
268      def test_get_cli_root_dir_is_parent_of_cli(self):
269          """Test that returned path is parent of cli directory"""
270          from cli.utils import get_cli_root_dir
271  
272          result = get_cli_root_dir()
273          cli_dir = result / "cli"
274  
275          assert cli_dir.exists()
276          assert cli_dir.is_dir()
277  
278  
279  class TestLoadTemplate:
280      """Tests for load_template function"""
281  
282      def test_load_template_success(self, tmp_path, mocker):
283          """Test successful template loading"""
284          from cli.utils import load_template
285  
286          # Mock get_cli_root_dir to return tmp_path
287          mocker.patch("cli.utils.get_cli_root_dir", return_value=tmp_path)
288  
289          # Create a template file
290          templates_dir = tmp_path / "templates"
291          templates_dir.mkdir()
292          template_file = templates_dir / "test_template.txt"
293          template_file.write_text("Hello, World!")
294  
295          result = load_template("test_template.txt")
296  
297          assert result == "Hello, World!"
298  
299      def test_load_template_file_not_found(self, tmp_path, mocker):
300          """Test FileNotFoundError when template doesn't exist"""
301          from cli.utils import load_template
302  
303          mocker.patch("cli.utils.get_cli_root_dir", return_value=tmp_path)
304  
305          with pytest.raises(FileNotFoundError) as exc_info:
306              load_template("nonexistent.txt")
307  
308          assert "does not exist" in str(exc_info.value)
309  
310      def test_load_template_with_parser(self, tmp_path, mocker):
311          """Test template loading with parser function"""
312          from cli.utils import load_template
313  
314          mocker.patch("cli.utils.get_cli_root_dir", return_value=tmp_path)
315  
316          templates_dir = tmp_path / "templates"
317          templates_dir.mkdir()
318          template_file = templates_dir / "test_template.txt"
319          template_file.write_text("hello world")
320  
321          def uppercase_parser(content, suffix):
322              return content.upper() + suffix
323  
324          result = load_template("test_template.txt", uppercase_parser, "!!!")
325  
326          assert result == "HELLO WORLD!!!"
327  
328      def test_load_template_without_parser(self, tmp_path, mocker):
329          """Test template loading without parser returns raw content"""
330          from cli.utils import load_template
331  
332          mocker.patch("cli.utils.get_cli_root_dir", return_value=tmp_path)
333  
334          templates_dir = tmp_path / "templates"
335          templates_dir.mkdir()
336          template_file = templates_dir / "test_template.txt"
337          template_file.write_text("Raw content\nMultiple lines")
338  
339          result = load_template("test_template.txt")
340  
341          assert result == "Raw content\nMultiple lines"
342  
343  
344  class TestGetFormattedNames:
345      """Tests for get_formatted_names function"""
346  
347      def test_get_formatted_names_camel_case(self):
348          """Test formatting from camelCase"""
349          from cli.utils import get_formatted_names
350  
351          result = get_formatted_names("myTestName")
352  
353          assert result["KEBAB_CASE_NAME"] == "my-test-name"
354          assert result["PASCAL_CASE_NAME"] == "MyTestName"
355          assert result["SNAKE_CASE_NAME"] == "my_test_name"
356          assert result["SNAKE_UPPER_CASE_NAME"] == "MY_TEST_NAME"
357          assert result["SPACED_NAME"] == "my test name"
358          assert result["SPACED_CAPITALIZED_NAME"] == "My Test Name"
359  
360      def test_get_formatted_names_snake_case(self):
361          """Test formatting from snake_case"""
362          from cli.utils import get_formatted_names
363  
364          result = get_formatted_names("my_test_name")
365  
366          assert result["KEBAB_CASE_NAME"] == "my-test-name"
367          assert result["PASCAL_CASE_NAME"] == "MyTestName"
368          assert result["SNAKE_CASE_NAME"] == "my_test_name"
369          assert result["SNAKE_UPPER_CASE_NAME"] == "MY_TEST_NAME"
370          assert result["SPACED_NAME"] == "my test name"
371          assert result["SPACED_CAPITALIZED_NAME"] == "My Test Name"
372  
373      def test_get_formatted_names_kebab_case(self):
374          """Test formatting from kebab-case"""
375          from cli.utils import get_formatted_names
376  
377          result = get_formatted_names("my-test-name")
378  
379          assert result["KEBAB_CASE_NAME"] == "my-test-name"
380          assert result["PASCAL_CASE_NAME"] == "MyTestName"
381          assert result["SNAKE_CASE_NAME"] == "my_test_name"
382          assert result["SNAKE_UPPER_CASE_NAME"] == "MY_TEST_NAME"
383          assert result["SPACED_NAME"] == "my test name"
384          assert result["SPACED_CAPITALIZED_NAME"] == "My Test Name"
385  
386      def test_get_formatted_names_spaces(self):
387          """Test formatting from spaced name"""
388          from cli.utils import get_formatted_names
389  
390          result = get_formatted_names("my test name")
391  
392          assert result["KEBAB_CASE_NAME"] == "my-test-name"
393          assert result["PASCAL_CASE_NAME"] == "MyTestName"
394          assert result["SNAKE_CASE_NAME"] == "my_test_name"
395          assert result["SNAKE_UPPER_CASE_NAME"] == "MY_TEST_NAME"
396          assert result["SPACED_NAME"] == "my test name"
397          assert result["SPACED_CAPITALIZED_NAME"] == "My Test Name"
398  
399      def test_get_formatted_names_acronym(self):
400          """Test formatting with acronyms like API"""
401          from cli.utils import get_formatted_names
402  
403          result = get_formatted_names("APIKey")
404  
405          assert result["KEBAB_CASE_NAME"] == "api-key"
406          assert result["PASCAL_CASE_NAME"] == "ApiKey"
407          assert result["SNAKE_CASE_NAME"] == "api_key"
408          assert result["SNAKE_UPPER_CASE_NAME"] == "API_KEY"
409          assert result["SPACED_NAME"] == "api key"
410          assert result["SPACED_CAPITALIZED_NAME"] == "API Key"
411  
412      def test_get_formatted_names_all_caps_acronym(self):
413          """Test formatting with all caps acronym"""
414          from cli.utils import get_formatted_names
415  
416          result = get_formatted_names("API")
417  
418          assert result["KEBAB_CASE_NAME"] == "api"
419          assert result["PASCAL_CASE_NAME"] == "Api"
420          assert result["SNAKE_CASE_NAME"] == "api"
421          assert result["SNAKE_UPPER_CASE_NAME"] == "API"
422          assert result["SPACED_NAME"] == "api"
423          assert result["SPACED_CAPITALIZED_NAME"] == "API"
424  
425      def test_get_formatted_names_mixed_separators(self):
426          """Test formatting with mixed separators"""
427          from cli.utils import get_formatted_names
428  
429          result = get_formatted_names("my-test_name")
430  
431          assert result["KEBAB_CASE_NAME"] == "my-test-name"
432          assert result["PASCAL_CASE_NAME"] == "MyTestName"
433          assert result["SNAKE_CASE_NAME"] == "my_test_name"
434  
435      def test_get_formatted_names_pascal_case(self):
436          """Test formatting from PascalCase"""
437          from cli.utils import get_formatted_names
438  
439          result = get_formatted_names("MyTestName")
440  
441          assert result["KEBAB_CASE_NAME"] == "my-test-name"
442          assert result["PASCAL_CASE_NAME"] == "MyTestName"
443          assert result["SNAKE_CASE_NAME"] == "my_test_name"
444          assert result["SPACED_CAPITALIZED_NAME"] == "My Test Name"
445  
446  
447  class TestGetModulePath:
448      """Tests for get_module_path function"""
449  
450      def test_get_module_path_valid_module(self, mocker):
451          """Test getting path for a valid module"""
452          from cli.utils import get_module_path
453  
454          mock_module = MagicMock()
455          mock_module.__path__ = ["/path/to/module"]
456          mock_import = mocker.patch("cli.utils.importlib.import_module", return_value=mock_module)
457  
458          result = get_module_path("test_module")
459  
460          assert result == "/path/to/module"
461          mock_import.assert_called_once_with("test_module")
462  
463  
464  class TestErrorExit:
465      """Tests for error_exit function"""
466  
467      def test_error_exit_with_message(self, mock_click_echo):
468          """Test error_exit with a message"""
469          from cli.utils import error_exit
470  
471          with pytest.raises(click.Abort):
472              error_exit("Something went wrong")
473  
474          mock_click_echo.assert_called_once()
475          call_args = mock_click_echo.call_args
476          assert "Something went wrong" in str(call_args)
477          assert call_args[1]["err"] is True
478  
479      def test_error_exit_without_message(self, mock_click_echo):
480          """Test error_exit without a message"""
481          from cli.utils import error_exit
482  
483          with pytest.raises(click.Abort):
484              error_exit()
485  
486          mock_click_echo.assert_not_called()
487  
488  
489  class TestGetSamCliHomeDir:
490      """Tests for get_sam_cli_home_dir function"""
491  
492      def test_get_sam_cli_home_dir_with_absolute_env_var(self, tmp_path, monkeypatch):
493          """Test with SAM_CLI_HOME set to absolute path"""
494          from cli.utils import get_sam_cli_home_dir
495  
496          sam_home = tmp_path / "sam_home"
497          monkeypatch.setenv("SAM_CLI_HOME", str(sam_home))
498  
499          result = get_sam_cli_home_dir()
500  
501          assert result == sam_home.resolve()
502          assert sam_home.exists()
503  
504      def test_get_sam_cli_home_dir_with_relative_env_var(self, tmp_path, monkeypatch):
505          """Test with SAM_CLI_HOME set to relative path"""
506          from cli.utils import get_sam_cli_home_dir
507  
508          monkeypatch.chdir(tmp_path)
509          monkeypatch.setenv("SAM_CLI_HOME", "relative/sam")
510  
511          result = get_sam_cli_home_dir()
512  
513          expected = (tmp_path / "relative" / "sam").resolve()
514          assert result == expected
515          assert result.exists()
516  
517      def test_get_sam_cli_home_dir_without_env_var(self, tmp_path, monkeypatch):
518          """Test without SAM_CLI_HOME env var (uses default .sam)"""
519          from cli.utils import get_sam_cli_home_dir
520  
521          monkeypatch.chdir(tmp_path)
522          monkeypatch.delenv("SAM_CLI_HOME", raising=False)
523  
524          result = get_sam_cli_home_dir()
525  
526          expected = (tmp_path / ".sam").resolve()
527          assert result == expected
528          assert result.exists()
529  
530      def test_get_sam_cli_home_dir_creates_directory(self, tmp_path, monkeypatch):
531          """Test that directory is created if it doesn't exist"""
532          from cli.utils import get_sam_cli_home_dir
533  
534          sam_home = tmp_path / "new" / "sam" / "home"
535          monkeypatch.setenv("SAM_CLI_HOME", str(sam_home))
536  
537          result = get_sam_cli_home_dir()
538  
539          assert result == sam_home.resolve()
540          assert sam_home.exists()
541          assert sam_home.is_dir()
542  
543      def test_get_sam_cli_home_dir_oserror_handling(self, tmp_path, monkeypatch, mocker):
544          """Test OSError handling when directory creation fails"""
545          from cli.utils import get_sam_cli_home_dir
546  
547          sam_home = tmp_path / "sam_home"
548          monkeypatch.setenv("SAM_CLI_HOME", str(sam_home))
549  
550          mock_path = mocker.patch("cli.utils.Path")
551          mock_instance = MagicMock()
552          mock_instance.mkdir.side_effect = OSError("Permission denied")
553          mock_path.return_value = mock_instance
554          mock_path.cwd.return_value = tmp_path
555  
556          with pytest.raises(click.Abort):
557              get_sam_cli_home_dir()
558  
559  
560  class TestIndentMultilineString:
561      """Tests for indent_multiline_string function"""
562  
563      def test_indent_multiline_string_default_indent(self):
564          """Test with default 4-space indent"""
565          from cli.utils import indent_multiline_string
566  
567          text = "line1\nline2\nline3"
568          result = indent_multiline_string(text)
569  
570          assert result == "line1\n    line2\n    line3"
571  
572      def test_indent_multiline_string_custom_indent(self):
573          """Test with custom indent level"""
574          from cli.utils import indent_multiline_string
575  
576          text = "line1\nline2"
577          result = indent_multiline_string(text, indent=2)
578  
579          assert result == "line1\n  line2"
580  
581      def test_indent_multiline_string_with_initial_indent(self):
582          """Test with initial_indent=True"""
583          from cli.utils import indent_multiline_string
584  
585          text = "line1\nline2"
586          result = indent_multiline_string(text, indent=4, initial_indent=True)
587  
588          assert result == "\n    line1\n    line2"
589  
590      def test_indent_multiline_string_single_line(self):
591          """Test with single line"""
592          from cli.utils import indent_multiline_string
593  
594          text = "single line"
595          result = indent_multiline_string(text)
596  
597          assert result == "single line"
598  
599  
600  class TestWaitForServer:
601      """Tests for wait_for_server function"""
602  
603      def test_wait_for_server_success(self, mocker):
604          """Test successful server connection"""
605          from cli.utils import wait_for_server
606  
607          mock_response = MagicMock()
608          mock_response.status_code = 200
609          mock_get = mocker.patch("cli.utils.requests.get", return_value=mock_response)
610  
611          result = wait_for_server("http://localhost:8000", timeout=5)
612  
613          assert result is True
614          mock_get.assert_called_with("http://localhost:8000")
615  
616      def test_wait_for_server_timeout(self, mocker):
617          """Test server connection timeout"""
618          from cli.utils import wait_for_server
619  
620          mock_get = mocker.patch("cli.utils.requests.get", side_effect=Exception("Connection refused"))
621          mocker.patch("cli.utils.sleep")
622  
623          result = wait_for_server("http://localhost:8000", timeout=1)
624  
625          assert result is False
626          assert mock_get.call_count >= 2
627  
628  
629  class TestCreateAndValidateDatabase:
630      """Tests for create_and_validate_database function"""
631  
632      def test_create_and_validate_database_sqlite(self, tmp_path, mocker, mock_click_echo):
633          """Test SQLite database creation and validation"""
634          from cli.utils import create_and_validate_database
635  
636          db_file = tmp_path / "test.db"
637          database_url = f"sqlite:///{db_file}"
638  
639          mock_engine = MagicMock()
640          mock_connection = MagicMock()
641          mock_engine.connect.return_value.__enter__.return_value = mock_connection
642          mock_create_engine = mocker.patch("cli.utils.create_engine", return_value=mock_engine)
643          mocker.patch("cli.utils.event")
644  
645          result = create_and_validate_database(database_url, "test_db")
646  
647          assert result is True
648          mock_create_engine.assert_called_once_with(database_url)
649          mock_engine.dispose.assert_called_once()
650          assert db_file.parent.exists()
651  
652      def test_create_and_validate_database_postgresql_with_psycopg2(self, mocker, mock_click_echo):
653          """Test PostgreSQL database with psycopg2 available"""
654          from cli.utils import create_and_validate_database
655  
656          database_url = "postgresql://user:pass@localhost/db"
657  
658          mock_engine = MagicMock()
659          mock_connection = MagicMock()
660          mock_engine.connect.return_value.__enter__.return_value = mock_connection
661          mock_create_engine = mocker.patch("cli.utils.create_engine", return_value=mock_engine)
662          mocker.patch.dict("sys.modules", {"psycopg2": MagicMock()})
663  
664          result = create_and_validate_database(database_url, "postgres_db")
665  
666          assert result is True
667          mock_create_engine.assert_called_once_with(database_url)
668          mock_engine.dispose.assert_called_once()
669  
670      def test_create_and_validate_database_postgresql_without_psycopg2(self, mocker, mock_click_echo):
671          """Test PostgreSQL database without psycopg2 raises ValueError"""
672          from cli.utils import create_and_validate_database
673  
674          database_url = "postgresql://user:pass@localhost/db"
675  
676          import builtins
677          real_import = builtins.__import__
678  
679          def mock_import(name, *args, **kwargs):
680              if name == "psycopg2":
681                  raise ImportError("No module named psycopg2")
682              return real_import(name, *args, **kwargs)
683  
684          mocker.patch("builtins.__import__", side_effect=mock_import)
685  
686          with pytest.raises(ValueError) as exc_info:
687              create_and_validate_database(database_url, "postgres_db")
688  
689          assert "psycopg2" in str(exc_info.value)
690  
691      def test_create_and_validate_database_generic(self, mocker, mock_click_echo):
692          """Test generic database URL"""
693          from cli.utils import create_and_validate_database
694  
695          database_url = "mysql://user:pass@localhost/db"
696  
697          mock_engine = MagicMock()
698          mock_connection = MagicMock()
699          mock_engine.connect.return_value.__enter__.return_value = mock_connection
700          mock_create_engine = mocker.patch("cli.utils.create_engine", return_value=mock_engine)
701  
702          result = create_and_validate_database(database_url, "mysql_db")
703  
704          assert result is True
705          mock_create_engine.assert_called_once_with(database_url)
706          mock_engine.dispose.assert_called_once()
707  
708      def test_create_and_validate_database_connection_failure(self, mocker, mock_click_echo):
709          """Test database connection failure"""
710          from cli.utils import create_and_validate_database
711  
712          database_url = "sqlite:///test.db"
713  
714          mock_engine = MagicMock()
715          mock_engine.connect.side_effect = Exception("Connection failed")
716          mocker.patch("cli.utils.create_engine", return_value=mock_engine)
717          mocker.patch("cli.utils.event")
718  
719          with pytest.raises(ValueError) as exc_info:
720              create_and_validate_database(database_url, "test_db")
721  
722          assert "Connection failed" in str(exc_info.value)