Big-Link-Man/tests/unit/test_cli_commands.py

533 lines
20 KiB
Python

"""
Unit tests for CLI commands
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from click.testing import CliRunner
from datetime import datetime
from src.cli.commands import app, authenticate_admin, prompt_admin_credentials
from src.database.models import User
class TestAuthenticateAdmin:
"""Tests for authenticate_admin function"""
@patch('src.cli.commands.db_manager')
def test_authenticate_admin_success(self, mock_db_manager):
"""Test successful admin authentication"""
# Setup
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
mock_user = User(
id=1,
username="admin",
hashed_password="hashed_password",
role="Admin",
created_at=datetime.now(),
updated_at=datetime.now()
)
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
with patch('src.cli.commands.AuthService') as mock_auth_class:
mock_auth = Mock()
mock_auth_class.return_value = mock_auth
mock_auth.authenticate_user.return_value = mock_user
# Execute
result = authenticate_admin("admin", "password")
# Assert
assert result == mock_user
mock_auth.authenticate_user.assert_called_once_with("admin", "password")
mock_session.close.assert_called_once()
@patch('src.cli.commands.db_manager')
def test_authenticate_admin_not_admin_role(self, mock_db_manager):
"""Test authentication fails when user is not admin"""
# Setup
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
mock_user = User(
id=1,
username="user",
hashed_password="hashed_password",
role="User",
created_at=datetime.now(),
updated_at=datetime.now()
)
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
with patch('src.cli.commands.AuthService') as mock_auth_class:
mock_auth = Mock()
mock_auth_class.return_value = mock_auth
mock_auth.authenticate_user.return_value = mock_user
# Execute
result = authenticate_admin("user", "password")
# Assert
assert result is None
mock_session.close.assert_called_once()
@patch('src.cli.commands.db_manager')
def test_authenticate_admin_invalid_credentials(self, mock_db_manager):
"""Test authentication fails with invalid credentials"""
# Setup
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
with patch('src.cli.commands.AuthService') as mock_auth_class:
mock_auth = Mock()
mock_auth_class.return_value = mock_auth
mock_auth.authenticate_user.return_value = None
# Execute
result = authenticate_admin("admin", "wrongpassword")
# Assert
assert result is None
mock_session.close.assert_called_once()
class TestAddUserCommand:
"""Tests for add-user CLI command"""
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_add_user_success_with_admin_credentials(self, mock_auth_admin, mock_db_manager):
"""Test successfully adding a user with admin credentials provided"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
mock_new_user = Mock(username="newuser", role="User")
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
with patch('src.cli.commands.AuthService') as mock_auth_class:
mock_auth = Mock()
mock_auth_class.return_value = mock_auth
mock_auth.create_user_with_hashed_password.return_value = mock_new_user
# Execute
result = runner.invoke(app, [
'add-user',
'--username', 'newuser',
'--password', 'password123',
'--role', 'User',
'--admin-user', 'admin',
'--admin-password', 'adminpass'
])
# Assert
assert result.exit_code == 0
assert "Success: User 'newuser' created with role 'User'" in result.output
mock_auth_admin.assert_called_once_with('admin', 'adminpass')
mock_auth.create_user_with_hashed_password.assert_called_once_with(
username='newuser',
password='password123',
role='User'
)
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_add_user_authentication_fails(self, mock_auth_admin, mock_db_manager):
"""Test add-user fails when admin authentication fails"""
# Setup
runner = CliRunner()
mock_auth_admin.return_value = None
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
# Execute
result = runner.invoke(app, [
'add-user',
'--username', 'newuser',
'--password', 'password123',
'--role', 'User',
'--admin-user', 'admin',
'--admin-password', 'wrongpass'
])
# Assert
assert result.exit_code == 1
assert "Authentication failed or insufficient permissions" in result.output
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_add_user_duplicate_username(self, mock_auth_admin, mock_db_manager):
"""Test add-user fails when username already exists"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
with patch('src.cli.commands.AuthService') as mock_auth_class:
mock_auth = Mock()
mock_auth_class.return_value = mock_auth
mock_auth.create_user_with_hashed_password.side_effect = ValueError(
"User with username 'existinguser' already exists"
)
# Execute
result = runner.invoke(app, [
'add-user',
'--username', 'existinguser',
'--password', 'password123',
'--role', 'User',
'--admin-user', 'admin',
'--admin-password', 'adminpass'
])
# Assert
assert result.exit_code == 1
assert "User with username 'existinguser' already exists" in result.output
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
@patch('src.cli.commands.prompt_admin_credentials')
def test_add_user_prompts_for_credentials(self, mock_prompt, mock_auth_admin,
mock_db_manager):
"""Test add-user prompts for admin credentials when not provided"""
# Setup
runner = CliRunner()
mock_prompt.return_value = ('admin', 'adminpass')
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
mock_new_user = Mock(username="newuser", role="User")
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
with patch('src.cli.commands.AuthService') as mock_auth_class:
mock_auth = Mock()
mock_auth_class.return_value = mock_auth
mock_auth.create_user_with_hashed_password.return_value = mock_new_user
# Execute
result = runner.invoke(app, [
'add-user',
'--username', 'newuser',
'--password', 'password123',
'--role', 'User'
])
# Assert
assert result.exit_code == 0
mock_prompt.assert_called_once()
mock_auth_admin.assert_called_once_with('admin', 'adminpass')
class TestDeleteUserCommand:
"""Tests for delete-user CLI command"""
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_delete_user_success(self, mock_auth_admin, mock_db_manager):
"""Test successfully deleting a user"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
mock_user_to_delete = Mock(id=2, username="deleteuser")
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
mock_repo.get_by_username.return_value = mock_user_to_delete
mock_repo.delete.return_value = True
# Execute
result = runner.invoke(app, [
'delete-user',
'--username', 'deleteuser',
'--admin-user', 'admin',
'--admin-password', 'adminpass',
'--yes'
])
# Assert
assert result.exit_code == 0
assert "Success: User 'deleteuser' has been deleted" in result.output
mock_repo.get_by_username.assert_called_once_with('deleteuser')
mock_repo.delete.assert_called_once_with(2)
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_delete_user_not_found(self, mock_auth_admin, mock_db_manager):
"""Test delete-user fails when user doesn't exist"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
mock_repo.get_by_username.return_value = None
# Execute
result = runner.invoke(app, [
'delete-user',
'--username', 'nonexistent',
'--admin-user', 'admin',
'--admin-password', 'adminpass',
'--yes'
])
# Assert
assert result.exit_code == 1
assert "User 'nonexistent' not found" in result.output
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_delete_user_cannot_delete_self(self, mock_auth_admin, mock_db_manager):
"""Test admin cannot delete their own account"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
# Execute
result = runner.invoke(app, [
'delete-user',
'--username', 'admin',
'--admin-user', 'admin',
'--admin-password', 'adminpass',
'--yes'
])
# Assert
assert result.exit_code == 1
assert "Cannot delete your own account" in result.output
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_delete_user_authentication_fails(self, mock_auth_admin, mock_db_manager):
"""Test delete-user fails when admin authentication fails"""
# Setup
runner = CliRunner()
mock_auth_admin.return_value = None
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
# Execute
result = runner.invoke(app, [
'delete-user',
'--username', 'someuser',
'--admin-user', 'admin',
'--admin-password', 'wrongpass',
'--yes'
])
# Assert
assert result.exit_code == 1
assert "Authentication failed or insufficient permissions" in result.output
class TestListUsersCommand:
"""Tests for list-users CLI command"""
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_list_users_success(self, mock_auth_admin, mock_db_manager):
"""Test successfully listing users"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
mock_users = [
Mock(id=1, username="admin", role="Admin",
created_at=datetime(2024, 1, 1, 12, 0, 0)),
Mock(id=2, username="user1", role="User",
created_at=datetime(2024, 1, 2, 12, 0, 0)),
Mock(id=3, username="user2", role="User",
created_at=datetime(2024, 1, 3, 12, 0, 0))
]
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
mock_repo.get_all.return_value = mock_users
# Execute
result = runner.invoke(app, [
'list-users',
'--admin-user', 'admin',
'--admin-password', 'adminpass'
])
# Assert
assert result.exit_code == 0
assert "Total users: 3" in result.output
assert "admin" in result.output
assert "user1" in result.output
assert "user2" in result.output
assert "Admin" in result.output
assert "User" in result.output
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_list_users_empty(self, mock_auth_admin, mock_db_manager):
"""Test listing users when no users exist"""
# Setup
runner = CliRunner()
mock_admin = Mock(username="admin", role="Admin")
mock_auth_admin.return_value = mock_admin
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
with patch('src.cli.commands.UserRepository') as mock_repo_class:
mock_repo = Mock()
mock_repo_class.return_value = mock_repo
mock_repo.get_all.return_value = []
# Execute
result = runner.invoke(app, [
'list-users',
'--admin-user', 'admin',
'--admin-password', 'adminpass'
])
# Assert
assert result.exit_code == 0
assert "No users found" in result.output
@patch('src.cli.commands.db_manager')
@patch('src.cli.commands.authenticate_admin')
def test_list_users_authentication_fails(self, mock_auth_admin, mock_db_manager):
"""Test list-users fails when admin authentication fails"""
# Setup
runner = CliRunner()
mock_auth_admin.return_value = None
mock_session = Mock()
mock_db_manager.get_session.return_value = mock_session
# Execute
result = runner.invoke(app, [
'list-users',
'--admin-user', 'admin',
'--admin-password', 'wrongpass'
])
# Assert
assert result.exit_code == 1
assert "Authentication failed or insufficient permissions" in result.output
class TestExistingCommands:
"""Tests for existing CLI commands (config, health, models)"""
@patch('src.cli.commands.get_config')
def test_config_command_success(self, mock_get_config):
"""Test config command displays configuration"""
# Setup
runner = CliRunner()
mock_config = Mock()
mock_config.application.name = "Test App"
mock_config.application.version = "1.0.0"
mock_config.application.environment = "test"
mock_config.database.url = "sqlite:///test.db"
mock_config.ai_service.model = "test-model"
mock_config.logging.level = "INFO"
mock_get_config.return_value = mock_config
# Execute
result = runner.invoke(app, ['config'])
# Assert
assert result.exit_code == 0
assert "Test App" in result.output
assert "1.0.0" in result.output
assert "test" in result.output
@patch('src.cli.commands.get_config')
def test_health_command_success(self, mock_get_config):
"""Test health command shows system is healthy"""
# Setup
runner = CliRunner()
mock_config = Mock()
mock_get_config.return_value = mock_config
# Execute
result = runner.invoke(app, ['health'])
# Assert
assert result.exit_code == 0
assert "[OK] Configuration loaded successfully" in result.output
assert "[OK] System is healthy" in result.output
@patch('src.cli.commands.get_config')
def test_models_command_success(self, mock_get_config):
"""Test models command lists available models"""
# Setup
runner = CliRunner()
mock_config = Mock()
mock_config.ai_service.model = "test-model-1"
mock_config.ai_service.provider = "test-provider"
mock_config.ai_service.base_url = "https://test.api"
mock_config.ai_service.available_models = {
"model1": "test-model-1",
"model2": "test-model-2"
}
mock_get_config.return_value = mock_config
# Execute
result = runner.invoke(app, ['models'])
# Assert
assert result.exit_code == 0
assert "test-provider" in result.output
assert "test-model-1" in result.output
assert "test-model-2" in result.output