feat: add blockchain RPC blocks-range endpoint and marketplace bid listing

Blockchain Node:
- Replace /blocks (pagination) with /blocks-range (height range query)
- Add start/end height parameters with 1000-block max range validation
- Return blocks in ascending height order instead of descending
- Update metrics names (rpc_get_blocks_range_*)
- Remove total count from response (return start/end/count instead)

Coordinator API:
- Add effective_url property to DatabaseConfig (SQLite/PostgreSQL defaults
This commit is contained in:
oib
2026-02-16 22:54:08 +01:00
parent fdc3012780
commit 31d3d70836
20 changed files with 3330 additions and 80 deletions

595
tests/cli/test_exchange.py Normal file
View File

@@ -0,0 +1,595 @@
"""Tests for exchange CLI commands"""
import pytest
import json
import time
from click.testing import CliRunner
from unittest.mock import Mock, patch
from aitbc_cli.commands.exchange import exchange
@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 TestExchangeRatesCommand:
"""Test exchange rates command"""
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_rates_success(self, mock_client_class, runner, mock_config):
"""Test successful exchange rates 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 = {
"btc_to_aitbc": 100000,
"aitbc_to_btc": 0.00001,
"fee_percent": 0.5
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, ['rates'],
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 JSON part
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['btc_to_aitbc'] == 100000
assert data['aitbc_to_btc'] == 0.00001
assert data['fee_percent'] == 0.5
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/exchange/rates',
timeout=10
)
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_rates_api_error(self, mock_client_class, runner, mock_config):
"""Test exchange rates with API error"""
# 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_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, ['rates'],
obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Failed to get exchange rates: 500' in result.output
class TestExchangeCreatePaymentCommand:
"""Test exchange create-payment command"""
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_create_payment_with_aitbc_amount(self, mock_client_class, runner, mock_config):
"""Test creating payment with AITBC amount"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
# Mock rates response
rates_response = Mock()
rates_response.status_code = 200
rates_response.json.return_value = {
"btc_to_aitbc": 100000,
"aitbc_to_btc": 0.00001,
"fee_percent": 0.5
}
# Mock payment creation response
payment_response = Mock()
payment_response.status_code = 200
payment_response.json.return_value = {
"payment_id": "pay_123456",
"user_id": "cli_user",
"aitbc_amount": 1000,
"btc_amount": 0.01,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "pending",
"created_at": int(time.time()),
"expires_at": int(time.time()) + 3600
}
mock_client.get.return_value = rates_response
mock_client.post.return_value = payment_response
# Run command
result = runner.invoke(exchange, [
'create-payment',
'--aitbc-amount', '1000',
'--user-id', 'test_user',
'--notes', 'Test payment'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Payment created: pay_123456' in result.output
assert 'Send 0.01000000 BTC to:' in result.output
# Verify API calls
assert mock_client.get.call_count == 1 # Get rates
assert mock_client.post.call_count == 1 # Create payment
# Check payment creation call
payment_call = mock_client.post.call_args
assert payment_call[0][0] == 'http://test:8000/v1/exchange/create-payment'
payment_data = payment_call[1]['json']
assert payment_data['user_id'] == 'test_user'
assert payment_data['aitbc_amount'] == 1000
assert payment_data['btc_amount'] == 0.01
assert payment_data['notes'] == 'Test payment'
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_create_payment_with_btc_amount(self, mock_client_class, runner, mock_config):
"""Test creating payment with BTC amount"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
# Mock rates response
rates_response = Mock()
rates_response.status_code = 200
rates_response.json.return_value = {
"btc_to_aitbc": 100000,
"aitbc_to_btc": 0.00001,
"fee_percent": 0.5
}
# Mock payment creation response
payment_response = Mock()
payment_response.status_code = 200
payment_response.json.return_value = {
"payment_id": "pay_789012",
"user_id": "cli_user",
"aitbc_amount": 500,
"btc_amount": 0.005,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "pending",
"created_at": int(time.time()),
"expires_at": int(time.time()) + 3600
}
mock_client.get.return_value = rates_response
mock_client.post.return_value = payment_response
# Run command
result = runner.invoke(exchange, [
'create-payment',
'--btc-amount', '0.005'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Payment created: pay_789012' in result.output
# Check payment data
payment_call = mock_client.post.call_args
payment_data = payment_call[1]['json']
assert payment_data['aitbc_amount'] == 500
assert payment_data['btc_amount'] == 0.005
def test_create_payment_no_amount(self, runner, mock_config):
"""Test creating payment without specifying amount"""
# Run command without amount
result = runner.invoke(exchange, ['create-payment'],
obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Either --aitbc-amount or --btc-amount must be specified' in result.output
def test_create_payment_invalid_aitbc_amount(self, runner, mock_config):
"""Test creating payment with invalid AITBC amount"""
# Run command with invalid amount
result = runner.invoke(exchange, [
'create-payment',
'--aitbc-amount', '0'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'AITBC amount must be greater than 0' in result.output
def test_create_payment_invalid_btc_amount(self, runner, mock_config):
"""Test creating payment with invalid BTC amount"""
# Run command with invalid amount
result = runner.invoke(exchange, [
'create-payment',
'--btc-amount', '-0.01'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'BTC amount must be greater than 0' in result.output
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_create_payment_rates_error(self, mock_client_class, runner, mock_config):
"""Test creating payment when rates API fails"""
# Setup mock for rates error
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
rates_response = Mock()
rates_response.status_code = 500
mock_client.get.return_value = rates_response
# Run command
result = runner.invoke(exchange, [
'create-payment',
'--aitbc-amount', '1000'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Failed to get exchange rates' in result.output
class TestExchangePaymentStatusCommand:
"""Test exchange payment-status command"""
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_payment_status_pending(self, mock_client_class, runner, mock_config):
"""Test checking pending payment 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 = {
"payment_id": "pay_123456",
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": 0.01,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "pending",
"created_at": int(time.time()),
"expires_at": int(time.time()) + 3600,
"confirmations": 0
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, [
'payment-status',
'--payment-id', 'pay_123456'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Payment pay_123456 is pending confirmation' in result.output
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/exchange/payment-status/pay_123456',
timeout=10
)
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_payment_status_confirmed(self, mock_client_class, runner, mock_config):
"""Test checking confirmed payment 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 = {
"payment_id": "pay_123456",
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": 0.01,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "confirmed",
"created_at": int(time.time()),
"expires_at": int(time.time()) + 3600,
"confirmations": 1,
"confirmed_at": int(time.time())
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, [
'payment-status',
'--payment-id', 'pay_123456'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Payment pay_123456 is confirmed!' in result.output
assert 'AITBC amount: 1000' in result.output
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_payment_status_expired(self, mock_client_class, runner, mock_config):
"""Test checking expired payment 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 = {
"payment_id": "pay_123456",
"user_id": "test_user",
"aitbc_amount": 1000,
"btc_amount": 0.01,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "expired",
"created_at": int(time.time()),
"expires_at": int(time.time()) - 3600, # Expired
"confirmations": 0
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, [
'payment-status',
'--payment-id', 'pay_123456'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Payment pay_123456 has expired' in result.output
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_payment_status_not_found(self, mock_client_class, runner, mock_config):
"""Test checking status for non-existent payment"""
# Setup mock for 404 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(exchange, [
'payment-status',
'--payment-id', 'nonexistent'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Failed to get payment status: 404' in result.output
class TestExchangeMarketStatsCommand:
"""Test exchange market-stats command"""
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_market_stats_success(self, mock_client_class, runner, mock_config):
"""Test successful market stats 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 = {
"price": 0.00001,
"price_change_24h": 5.2,
"daily_volume": 50000,
"daily_volume_btc": 0.5,
"total_payments": 10,
"pending_payments": 2
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, ['market-stats'],
obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Exchange market statistics:' in result.output
# Extract and verify JSON
import re
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
lines = clean_output.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.endswith('}'):
break
json_str = '\n'.join(json_lines)
assert json_str, "No JSON found in output"
data = json.loads(json_str)
assert data['price'] == 0.00001
assert data['price_change_24h'] == 5.2
assert data['daily_volume'] == 50000
assert data['total_payments'] == 10
assert data['pending_payments'] == 2
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/exchange/market-stats',
timeout=10
)
class TestExchangeWalletCommands:
"""Test exchange wallet commands"""
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_wallet_balance_success(self, mock_client_class, runner, mock_config):
"""Test successful wallet balance 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 = {
"address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"balance": 1.5,
"unconfirmed_balance": 0.1,
"total_received": 10.0,
"total_sent": 8.5
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, ['wallet', 'balance'],
obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Bitcoin wallet balance:' in result.output
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/exchange/wallet/balance',
timeout=10
)
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_wallet_info_success(self, mock_client_class, runner, mock_config):
"""Test successful wallet info 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 = {
"address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"balance": 1.5,
"unconfirmed_balance": 0.1,
"total_received": 10.0,
"total_sent": 8.5,
"transactions": [],
"network": "testnet",
"block_height": 2500000
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(exchange, ['wallet', 'info'],
obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Bitcoin wallet information:' in result.output
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/exchange/wallet/info',
timeout=10
)
class TestExchangeIntegration:
"""Test exchange integration workflows"""
@patch('aitbc_cli.commands.exchange.httpx.Client')
def test_complete_exchange_workflow(self, mock_client_class, runner, mock_config):
"""Test complete exchange workflow: rates → create payment → check status"""
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
# Step 1: Get rates
rates_response = Mock()
rates_response.status_code = 200
rates_response.json.return_value = {
"btc_to_aitbc": 100000,
"aitbc_to_btc": 0.00001,
"fee_percent": 0.5
}
# Step 2: Create payment
payment_response = Mock()
payment_response.status_code = 200
payment_response.json.return_value = {
"payment_id": "pay_workflow_123",
"user_id": "cli_user",
"aitbc_amount": 1000,
"btc_amount": 0.01,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "pending",
"created_at": int(time.time()),
"expires_at": int(time.time()) + 3600
}
# Step 3: Check payment status
status_response = Mock()
status_response.status_code = 200
status_response.json.return_value = {
"payment_id": "pay_workflow_123",
"user_id": "cli_user",
"aitbc_amount": 1000,
"btc_amount": 0.01,
"payment_address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"status": "pending",
"created_at": int(time.time()),
"expires_at": int(time.time()) + 3600,
"confirmations": 0
}
# Configure mock to return different responses for different calls
mock_client.get.side_effect = [rates_response, status_response]
mock_client.post.return_value = payment_response
# Execute workflow
# Get rates
result1 = runner.invoke(exchange, ['rates'],
obj={'config': mock_config, 'output_format': 'json'})
assert result1.exit_code == 0
# Create payment
result2 = runner.invoke(exchange, [
'create-payment',
'--aitbc-amount', '1000'
], obj={'config': mock_config, 'output_format': 'json'})
assert result2.exit_code == 0
# Check payment status
result3 = runner.invoke(exchange, [
'payment-status',
'--payment-id', 'pay_workflow_123'
], obj={'config': mock_config, 'output_format': 'json'})
assert result3.exit_code == 0
# Verify all API calls were made
assert mock_client.get.call_count == 3 # rates (standalone) + rates (create-payment) + payment status
assert mock_client.post.call_count == 1 # create payment

View File

@@ -0,0 +1,497 @@
"""Tests for marketplace bid 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 TestMarketplaceBidCommands:
"""Test marketplace bid command group"""
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_submit_success(self, mock_client_class, runner, mock_config):
"""Test successful bid submission"""
# Setup mock
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
mock_response = Mock()
mock_response.status_code = 202
mock_response.json.return_value = {
"id": "bid123",
"status": "pending"
}
mock_client.post.return_value = mock_response
# Run command
result = runner.invoke(marketplace, [
'bid',
'submit',
'--provider', 'miner123',
'--capacity', '100',
'--price', '0.05',
'--notes', 'Need GPU capacity for AI training'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
# Extract JSON from output (success message + JSON)
# Remove ANSI escape codes and extract JSON part
import re
clean_output = re.sub(r'\x1b\[[0-9;]*m', '', result.output)
lines = clean_output.strip().split('\n')
# Find JSON part (multiline JSON with ANSI codes removed)
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['id'] == 'bid123'
# Verify API call
mock_client.post.assert_called_once_with(
'http://test:8000/v1/marketplace/bids',
json={
"provider": "miner123",
"capacity": 100,
"price": 0.05,
"notes": "Need GPU capacity for AI training"
},
headers={
"Content-Type": "application/json",
"X-Api-Key": "test_api_key"
}
)
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_submit_validation_error(self, mock_client_class, runner, mock_config):
"""Test bid submission with invalid capacity"""
# Run command with invalid capacity
result = runner.invoke(marketplace, [
'bid',
'submit',
'--provider', 'miner123',
'--capacity', '0', # Invalid: must be > 0
'--price', '0.05'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Capacity must be greater than 0' in result.output
# Verify no API call was made
mock_client_class.assert_not_called()
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_submit_price_validation_error(self, mock_client_class, runner, mock_config):
"""Test bid submission with invalid price"""
# Run command with invalid price
result = runner.invoke(marketplace, [
'bid',
'submit',
'--provider', 'miner123',
'--capacity', '100',
'--price', '-0.05' # Invalid: must be > 0
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Price must be greater than 0' in result.output
# Verify no API call was made
mock_client_class.assert_not_called()
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_submit_api_error(self, mock_client_class, runner, mock_config):
"""Test bid submission with API error"""
# 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 = 400
mock_response.text = "Invalid provider"
mock_client.post.return_value = mock_response
# Run command
result = runner.invoke(marketplace, [
'bid',
'submit',
'--provider', 'invalid_provider',
'--capacity', '100',
'--price', '0.05'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Failed to submit bid: 400' in result.output
assert 'Invalid provider' in result.output
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_list_all(self, mock_client_class, runner, mock_config):
"""Test listing all bids"""
# 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 = {
"bids": [
{
"id": "bid1",
"provider": "miner1",
"capacity": 100,
"price": 0.05,
"status": "pending",
"submitted_at": "2024-01-01T00:00:00"
},
{
"id": "bid2",
"provider": "miner2",
"capacity": 50,
"price": 0.03,
"status": "accepted",
"submitted_at": "2024-01-01T01:00:00"
}
]
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(marketplace, [
'bid',
'list'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert len(data['bids']) == 2
assert data['bids'][0]['provider'] == 'miner1'
assert data['bids'][0]['status'] == 'pending'
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/marketplace/bids',
params={"limit": 20},
headers={"X-Api-Key": "test_api_key"}
)
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_list_with_filters(self, mock_client_class, runner, mock_config):
"""Test listing bids 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 = {
"bids": [
{
"id": "bid1",
"provider": "miner123",
"capacity": 100,
"price": 0.05,
"status": "pending",
"submitted_at": "2024-01-01T00:00:00"
}
]
}
mock_client.get.return_value = mock_response
# Run command with filters
result = runner.invoke(marketplace, [
'bid',
'list',
'--status', 'pending',
'--provider', 'miner123',
'--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'] == 'pending'
assert call_args[1]['params']['provider'] == 'miner123'
assert call_args[1]['params']['limit'] == 10
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_details(self, mock_client_class, runner, mock_config):
"""Test getting bid 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": "bid123",
"provider": "miner123",
"capacity": 100,
"price": 0.05,
"notes": "Need GPU capacity for AI training",
"status": "pending",
"submitted_at": "2024-01-01T00:00:00"
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(marketplace, [
'bid',
'details',
'bid123'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert data['id'] == 'bid123'
assert data['provider'] == 'miner123'
assert data['capacity'] == 100
assert data['price'] == 0.05
assert data['notes'] == 'Need GPU capacity for AI training'
assert data['status'] == 'pending'
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/marketplace/bids/bid123',
headers={"X-Api-Key": "test_api_key"}
)
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_bid_details_not_found(self, mock_client_class, runner, mock_config):
"""Test getting details for non-existent bid"""
# Setup mock for 404 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, [
'bid',
'details',
'nonexistent'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
assert 'Bid not found: 404' in result.output
class TestMarketplaceOffersCommands:
"""Test marketplace offers command group"""
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_offers_list_all(self, mock_client_class, runner, mock_config):
"""Test listing all offers"""
# 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 = {
"offers": [
{
"id": "offer1",
"provider": "miner1",
"capacity": 200,
"price": 0.10,
"status": "open",
"gpu_model": "RTX4090",
"gpu_memory_gb": 24,
"region": "us-west"
},
{
"id": "offer2",
"provider": "miner2",
"capacity": 100,
"price": 0.08,
"status": "reserved",
"gpu_model": "RTX3080",
"gpu_memory_gb": 10,
"region": "us-east"
}
]
}
mock_client.get.return_value = mock_response
# Run command
result = runner.invoke(marketplace, [
'offers',
'list'
], obj={'config': mock_config, 'output_format': 'json'})
# Assertions
assert result.exit_code == 0
data = json.loads(result.output)
assert len(data['offers']) == 2
assert data['offers'][0]['gpu_model'] == 'RTX4090'
assert data['offers'][0]['status'] == 'open'
# Verify API call
mock_client.get.assert_called_once_with(
'http://test:8000/v1/marketplace/offers',
params={"limit": 20},
headers={"X-Api-Key": "test_api_key"}
)
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_offers_list_with_filters(self, mock_client_class, runner, mock_config):
"""Test listing offers 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 = {
"offers": [
{
"id": "offer1",
"provider": "miner1",
"capacity": 200,
"price": 0.10,
"status": "open",
"gpu_model": "RTX4090",
"gpu_memory_gb": 24,
"region": "us-west"
}
]
}
mock_client.get.return_value = mock_response
# Run command with filters
result = runner.invoke(marketplace, [
'offers',
'list',
'--status', 'open',
'--gpu-model', 'RTX4090',
'--price-max', '0.15',
'--memory-min', '16',
'--region', 'us-west',
'--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
params = call_args[1]['params']
assert params['status'] == 'open'
assert params['gpu_model'] == 'RTX4090'
assert params['price_max'] == 0.15
assert params['memory_min'] == 16
assert params['region'] == 'us-west'
assert params['limit'] == 10
class TestMarketplaceBidIntegration:
"""Test marketplace bid integration workflows"""
@patch('aitbc_cli.commands.marketplace.httpx.Client')
def test_complete_bid_workflow(self, mock_client_class, runner, mock_config):
"""Test complete workflow: list offers -> submit bid -> track status"""
mock_client = Mock()
mock_client_class.return_value.__enter__.return_value = mock_client
# Step 1: List offers
offers_response = Mock()
offers_response.status_code = 200
offers_response.json.return_value = {
"offers": [
{
"id": "offer1",
"provider": "miner1",
"capacity": 200,
"price": 0.10,
"status": "open",
"gpu_model": "RTX4090"
}
]
}
# Step 2: Submit bid
bid_response = Mock()
bid_response.status_code = 202
bid_response.json.return_value = {
"id": "bid123",
"status": "pending"
}
# Step 3: Get bid details
bid_details_response = Mock()
bid_details_response.status_code = 200
bid_details_response.json.return_value = {
"id": "bid123",
"provider": "miner123",
"capacity": 100,
"price": 0.05,
"status": "pending",
"submitted_at": "2024-01-01T00:00:00"
}
# Configure mock to return different responses for different calls
mock_client.get.side_effect = [offers_response, bid_details_response]
mock_client.post.return_value = bid_response
# Execute workflow
# List offers
result1 = runner.invoke(marketplace, [
'offers',
'list',
'--status', 'open'
], obj={'config': mock_config, 'output_format': 'json'})
assert result1.exit_code == 0
# Submit bid
result2 = runner.invoke(marketplace, [
'bid',
'submit',
'--provider', 'miner123',
'--capacity', '100',
'--price', '0.05'
], obj={'config': mock_config, 'output_format': 'json'})
assert result2.exit_code == 0
# Check bid details
result3 = runner.invoke(marketplace, [
'bid',
'details',
'bid123'
], obj={'config': mock_config, 'output_format': 'json'})
assert result3.exit_code == 0
# Verify all API calls were made
assert mock_client.get.call_count == 2
assert mock_client.post.call_count == 1