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
This commit is contained in:
392
tests/cli/test_admin.py
Normal file
392
tests/cli/test_admin.py
Normal file
@@ -0,0 +1,392 @@
|
||||
"""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
|
||||
361
tests/cli/test_auth.py
Normal file
361
tests/cli/test_auth.py
Normal file
@@ -0,0 +1,361 @@
|
||||
"""Tests for auth CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.auth import auth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
return {}
|
||||
|
||||
|
||||
class TestAuthCommands:
|
||||
"""Test auth command group"""
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_login_success(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test successful login"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'login',
|
||||
'test_api_key_12345',
|
||||
'--environment', 'dev'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'logged_in'
|
||||
assert data['environment'] == 'dev'
|
||||
|
||||
# Verify credential stored
|
||||
mock_auth_manager.store_credential.assert_called_once_with(
|
||||
'client', 'test_api_key_12345', 'dev'
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_login_invalid_key(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test login with invalid API key"""
|
||||
# Run command with short key
|
||||
result = runner.invoke(auth, [
|
||||
'login',
|
||||
'short',
|
||||
'--environment', 'dev'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code != 0
|
||||
assert 'Invalid API key' in result.output
|
||||
|
||||
# Verify credential not stored
|
||||
mock_auth_manager_class.return_value.store_credential.assert_not_called()
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_logout_success(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test successful logout"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'logout',
|
||||
'--environment', 'prod'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'logged_out'
|
||||
assert data['environment'] == 'prod'
|
||||
|
||||
# Verify credential deleted
|
||||
mock_auth_manager.delete_credential.assert_called_once_with(
|
||||
'client', 'prod'
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_token_show(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test token command with show flag"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.get_credential.return_value = 'secret_key_123'
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'token',
|
||||
'--show',
|
||||
'--environment', 'staging'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['api_key'] == 'secret_key_123'
|
||||
assert data['environment'] == 'staging'
|
||||
|
||||
# Verify credential retrieved
|
||||
mock_auth_manager.get_credential.assert_called_once_with(
|
||||
'client', 'staging'
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_token_hidden(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test token command without show flag"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.get_credential.return_value = 'secret_key_123'
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'token',
|
||||
'--environment', 'staging'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['api_key'] == '***REDACTED***'
|
||||
assert data['length'] == len('secret_key_123')
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_token_not_found(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test token command when no credential stored"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.get_credential.return_value = None
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'token',
|
||||
'--environment', 'nonexistent'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['message'] == 'No API key stored'
|
||||
assert data['environment'] == 'nonexistent'
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_status_authenticated(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test status when authenticated"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.list_credentials.return_value = ['client@dev', 'miner@prod']
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'authenticated'
|
||||
assert len(data['stored_credentials']) == 2
|
||||
assert 'client@dev' in data['stored_credentials']
|
||||
assert 'miner@prod' in data['stored_credentials']
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_status_not_authenticated(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test status when not authenticated"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.list_credentials.return_value = []
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'not_authenticated'
|
||||
assert data['message'] == 'No stored credentials found'
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_refresh_success(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test refresh command"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.get_credential.return_value = 'valid_key'
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'refresh',
|
||||
'--environment', 'dev'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'refreshed'
|
||||
assert data['environment'] == 'dev'
|
||||
assert 'placeholder' in data['message']
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_refresh_no_key(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test refresh with no stored key"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.get_credential.return_value = None
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'refresh',
|
||||
'--environment', 'nonexistent'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code != 0
|
||||
assert 'No API key found' in result.output
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_keys_list(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test keys list command"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager.list_credentials.return_value = [
|
||||
'client@dev', 'miner@dev', 'admin@prod'
|
||||
]
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'keys',
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['credentials']) == 3
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_keys_create(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test keys create command"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'keys',
|
||||
'create',
|
||||
'miner',
|
||||
'miner_key_abcdef',
|
||||
'--permissions', 'mine,poll',
|
||||
'--environment', 'prod'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'created'
|
||||
assert data['name'] == 'miner'
|
||||
assert data['environment'] == 'prod'
|
||||
assert data['permissions'] == 'mine,poll'
|
||||
|
||||
# Verify credential stored
|
||||
mock_auth_manager.store_credential.assert_called_once_with(
|
||||
'miner', 'miner_key_abcdef', 'prod'
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_keys_revoke(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test keys revoke command"""
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'keys',
|
||||
'revoke',
|
||||
'old_miner',
|
||||
'--environment', 'dev'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'revoked'
|
||||
assert data['name'] == 'old_miner'
|
||||
assert data['environment'] == 'dev'
|
||||
|
||||
# Verify credential deleted
|
||||
mock_auth_manager.delete_credential.assert_called_once_with(
|
||||
'old_miner', 'dev'
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {'CLIENT_API_KEY': 'env_test_key'})
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_import_env_success(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test successful import from environment"""
|
||||
import os
|
||||
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'import-env',
|
||||
'client'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['status'] == 'imported'
|
||||
assert data['name'] == 'client'
|
||||
assert data['source'] == 'CLIENT_API_KEY'
|
||||
|
||||
# Verify credential stored
|
||||
mock_auth_manager.store_credential.assert_called_once_with(
|
||||
'client', 'env_test_key'
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {})
|
||||
@patch('aitbc_cli.commands.auth.AuthManager')
|
||||
def test_import_env_not_set(self, mock_auth_manager_class, runner, mock_config):
|
||||
"""Test import when environment variable not set"""
|
||||
import os
|
||||
|
||||
# Setup mock
|
||||
mock_auth_manager = Mock()
|
||||
mock_auth_manager_class.return_value = mock_auth_manager
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(auth, [
|
||||
'import-env',
|
||||
'client'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code != 0
|
||||
assert 'CLIENT_API_KEY not set' in result.output
|
||||
357
tests/cli/test_blockchain.py
Normal file
357
tests/cli/test_blockchain.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""Tests for blockchain CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.blockchain import blockchain
|
||||
|
||||
|
||||
@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_api_key"
|
||||
return config
|
||||
|
||||
|
||||
class TestBlockchainCommands:
|
||||
"""Test blockchain command group"""
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_blocks_success(self, mock_client_class, runner, mock_config):
|
||||
"""Test successful block 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 = {
|
||||
"blocks": [
|
||||
{"height": 100, "hash": "0xabc123", "timestamp": "2024-01-01T00:00:00"},
|
||||
{"height": 99, "hash": "0xdef456", "timestamp": "2024-01-01T00:01:00"}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'blocks',
|
||||
'--limit', '2'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['blocks']) == 2
|
||||
assert data['blocks'][0]['height'] == 100
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/explorer/blocks',
|
||||
params={"limit": 2},
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_block_details(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting block details"""
|
||||
# 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 = {
|
||||
"height": 100,
|
||||
"hash": "0xabc123",
|
||||
"transactions": ["0xtx1", "0xtx2"],
|
||||
"timestamp": "2024-01-01T00:00:00",
|
||||
"validator": "validator1"
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'block',
|
||||
'100'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['height'] == 100
|
||||
assert data['hash'] == '0xabc123'
|
||||
assert len(data['transactions']) == 2
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/explorer/blocks/100',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_transaction(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting transaction details"""
|
||||
# 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 = {
|
||||
"hash": "0xtx123",
|
||||
"block": 100,
|
||||
"from": "0xabc",
|
||||
"to": "0xdef",
|
||||
"amount": "1000",
|
||||
"fee": "10",
|
||||
"status": "confirmed"
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'transaction',
|
||||
'0xtx123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['hash'] == '0xtx123'
|
||||
assert data['block'] == 100
|
||||
assert data['status'] == 'confirmed'
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/explorer/transactions/0xtx123',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_node_status(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting node status"""
|
||||
# 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": "running",
|
||||
"version": "1.0.0",
|
||||
"height": 1000,
|
||||
"peers": 5,
|
||||
"synced": True
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'status',
|
||||
'--node', '1'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['node'] == 1
|
||||
assert data['rpc_url'] == 'http://localhost:8082'
|
||||
assert data['status']['status'] == 'running'
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://localhost:8082/status',
|
||||
timeout=5
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_sync_status(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting sync status"""
|
||||
# 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 = {
|
||||
"synced": True,
|
||||
"current_height": 1000,
|
||||
"target_height": 1000,
|
||||
"sync_percentage": 100.0
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'sync-status'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['synced'] == True
|
||||
assert data['sync_percentage'] == 100.0
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/blockchain/sync',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_peers(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing peers"""
|
||||
# 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 = {
|
||||
"peers": [
|
||||
{"id": "peer1", "address": "1.2.3.4:8080", "connected": True},
|
||||
{"id": "peer2", "address": "5.6.7.8:8080", "connected": False}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'peers'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['peers']) == 2
|
||||
assert data['peers'][0]['connected'] == True
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/blockchain/peers',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_info(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting blockchain info"""
|
||||
# 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 = {
|
||||
"network": "aitbc-mainnet",
|
||||
"chain_id": "aitbc-1",
|
||||
"block_time": 5,
|
||||
"min_stake": 1000,
|
||||
"total_supply": "1000000000"
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'info'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['network'] == 'aitbc-mainnet'
|
||||
assert data['block_time'] == 5
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/blockchain/info',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_supply(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting token supply"""
|
||||
# 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_supply": "1000000000",
|
||||
"circulating_supply": "500000000",
|
||||
"staked": "300000000",
|
||||
"burned": "200000000"
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'supply'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['total_supply'] == '1000000000'
|
||||
assert data['circulating_supply'] == '500000000'
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/blockchain/supply',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.httpx.Client')
|
||||
def test_validators(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing validators"""
|
||||
# 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 = {
|
||||
"validators": [
|
||||
{"address": "0xval1", "stake": "100000", "status": "active"},
|
||||
{"address": "0xval2", "stake": "50000", "status": "active"}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'validators'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['validators']) == 2
|
||||
assert data['validators'][0]['stake'] == '100000'
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/blockchain/validators',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.blockchain.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 = 404
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(blockchain, [
|
||||
'block',
|
||||
'999999'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0 # The command doesn't exit on error
|
||||
assert 'not found' in result.output
|
||||
244
tests/cli/test_client.py
Normal file
244
tests/cli/test_client.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""Tests for client CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.client import client
|
||||
|
||||
|
||||
@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_key"
|
||||
return config
|
||||
|
||||
|
||||
class TestClientCommands:
|
||||
"""Test client command group"""
|
||||
|
||||
@patch('aitbc_cli.commands.client.httpx.Client')
|
||||
def test_submit_job_success(self, mock_client_class, runner, mock_config):
|
||||
"""Test successful job submission"""
|
||||
# Setup mock
|
||||
mock_client = Mock()
|
||||
mock_client_class.return_value.__enter__.return_value = mock_client
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.json.return_value = {"job_id": "test_job_123"}
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(client, [
|
||||
'submit',
|
||||
'--type', 'inference',
|
||||
'--prompt', 'Test prompt',
|
||||
'--model', 'test_model'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'job_id' in result.output
|
||||
|
||||
# Verify API call
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
assert '/v1/jobs' in call_args[0][0]
|
||||
assert call_args[1]['json']['payload']['type'] == 'inference'
|
||||
assert call_args[1]['json']['payload']['prompt'] == 'Test prompt'
|
||||
|
||||
@patch('aitbc_cli.commands.client.httpx.Client')
|
||||
def test_submit_job_from_file(self, mock_client_class, runner, mock_config, tmp_path):
|
||||
"""Test job submission from file"""
|
||||
# Create test job file
|
||||
job_file = tmp_path / "test_job.json"
|
||||
job_data = {
|
||||
"type": "training",
|
||||
"model": "gpt-3",
|
||||
"dataset": "test_data"
|
||||
}
|
||||
job_file.write_text(json.dumps(job_data))
|
||||
|
||||
# Setup mock
|
||||
mock_client = Mock()
|
||||
mock_client_class.return_value.__enter__.return_value = mock_client
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.json.return_value = {"job_id": "test_job_456"}
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(client, [
|
||||
'submit',
|
||||
'--file', str(job_file)
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'job_id' in result.output
|
||||
|
||||
# Verify API call used file data
|
||||
call_args = mock_client.post.call_args
|
||||
assert call_args[1]['json']['payload']['type'] == 'training'
|
||||
assert call_args[1]['json']['payload']['model'] == 'gpt-3'
|
||||
|
||||
@patch('aitbc_cli.commands.client.httpx.Client')
|
||||
def test_status_success(self, mock_client_class, runner, mock_config):
|
||||
"""Test successful job 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 = {
|
||||
"job_id": "test_job_123",
|
||||
"state": "completed",
|
||||
"result": "Test result"
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(client, [
|
||||
'status',
|
||||
'test_job_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'completed' in result.output
|
||||
assert 'test_job_123' in result.output
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/jobs/test_job_123',
|
||||
headers={"X-Api-Key": "test_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.client.httpx.Client')
|
||||
def test_cancel_job_success(self, mock_client_class, runner, mock_config):
|
||||
"""Test successful job cancellation"""
|
||||
# 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(client, [
|
||||
'cancel',
|
||||
'test_job_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify API call
|
||||
mock_client.post.assert_called_once_with(
|
||||
'http://test:8000/v1/jobs/test_job_123/cancel',
|
||||
headers={"X-Api-Key": "test_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.client.httpx.Client')
|
||||
def test_blocks_success(self, mock_client_class, runner, mock_config):
|
||||
"""Test successful blocks 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 = {
|
||||
"items": [
|
||||
{"height": 100, "hash": "0x123"},
|
||||
{"height": 101, "hash": "0x456"}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(client, [
|
||||
'blocks',
|
||||
'--limit', '2'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'items' in result.output
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once()
|
||||
call_args = mock_client.get.call_args
|
||||
assert '/v1/explorer/blocks' in call_args[0][0]
|
||||
assert call_args[1]['params']['limit'] == 2
|
||||
|
||||
@patch('aitbc_cli.commands.client.httpx.Client')
|
||||
def test_history_with_filters(self, mock_client_class, runner, mock_config):
|
||||
"""Test job history 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": "failed"}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command with filters
|
||||
result = runner.invoke(client, [
|
||||
'history',
|
||||
'--status', 'completed',
|
||||
'--type', 'inference',
|
||||
'--limit', '10'
|
||||
], 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 call_args[1]['params']['status'] == 'completed'
|
||||
assert call_args[1]['params']['type'] == 'inference'
|
||||
assert call_args[1]['params']['limit'] == 10
|
||||
|
||||
@patch('aitbc_cli.commands.client.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 = 500
|
||||
mock_response.text = "Internal Server Error"
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(client, [
|
||||
'status',
|
||||
'test_job_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code != 0
|
||||
assert 'Error' in result.output
|
||||
|
||||
def test_submit_missing_required_args(self, runner, mock_config):
|
||||
"""Test submit command with missing required arguments"""
|
||||
result = runner.invoke(client, [
|
||||
'submit'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'Error' in result.output
|
||||
654
tests/cli/test_config.py
Normal file
654
tests/cli/test_config.py
Normal file
@@ -0,0 +1,654 @@
|
||||
"""Tests for config CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import yaml
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.config import config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://127.0.0.1:18000"
|
||||
config.api_key = None
|
||||
config.timeout = 30
|
||||
config.config_file = "/home/oib/.aitbc/config.yaml"
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_config_file():
|
||||
"""Create temporary config file"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||
config_data = {
|
||||
"coordinator_url": "http://test:8000",
|
||||
"api_key": "test_key",
|
||||
"timeout": 60
|
||||
}
|
||||
yaml.dump(config_data, f)
|
||||
temp_path = f.name
|
||||
|
||||
yield temp_path
|
||||
|
||||
# Cleanup
|
||||
os.unlink(temp_path)
|
||||
|
||||
|
||||
class TestConfigCommands:
|
||||
"""Test config command group"""
|
||||
|
||||
def test_show_config(self, runner, mock_config):
|
||||
"""Test showing current configuration"""
|
||||
result = runner.invoke(config, [
|
||||
'show'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['coordinator_url'] == 'http://127.0.0.1:18000'
|
||||
assert data['api_key'] is None # mock_config has api_key=None
|
||||
assert data['timeout'] == 30
|
||||
|
||||
def test_set_coordinator_url(self, runner, mock_config, tmp_path):
|
||||
"""Test setting coordinator URL"""
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(config, [
|
||||
'set',
|
||||
'coordinator_url',
|
||||
'http://new:8000'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Coordinator URL set to: http://new:8000' in result.output
|
||||
|
||||
# Verify file was created in current directory
|
||||
config_file = Path.cwd() / ".aitbc.yaml"
|
||||
assert config_file.exists()
|
||||
with open(config_file) as f:
|
||||
saved_config = yaml.safe_load(f)
|
||||
assert saved_config['coordinator_url'] == 'http://new:8000'
|
||||
|
||||
def test_set_api_key(self, runner, mock_config):
|
||||
"""Test setting API key"""
|
||||
result = runner.invoke(config, [
|
||||
'set',
|
||||
'api_key',
|
||||
'new_test_key_12345'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'API key set (use --global to set permanently)' in result.output
|
||||
|
||||
def test_set_timeout(self, runner, mock_config):
|
||||
"""Test setting timeout"""
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(config, [
|
||||
'set',
|
||||
'timeout',
|
||||
'45'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Timeout set to: 45s' in result.output
|
||||
|
||||
def test_set_invalid_timeout(self, runner, mock_config):
|
||||
"""Test setting invalid timeout"""
|
||||
result = runner.invoke(config, [
|
||||
'set',
|
||||
'timeout',
|
||||
'invalid'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'Timeout must be an integer' in result.output
|
||||
|
||||
def test_set_invalid_key(self, runner, mock_config):
|
||||
"""Test setting invalid configuration key"""
|
||||
result = runner.invoke(config, [
|
||||
'set',
|
||||
'invalid_key',
|
||||
'value'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'Unknown configuration key' in result.output
|
||||
|
||||
def test_path_command(self, runner, mock_config, tmp_path):
|
||||
"""Test showing configuration file path"""
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(config, [
|
||||
'path'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '.aitbc.yaml' in result.output
|
||||
|
||||
def test_path_global(self, runner, mock_config):
|
||||
"""Test showing global config path"""
|
||||
result = runner.invoke(config, [
|
||||
'path',
|
||||
'--global'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert '.config/aitbc/config.yaml' in result.output
|
||||
|
||||
@patch('os.system')
|
||||
def test_edit_command(self, mock_system, runner, mock_config, tmp_path):
|
||||
"""Test editing configuration file"""
|
||||
|
||||
# Change to the tmp_path directory
|
||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
||||
# The actual config file will be in the current working directory
|
||||
actual_config_file = Path.cwd() / ".aitbc.yaml"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'edit'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Verify editor was called
|
||||
mock_system.assert_called_once()
|
||||
assert 'nano' in mock_system.call_args[0][0]
|
||||
assert str(actual_config_file) in mock_system.call_args[0][0]
|
||||
|
||||
def test_reset_config_cancelled(self, runner, mock_config, temp_config_file):
|
||||
"""Test config reset cancelled by user"""
|
||||
# Change to the directory containing the config file
|
||||
config_dir = Path(temp_config_file).parent
|
||||
with runner.isolated_filesystem(temp_dir=config_dir):
|
||||
# Copy the config file to the current directory
|
||||
import shutil
|
||||
local_config = Path.cwd() / ".aitbc.yaml"
|
||||
shutil.copy2(temp_config_file, local_config)
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'reset'
|
||||
], obj={'config': mock_config, 'output_format': 'json'}, input='n\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
# File should still exist
|
||||
assert local_config.exists()
|
||||
|
||||
def test_reset_config_confirmed(self, runner, mock_config, temp_config_file):
|
||||
"""Test config reset confirmed"""
|
||||
# Change to the directory containing the config file
|
||||
config_dir = Path(temp_config_file).parent
|
||||
with runner.isolated_filesystem(temp_dir=config_dir):
|
||||
# Copy the config file to the current directory
|
||||
import shutil
|
||||
local_config = Path.cwd() / ".aitbc.yaml"
|
||||
shutil.copy2(temp_config_file, local_config)
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'reset'
|
||||
], obj={'config': mock_config, 'output_format': 'table'}, input='y\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Configuration reset' in result.output
|
||||
# File should be deleted
|
||||
assert not local_config.exists()
|
||||
|
||||
def test_reset_no_config(self, runner, mock_config):
|
||||
"""Test reset when no config file exists"""
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(config, [
|
||||
'reset'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'No configuration file found' in result.output
|
||||
|
||||
def test_export_yaml(self, runner, mock_config, temp_config_file):
|
||||
"""Test exporting configuration as YAML"""
|
||||
# Change to the directory containing the config file
|
||||
config_dir = Path(temp_config_file).parent
|
||||
with runner.isolated_filesystem(temp_dir=config_dir):
|
||||
# Copy the config file to the current directory
|
||||
import shutil
|
||||
local_config = Path.cwd() / ".aitbc.yaml"
|
||||
shutil.copy2(temp_config_file, local_config)
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'export',
|
||||
'--format', 'yaml'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
output_data = yaml.safe_load(result.output)
|
||||
assert output_data['coordinator_url'] == 'http://test:8000'
|
||||
assert output_data['api_key'] == '***REDACTED***'
|
||||
|
||||
def test_export_json(self, runner, mock_config, temp_config_file):
|
||||
"""Test exporting configuration as JSON"""
|
||||
# Change to the directory containing the config file
|
||||
config_dir = Path(temp_config_file).parent
|
||||
with runner.isolated_filesystem(temp_dir=config_dir):
|
||||
# Copy the config file to the current directory
|
||||
import shutil
|
||||
local_config = Path.cwd() / ".aitbc.yaml"
|
||||
shutil.copy2(temp_config_file, local_config)
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'export',
|
||||
'--format', 'json'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['coordinator_url'] == 'http://test:8000'
|
||||
assert data['api_key'] == '***REDACTED***'
|
||||
|
||||
def test_export_no_config(self, runner, mock_config):
|
||||
"""Test export when no config file exists"""
|
||||
with runner.isolated_filesystem():
|
||||
result = runner.invoke(config, [
|
||||
'export'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'No configuration file found' in result.output
|
||||
|
||||
def test_import_config_yaml(self, runner, mock_config, tmp_path):
|
||||
"""Test importing YAML configuration"""
|
||||
# Create import file
|
||||
import_file = tmp_path / "import.yaml"
|
||||
import_data = {
|
||||
"coordinator_url": "http://imported:8000",
|
||||
"timeout": 90
|
||||
}
|
||||
import_file.write_text(yaml.dump(import_data))
|
||||
|
||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
||||
# The config file will be created in the current directory
|
||||
actual_config_file = Path.cwd() / ".aitbc.yaml"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'import-config',
|
||||
str(import_file)
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Configuration imported' in result.output
|
||||
|
||||
# Verify import
|
||||
with open(actual_config_file) as f:
|
||||
saved_config = yaml.safe_load(f)
|
||||
assert saved_config['coordinator_url'] == 'http://imported:8000'
|
||||
assert saved_config['timeout'] == 90
|
||||
|
||||
def test_import_config_json(self, runner, mock_config, tmp_path):
|
||||
"""Test importing JSON configuration"""
|
||||
# Create import file
|
||||
import_file = tmp_path / "import.json"
|
||||
import_data = {
|
||||
"coordinator_url": "http://json:8000",
|
||||
"timeout": 60
|
||||
}
|
||||
import_file.write_text(json.dumps(import_data))
|
||||
|
||||
config_file = tmp_path / ".aitbc.yaml"
|
||||
|
||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
||||
# The config file will be created in the current directory
|
||||
actual_config_file = Path.cwd() / ".aitbc.yaml"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'import-config',
|
||||
str(import_file)
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify import
|
||||
with open(actual_config_file) as f:
|
||||
saved_config = yaml.safe_load(f)
|
||||
assert saved_config['coordinator_url'] == 'http://json:8000'
|
||||
assert saved_config['timeout'] == 60
|
||||
|
||||
def test_import_merge(self, runner, mock_config, temp_config_file, tmp_path):
|
||||
"""Test importing with merge option"""
|
||||
# Create import file
|
||||
import_file = tmp_path / "import.yaml"
|
||||
import_data = {
|
||||
"timeout": 45
|
||||
}
|
||||
import_file.write_text(yaml.dump(import_data))
|
||||
|
||||
# Change to the directory containing the config file
|
||||
config_dir = Path(temp_config_file).parent
|
||||
with runner.isolated_filesystem(temp_dir=config_dir):
|
||||
# Copy the config file to the current directory
|
||||
import shutil
|
||||
local_config = Path.cwd() / ".aitbc.yaml"
|
||||
shutil.copy2(temp_config_file, local_config)
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'import-config',
|
||||
str(import_file),
|
||||
'--merge'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify merge - original values should remain
|
||||
with open(local_config) as f:
|
||||
saved_config = yaml.safe_load(f)
|
||||
assert saved_config['coordinator_url'] == 'http://test:8000' # Original
|
||||
assert saved_config['timeout'] == 45 # Updated
|
||||
|
||||
def test_import_nonexistent_file(self, runner, mock_config):
|
||||
"""Test importing non-existent file"""
|
||||
result = runner.invoke(config, [
|
||||
'import-config',
|
||||
'/nonexistent/file.yaml'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'File not found' in result.output
|
||||
|
||||
def test_validate_valid_config(self, runner, mock_config):
|
||||
"""Test validating valid configuration"""
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Configuration valid' in result.output
|
||||
|
||||
def test_validate_missing_url(self, runner, mock_config):
|
||||
"""Test validating config with missing URL"""
|
||||
mock_config.coordinator_url = None
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'validation failed' in result.output
|
||||
|
||||
def test_validate_invalid_url(self, runner, mock_config):
|
||||
"""Test validating config with invalid URL"""
|
||||
mock_config.coordinator_url = "invalid-url"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'validation failed' in result.output
|
||||
|
||||
def test_validate_short_api_key(self, runner, mock_config):
|
||||
"""Test validating config with short API key"""
|
||||
mock_config.api_key = "short"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'validation failed' in result.output
|
||||
|
||||
def test_validate_no_api_key(self, runner, mock_config):
|
||||
"""Test validating config without API key (warning)"""
|
||||
mock_config.api_key = None
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'valid with warnings' in result.output
|
||||
|
||||
@patch.dict(os.environ, {'CLIENT_API_KEY': 'env_key_123'})
|
||||
def test_environments(self, runner, mock_config):
|
||||
"""Test listing environment variables"""
|
||||
result = runner.invoke(config, [
|
||||
'environments'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'CLIENT_API_KEY' in result.output
|
||||
|
||||
def test_profiles_save(self, runner, mock_config, tmp_path):
|
||||
"""Test saving a configuration profile"""
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'save',
|
||||
'test_profile'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Profile test_profile saved' in result.output
|
||||
|
||||
# Verify profile was created
|
||||
profile_file = profiles_dir / "test_profile.yaml"
|
||||
assert profile_file.exists()
|
||||
with open(profile_file) as f:
|
||||
profile_data = yaml.safe_load(f)
|
||||
assert profile_data['coordinator_url'] == 'http://127.0.0.1:18000'
|
||||
|
||||
def test_profiles_list(self, runner, mock_config, tmp_path):
|
||||
"""Test listing configuration profiles"""
|
||||
# Create test profiles
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profile1 = profiles_dir / "profile1.yaml"
|
||||
profile1.write_text(yaml.dump({"coordinator_url": "http://test1:8000"}))
|
||||
|
||||
profile2 = profiles_dir / "profile2.yaml"
|
||||
profile2.write_text(yaml.dump({"coordinator_url": "http://test2:8000"}))
|
||||
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'profile1' in result.output
|
||||
assert 'profile2' in result.output
|
||||
|
||||
def test_profiles_load(self, runner, mock_config, tmp_path):
|
||||
"""Test loading a configuration profile"""
|
||||
# Create test profile
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profile_file = profiles_dir / "test.yaml"
|
||||
profile_data = {
|
||||
"coordinator_url": "http://loaded:8000",
|
||||
"timeout": 75
|
||||
}
|
||||
profile_file.write_text(yaml.dump(profile_data))
|
||||
|
||||
config_file = tmp_path / ".aitbc.yaml"
|
||||
|
||||
with runner.isolated_filesystem(temp_dir=tmp_path):
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'load',
|
||||
'test'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'Profile test loaded' in result.output
|
||||
|
||||
def test_validate_invalid_url(self, runner, mock_config):
|
||||
"""Test validating config with invalid URL"""
|
||||
mock_config.coordinator_url = "invalid-url"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'validation failed' in result.output
|
||||
|
||||
def test_validate_short_api_key(self, runner, mock_config):
|
||||
"""Test validating config with short API key"""
|
||||
mock_config.api_key = "short"
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'validation failed' in result.output
|
||||
|
||||
def test_validate_no_api_key(self, runner, mock_config):
|
||||
"""Test validating config without API key (warning)"""
|
||||
mock_config.api_key = None
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'validate'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'valid with warnings' in result.output
|
||||
|
||||
@patch.dict(os.environ, {'CLIENT_API_KEY': 'env_key_123'})
|
||||
def test_environments(self, runner, mock_config):
|
||||
"""Test listing environment variables"""
|
||||
result = runner.invoke(config, [
|
||||
'environments'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'CLIENT_API_KEY' in result.output
|
||||
|
||||
def test_profiles_save(self, runner, mock_config, tmp_path):
|
||||
"""Test saving a configuration profile"""
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'save',
|
||||
'test_profile'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Profile 'test_profile' saved" in result.output
|
||||
|
||||
# Verify profile was created
|
||||
profile_file = tmp_path / ".config" / "aitbc" / "profiles" / "test_profile.yaml"
|
||||
assert profile_file.exists()
|
||||
with open(profile_file) as f:
|
||||
profile_data = yaml.safe_load(f)
|
||||
assert profile_data['coordinator_url'] == 'http://127.0.0.1:18000'
|
||||
|
||||
def test_profiles_list(self, runner, mock_config, tmp_path):
|
||||
"""Test listing configuration profiles"""
|
||||
# Create test profiles
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profile1 = profiles_dir / "profile1.yaml"
|
||||
profile1.write_text(yaml.dump({"coordinator_url": "http://test1:8000"}))
|
||||
|
||||
profile2 = profiles_dir / "profile2.yaml"
|
||||
profile2.write_text(yaml.dump({"coordinator_url": "http://test2:8000"}))
|
||||
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert 'profile1' in result.output
|
||||
assert 'profile2' in result.output
|
||||
|
||||
def test_profiles_load(self, runner, mock_config, tmp_path):
|
||||
"""Test loading a configuration profile"""
|
||||
# Create test profile
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profile_file = profiles_dir / "load_me.yaml"
|
||||
profile_file.write_text(yaml.dump({"coordinator_url": "http://127.0.0.1:18000"}))
|
||||
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'load',
|
||||
'load_me'
|
||||
], obj={'config': mock_config, 'output_format': 'table'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Profile 'load_me' loaded" in result.output
|
||||
|
||||
def test_profiles_delete(self, runner, mock_config, tmp_path):
|
||||
"""Test deleting a configuration profile"""
|
||||
# Create test profile
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profile_file = profiles_dir / "delete_me.yaml"
|
||||
profile_file.write_text(yaml.dump({"coordinator_url": "http://test:8000"}))
|
||||
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'delete',
|
||||
'delete_me'
|
||||
], obj={'config': mock_config, 'output_format': 'table'}, input='y\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Profile 'delete_me' deleted" in result.output
|
||||
assert not profile_file.exists()
|
||||
|
||||
def test_profiles_delete_cancelled(self, runner, mock_config, tmp_path):
|
||||
"""Test profile deletion cancelled by user"""
|
||||
# Create test profile
|
||||
profiles_dir = tmp_path / ".config" / "aitbc" / "profiles"
|
||||
profiles_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
profile_file = profiles_dir / "keep_me.yaml"
|
||||
profile_file.write_text(yaml.dump({"coordinator_url": "http://test:8000"}))
|
||||
|
||||
# Patch Path.home to return tmp_path
|
||||
with patch('pathlib.Path.home') as mock_home:
|
||||
mock_home.return_value = tmp_path
|
||||
|
||||
result = runner.invoke(config, [
|
||||
'profiles',
|
||||
'delete',
|
||||
'keep_me'
|
||||
], obj={'config': mock_config, 'output_format': 'json'}, input='n\n')
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert profile_file.exists() # Should still exist
|
||||
553
tests/cli/test_marketplace.py
Normal file
553
tests/cli/test_marketplace.py
Normal file
@@ -0,0 +1,553 @@
|
||||
"""Tests for marketplace CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.marketplace import marketplace
|
||||
|
||||
|
||||
@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_api_key"
|
||||
return config
|
||||
|
||||
|
||||
class TestMarketplaceCommands:
|
||||
"""Test marketplace command group"""
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_gpu_list_all(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing all GPUs"""
|
||||
# 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 = {
|
||||
"gpus": [
|
||||
{
|
||||
"id": "gpu1",
|
||||
"model": "RTX4090",
|
||||
"memory": "24GB",
|
||||
"price_per_hour": 0.5,
|
||||
"available": True,
|
||||
"provider": "miner1"
|
||||
},
|
||||
{
|
||||
"id": "gpu2",
|
||||
"model": "RTX3080",
|
||||
"memory": "10GB",
|
||||
"price_per_hour": 0.3,
|
||||
"available": False,
|
||||
"provider": "miner2"
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['gpus']) == 2
|
||||
assert data['gpus'][0]['model'] == 'RTX4090'
|
||||
assert data['gpus'][0]['available'] == True
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/list',
|
||||
params={"limit": 20},
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_gpu_list_available(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing only available GPUs"""
|
||||
# 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 = {
|
||||
"gpus": [
|
||||
{
|
||||
"id": "gpu1",
|
||||
"model": "RTX4090",
|
||||
"memory": "24GB",
|
||||
"price_per_hour": 0.5,
|
||||
"available": True,
|
||||
"provider": "miner1"
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'list',
|
||||
'--available'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['gpus']) == 1
|
||||
assert data['gpus'][0]['available'] == True
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/list',
|
||||
params={"available": "true", "limit": 20},
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_gpu_list_with_filters(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing GPUs 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 = {
|
||||
"gpus": [
|
||||
{
|
||||
"id": "gpu1",
|
||||
"model": "RTX4090",
|
||||
"memory": "24GB",
|
||||
"price_per_hour": 0.5,
|
||||
"available": True,
|
||||
"provider": "miner1"
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command with filters
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'list',
|
||||
'--model', 'RTX4090',
|
||||
'--memory-min', '16',
|
||||
'--price-max', '1.0'
|
||||
], 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 call_args[1]['params']['model'] == 'RTX4090'
|
||||
assert call_args[1]['params']['memory_min'] == 16
|
||||
assert call_args[1]['params']['price_max'] == 1.0
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_gpu_details(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting GPU details"""
|
||||
# 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": "gpu1",
|
||||
"model": "RTX4090",
|
||||
"memory": "24GB",
|
||||
"price_per_hour": 0.5,
|
||||
"available": True,
|
||||
"provider": "miner1",
|
||||
"specs": {
|
||||
"cuda_cores": 16384,
|
||||
"tensor_cores": 512,
|
||||
"base_clock": 2230
|
||||
},
|
||||
"location": "us-west",
|
||||
"rating": 4.8
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'details',
|
||||
'gpu1'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['id'] == 'gpu1'
|
||||
assert data['model'] == 'RTX4090'
|
||||
assert data['specs']['cuda_cores'] == 16384
|
||||
assert data['rating'] == 4.8
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/gpu1',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_gpu_book(self, mock_client_class, runner, mock_config):
|
||||
"""Test booking a GPU"""
|
||||
# Setup mock
|
||||
mock_client = Mock()
|
||||
mock_client_class.return_value.__enter__.return_value = mock_client
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.json.return_value = {
|
||||
"booking_id": "booking123",
|
||||
"gpu_id": "gpu1",
|
||||
"duration_hours": 2,
|
||||
"total_cost": 1.0,
|
||||
"status": "booked"
|
||||
}
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'book',
|
||||
'gpu1',
|
||||
'--hours', '2'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
# Extract JSON from output (success message + JSON)
|
||||
# Remove ANSI escape codes
|
||||
import re
|
||||
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
|
||||
lines = clean_output.strip().split('\n')
|
||||
|
||||
# Find all lines that contain JSON and join them
|
||||
json_lines = []
|
||||
in_json = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{'):
|
||||
in_json = True
|
||||
json_lines.append(stripped)
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
if stripped.endswith('}'):
|
||||
break
|
||||
|
||||
json_str = '\n'.join(json_lines)
|
||||
assert json_str, "No JSON found in output"
|
||||
data = json.loads(json_str)
|
||||
assert data['booking_id'] == 'booking123'
|
||||
assert data['status'] == 'booked'
|
||||
assert data['total_cost'] == 1.0
|
||||
|
||||
# Verify API call
|
||||
mock_client.post.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/gpu1/book',
|
||||
json={"gpu_id": "gpu1", "duration_hours": 2.0},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": "test_api_key"
|
||||
}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_gpu_release(self, mock_client_class, runner, mock_config):
|
||||
"""Test releasing a GPU"""
|
||||
# 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": "released",
|
||||
"gpu_id": "gpu1",
|
||||
"refund": 0.5,
|
||||
"message": "GPU gpu1 released successfully"
|
||||
}
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'release',
|
||||
'gpu1'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
# Extract JSON from output (success message + JSON)
|
||||
# Remove ANSI escape codes
|
||||
import re
|
||||
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
|
||||
lines = clean_output.strip().split('\n')
|
||||
|
||||
# Find all lines that contain JSON and join them
|
||||
json_lines = []
|
||||
in_json = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{'):
|
||||
in_json = True
|
||||
json_lines.append(stripped)
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
if stripped.endswith('}'):
|
||||
break
|
||||
|
||||
json_str = '\n'.join(json_lines)
|
||||
assert json_str, "No JSON found in output"
|
||||
data = json.loads(json_str)
|
||||
assert data['status'] == 'released'
|
||||
assert data['gpu_id'] == 'gpu1'
|
||||
|
||||
# Verify API call
|
||||
mock_client.post.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/gpu1/release',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_orders_list(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing orders"""
|
||||
# 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 = [
|
||||
{
|
||||
"order_id": "order123",
|
||||
"gpu_id": "gpu1",
|
||||
"gpu_model": "RTX 4090",
|
||||
"status": "active",
|
||||
"duration_hours": 2,
|
||||
"total_cost": 1.0,
|
||||
"created_at": "2024-01-01T00:00:00"
|
||||
}
|
||||
]
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'orders'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
# Extract JSON from output
|
||||
import re
|
||||
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
|
||||
lines = clean_output.strip().split('\n')
|
||||
|
||||
# Find all lines that contain JSON and join them
|
||||
json_lines = []
|
||||
in_json = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('['):
|
||||
in_json = True
|
||||
json_lines.append(stripped)
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
if stripped.endswith(']'):
|
||||
break
|
||||
|
||||
json_str = '\n'.join(json_lines)
|
||||
assert json_str, "No JSON found in output"
|
||||
data = json.loads(json_str)
|
||||
assert len(data) == 1
|
||||
assert data[0]['status'] == 'active'
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/orders',
|
||||
params={"limit": 10},
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_pricing_info(self, mock_client_class, runner, mock_config):
|
||||
"""Test getting pricing information"""
|
||||
# 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 = {
|
||||
"average_price": 0.4,
|
||||
"price_range": {
|
||||
"min": 0.2,
|
||||
"max": 0.8
|
||||
},
|
||||
"price_by_model": {
|
||||
"RTX4090": 0.5,
|
||||
"RTX3080": 0.3,
|
||||
"A100": 1.0
|
||||
}
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'pricing',
|
||||
'RTX4090'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['average_price'] == 0.4
|
||||
assert data['price_range']['min'] == 0.2
|
||||
assert data['price_by_model']['RTX4090'] == 0.5
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/pricing/RTX4090',
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_reviews_list(self, mock_client_class, runner, mock_config):
|
||||
"""Test listing reviews for a GPU"""
|
||||
# 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 = {
|
||||
"reviews": [
|
||||
{
|
||||
"id": "review1",
|
||||
"user": "user1",
|
||||
"rating": 5,
|
||||
"comment": "Excellent performance!",
|
||||
"created_at": "2024-01-01T00:00:00"
|
||||
},
|
||||
{
|
||||
"id": "review2",
|
||||
"user": "user2",
|
||||
"rating": 4,
|
||||
"comment": "Good value for money",
|
||||
"created_at": "2024-01-02T00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'reviews',
|
||||
'gpu1'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert len(data['reviews']) == 2
|
||||
assert data['reviews'][0]['rating'] == 5
|
||||
|
||||
# Verify API call
|
||||
mock_client.get.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/gpu1/reviews',
|
||||
params={"limit": 10},
|
||||
headers={"X-Api-Key": "test_api_key"}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.httpx.Client')
|
||||
def test_add_review(self, mock_client_class, runner, mock_config):
|
||||
"""Test adding a review for a GPU"""
|
||||
# Setup mock
|
||||
mock_client = Mock()
|
||||
mock_client_class.return_value.__enter__.return_value = mock_client
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.json.return_value = {
|
||||
"status": "review_added",
|
||||
"gpu_id": "gpu1",
|
||||
"review_id": "review_1",
|
||||
"average_rating": 5.0
|
||||
}
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'review',
|
||||
'gpu1',
|
||||
'--rating', '5',
|
||||
'--comment', 'Amazing GPU!'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
# Extract JSON from output (success message + JSON)
|
||||
# Remove ANSI escape codes
|
||||
import re
|
||||
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
|
||||
lines = clean_output.strip().split('\n')
|
||||
|
||||
# Find all lines that contain JSON and join them
|
||||
json_lines = []
|
||||
in_json = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{'):
|
||||
in_json = True
|
||||
json_lines.append(stripped)
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
if stripped.endswith('}'):
|
||||
break
|
||||
|
||||
json_str = '\n'.join(json_lines)
|
||||
assert json_str, "No JSON found in output"
|
||||
data = json.loads(json_str)
|
||||
assert data['status'] == 'review_added'
|
||||
assert data['gpu_id'] == 'gpu1'
|
||||
|
||||
# Verify API call
|
||||
mock_client.post.assert_called_once_with(
|
||||
'http://test:8000/v1/marketplace/gpu/gpu1/reviews',
|
||||
json={"rating": 5, "comment": "Amazing GPU!"},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": "test_api_key"
|
||||
}
|
||||
)
|
||||
|
||||
@patch('aitbc_cli.commands.marketplace.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 = 404
|
||||
mock_client.get.return_value = mock_response
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(marketplace, [
|
||||
'gpu',
|
||||
'details',
|
||||
'nonexistent'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0 # The command doesn't exit on error
|
||||
assert 'not found' in result.output
|
||||
371
tests/cli/test_simulate.py
Normal file
371
tests/cli/test_simulate.py
Normal file
@@ -0,0 +1,371 @@
|
||||
"""Tests for simulate CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock, Mock
|
||||
from click.testing import CliRunner
|
||||
from aitbc_cli.commands.simulate import simulate
|
||||
|
||||
|
||||
def extract_json_from_output(output):
|
||||
"""Extract first JSON object from CLI output that may contain ANSI escape codes and success messages"""
|
||||
import re
|
||||
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', output)
|
||||
lines = clean_output.strip().split('\n')
|
||||
|
||||
# Find all lines that contain JSON and join them
|
||||
json_lines = []
|
||||
in_json = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{'):
|
||||
in_json = True
|
||||
json_lines.append(stripped)
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
if stripped.endswith('}'):
|
||||
break
|
||||
|
||||
assert json_lines, "No JSON found in output"
|
||||
json_str = '\n'.join(json_lines)
|
||||
return json.loads(json_str)
|
||||
|
||||
|
||||
def extract_last_json_from_output(output):
|
||||
"""Extract the last JSON object from CLI output (for commands that emit multiple JSON objects)"""
|
||||
import re
|
||||
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', output)
|
||||
lines = clean_output.strip().split('\n')
|
||||
|
||||
all_objects = []
|
||||
json_lines = []
|
||||
in_json = False
|
||||
brace_depth = 0
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{') and not in_json:
|
||||
in_json = True
|
||||
brace_depth = stripped.count('{') - stripped.count('}')
|
||||
json_lines = [stripped]
|
||||
if brace_depth == 0:
|
||||
try:
|
||||
all_objects.append(json.loads('\n'.join(json_lines)))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
json_lines = []
|
||||
in_json = False
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
brace_depth += stripped.count('{') - stripped.count('}')
|
||||
if brace_depth <= 0:
|
||||
try:
|
||||
all_objects.append(json.loads('\n'.join(json_lines)))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
json_lines = []
|
||||
in_json = False
|
||||
|
||||
assert all_objects, "No JSON found in output"
|
||||
return all_objects[-1]
|
||||
|
||||
|
||||
@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_api_key"
|
||||
return config
|
||||
|
||||
|
||||
class TestSimulateCommands:
|
||||
"""Test simulate command group"""
|
||||
|
||||
def test_init_economy(self, runner, mock_config):
|
||||
"""Test initializing test economy"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
# Make Path return our temp directory
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(simulate, [
|
||||
'init',
|
||||
'--distribute', '5000,2000'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['status'] == 'initialized'
|
||||
assert data['distribution']['client'] == 5000.0
|
||||
assert data['distribution']['miner'] == 2000.0
|
||||
|
||||
def test_init_with_reset(self, runner, mock_config):
|
||||
"""Test initializing with reset flag"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory with existing files
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Create existing wallet files
|
||||
(home_dir / "client_wallet.json").write_text("{}")
|
||||
(home_dir / "miner_wallet.json").write_text("{}")
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(simulate, [
|
||||
'init',
|
||||
'--reset'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'resetting' in result.output.lower()
|
||||
|
||||
def test_create_user(self, runner, mock_config):
|
||||
"""Test creating a test user"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(simulate, [
|
||||
'user',
|
||||
'create',
|
||||
'--type', 'client',
|
||||
'--name', 'testuser',
|
||||
'--balance', '1000'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['user_id'] == 'client_testuser'
|
||||
assert data['balance'] == 1000
|
||||
|
||||
def test_list_users(self, runner, mock_config):
|
||||
"""Test listing test users"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Create some test wallet files
|
||||
(home_dir / "client_user1_wallet.json").write_text('{"address": "aitbc1test", "balance": 1000}')
|
||||
(home_dir / "miner_user2_wallet.json").write_text('{"address": "aitbc1test2", "balance": 2000}')
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(simulate, [
|
||||
'user',
|
||||
'list'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'users' in data
|
||||
assert isinstance(data['users'], list)
|
||||
assert len(data['users']) == 2
|
||||
|
||||
def test_user_balance(self, runner, mock_config):
|
||||
"""Test checking user balance"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Create a test wallet file
|
||||
(home_dir / "testuser_wallet.json").write_text('{"address": "aitbc1testuser", "balance": 1500}')
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(simulate, [
|
||||
'user',
|
||||
'balance',
|
||||
'testuser'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['balance'] == 1500
|
||||
|
||||
def test_fund_user(self, runner, mock_config):
|
||||
"""Test funding a test user"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Create genesis and user wallet files
|
||||
(home_dir / "genesis_wallet.json").write_text('{"address": "aitbc1genesis", "balance": 1000000, "transactions": []}')
|
||||
(home_dir / "testuser_wallet.json").write_text('{"address": "aitbc1testuser", "balance": 1000, "transactions": []}')
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(simulate, [
|
||||
'user',
|
||||
'fund',
|
||||
'testuser',
|
||||
'500'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
# Extract JSON from output
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['amount'] == 500
|
||||
assert data['new_balance'] == 1500
|
||||
|
||||
def test_workflow_command(self, runner, mock_config):
|
||||
"""Test workflow simulation command"""
|
||||
result = runner.invoke(simulate, [
|
||||
'workflow',
|
||||
'--jobs', '5',
|
||||
'--rounds', '2'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# The command should exist
|
||||
assert result.exit_code == 0
|
||||
# Extract last JSON from output (workflow emits multiple JSON objects)
|
||||
data = extract_last_json_from_output(result.output)
|
||||
assert data['status'] == 'completed'
|
||||
assert data['total_jobs'] == 10
|
||||
|
||||
def test_load_test_command(self, runner, mock_config):
|
||||
"""Test load test command"""
|
||||
result = runner.invoke(simulate, [
|
||||
'load-test',
|
||||
'--clients', '2',
|
||||
'--miners', '1',
|
||||
'--duration', '5',
|
||||
'--job-rate', '2'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# The command should exist
|
||||
assert result.exit_code == 0
|
||||
# Extract last JSON from output (load_test emits multiple JSON objects)
|
||||
data = extract_last_json_from_output(result.output)
|
||||
assert data['status'] == 'completed'
|
||||
assert 'duration' in data
|
||||
assert 'jobs_submitted' in data
|
||||
|
||||
def test_scenario_commands(self, runner, mock_config):
|
||||
"""Test scenario commands"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a test scenario file
|
||||
scenario_file = Path("test_scenario.json")
|
||||
scenario_data = {
|
||||
"name": "Test Scenario",
|
||||
"description": "A test scenario",
|
||||
"steps": [
|
||||
{
|
||||
"type": "submit_jobs",
|
||||
"name": "Initial jobs",
|
||||
"count": 2,
|
||||
"prompt": "Test job"
|
||||
},
|
||||
{
|
||||
"type": "wait",
|
||||
"name": "Wait step",
|
||||
"duration": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
scenario_file.write_text(json.dumps(scenario_data))
|
||||
|
||||
# Run scenario
|
||||
result = runner.invoke(simulate, [
|
||||
'scenario',
|
||||
'--file', str(scenario_file)
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Running scenario: Test Scenario" in result.output
|
||||
|
||||
def test_results_command(self, runner, mock_config):
|
||||
"""Test results command"""
|
||||
result = runner.invoke(simulate, [
|
||||
'results',
|
||||
'sim_123'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
# Extract JSON from output
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['simulation_id'] == 'sim_123'
|
||||
|
||||
def test_reset_command(self, runner, mock_config):
|
||||
"""Test reset command"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create a temporary home directory
|
||||
home_dir = Path("temp_home")
|
||||
home_dir.mkdir()
|
||||
|
||||
# Create existing wallet files
|
||||
(home_dir / "client_wallet.json").write_text("{}")
|
||||
(home_dir / "miner_wallet.json").write_text("{}")
|
||||
|
||||
# Patch the hardcoded path
|
||||
with patch('aitbc_cli.commands.simulate.Path') as mock_path_class:
|
||||
mock_path_class.return_value = home_dir
|
||||
mock_path_class.side_effect = lambda x: home_dir if x == "/home/oib/windsurf/aitbc/home" else Path(x)
|
||||
|
||||
# Run command with reset flag
|
||||
result = runner.invoke(simulate, [
|
||||
'init',
|
||||
'--reset'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'resetting' in result.output.lower()
|
||||
|
||||
def test_invalid_distribution_format(self, runner, mock_config):
|
||||
"""Test invalid distribution format"""
|
||||
result = runner.invoke(simulate, [
|
||||
'init',
|
||||
'--distribute', 'invalid'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Assertions
|
||||
assert result.exit_code == 0
|
||||
assert 'invalid distribution' in result.output.lower()
|
||||
358
tests/cli/test_wallet.py
Normal file
358
tests/cli/test_wallet.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""Tests for wallet CLI commands"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from click.testing import CliRunner
|
||||
from unittest.mock import Mock, patch
|
||||
from aitbc_cli.commands.wallet import wallet
|
||||
|
||||
|
||||
def extract_json_from_output(output):
|
||||
"""Extract JSON from CLI output that may contain Rich panel markup"""
|
||||
clean = re.sub(r'\x1b\[[0-9;]*m', '', output)
|
||||
lines = clean.strip().split('\n')
|
||||
json_lines = []
|
||||
in_json = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('{'):
|
||||
in_json = True
|
||||
json_lines.append(stripped)
|
||||
elif in_json:
|
||||
json_lines.append(stripped)
|
||||
if stripped.startswith('}'):
|
||||
break
|
||||
return json.loads('\n'.join(json_lines))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
"""Create CLI runner"""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_wallet():
|
||||
"""Create temporary wallet file"""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
wallet_data = {
|
||||
"address": "aitbc1test",
|
||||
"balance": 100.0,
|
||||
"transactions": [
|
||||
{
|
||||
"type": "earn",
|
||||
"amount": 50.0,
|
||||
"description": "Test job",
|
||||
"timestamp": "2024-01-01T00:00:00"
|
||||
}
|
||||
],
|
||||
"created_at": "2024-01-01T00:00:00"
|
||||
}
|
||||
json.dump(wallet_data, f)
|
||||
temp_path = f.name
|
||||
|
||||
yield temp_path
|
||||
|
||||
# Cleanup
|
||||
os.unlink(temp_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Mock configuration"""
|
||||
config = Mock()
|
||||
config.coordinator_url = "http://test:8000"
|
||||
config.api_key = "test_key"
|
||||
return config
|
||||
|
||||
|
||||
class TestWalletCommands:
|
||||
"""Test wallet command group"""
|
||||
|
||||
def test_balance_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test wallet balance command"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'balance'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['balance'] == 100.0
|
||||
assert data['address'] == 'aitbc1test'
|
||||
|
||||
def test_balance_new_wallet(self, runner, mock_config, tmp_path):
|
||||
"""Test balance with new wallet (auto-creation)"""
|
||||
wallet_path = tmp_path / "new_wallet.json"
|
||||
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', str(wallet_path),
|
||||
'balance'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert wallet_path.exists()
|
||||
|
||||
data = json.loads(result.output)
|
||||
assert data['balance'] == 0.0
|
||||
assert 'address' in data
|
||||
|
||||
def test_earn_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test earning command"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'earn',
|
||||
'25.5',
|
||||
'job_456',
|
||||
'--desc', 'Another test job'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['new_balance'] == 125.5 # 100 + 25.5
|
||||
assert data['job_id'] == 'job_456'
|
||||
|
||||
# Verify wallet file updated
|
||||
with open(temp_wallet) as f:
|
||||
wallet_data = json.load(f)
|
||||
assert wallet_data['balance'] == 125.5
|
||||
assert len(wallet_data['transactions']) == 2
|
||||
|
||||
def test_spend_command_success(self, runner, temp_wallet, mock_config):
|
||||
"""Test successful spend command"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'spend',
|
||||
'30.0',
|
||||
'GPU rental'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['new_balance'] == 70.0 # 100 - 30
|
||||
assert data['description'] == 'GPU rental'
|
||||
|
||||
def test_spend_insufficient_balance(self, runner, temp_wallet, mock_config):
|
||||
"""Test spend with insufficient balance"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'spend',
|
||||
'200.0',
|
||||
'Too much'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'Insufficient balance' in result.output
|
||||
|
||||
def test_history_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test transaction history"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'history',
|
||||
'--limit', '5'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'transactions' in data
|
||||
assert len(data['transactions']) == 1
|
||||
assert data['transactions'][0]['amount'] == 50.0
|
||||
|
||||
def test_address_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test address command"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'address'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['address'] == 'aitbc1test'
|
||||
|
||||
def test_stats_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test wallet statistics"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'stats'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['current_balance'] == 100.0
|
||||
assert data['total_earned'] == 50.0
|
||||
assert data['total_spent'] == 0.0
|
||||
assert data['jobs_completed'] == 1
|
||||
assert data['transaction_count'] == 1
|
||||
|
||||
@patch('aitbc_cli.commands.wallet.httpx.Client')
|
||||
def test_send_command_success(self, mock_client_class, runner, temp_wallet, mock_config):
|
||||
"""Test successful send command"""
|
||||
# Setup mock
|
||||
mock_client = Mock()
|
||||
mock_client_class.return_value.__enter__.return_value = mock_client
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.json.return_value = {"hash": "0xabc123"}
|
||||
mock_client.post.return_value = mock_response
|
||||
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'send',
|
||||
'aitbc1recipient',
|
||||
'25.0',
|
||||
'--description', 'Payment'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['new_balance'] == 75.0 # 100 - 25
|
||||
assert data['tx_hash'] == '0xabc123'
|
||||
|
||||
# Verify API call
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
assert '/transactions' in call_args[0][0]
|
||||
assert call_args[1]['json']['amount'] == 25.0
|
||||
assert call_args[1]['json']['to'] == 'aitbc1recipient'
|
||||
|
||||
def test_request_payment_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test payment request command"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'request-payment',
|
||||
'aitbc1payer',
|
||||
'50.0',
|
||||
'--description', 'Service payment'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert 'payment_request' in data
|
||||
assert data['payment_request']['from_address'] == 'aitbc1payer'
|
||||
assert data['payment_request']['to_address'] == 'aitbc1test'
|
||||
assert data['payment_request']['amount'] == 50.0
|
||||
|
||||
@patch('aitbc_cli.commands.wallet.httpx.Client')
|
||||
def test_send_insufficient_balance(self, mock_client_class, runner, temp_wallet, mock_config):
|
||||
"""Test send with insufficient balance"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'send',
|
||||
'aitbc1recipient',
|
||||
'200.0'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'Insufficient balance' in result.output
|
||||
|
||||
def test_wallet_file_creation(self, runner, mock_config, tmp_path):
|
||||
"""Test wallet file is created in correct directory"""
|
||||
wallet_dir = tmp_path / "wallets"
|
||||
wallet_path = wallet_dir / "test_wallet.json"
|
||||
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', str(wallet_path),
|
||||
'balance'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert wallet_path.exists()
|
||||
assert wallet_path.parent.exists()
|
||||
|
||||
def test_stake_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test staking tokens"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'stake',
|
||||
'50.0',
|
||||
'--duration', '30'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['amount'] == 50.0
|
||||
assert data['duration_days'] == 30
|
||||
assert data['new_balance'] == 50.0 # 100 - 50
|
||||
assert 'stake_id' in data
|
||||
assert 'apy' in data
|
||||
|
||||
# Verify wallet file updated
|
||||
with open(temp_wallet) as f:
|
||||
wallet_data = json.load(f)
|
||||
assert wallet_data['balance'] == 50.0
|
||||
assert len(wallet_data['staking']) == 1
|
||||
assert wallet_data['staking'][0]['status'] == 'active'
|
||||
|
||||
def test_stake_insufficient_balance(self, runner, temp_wallet, mock_config):
|
||||
"""Test staking with insufficient balance"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'stake',
|
||||
'200.0'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'Insufficient balance' in result.output
|
||||
|
||||
def test_unstake_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test unstaking tokens"""
|
||||
# First stake
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'stake',
|
||||
'50.0',
|
||||
'--duration', '30'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
assert result.exit_code == 0
|
||||
stake_data = extract_json_from_output(result.output)
|
||||
stake_id = stake_data['stake_id']
|
||||
|
||||
# Then unstake
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'unstake',
|
||||
stake_id
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = extract_json_from_output(result.output)
|
||||
assert data['stake_id'] == stake_id
|
||||
assert data['principal'] == 50.0
|
||||
assert 'rewards' in data
|
||||
assert data['total_returned'] >= 50.0
|
||||
assert data['new_balance'] >= 100.0 # Got back principal + rewards
|
||||
|
||||
def test_unstake_invalid_id(self, runner, temp_wallet, mock_config):
|
||||
"""Test unstaking with invalid stake ID"""
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'unstake',
|
||||
'nonexistent_stake'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert 'not found' in result.output
|
||||
|
||||
def test_staking_info_command(self, runner, temp_wallet, mock_config):
|
||||
"""Test staking info command"""
|
||||
# Stake first
|
||||
runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'stake', '30.0', '--duration', '60'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
# Check staking info
|
||||
result = runner.invoke(wallet, [
|
||||
'--wallet-path', temp_wallet,
|
||||
'staking-info'
|
||||
], obj={'config': mock_config, 'output_format': 'json'})
|
||||
|
||||
assert result.exit_code == 0
|
||||
data = json.loads(result.output)
|
||||
assert data['total_staked'] == 30.0
|
||||
assert data['active_stakes'] == 1
|
||||
assert len(data['stakes']) == 1
|
||||
Reference in New Issue
Block a user