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

554 lines
19 KiB
Python

"""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