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

793 lines
28 KiB
Python

"""Tests for main CLI global options stored in context."""
from unittest.mock import MagicMock, Mock, patch
import pytest
from typer.testing import CliRunner
from openwebui_cli.main import app
runner = CliRunner()
@pytest.fixture
def mock_config(tmp_path, monkeypatch):
"""Mock configuration for testing."""
config_dir = tmp_path / "openwebui"
config_path = config_dir / "config.yaml"
monkeypatch.setattr("openwebui_cli.config.get_config_dir", lambda: config_dir)
monkeypatch.setattr("openwebui_cli.config.get_config_path", lambda: config_path)
# Create default config
from openwebui_cli.config import Config, save_config
config = Config()
save_config(config)
return config_path
@pytest.fixture
def mock_keyring(monkeypatch):
"""Mock keyring for testing."""
token_store = {}
def get_password(service, key):
return token_store.get(f"{service}:{key}")
def set_password(service, key, password):
token_store[f"{service}:{key}"] = password
monkeypatch.setattr("keyring.get_password", get_password)
monkeypatch.setattr("keyring.set_password", set_password)
@pytest.fixture
def mock_client():
"""Mock HTTP client for testing."""
response_data = {
"choices": [
{
"message": {
"content": "Test response"
}
}
]
}
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = response_data
mock_http_client.post.return_value = mock_response
return mock_http_client
class TestGlobalOptionsStorage:
"""Test that global options are properly stored in context."""
def test_profile_option_stored_in_context(self, mock_config, mock_keyring, mock_client):
"""Test --profile option is stored in context."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--profile", "test-profile",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify profile was passed to create_client
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "test-profile"
def test_uri_option_stored_in_context(self, mock_config, mock_keyring, mock_client):
"""Test --uri option is stored in context."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--uri", "http://test.local:9000",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify URI was passed to create_client
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("uri") == "http://test.local:9000"
def test_token_option_stored_in_context(self, mock_config, mock_keyring, mock_client):
"""Test --token option is stored in context."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--token", "secret-token-123",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify token was passed to create_client
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("token") == "secret-token-123"
def test_timeout_option_stored_in_context(self, mock_config, mock_keyring, mock_client):
"""Test --timeout option is stored in context."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--timeout", "60",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify timeout was passed to create_client as integer
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("timeout") == 60
assert isinstance(call_kwargs.get("timeout"), int)
def test_format_option_stored_in_context(self, mock_config, mock_keyring, mock_client):
"""Test --format option is stored in context."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--format", "json",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Verify format was used for output
assert "choices" in result.stdout # JSON output has 'choices' key
class TestFormatOptionDefault:
"""Test format option defaults to 'text'."""
def test_format_defaults_to_text(self, mock_config, mock_keyring, mock_client):
"""Test --format defaults to 'text' when not specified."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Text format should show plain content without JSON structure
assert "Test response" in result.stdout
def test_format_text_explicit(self, mock_config, mock_keyring, mock_client):
"""Test --format text explicitly."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--format", "text",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
assert "Test response" in result.stdout
def test_format_json_explicit(self, mock_config, mock_keyring, mock_client):
"""Test --format json explicitly."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--format", "json",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# JSON format should have structured output
assert "choices" in result.stdout
class TestQuietFlag:
"""Test --quiet flag."""
def test_quiet_flag_recognized(self, mock_config, mock_keyring, mock_client):
"""Test --quiet flag is recognized."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--quiet",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
def test_quiet_flag_short_form(self, mock_config, mock_keyring, mock_client):
"""Test -q short form of --quiet."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-q",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
def test_quiet_flag_default_false(self, mock_config, mock_keyring, mock_client):
"""Test quiet flag defaults to False."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
# Without quiet flag, output should be normal
assert len(result.stdout) > 0
class TestVerboseFlag:
"""Test --verbose flag."""
def test_verbose_flag_recognized(self, mock_config, mock_keyring, mock_client):
"""Test --verbose flag is recognized."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--verbose",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
def test_verbose_flag_debug_alias(self, mock_config, mock_keyring, mock_client):
"""Test --debug is alias for --verbose."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--debug",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
def test_verbose_flag_default_false(self, mock_config, mock_keyring, mock_client):
"""Test verbose flag defaults to False."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
class TestShortFormOptions:
"""Test short form global options."""
def test_profile_short_form_p_upper(self, mock_config, mock_keyring, mock_client):
"""Test -P short form for --profile."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-P", "prod-profile",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "prod-profile"
def test_uri_short_form_u_upper(self, mock_config, mock_keyring, mock_client):
"""Test -U short form for --uri."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-U", "http://prod.example.com",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("uri") == "http://prod.example.com"
def test_format_short_form_f(self, mock_config, mock_keyring, mock_client):
"""Test -f short form for --format."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-f", "json",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
assert "choices" in result.stdout
def test_timeout_short_form_t(self, mock_config, mock_keyring, mock_client):
"""Test -t short form for --timeout."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-t", "30",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("timeout") == 30
class TestMultipleGlobalOptions:
"""Test multiple global options together."""
def test_all_options_together(self, mock_config, mock_keyring, mock_client):
"""Test all global options together."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--profile", "test-profile",
"--uri", "http://test.local:9000",
"--token", "secret-token",
"--format", "json",
"--timeout", "45",
"--verbose",
"--quiet",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "test-profile"
assert call_kwargs.get("uri") == "http://test.local:9000"
assert call_kwargs.get("token") == "secret-token"
assert call_kwargs.get("timeout") == 45
def test_short_form_options_combined(self, mock_config, mock_keyring, mock_client):
"""Test short form options can be combined."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-P", "profile1",
"-U", "http://server1.com",
"-f", "json",
"-t", "50",
"-q",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "profile1"
assert call_kwargs.get("uri") == "http://server1.com"
assert call_kwargs.get("timeout") == 50
def test_mixed_short_and_long_options(self, mock_config, mock_keyring, mock_client):
"""Test mixing short and long form options."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"-P", "profile2",
"--uri", "http://mixed.com",
"-f", "text",
"--token", "mixed-token",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "profile2"
assert call_kwargs.get("uri") == "http://mixed.com"
assert call_kwargs.get("token") == "mixed-token"
class TestGlobalOptionsWithDifferentCommands:
"""Test global options work with different subcommands."""
def test_global_options_with_models_command(self, mock_config, mock_keyring):
"""Test global options are available for models command."""
with patch("openwebui_cli.commands.models.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": []}
mock_http_client.get.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"--profile", "test-profile",
"--uri", "http://test.local",
"--token", "test-token",
"models", "list"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "test-profile"
assert call_kwargs.get("uri") == "http://test.local"
assert call_kwargs.get("token") == "test-token"
def test_global_options_with_auth_command(self, mock_config, mock_keyring):
"""Test global options are available for auth command."""
with patch("openwebui_cli.commands.auth.create_client") as mock_create_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"name": "test", "email": "test@example.com", "role": "user"}
mock_http_client.get.return_value = mock_response
mock_create_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"--profile", "auth-profile",
"--uri", "http://auth.local",
"auth", "whoami"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "auth-profile"
assert call_kwargs.get("uri") == "http://auth.local"
class TestGlobalOptionsEdgeCases:
"""Test edge cases for global options."""
def test_timeout_zero_value(self, mock_config, mock_keyring, mock_client):
"""Test timeout with zero value."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--timeout", "0",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("timeout") == 0
def test_timeout_large_value(self, mock_config, mock_keyring, mock_client):
"""Test timeout with large value."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--timeout", "3600",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("timeout") == 3600
def test_profile_with_special_characters(self, mock_config, mock_keyring, mock_client):
"""Test profile with special characters."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--profile", "test-profile_v2",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("profile") == "test-profile_v2"
def test_uri_with_special_characters(self, mock_config, mock_keyring, mock_client):
"""Test URI with special characters and ports."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--uri", "http://test.example.com:9000/api",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("uri") == "http://test.example.com:9000/api"
def test_token_with_special_characters(self, mock_config, mock_keyring, mock_client):
"""Test token with special characters."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
special_token = "sk-test_1234-5678$%&!@#"
result = runner.invoke(
app,
[
"--token", special_token,
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
assert call_kwargs.get("token") == special_token
def test_none_values_handled_correctly(self, mock_config, mock_keyring, mock_client):
"""Test that None values are handled correctly."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_create_client.call_args[1]
# When not provided, these should be None
assert call_kwargs.get("profile") is None
assert call_kwargs.get("uri") is None
assert call_kwargs.get("token") is None
def test_format_with_unrecognized_value(self, mock_config, mock_keyring, mock_client):
"""Test format with unrecognized value (still stored, usage depends on command)."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
result = runner.invoke(
app,
[
"--format", "yaml",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
# Command succeeds; format validation is command-specific
assert result.exit_code == 0
class TestGlobalOptionsContextIsolation:
"""Test that context is properly isolated between commands."""
def test_context_not_shared_between_invocations(self, mock_config, mock_keyring, mock_client):
"""Test that context from one invocation doesn't leak to next."""
with patch("openwebui_cli.commands.chat.create_client") as mock_create_client:
mock_create_client.return_value = mock_client
# First invocation with profile1
result1 = runner.invoke(
app,
[
"--profile", "profile1",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result1.exit_code == 0
# Second invocation with profile2
result2 = runner.invoke(
app,
[
"--profile", "profile2",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result2.exit_code == 0
# Verify each invocation got the right profile
calls = mock_create_client.call_args_list
assert calls[0][1].get("profile") == "profile1"
assert calls[1][1].get("profile") == "profile2"
def test_context_persists_across_subcommand_calls(self, mock_config, mock_keyring):
"""Test that context persists when calling subcommands."""
with patch("openwebui_cli.commands.chat.create_client") as mock_chat_client:
mock_http_client = MagicMock()
mock_http_client.__enter__.return_value = mock_http_client
mock_http_client.__exit__.return_value = None
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"choices": [{"message": {"content": "Response"}}]
}
mock_http_client.post.return_value = mock_response
mock_chat_client.return_value = mock_http_client
result = runner.invoke(
app,
[
"--token", "persistent-token",
"chat", "send",
"-m", "test-model",
"-p", "Hello",
"--no-stream"
],
)
assert result.exit_code == 0
call_kwargs = mock_chat_client.call_args[1]
assert call_kwargs.get("token") == "persistent-token"