diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3c2e3dd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestArgs": [ + "tests" + ], + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.unittestArgs": [ + "-v", + "-s", + "./tests", + "-p", + "test_*.py" + ] +} \ No newline at end of file diff --git a/penify_hook/commands/config_commands.py b/penify_hook/commands/config_commands.py index 77779f4..4d27c42 100644 --- a/penify_hook/commands/config_commands.py +++ b/penify_hook/commands/config_commands.py @@ -8,6 +8,7 @@ from pathlib import Path from threading import Thread import logging +from penify_hook.utils import recursive_search_git_folder def get_penify_config() -> Path: @@ -16,8 +17,6 @@ def get_penify_config() -> Path: This function searches for the .penify file in the current directory and its parent directories until it finds it or reaches the home directory. """ - from penify_hook.utils import recursive_search_git_folder - current_dir = os.getcwd() home_dir = recursive_search_git_folder(current_dir) @@ -55,8 +54,9 @@ def save_llm_config(model, api_base, api_key): try: with open(penify_file, 'r') as f: config = json.load(f) - except json.JSONDecodeError: - pass + except (json.JSONDecodeError, IOError, OSError) as e: + print(f"Error reading configuration file: {str(e)}") + # Continue with empty config # Update or add LLM configuration config['llm'] = { diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..51e40e5 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +python_files = test_*.py +python_classes = Test* +python_functions = test_* +testpaths = tests diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..85d064b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Empty file to make tests directory a proper Python package diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7461f07 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest +import os +import sys + +# Add project root to sys.path for imports to work +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +# Common fixtures can be added here diff --git a/tests/test_commit_commands.py b/tests/test_commit_commands.py new file mode 100644 index 0000000..75a34c9 --- /dev/null +++ b/tests/test_commit_commands.py @@ -0,0 +1,254 @@ +import os +import sys +import pytest +from unittest.mock import patch, MagicMock, call + +from penify_hook.commands.commit_commands import commit_code, setup_commit_parser, handle_commit + +class TestCommitCommands: + + @pytest.fixture + def mock_api_client(self): + with patch('penify_hook.api_client.APIClient', create=True) as mock: + api_client_instance = MagicMock() + mock.return_value = api_client_instance + yield mock, api_client_instance + + @pytest.fixture + def mock_llm_client(self): + with patch('penify_hook.llm_client.LLMClient', create=True) as mock: + llm_client_instance = MagicMock() + mock.return_value = llm_client_instance + yield mock, llm_client_instance + + @pytest.fixture + def mock_jira_client(self): + with patch('penify_hook.jira_client.JiraClient', create=True) as mock: + jira_instance = MagicMock() + jira_instance.is_connected.return_value = True + mock.return_value = jira_instance + yield mock, jira_instance + + @pytest.fixture + def mock_commit_doc_gen(self): + with patch('penify_hook.commit_analyzer.CommitDocGenHook', create=True) as mock: + doc_gen_instance = MagicMock() + mock.return_value = doc_gen_instance + yield mock, doc_gen_instance + + @pytest.fixture + def mock_git_folder_search(self): + with patch('penify_hook.utils.recursive_search_git_folder', create=True) as mock: + mock.return_value = '/mock/git/folder' + yield mock + + @pytest.fixture + def mock_print_functions(self): + with patch('penify_hook.ui_utils.print_info', create=True) as mock_info, \ + patch('penify_hook.ui_utils.print_warning', create=True) as mock_warning, \ + patch('penify_hook.ui_utils.print_error', create=True) as mock_error: + yield mock_info, mock_warning, mock_error + + @patch('penify_hook.api_client.APIClient', create=True) + @patch('penify_hook.llm_client.LLMClient', create=True) + @patch('penify_hook.commit_analyzer.CommitDocGenHook', create=True) + @patch('penify_hook.utils.recursive_search_git_folder', create=True) + @patch('penify_hook.ui_utils.print_info', create=True) + @patch('penify_hook.ui_utils.print_warning', create=True) + @patch('penify_hook.ui_utils.print_error', create=True) + def test_commit_code_with_llm_client(self, mock_error, mock_warning, mock_info, + mock_git_folder_search, mock_doc_gen, + mock_llm_client, mock_api_client): + # Setup mocks + api_instance = MagicMock() + mock_api_client.return_value = api_instance + + llm_instance = MagicMock() + mock_llm_client.return_value = llm_instance + + doc_gen_instance = MagicMock() + mock_doc_gen.return_value = doc_gen_instance + + mock_git_folder_search.return_value = '/mock/git/folder' + + # Call function with LLM parameters + commit_code( + api_url="http://api.example.com", + token="api-token", + message="test commit", + open_terminal=False, + generate_description=True, + llm_model="gpt-4", + llm_api_base="http://llm-api.example.com", + llm_api_key="llm-api-key" + ) + + # Verify calls + mock_api_client.assert_called_once_with("http://api.example.com", "api-token") + mock_llm_client.assert_called_once_with( + model="gpt-4", + api_base="http://llm-api.example.com", + api_key="llm-api-key" + ) + mock_doc_gen.assert_called_once_with('/mock/git/folder', api_instance, llm_instance, None) + doc_gen_instance.run.assert_called_once_with("test commit", False, True) + + @patch('penify_hook.api_client.APIClient', create=True) + @patch('penify_hook.llm_client.LLMClient', create=True) + @patch('penify_hook.jira_client.JiraClient', create=True) + @patch('penify_hook.commit_analyzer.CommitDocGenHook', create=True) + @patch('penify_hook.utils.recursive_search_git_folder', create=True) + @patch('penify_hook.ui_utils.print_info', create=True) + @patch('penify_hook.ui_utils.print_warning', create=True) + @patch('penify_hook.ui_utils.print_error', create=True) + def test_commit_code_with_jira_client(self, mock_error, mock_warning, mock_info, + mock_git_folder_search, mock_doc_gen, + mock_jira_client, mock_llm_client, mock_api_client): + # Setup mocks + api_instance = MagicMock() + mock_api_client.return_value = api_instance + + llm_instance = MagicMock() + mock_llm_client.return_value = llm_instance + + jira_instance = MagicMock() + jira_instance.is_connected.return_value = True + mock_jira_client.return_value = jira_instance + + doc_gen_instance = MagicMock() + mock_doc_gen.return_value = doc_gen_instance + + mock_git_folder_search.return_value = '/mock/git/folder' + + # Call function with JIRA parameters + commit_code( + api_url="http://api.example.com", + token="api-token", + message="test commit", + open_terminal=False, + generate_description=True, + llm_model="gpt-4", + llm_api_base="http://llm-api.example.com", + llm_api_key="llm-api-key", + jira_url="https://jira.example.com", + jira_user="jira-user", + jira_api_token="jira-token" + ) + + # Verify calls + mock_jira_client.assert_called_once_with( + jira_url="https://jira.example.com", + jira_user="jira-user", + jira_api_token="jira-token" + ) + mock_doc_gen.assert_called_once_with('/mock/git/folder', api_instance, llm_instance, jira_instance) + + @patch('penify_hook.api_client.APIClient', create=True) + @patch('penify_hook.jira_client.JiraClient', create=True) + @patch('penify_hook.commit_analyzer.CommitDocGenHook', create=True) + @patch('penify_hook.utils.recursive_search_git_folder', create=True) + @patch('penify_hook.ui_utils.print_info', create=True) + @patch('penify_hook.ui_utils.print_warning', create=True) + @patch('penify_hook.ui_utils.print_error', create=True) + def test_commit_code_with_jira_connection_failure(self, mock_error, mock_warning, mock_info, + mock_git_folder_search, mock_doc_gen, + mock_jira_client, mock_api_client): + # Setup mocks + api_instance = MagicMock() + mock_api_client.return_value = api_instance + + jira_instance = MagicMock() + jira_instance.is_connected.return_value = False + mock_jira_client.return_value = jira_instance + + doc_gen_instance = MagicMock() + mock_doc_gen.return_value = doc_gen_instance + + mock_git_folder_search.return_value = '/mock/git/folder' + + # Call function + commit_code( + api_url="http://api.example.com", + token="api-token", + message="test commit", + open_terminal=False, + generate_description=True, + llm_model=None, + jira_url="https://jira.example.com", + jira_user="jira-user", + jira_api_token="jira-token" + ) + + # Verify JIRA warning + mock_doc_gen.assert_called_once_with('/mock/git/folder', api_instance, None, None) + + @patch('penify_hook.api_client.APIClient', create=True) + @patch('penify_hook.commit_analyzer.CommitDocGenHook', create=True) + @patch('penify_hook.utils.recursive_search_git_folder', create=True) + @patch('sys.exit') + @patch('builtins.print') + def test_commit_code_error_handling(self, mock_print, mock_exit, + mock_git_folder_search, mock_doc_gen, mock_api_client): + # Setup mocks + mock_doc_gen.side_effect = Exception("Test error") + mock_git_folder_search.return_value = '/mock/git/folder' + + # Call function + commit_code( + api_url="http://api.example.com", + token="api-token", + message="test commit", + open_terminal=False, + generate_description=True + ) + + mock_print.assert_called_once_with("Error: Test error") + mock_exit.assert_called_once_with(1) + + def test_setup_commit_parser(self): + parser = MagicMock() + setup_commit_parser(parser) + + # Verify parser configuration + assert parser.add_argument.call_count == 3 + parser.add_argument.assert_any_call("-m", "--message", required=False, help="Commit with contextual commit message.", default="N/A") + parser.add_argument.assert_any_call("-e", "--terminal", action="store_true", help="Open edit terminal before committing.") + parser.add_argument.assert_any_call("-d", "--description", action="store_false", help="It will generate commit message with title and description.", default=False) + + @patch('penify_hook.commands.commit_commands.get_token') + @patch('penify_hook.commands.commit_commands.get_jira_config') + @patch('penify_hook.commands.commit_commands.get_llm_config') + @patch('penify_hook.commands.commit_commands.commit_code') + @patch('penify_hook.commands.commit_commands.print_info') + @patch('penify_hook.constants.API_URL', "http://api.example.com") + def test_handle_commit(self, mock_print_info, mock_commit_code, mock_get_token, + mock_get_llm_config, mock_get_jira_config): + # Setup mocks + mock_get_llm_config.return_value = { + 'model': 'test-model', + 'api_base': 'http://llm-api.example.com', + 'api_key': 'llm-key' + } + mock_get_token.return_value = 'api-token' + mock_get_jira_config.return_value = { + 'url': 'https://jira.example.com', + 'username': 'jira-user', + 'api_token': 'jira-token' + } + + # Create args + args = MagicMock() + args.message = "test commit" + args.terminal = True + args.description = True + + # Call function + handle_commit(args) + + # Verify + mock_print_info.assert_called_with("Generate Commit Description: True") + mock_commit_code.assert_called_once_with( + "http://api.example.com", 'api-token', "test commit", True, True, + 'test-model', 'http://llm-api.example.com', 'llm-key', + 'https://jira.example.com', 'jira-user', 'jira-token' + ) diff --git a/tests/test_config_commands.py b/tests/test_config_commands.py new file mode 100644 index 0000000..ba84c10 --- /dev/null +++ b/tests/test_config_commands.py @@ -0,0 +1,268 @@ +import os +import json +import pytest +from unittest.mock import patch, mock_open, MagicMock +from pathlib import Path + +from penify_hook.commands.config_commands import ( + get_penify_config, + get_llm_config, + get_jira_config, + save_llm_config, + save_jira_config, + get_token +) + +class TestConfigCommands: + + @patch('penify_hook.commands.config_commands.recursive_search_git_folder') # Updated patch path here + @patch('penify_hook.commands.config_commands.Path') + @patch('os.makedirs') + @patch('builtins.open', new_callable=mock_open) + def test_get_penify_config_existing_dir(self, mock_file_open, mock_makedirs, mock_path, mock_git_folder): + # Mock git folder search + mock_git_folder.return_value = '/mock/git/folder' + + # Mock Path operations + mock_path_instance = MagicMock() + mock_path.return_value = mock_path_instance + mock_path_instance.__truediv__.return_value = mock_path_instance + + # Path exists for .penify dir + mock_path_instance.exists.return_value = True + + # Call function + result = get_penify_config() + + # Assertions + mock_git_folder.assert_called_once_with(os.getcwd()) + mock_path.assert_called_once_with('/mock/git/folder') + mock_path_instance.__truediv__.assert_called_with('.penify') + assert mock_makedirs.call_count == 0 # Should not create directory + + @patch('penify_hook.utils.recursive_search_git_folder') + @patch('penify_hook.commands.config_commands.Path') + @patch('os.makedirs') + @patch('builtins.open', new_callable=mock_open) + def test_get_penify_config_new_dir(self, mock_file_open, mock_makedirs, mock_path, mock_git_folder): + # Mock git folder search + mock_git_folder.return_value = '/mock/git/folder' + + # Mock Path operations + mock_path_instance = MagicMock() + mock_path.return_value = mock_path_instance + mock_path_instance.__truediv__.return_value = mock_path_instance + + # Path doesn't exist for .penify dir + mock_path_instance.exists.side_effect = [False, False] + + # Call function + result = get_penify_config() + + # Assertions + mock_makedirs.assert_called_with(mock_path_instance, exist_ok=True) + mock_file_open.assert_called_once() + mock_file_open().write.assert_called_once_with('{}') + + @patch('penify_hook.commands.config_commands.get_penify_config') + @patch('builtins.open', new_callable=mock_open, read_data='{"llm": {"model": "gpt-4", "api_base": "https://api.openai.com", "api_key": "test-key"}}') + def test_get_llm_config_exists(self, mock_file_open, mock_get_config): + # Setup mock + mock_config_file = MagicMock() + mock_config_file.exists.return_value = True + mock_get_config.return_value = mock_config_file + + # Call function + result = get_llm_config() + + # Assertions + assert result == { + 'model': 'gpt-4', + 'api_base': 'https://api.openai.com', + 'api_key': 'test-key' + } + mock_file_open.assert_called_once_with(mock_config_file, 'r') + + @patch('penify_hook.commands.config_commands.get_penify_config') + @patch('builtins.open', new_callable=mock_open, read_data='{}') + def test_get_llm_config_empty(self, mock_file_open, mock_get_config): + # Setup mock + mock_config_file = MagicMock() + mock_config_file.exists.return_value = True + mock_get_config.return_value = mock_config_file + + # Call function + result = get_llm_config() + + # Assertions + assert result == {} + mock_file_open.assert_called_once_with(mock_config_file, 'r') + + @patch('penify_hook.commands.config_commands.get_penify_config') + @patch('builtins.open', new_callable=mock_open, read_data='invalid json') + @patch('builtins.print') + def test_get_llm_config_invalid_json(self, mock_print, mock_file_open, mock_get_config): + # Setup mock + mock_config_file = MagicMock() + mock_config_file.exists.return_value = True + mock_get_config.return_value = mock_config_file + + # Call function + result = get_llm_config() + + # Assertions + assert result == {} + mock_print.assert_called_once() + assert 'Error reading .penify config file' in mock_print.call_args[0][0] + + @patch('penify_hook.commands.config_commands.get_penify_config') + @patch('builtins.open', new_callable=mock_open, read_data='{"jira": {"url": "https://jira.example.com", "username": "user", "api_token": "token"}}') + def test_get_jira_config_exists(self, mock_file_open, mock_get_config): + # Setup mock + mock_config_file = MagicMock() + mock_config_file.exists.return_value = True + mock_get_config.return_value = mock_config_file + + # Call function + result = get_jira_config() + + # Assertions + assert result == { + 'url': 'https://jira.example.com', + 'username': 'user', + 'api_token': 'token' + } + mock_file_open.assert_called_once_with(mock_config_file, 'r') + + @patch('penify_hook.commands.config_commands.get_penify_config') + @patch('builtins.open', new_callable=mock_open) + @patch('json.dump') + @patch('builtins.print') + def test_save_llm_config_success(self, mock_print, mock_json_dump, mock_file_open, mock_get_config): + # Setup mock + mock_config_file = MagicMock() + mock_get_config.return_value = mock_config_file + mock_file_open.return_value.__enter__.return_value = mock_file_open + + # Mock json.load to return empty dict when reading + with patch('json.load', return_value={}): + # Call function + result = save_llm_config("gpt-4", "https://api.openai.com", "test-key") + + # Assertions + assert result == True + mock_json_dump.assert_called_once() + expected_config = { + 'llm': { + 'model': 'gpt-4', + 'api_base': 'https://api.openai.com', + 'api_key': 'test-key' + } + } + assert mock_json_dump.call_args[0][0] == expected_config + mock_print.assert_called_once() + assert 'configuration saved' in mock_print.call_args[0][0] + + @patch('penify_hook.commands.config_commands.get_penify_config') + @patch('builtins.open', side_effect=IOError("Permission denied")) + @patch('builtins.print') + def test_save_llm_config_failure(self, mock_print, mock_file_open, mock_get_config): + # Setup mock + mock_config_file = MagicMock() + mock_config_file.exists.return_value = True + mock_get_config.return_value = mock_config_file + + # Call function + result = save_llm_config("gpt-4", "https://api.openai.com", "test-key") + + # Assert + assert result is False + mock_print.assert_called_with("Error saving LLM configuration: Permission denied") + + @patch('penify_hook.commands.config_commands.Path') + @patch('builtins.open', new_callable=mock_open) + @patch('json.dump') + @patch('builtins.print') + def test_save_jira_config_success(self, mock_print, mock_json_dump, mock_file_open, mock_path): + # Setup mock + mock_home_dir = MagicMock() + mock_path.home.return_value = mock_home_dir + mock_home_dir.__truediv__.return_value = mock_home_dir + mock_home_dir.exists.return_value = True + + # Mock json.load to return empty dict when reading + with patch('json.load', return_value={}): + # Call function + result = save_jira_config("https://jira.example.com", "user", "token") + + # Assertions + assert result == True + mock_json_dump.assert_called_once() + expected_config = { + 'jira': { + 'url': 'https://jira.example.com', + 'username': 'user', + 'api_token': 'token' + } + } + assert mock_json_dump.call_args[0][0] == expected_config + mock_print.assert_called_once() + assert 'configuration saved' in mock_print.call_args[0][0] + + @patch('os.getenv') + @patch('penify_hook.commands.config_commands.Path') + @patch('builtins.open', new_callable=mock_open, read_data='{"api_keys": "config-token"}') + def test_get_token_from_env(self, mock_file_open, mock_path, mock_getenv): + # Setup mock for env var + mock_getenv.return_value = "env-token" + + # Call function + result = get_token() + + # Assertions + assert result == "env-token" + mock_getenv.assert_called_once_with('PENIFY_API_TOKEN') + # File should not be read if env var exists + assert mock_file_open.call_count == 0 + + @patch('os.getenv') + @patch('penify_hook.commands.config_commands.Path') + @patch('builtins.open', new_callable=mock_open, read_data='{"api_keys": "config-token"}') + def test_get_token_from_config(self, mock_file_open, mock_path, mock_getenv): + # Setup mock for env var (not found) + mock_getenv.return_value = None + + # Setup mock for config file + mock_home_dir = MagicMock() + mock_path.home.return_value = mock_home_dir + mock_home_dir.__truediv__.return_value = mock_home_dir + mock_home_dir.exists.return_value = True + + # Call function + result = get_token() + + # Assertions + assert result == "config-token" + mock_getenv.assert_called_once_with('PENIFY_API_TOKEN') + mock_file_open.assert_called_once_with(mock_home_dir, 'r') + + @patch('os.getenv') + @patch('penify_hook.commands.config_commands.Path') + @patch('builtins.open', new_callable=mock_open, read_data='{"other_key": "value"}') + def test_get_token_not_found(self, mock_file_open, mock_path, mock_getenv): + # Setup mock for env var (not found) + mock_getenv.return_value = None + + # Setup mock for config file + mock_home_dir = MagicMock() + mock_path.home.return_value = mock_home_dir + mock_home_dir.__truediv__.return_value = mock_home_dir + mock_home_dir.exists.return_value = True + + # Call function + result = get_token() + + # Assertions + assert result is None + mock_getenv.assert_called_once_with('PENIFY_API_TOKEN') + mock_file_open.assert_called_once_with(mock_home_dir, 'r') diff --git a/tests/test_doc_commands.py b/tests/test_doc_commands.py new file mode 100644 index 0000000..06ad41c --- /dev/null +++ b/tests/test_doc_commands.py @@ -0,0 +1,207 @@ +import pytest +import sys +import os +from argparse import ArgumentParser +from unittest.mock import patch, MagicMock + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from penify_hook.commands.doc_commands import ( + generate_doc, + setup_docgen_parser, + handle_docgen +) + + +@patch('penify_hook.commands.doc_commands.GitDocGenHook') +@patch('penify_hook.commands.doc_commands.FileAnalyzerGenHook') +@patch('penify_hook.commands.doc_commands.FolderAnalyzerGenHook') +@patch('penify_hook.commands.doc_commands.APIClient') +@patch('penify_hook.commands.doc_commands.os.getcwd') +def test_generate_doc_no_location(mock_getcwd, mock_api_client, + mock_folder_analyzer, mock_file_analyzer, + mock_git_analyzer): + # Setup + mock_api_instance = MagicMock() + mock_api_client.return_value = mock_api_instance + mock_getcwd.return_value = '/fake/current/dir' + mock_git_instance = MagicMock() + mock_git_analyzer.return_value = mock_git_instance + + # Call function + generate_doc('http://api.example.com', 'fake-token', None) + + # Assertions + mock_api_client.assert_called_once_with('http://api.example.com', 'fake-token') + mock_git_analyzer.assert_called_once_with('/fake/current/dir', mock_api_instance) + mock_git_instance.run.assert_called_once() + mock_file_analyzer.assert_not_called() + mock_folder_analyzer.assert_not_called() + + +@patch('penify_hook.commands.doc_commands.GitDocGenHook') +@patch('penify_hook.commands.doc_commands.FileAnalyzerGenHook') +@patch('penify_hook.commands.doc_commands.FolderAnalyzerGenHook') +@patch('penify_hook.commands.doc_commands.APIClient') +def test_generate_doc_file_location(mock_api_client, mock_folder_analyzer, + mock_file_analyzer, mock_git_analyzer): + # Setup + mock_api_instance = MagicMock() + mock_api_client.return_value = mock_api_instance + mock_file_instance = MagicMock() + mock_file_analyzer.return_value = mock_file_instance + + # Call function + generate_doc('http://api.example.com', 'fake-token', 'example.py') + + # Assertions + mock_api_client.assert_called_once_with('http://api.example.com', 'fake-token') + mock_file_analyzer.assert_called_once_with('example.py', mock_api_instance) + mock_file_instance.run.assert_called_once() + mock_git_analyzer.assert_not_called() + mock_folder_analyzer.assert_not_called() + + +@patch('penify_hook.commands.doc_commands.GitDocGenHook') +@patch('penify_hook.commands.doc_commands.FileAnalyzerGenHook') +@patch('penify_hook.commands.doc_commands.FolderAnalyzerGenHook') +@patch('penify_hook.commands.doc_commands.APIClient') +def test_generate_doc_folder_location(mock_api_client, mock_folder_analyzer, + mock_file_analyzer, mock_git_analyzer): + # Setup + mock_api_instance = MagicMock() + mock_api_client.return_value = mock_api_instance + mock_folder_instance = MagicMock() + mock_folder_analyzer.return_value = mock_folder_instance + + # Call function + generate_doc('http://api.example.com', 'fake-token', 'src') + + # Assertions + mock_api_client.assert_called_once_with('http://api.example.com', 'fake-token') + mock_folder_analyzer.assert_called_once_with('src', mock_api_instance) + mock_folder_instance.run.assert_called_once() + mock_git_analyzer.assert_not_called() + mock_file_analyzer.assert_not_called() + + +@patch('sys.exit') +@patch('penify_hook.commands.doc_commands.GitDocGenHook') +@patch('penify_hook.commands.doc_commands.APIClient') +def test_generate_doc_error_handling(mock_api_client, mock_git_analyzer, mock_exit): + # Setup + mock_api_instance = MagicMock() + mock_api_client.return_value = mock_api_instance + mock_git_analyzer.side_effect = Exception("Test error") + + # Call function + generate_doc('http://api.example.com', 'fake-token', None) + + # Assertions + mock_exit.assert_called_once_with(1) + + +def test_setup_docgen_parser(): + parser = ArgumentParser() + setup_docgen_parser(parser) + + # Check that docgen options are properly set up + args = parser.parse_args(['-l', 'test_location']) + assert args.location == 'test_location' + + # Check install-hook subcommand + args = parser.parse_args(['install-hook', '-l', 'hook_location']) + assert args.docgen_subcommand == 'install-hook' + assert args.location == 'hook_location' + + # Check uninstall-hook subcommand + args = parser.parse_args(['uninstall-hook', '-l', 'hook_location']) + assert args.docgen_subcommand == 'uninstall-hook' + assert args.location == 'hook_location' + + +@patch('penify_hook.commands.doc_commands.install_git_hook') +@patch('penify_hook.commands.doc_commands.uninstall_git_hook') +@patch('penify_hook.commands.doc_commands.generate_doc') +@patch('penify_hook.commands.doc_commands.get_token') +@patch('sys.exit') +def test_handle_docgen_install_hook(mock_exit, mock_get_token, mock_generate_doc, + mock_uninstall_hook, mock_install_hook): + # Setup + mock_get_token.return_value = 'fake-token' + + # Test install-hook subcommand + args = MagicMock(docgen_subcommand='install-hook', location='hook_location') + handle_docgen(args) + mock_install_hook.assert_called_once_with('hook_location', 'fake-token') + mock_generate_doc.assert_not_called() + mock_uninstall_hook.assert_not_called() + + +@patch('penify_hook.commands.doc_commands.install_git_hook') +@patch('penify_hook.commands.doc_commands.uninstall_git_hook') +@patch('penify_hook.commands.doc_commands.generate_doc') +@patch('penify_hook.commands.doc_commands.get_token') +@patch('sys.exit') +def test_handle_docgen_uninstall_hook(mock_exit, mock_get_token, mock_generate_doc, + mock_uninstall_hook, mock_install_hook): + # Setup + mock_get_token.return_value = 'fake-token' + + # Test uninstall-hook subcommand + args = MagicMock(docgen_subcommand='uninstall-hook', location='hook_location') + handle_docgen(args) + mock_uninstall_hook.assert_called_once_with('hook_location') + mock_generate_doc.assert_not_called() + mock_install_hook.assert_not_called() + + +@patch('penify_hook.commands.doc_commands.install_git_hook') +@patch('penify_hook.commands.doc_commands.uninstall_git_hook') +@patch('penify_hook.commands.doc_commands.generate_doc') +@patch('penify_hook.commands.doc_commands.get_token') +def test_handle_docgen_generate(mock_get_token, mock_generate_doc, + mock_uninstall_hook, mock_install_hook): + # Setup + mock_get_token.return_value = 'fake-token' + + # Test direct documentation generation + args = MagicMock(docgen_subcommand=None, location='doc_location') + handle_docgen(args) + mock_generate_doc.assert_called_once() + mock_install_hook.assert_not_called() + mock_uninstall_hook.assert_not_called() + + +@patch('penify_hook.commands.doc_commands.get_token') +@patch('sys.exit') +def test_handle_docgen_no_token(mock_exit, mock_get_token): + # Test with no token + mock_get_token.return_value = None + args = MagicMock(docgen_subcommand=None, location='doc_location') + handle_docgen(args) + mock_exit.assert_called_once_with(1) + + +@patch('penify_hook.commands.doc_commands.os.getcwd') +@patch('penify_hook.commands.doc_commands.APIClient') +def test_generate_doc_with_file_exception(mock_api_client, mock_getcwd): + # Setup + mock_api_client.side_effect = Exception("API error") + mock_getcwd.return_value = '/fake/current/dir' + + # Test file location with exception + with pytest.raises(SystemExit): + generate_doc('http://api.example.com', 'fake-token', 'example.py') + + +@patch('penify_hook.commands.doc_commands.os.getcwd') +@patch('penify_hook.commands.doc_commands.APIClient') +def test_generate_doc_with_folder_exception(mock_api_client, mock_getcwd): + # Setup + mock_api_client.side_effect = Exception("API error") + mock_getcwd.return_value = '/fake/current/dir' + + # Test folder location with exception + with pytest.raises(SystemExit): + generate_doc('http://api.example.com', 'fake-token', 'src_folder') diff --git a/tests/test_web_config.py b/tests/test_web_config.py new file mode 100644 index 0000000..09e10da --- /dev/null +++ b/tests/test_web_config.py @@ -0,0 +1,65 @@ +import pytest +import json +import http.server +import threading +import socketserver +import time +from unittest.mock import patch, MagicMock, mock_open +import webbrowser + +from penify_hook.commands.config_commands import config_llm_web, config_jira_web + + +class TestWebConfig: + + @patch('webbrowser.open') + @patch('socketserver.TCPServer') + @patch('pkg_resources.resource_filename') + def test_config_llm_web_server_setup(self, mock_resource_filename, mock_server, mock_webbrowser): + # Setup mocks + mock_resource_filename.return_value = 'mock/template/path' + mock_server_instance = MagicMock() + mock_server.return_value.__enter__.return_value = mock_server_instance + + # Mock the serve_forever method to stop after being called once + def stop_server_after_call(): + mock_server_instance.shutdown() + mock_server_instance.serve_forever.side_effect = stop_server_after_call + + # Call function with patched webbrowser + with patch('builtins.print'): # Suppress print statements + config_llm_web() + + # Verify webbrowser was opened + mock_webbrowser.assert_called_once() + assert mock_webbrowser.call_args[0][0].startswith('http://localhost:') + + # Verify server was started + mock_server.assert_called_once() + mock_server_instance.serve_forever.assert_called_once() + + @patch('webbrowser.open') + @patch('socketserver.TCPServer') + @patch('pkg_resources.resource_filename') + def test_config_jira_web_server_setup(self, mock_resource_filename, mock_server, mock_webbrowser): + # Setup mocks + mock_resource_filename.return_value = 'mock/template/path' + mock_server_instance = MagicMock() + mock_server.return_value.__enter__.return_value = mock_server_instance + + # Mock the serve_forever method to stop after being called once + def stop_server_after_call(): + mock_server_instance.shutdown() + mock_server_instance.serve_forever.side_effect = stop_server_after_call + + # Call function with patched webbrowser + with patch('builtins.print'): # Suppress print statements + config_jira_web() + + # Verify webbrowser was opened + mock_webbrowser.assert_called_once() + assert mock_webbrowser.call_args[0][0].startswith('http://localhost:') + + # Verify server was started + mock_server.assert_called_once() + mock_server_instance.serve_forever.assert_called_once()