Files
aitbc/tests/cli/test_admin.py
oib 5120861e17 feat: add GPU-specific fields to marketplace offers and create dedicated GPU marketplace router
- Add GPU fields (model, memory, count, CUDA version, price, region) to MarketplaceOffer model
- Create new marketplace_gpu router for GPU-specific operations
- Update offer sync to populate GPU fields from miner capabilities
- Move GPU attributes from generic attributes dict to dedicated fields
- Update MarketplaceOfferView schema with GPU fields
- Expand CLI README with comprehensive documentation and
2026-02-12 19:08:17 +01:00

393 lines
14 KiB
Python

"""Tests for admin CLI commands"""
import pytest
import json
from click.testing import CliRunner
from unittest.mock import Mock, patch
from aitbc_cli.commands.admin import admin
@pytest.fixture
def runner():
"""Create CLI runner"""
return CliRunner()
@pytest.fixture
def mock_config():
"""Mock configuration"""
config = Mock()
config.coordinator_url = "http://test:8000"
config.api_key = "test_admin_key"
return config
class TestAdminCommands:
"""Test admin command group"""
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_status_success(self, mock_client_class, runner, mock_config):
"""Test successful system status check"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"status": "healthy",
"version": "1.0.0",
"uptime": 3600
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'status'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert data['status'] == 'healthy'
assert data['version'] == '1.0.0'
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/admin/status',
headers={"X-Api-Key": "test_admin_key"}
)
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_jobs_with_filter(self, mock_client_class, runner, mock_config):
"""Test jobs listing with filters"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"jobs": [
{"id": "job1", "status": "completed"},
{"id": "job2", "status": "running"}
]
}
mock_client.get.return_value = mock_response
# Run command with filters
result = runner.invoke(admin, [
'jobs',
'--status', 'running',
'--limit', '50'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
# Verify API call with filters
mock_client.get.assert_called_once()
call_args = mock_client.get.call_args
assert '/v1/admin/jobs' in call_args[0][0]
assert call_args[1]['params']['status'] == 'running'
assert call_args[1]['params']['limit'] == 50
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_job_details_success(self, mock_client_class, runner, mock_config):
"""Test successful job details retrieval"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "job123",
"status": "completed",
"result": "Test result",
"created_at": "2024-01-01T00:00:00"
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'job-details',
'job123'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert data['id'] == 'job123'
assert data['status'] == 'completed'
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/admin/jobs/job123',
headers={"X-Api-Key": "test_admin_key"}
)
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_delete_job_confirmed(self, mock_client_class, runner, mock_config):
"""Test successful job deletion with confirmation"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_client.delete.return_value = mock_response
# Run command with confirmation
result = runner.invoke(admin, [
'delete-job',
'job123'
], obj={'config': mock_config, 'output_format': 'json'}, input='y\n')
# Assertions
assert result.exit_code == 0
assert 'deleted' in result.output
# Verify API call
mock_client.delete.assert_called_once_with(
'http://test:8000/v1/admin/jobs/job123',
headers={"X-Api-Key": "test_admin_key"}
)
def test_delete_job_cancelled(self, runner, mock_config):
"""Test job deletion cancelled by user"""
# Run command with cancellation
result = runner.invoke(admin, [
'delete-job',
'job123'
], obj={'config': mock_config, 'output_format': 'json'}, input='n\n')
# Assertions
assert result.exit_code == 0
# No API calls should be made
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_miners_list(self, mock_client_class, runner, mock_config):
"""Test miners listing"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"miners": [
{"id": "miner1", "status": "active", "gpu": "RTX4090"},
{"id": "miner2", "status": "inactive", "gpu": "RTX3080"}
]
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'miners'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert len(data['miners']) == 2
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/admin/miners',
params={"limit": 50},
headers={"X-Api-Key": "test_admin_key"}
)
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_deactivate_miner(self, mock_client_class, runner, mock_config):
"""Test miner deactivation"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_client.post.return_value = mock_response
# Run command with confirmation
result = runner.invoke(admin, [
'deactivate-miner',
'miner123'
], obj={'config': mock_config, 'output_format': 'json'}, input='y\n')
# Assertions
assert result.exit_code == 0
assert 'deactivated' in result.output
# Verify API call
mock_client.post.assert_called_once_with(
'http://test:8000/v1/admin/miners/miner123/deactivate',
headers={"X-Api-Key": "test_admin_key"}
)
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_analytics(self, mock_client_class, runner, mock_config):
"""Test system analytics"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"total_jobs": 1000,
"completed_jobs": 950,
"active_miners": 50,
"average_processing_time": 120
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'analytics',
'--days', '7'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert data['total_jobs'] == 1000
assert data['active_miners'] == 50
# Verify API call
mock_client.get.assert_called_once()
call_args = mock_client.get.call_args
assert '/v1/admin/analytics' in call_args[0][0]
assert call_args[1]['params']['days'] == 7
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_logs_with_level(self, mock_client_class, runner, mock_config):
"""Test system logs with level filter"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"logs": [
{"level": "ERROR", "message": "Test error", "timestamp": "2024-01-01T00:00:00"}
]
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'logs',
'--level', 'ERROR',
'--limit', '50'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
# Verify API call
mock_client.get.assert_called_once()
call_args = mock_client.get.call_args
assert '/v1/admin/logs' in call_args[0][0]
assert call_args[1]['params']['level'] == 'ERROR'
assert call_args[1]['params']['limit'] == 50
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_prioritize_job(self, mock_client_class, runner, mock_config):
"""Test job prioritization"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_client.post.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'prioritize-job',
'job123',
'--reason', 'Urgent request'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'prioritized' in result.output
# Verify API call
mock_client.post.assert_called_once()
call_args = mock_client.post.call_args
assert '/v1/admin/jobs/job123/prioritize' in call_args[0][0]
assert call_args[1]['json']['reason'] == 'Urgent request'
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_execute_custom_action(self, mock_client_class, runner, mock_config):
"""Test custom action execution"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": "success", "result": "Action completed"}
mock_client.post.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'execute',
'--action', 'custom_command',
'--target', 'miner123',
'--data', '{"param": "value"}'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert data['status'] == 'success'
# Verify API call
mock_client.post.assert_called_once()
call_args = mock_client.post.call_args
assert '/v1/admin/execute/custom_command' in call_args[0][0]
assert call_args[1]['json']['target'] == 'miner123'
assert call_args[1]['json']['param'] == 'value'
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_maintenance_cleanup(self, mock_client_class, runner, mock_config):
"""Test maintenance cleanup"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"cleaned_items": 100}
mock_client.post.return_value = mock_response
# Run command with confirmation
result = runner.invoke(admin, [
'maintenance',
'cleanup'
], obj={'config': mock_config, 'output_format': 'json'}, input='y\n')
# Assertions
assert result.exit_code == 0
assert 'Cleanup completed' in result.output
# Verify API call
mock_client.post.assert_called_once_with(
'http://test:8000/v1/admin/maintenance/cleanup',
headers={"X-Api-Key": "test_admin_key"}
)
@patch('aitbc_cli.commands.admin.httpx.Client')
def test_api_error_handling(self, mock_client_class, runner, mock_config):
"""Test API error handling"""
# Setup mock for error response
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 403
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(admin, [
'status'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code != 0
assert 'Error' in result.output