network: add hub registration, Redis persistence, and federated mesh join protocol
Some checks failed
CLI Tests / test-cli (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled

- Change default P2P port from 7070 to 8001 in config and .env.example
- Add redis_url configuration option for hub persistence (default: redis://localhost:6379)
- Implement DNS-based hub registration/unregistration via HTTPS API endpoints
- Add Redis persistence for hub registrations with 1-hour TTL
- Add island join request/response protocol with member list and blockchain credentials
- Add GPU marketplace tracking (offers, bids, providers) in hub manager
- Add
This commit is contained in:
aitbc
2026-04-13 11:47:34 +02:00
parent fefa6c4435
commit d72945f20c
42 changed files with 3802 additions and 1022 deletions

View File

@@ -0,0 +1,252 @@
"""
Unit tests for Exchange Island CLI commands
"""
import pytest
import json
import os
from pathlib import Path
from click.testing import CliRunner
from unittest.mock import patch, MagicMock
@pytest.fixture
def mock_credentials_file(tmp_path):
"""Create a temporary credentials file for testing"""
credentials = {
"island_id": "test-island-id-12345",
"island_name": "test-island",
"island_chain_id": "ait-test",
"credentials": {
"genesis_block_hash": "0x1234567890abcdef",
"genesis_address": "0xabcdef1234567890",
"rpc_endpoint": "http://localhost:8006",
"p2p_port": 8001
},
"members": [],
"joined_at": "2024-01-01T00:00:00"
}
# Monkey patch the credentials path
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = str(tmp_path / "island_credentials.json")
# Write credentials to temp file
with open(ic_module.CREDENTIALS_PATH, 'w') as f:
json.dump(credentials, f)
yield credentials
# Cleanup
if os.path.exists(ic_module.CREDENTIALS_PATH):
os.remove(ic_module.CREDENTIALS_PATH)
ic_module.CREDENTIALS_PATH = original_path
@pytest.fixture
def mock_keystore(tmp_path):
"""Create a temporary keystore for testing"""
keystore = {
"test_key_id": {
"public_key_pem": "-----BEGIN PUBLIC KEY-----\ntest_public_key_data\n-----END PUBLIC KEY-----"
}
}
keystore_path = tmp_path / "validator_keys.json"
with open(keystore_path, 'w') as f:
json.dump(keystore, f)
# Monkey patch keystore path
import aitbc_cli.commands.exchange_island as ei_module
original_path = ei_module.__dict__.get('keystore_path')
yield str(keystore_path)
# Restore
if original_path:
ei_module.keystore_path = original_path
@pytest.fixture
def runner():
"""Create a Click CLI runner"""
return CliRunner()
def test_exchange_buy_command(mock_credentials_file, mock_keystore, runner):
"""Test exchange buy command"""
from aitbc_cli.commands.exchange_island import exchange_island
with patch('aitbc_cli.commands.exchange_island.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"transaction_id": "test_tx_id"}
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(exchange_island, ['buy', '100', 'BTC', '--max-price', '0.00001'])
assert result.exit_code == 0
assert "Buy order created successfully" in result.output
def test_exchange_buy_command_invalid_amount(mock_credentials_file, runner):
"""Test exchange buy command with invalid amount"""
from aitbc_cli.commands.exchange_island import exchange_island
result = runner.invoke(exchange_island, ['buy', '-10', 'BTC'])
assert result.exit_code != 0
assert "must be greater than 0" in result.output
def test_exchange_sell_command(mock_credentials_file, mock_keystore, runner):
"""Test exchange sell command"""
from aitbc_cli.commands.exchange_island import exchange_island
with patch('aitbc_cli.commands.exchange_island.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"transaction_id": "test_tx_id"}
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(exchange_island, ['sell', '100', 'ETH', '--min-price', '0.0005'])
assert result.exit_code == 0
assert "Sell order created successfully" in result.output
def test_exchange_sell_command_invalid_amount(mock_credentials_file, runner):
"""Test exchange sell command with invalid amount"""
from aitbc_cli.commands.exchange_island import exchange_island
result = runner.invoke(exchange_island, ['sell', '-10', 'ETH'])
assert result.exit_code != 0
assert "must be greater than 0" in result.output
def test_exchange_orderbook_command(mock_credentials_file, runner):
"""Test exchange orderbook command"""
from aitbc_cli.commands.exchange_island import exchange_island
with patch('aitbc_cli.commands.exchange_island.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{
"action": "buy",
"order_id": "exchange_buy_test",
"user_id": "test_user",
"pair": "AIT/BTC",
"side": "buy",
"amount": 100.0,
"max_price": 0.00001,
"status": "open",
"created_at": "2024-01-01T00:00:00"
},
{
"action": "sell",
"order_id": "exchange_sell_test",
"user_id": "test_user2",
"pair": "AIT/BTC",
"side": "sell",
"amount": 100.0,
"min_price": 0.000009,
"status": "open",
"created_at": "2024-01-01T00:00:00"
}
]
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
result = runner.invoke(exchange_island, ['orderbook', 'AIT/BTC'])
assert result.exit_code == 0
def test_exchange_rates_command(mock_credentials_file, runner):
"""Test exchange rates command"""
from aitbc_cli.commands.exchange_island import exchange_island
with patch('aitbc_cli.commands.exchange_island.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = []
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
result = runner.invoke(exchange_island, ['rates'])
assert result.exit_code == 0
def test_exchange_orders_command(mock_credentials_file, runner):
"""Test exchange orders command"""
from aitbc_cli.commands.exchange_island import exchange_island
with patch('aitbc_cli.commands.exchange_island.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{
"action": "buy",
"order_id": "exchange_buy_test",
"user_id": "test_user",
"pair": "AIT/BTC",
"side": "buy",
"amount": 100.0,
"max_price": 0.00001,
"status": "open",
"created_at": "2024-01-01T00:00:00"
}
]
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
result = runner.invoke(exchange_island, ['orders'])
assert result.exit_code == 0
def test_exchange_cancel_command(mock_credentials_file, mock_keystore, runner):
"""Test exchange cancel command"""
from aitbc_cli.commands.exchange_island import exchange_island
with patch('aitbc_cli.commands.exchange_island.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(exchange_island, ['cancel', 'exchange_buy_test123'])
assert result.exit_code == 0
assert "cancelled successfully" in result.output
def test_exchange_orderbook_invalid_pair(mock_credentials_file, runner):
"""Test exchange orderbook command with invalid pair"""
from aitbc_cli.commands.exchange_island import exchange_island
result = runner.invoke(exchange_island, ['orderbook', 'INVALID/PAIR'])
assert result.exit_code != 0
def test_exchange_buy_invalid_currency(mock_credentials_file, runner):
"""Test exchange buy command with invalid currency"""
from aitbc_cli.commands.exchange_island import exchange_island
result = runner.invoke(exchange_island, ['buy', '100', 'INVALID'])
assert result.exit_code != 0
def test_exchange_sell_invalid_currency(mock_credentials_file, runner):
"""Test exchange sell command with invalid currency"""
from aitbc_cli.commands.exchange_island import exchange_island
result = runner.invoke(exchange_island, ['sell', '100', 'INVALID'])
assert result.exit_code != 0
if __name__ == "__main__":
pytest.main([__file__])

View File

@@ -0,0 +1,246 @@
"""
Unit tests for GPU marketplace CLI commands
"""
import pytest
import json
import os
from pathlib import Path
from click.testing import CliRunner
from unittest.mock import patch, MagicMock
@pytest.fixture
def mock_credentials_file(tmp_path):
"""Create a temporary credentials file for testing"""
credentials = {
"island_id": "test-island-id-12345",
"island_name": "test-island",
"island_chain_id": "ait-test",
"credentials": {
"genesis_block_hash": "0x1234567890abcdef",
"genesis_address": "0xabcdef1234567890",
"rpc_endpoint": "http://localhost:8006",
"p2p_port": 8001
},
"members": [],
"joined_at": "2024-01-01T00:00:00"
}
# Monkey patch the credentials path
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = str(tmp_path / "island_credentials.json")
# Write credentials to temp file
with open(ic_module.CREDENTIALS_PATH, 'w') as f:
json.dump(credentials, f)
yield credentials
# Cleanup
if os.path.exists(ic_module.CREDENTIALS_PATH):
os.remove(ic_module.CREDENTIALS_PATH)
ic_module.CREDENTIALS_PATH = original_path
@pytest.fixture
def mock_keystore(tmp_path):
"""Create a temporary keystore for testing"""
keystore = {
"test_key_id": {
"public_key_pem": "-----BEGIN PUBLIC KEY-----\ntest_public_key_data\n-----END PUBLIC KEY-----"
}
}
keystore_path = tmp_path / "validator_keys.json"
with open(keystore_path, 'w') as f:
json.dump(keystore, f)
# Monkey patch keystore path
import aitbc_cli.commands.gpu_marketplace as gm_module
original_path = gm_module.__dict__.get('keystore_path')
yield str(keystore_path)
# Restore
if original_path:
gm_module.keystore_path = original_path
@pytest.fixture
def runner():
"""Create a Click CLI runner"""
return CliRunner()
def test_gpu_offer_command(mock_credentials_file, mock_keystore, runner):
"""Test GPU offer command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"transaction_id": "test_tx_id"}
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(gpu, ['offer', '2', '0.5', '24'])
assert result.exit_code == 0
assert "GPU offer created successfully" in result.output
def test_gpu_bid_command(mock_credentials_file, mock_keystore, runner):
"""Test GPU bid command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"transaction_id": "test_tx_id"}
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(gpu, ['bid', '2', '1.0', '24'])
assert result.exit_code == 0
assert "GPU bid created successfully" in result.output
def test_gpu_list_command(mock_credentials_file, runner):
"""Test GPU list command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{
"action": "offer",
"offer_id": "gpu_offer_test",
"gpu_count": 2,
"price_per_gpu": 0.5,
"duration_hours": 24,
"total_price": 24.0,
"status": "active",
"provider_node_id": "test_provider",
"created_at": "2024-01-01T00:00:00"
}
]
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
result = runner.invoke(gpu, ['list'])
assert result.exit_code == 0
def test_gpu_cancel_command(mock_credentials_file, mock_keystore, runner):
"""Test GPU cancel command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(gpu, ['cancel', 'gpu_offer_test123'])
assert result.exit_code == 0
assert "cancelled successfully" in result.output
def test_gpu_accept_command(mock_credentials_file, mock_keystore, runner):
"""Test GPU accept command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_client.return_value.__enter__.return_value.post.return_value = mock_response
result = runner.invoke(gpu, ['accept', 'gpu_bid_test123'])
assert result.exit_code == 0
assert "accepted successfully" in result.output
def test_gpu_status_command(mock_credentials_file, runner):
"""Test GPU status command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{
"action": "offer",
"offer_id": "gpu_offer_test",
"gpu_count": 2,
"price_per_gpu": 0.5,
"duration_hours": 24,
"total_price": 24.0,
"status": "active",
"provider_node_id": "test_provider",
"created_at": "2024-01-01T00:00:00"
}
]
mock_client.return_value.__enter__.return_value.get.return_value = mock_response
result = runner.invoke(gpu, ['status', 'gpu_offer_test'])
assert result.exit_code == 0
def test_gpu_match_command(mock_credentials_file, runner):
"""Test GPU match command"""
from aitbc_cli.commands.gpu_marketplace import gpu
with patch('aitbc_cli.commands.gpu_marketplace.httpx.Client') as mock_client:
# Mock the GET request for transactions
mock_get_response = MagicMock()
mock_get_response.status_code = 200
mock_get_response.json.return_value = [
{
"action": "offer",
"offer_id": "gpu_offer_test",
"gpu_count": 2,
"price_per_gpu": 0.5,
"duration_hours": 24,
"total_price": 24.0,
"status": "active",
"provider_node_id": "test_provider"
},
{
"action": "bid",
"bid_id": "gpu_bid_test",
"gpu_count": 2,
"max_price_per_gpu": 1.0,
"duration_hours": 24,
"max_total_price": 48.0,
"status": "pending",
"bidder_node_id": "test_bidder"
}
]
# Mock the POST request for match transaction
mock_post_response = MagicMock()
mock_post_response.status_code = 200
mock_client.return_value.__enter__.return_value.get.return_value = mock_get_response
mock_client.return_value.__enter__.return_value.post.return_value = mock_post_response
result = runner.invoke(gpu, ['match'])
assert result.exit_code == 0
def test_gpu_providers_command(mock_credentials_file, runner):
"""Test GPU providers command"""
from aitbc_cli.commands.gpu_marketplace import gpu
result = runner.invoke(gpu, ['providers'])
assert result.exit_code == 0
if __name__ == "__main__":
pytest.main([__file__])

View File

@@ -0,0 +1,201 @@
"""
Unit tests for island credential loading utility
"""
import pytest
import json
import os
from pathlib import Path
from aitbc_cli.utils.island_credentials import (
load_island_credentials,
get_rpc_endpoint,
get_chain_id,
get_island_id,
get_island_name,
get_genesis_block_hash,
get_genesis_address,
validate_credentials,
get_p2p_port
)
@pytest.fixture
def mock_credentials_file(tmp_path):
"""Create a temporary credentials file for testing"""
credentials = {
"island_id": "test-island-id-12345",
"island_name": "test-island",
"island_chain_id": "ait-test",
"credentials": {
"genesis_block_hash": "0x1234567890abcdef",
"genesis_address": "0xabcdef1234567890",
"rpc_endpoint": "http://localhost:8006",
"p2p_port": 8001
},
"joined_at": "2024-01-01T00:00:00"
}
# Monkey patch the credentials path
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = str(tmp_path / "island_credentials.json")
# Write credentials to temp file
with open(ic_module.CREDENTIALS_PATH, 'w') as f:
json.dump(credentials, f)
yield credentials
# Cleanup
if os.path.exists(ic_module.CREDENTIALS_PATH):
os.remove(ic_module.CREDENTIALS_PATH)
ic_module.CREDENTIALS_PATH = original_path
def test_load_island_credentials(mock_credentials_file):
"""Test loading island credentials"""
credentials = load_island_credentials()
assert credentials is not None
assert credentials['island_id'] == "test-island-id-12345"
assert credentials['island_name'] == "test-island"
assert credentials['island_chain_id'] == "ait-test"
assert 'credentials' in credentials
def test_load_island_credentials_file_not_found():
"""Test loading credentials when file doesn't exist"""
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = "/nonexistent/path/credentials.json"
with pytest.raises(FileNotFoundError):
load_island_credentials()
ic_module.CREDENTIALS_PATH = original_path
def test_load_island_credentials_invalid_json(tmp_path):
"""Test loading credentials with invalid JSON"""
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = str(tmp_path / "invalid.json")
with open(ic_module.CREDENTIALS_PATH, 'w') as f:
f.write("invalid json")
with pytest.raises(json.JSONDecodeError):
load_island_credentials()
ic_module.CREDENTIALS_PATH = original_path
def test_load_island_credentials_missing_fields(tmp_path):
"""Test loading credentials with missing required fields"""
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = str(tmp_path / "incomplete.json")
with open(ic_module.CREDENTIALS_PATH, 'w') as f:
json.dump({"island_id": "test"}, f)
with pytest.raises(ValueError):
load_island_credentials()
ic_module.CREDENTIALS_PATH = original_path
def test_get_rpc_endpoint(mock_credentials_file):
"""Test getting RPC endpoint from credentials"""
rpc_endpoint = get_rpc_endpoint()
assert rpc_endpoint == "http://localhost:8006"
def test_get_chain_id(mock_credentials_file):
"""Test getting chain ID from credentials"""
chain_id = get_chain_id()
assert chain_id == "ait-test"
def test_get_island_id(mock_credentials_file):
"""Test getting island ID from credentials"""
island_id = get_island_id()
assert island_id == "test-island-id-12345"
def test_get_island_name(mock_credentials_file):
"""Test getting island name from credentials"""
island_name = get_island_name()
assert island_name == "test-island"
def test_get_genesis_block_hash(mock_credentials_file):
"""Test getting genesis block hash from credentials"""
genesis_hash = get_genesis_block_hash()
assert genesis_hash == "0x1234567890abcdef"
def test_get_genesis_address(mock_credentials_file):
"""Test getting genesis address from credentials"""
genesis_address = get_genesis_address()
assert genesis_address == "0xabcdef1234567890"
def test_get_p2p_port(mock_credentials_file):
"""Test getting P2P port from credentials"""
p2p_port = get_p2p_port()
assert p2p_port == 8001
def test_validate_credentials_valid(mock_credentials_file):
"""Test validating valid credentials"""
is_valid = validate_credentials()
assert is_valid is True
def test_validate_credentials_invalid_file(tmp_path):
"""Test validating credentials when file doesn't exist"""
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
ic_module.CREDENTIALS_PATH = "/nonexistent/path/credentials.json"
is_valid = validate_credentials()
assert is_valid is False
ic_module.CREDENTIALS_PATH = original_path
def test_get_genesis_block_hash_missing(tmp_path):
"""Test getting genesis block hash when not present"""
import aitbc_cli.utils.island_credentials as ic_module
original_path = ic_module.CREDENTIALS_PATH
credentials = {
"island_id": "test-island-id",
"island_name": "test-island",
"island_chain_id": "ait-test",
"credentials": {}
}
ic_module.CREDENTIALS_PATH = str(tmp_path / "no_genesis.json")
with open(ic_module.CREDENTIALS_PATH, 'w') as f:
json.dump(credentials, f)
genesis_hash = get_genesis_block_hash()
assert genesis_hash is None
ic_module.CREDENTIALS_PATH = original_path
if __name__ == "__main__":
pytest.main([__file__])