openwebui-cli/tests/test_main_clierror.py
2025-12-01 04:24:51 +01:00

391 lines
15 KiB
Python

"""Tests for CLIError handling in main.py cli() function."""
import pytest
import typer
from io import StringIO
from unittest.mock import patch, MagicMock
from openwebui_cli.main import app, cli
from openwebui_cli.errors import CLIError, ExitCode, UsageError, AuthError, NetworkError, ServerError
class TestCLIErrorHandling:
"""Test that cli() function properly handles CLIError exceptions."""
def test_clierror_with_custom_exit_code(self):
"""Test CLIError with custom exit code is properly propagated."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Test error", exit_code=5)
# cli() catches CLIError and calls typer.Exit with the error code
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 5
def test_clierror_with_default_exit_code(self):
"""Test CLIError with default exit code (GENERAL_ERROR = 1)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Default error")
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == ExitCode.GENERAL_ERROR
assert exc_info.value.exit_code == 1
def test_clierror_exit_code_zero(self):
"""Test CLIError with exit code 0 is honored."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Success error", exit_code=0)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 0
def test_clierror_exit_code_two(self):
"""Test CLIError with exit code 2 (USAGE_ERROR)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Usage error", exit_code=ExitCode.USAGE_ERROR)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 2
def test_clierror_exit_code_three(self):
"""Test CLIError with exit code 3 (AUTH_ERROR)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Auth error", exit_code=ExitCode.AUTH_ERROR)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 3
def test_clierror_exit_code_four(self):
"""Test CLIError with exit code 4 (NETWORK_ERROR)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Network error", exit_code=ExitCode.NETWORK_ERROR)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 4
def test_clierror_exit_code_five(self):
"""Test CLIError with exit code 5 (SERVER_ERROR)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Server error", exit_code=ExitCode.SERVER_ERROR)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 5
def test_usage_error_exit_code(self):
"""Test UsageError subclass propagates correct exit code."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = UsageError("Invalid arguments")
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == ExitCode.USAGE_ERROR
assert exc_info.value.exit_code == 2
def test_auth_error_exit_code(self):
"""Test AuthError subclass propagates correct exit code."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = AuthError("Authentication failed")
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == ExitCode.AUTH_ERROR
assert exc_info.value.exit_code == 3
def test_network_error_exit_code(self):
"""Test NetworkError subclass propagates correct exit code."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = NetworkError("Connection timeout")
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == ExitCode.NETWORK_ERROR
assert exc_info.value.exit_code == 4
def test_server_error_exit_code(self):
"""Test ServerError subclass propagates correct exit code."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = ServerError("Internal server error")
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == ExitCode.SERVER_ERROR
assert exc_info.value.exit_code == 5
def test_error_message_is_printed(self):
"""Test that error messages are printed to console."""
with patch('openwebui_cli.main.app') as mock_app:
with patch('openwebui_cli.main.console') as mock_console:
error_message = "Test error message"
mock_app.side_effect = CLIError(error_message, exit_code=1)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 1
# Verify console.print was called with the error message
mock_console.print.assert_called_once()
call_args = mock_console.print.call_args[0][0]
assert error_message in call_args or error_message in str(call_args)
def test_multiple_clierror_scenarios(self):
"""Test various CLIError scenarios in sequence."""
error_scenarios = [
(CLIError("Error 1", exit_code=1), 1),
(CLIError("Error 2", exit_code=2), 2),
(AuthError("Auth failure"), 3),
(NetworkError("Network issue"), 4),
(ServerError("Server issue"), 5),
]
for error, expected_code in error_scenarios:
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = error
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == expected_code, \
f"Expected {expected_code} but got {exc_info.value.exit_code} for {error}"
def test_clierror_large_exit_code(self):
"""Test CLIError with larger exit code values."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Large exit code error", exit_code=127)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 127
def test_clierror_negative_exit_code(self):
"""Test CLIError with negative exit code (should still be respected)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Negative exit code", exit_code=-1)
with pytest.raises(typer.Exit) as exc_info:
cli()
# typer.Exit will accept negative codes
assert exc_info.value.exit_code == -1
def test_clierror_empty_message(self):
"""Test CLIError with empty message."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("", exit_code=1)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 1
def test_clierror_multiline_message(self):
"""Test CLIError with multiline message."""
with patch('openwebui_cli.main.app') as mock_app:
error_msg = "Line 1 of error\nLine 2 of error\nLine 3 of error"
mock_app.side_effect = CLIError(error_msg, exit_code=1)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 1
def test_clierror_with_special_characters(self):
"""Test CLIError message with special characters."""
with patch('openwebui_cli.main.app') as mock_app:
special_msg = "Error: $pecial ch@rs & symbols <>"
mock_app.side_effect = CLIError(special_msg, exit_code=1)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 1
def test_clierror_overrides_app_exit_code(self):
"""Test that CLIError exit code is used instead of app's exit code."""
with patch('openwebui_cli.main.app') as mock_app:
# Simulate app that would normally exit with different code
mock_app.side_effect = CLIError("Overridden error", exit_code=42)
with pytest.raises(typer.Exit) as exc_info:
cli()
# Should use CLIError's exit code, not any default
assert exc_info.value.exit_code == 42
def test_clierror_not_caught_by_other_handlers(self):
"""Test that non-CLIError exceptions are not caught by cli()."""
with patch('openwebui_cli.main.app') as mock_app:
# This should NOT be caught by cli()
mock_app.side_effect = ValueError("Some other error")
# cli() only catches CLIError, so this will raise ValueError
with pytest.raises(ValueError, match="Some other error"):
cli()
def test_clierror_none_exit_code_uses_default(self):
"""Test that CLIError with None exit_code uses the class default."""
with patch('openwebui_cli.main.app') as mock_app:
# Create CLIError without specifying exit_code
mock_app.side_effect = CLIError("Message without explicit code")
with pytest.raises(typer.Exit) as exc_info:
cli()
# Should default to GENERAL_ERROR = 1
assert exc_info.value.exit_code == ExitCode.GENERAL_ERROR
assert exc_info.value.exit_code == 1
def test_clierror_with_console_output(self):
"""Test that error messages appear in console output."""
with patch('openwebui_cli.main.app') as mock_app:
with patch('openwebui_cli.main.console.print') as mock_print:
error_msg = "Configuration Error: Invalid model specification"
mock_app.side_effect = CLIError(error_msg, exit_code=2)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 2
# Verify console.print was called
assert mock_print.called
class TestCLIErrorIntegration:
"""Integration tests for CLIError handling with actual commands."""
def test_clierror_caught_and_exit_code_applied(self):
"""Test that CLIError is caught and proper exit code is applied."""
with patch('openwebui_cli.main.app') as mock_app:
test_error = CLIError("Integration test error", exit_code=7)
mock_app.side_effect = test_error
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 7
def test_clierror_subclass_with_default_exit_code(self):
"""Test that CLIError subclasses use their class default exit code."""
error_subclasses = [
(UsageError("usage"), 2),
(AuthError("auth"), 3),
(NetworkError("network"), 4),
(ServerError("server"), 5),
]
for error_instance, expected_code in error_subclasses:
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = error_instance
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == expected_code
class TestCLIErrorEdgeCases:
"""Test edge cases and boundary conditions for CLIError handling."""
def test_clierror_with_unicode_characters(self):
"""Test CLIError with unicode characters."""
with patch('openwebui_cli.main.app') as mock_app:
unicode_msg = "Error: Database connection failed ✗ (timeout: 5000ms)"
mock_app.side_effect = CLIError(unicode_msg, exit_code=1)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 1
def test_clierror_exit_code_boundary_255(self):
"""Test CLIError with exit code 255 (max standard exit code)."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Max exit code", exit_code=255)
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 255
def test_clierror_exit_code_256(self):
"""Test CLIError with exit code beyond standard range."""
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = CLIError("Beyond standard range", exit_code=256)
with pytest.raises(typer.Exit) as exc_info:
cli()
# typer.Exit allows values beyond standard range
assert exc_info.value.exit_code == 256
def test_clierror_with_exception_wrapping(self):
"""Test CLIError created from another exception."""
with patch('openwebui_cli.main.app') as mock_app:
original_error = ValueError("Original error")
wrapped_error = CLIError(f"Wrapped: {str(original_error)}", exit_code=1)
mock_app.side_effect = wrapped_error
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 1
def test_clierror_different_subclass_exit_codes(self):
"""Test that each CLIError subclass maintains distinct exit codes."""
error_classes = [
(UsageError("usage"), ExitCode.USAGE_ERROR, 2),
(AuthError("auth"), ExitCode.AUTH_ERROR, 3),
(NetworkError("network"), ExitCode.NETWORK_ERROR, 4),
(ServerError("server"), ExitCode.SERVER_ERROR, 5),
]
for error, expected_exit_code_obj, expected_exit_code_int in error_classes:
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = error
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == expected_exit_code_int
assert error.exit_code == expected_exit_code_obj
assert error.exit_code == expected_exit_code_int
def test_clierror_instantiation_with_custom_exit_code(self):
"""Test CLIError instantiation correctly sets custom exit code."""
error1 = CLIError("Error", exit_code=10)
assert error1.exit_code == 10
error2 = CLIError("Error without code")
assert error2.exit_code == ExitCode.GENERAL_ERROR
def test_clierror_subclass_instantiation_with_override(self):
"""Test CLIError subclass with overridden exit code."""
# UsageError normally has exit_code = 2
# But can be overridden
error = UsageError("Custom usage error", exit_code=99)
assert error.exit_code == 99
with patch('openwebui_cli.main.app') as mock_app:
mock_app.side_effect = error
with pytest.raises(typer.Exit) as exc_info:
cli()
assert exc_info.value.exit_code == 99