533 lines
20 KiB
Python
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
|
|
|