feat: add transaction hash search to blockchain explorer and cleanup settlement storage
Blockchain Explorer: - Add transaction hash search support (64-char hex pattern validation) - Fetch and display transaction details in modal (hash, type, from/to, amount, fee, block) - Fix regex escape sequence in block height validation - Update search placeholder text to mention both search types - Add blank lines between function definitions for PEP 8 compliance Settlement Storage: - Add timedelta import for future
This commit is contained in:
@@ -1,457 +0,0 @@
|
||||
"""
|
||||
Unit tests for AITBC Blockchain Node
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.models import Block, Transaction, Receipt, Account
|
||||
from apps.blockchain_node.src.aitbc_chain.services.block_service import BlockService
|
||||
from apps.blockchain_node.src.aitbc_chain.services.transaction_pool import TransactionPool
|
||||
from apps.blockchain_node.src.aitbc_chain.services.consensus import ConsensusService
|
||||
from apps.blockchain_node.src.aitbc_chain.services.p2p_network import P2PNetwork
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBlockService:
|
||||
"""Test block creation and management"""
|
||||
|
||||
def test_create_block(self, sample_transactions, validator_address):
|
||||
"""Test creating a new block"""
|
||||
block_service = BlockService()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.block_service.BlockService.create_block') as mock_create:
|
||||
mock_create.return_value = Block(
|
||||
number=100,
|
||||
hash="0xblockhash123",
|
||||
parent_hash="0xparenthash456",
|
||||
transactions=sample_transactions,
|
||||
timestamp=datetime.utcnow(),
|
||||
validator=validator_address
|
||||
)
|
||||
|
||||
block = block_service.create_block(
|
||||
parent_hash="0xparenthash456",
|
||||
transactions=sample_transactions,
|
||||
validator=validator_address
|
||||
)
|
||||
|
||||
assert block.number == 100
|
||||
assert block.validator == validator_address
|
||||
assert len(block.transactions) == len(sample_transactions)
|
||||
|
||||
def test_validate_block(self, sample_block):
|
||||
"""Test block validation"""
|
||||
block_service = BlockService()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.block_service.BlockService.validate_block') as mock_validate:
|
||||
mock_validate.return_value = {"valid": True, "errors": []}
|
||||
|
||||
result = block_service.validate_block(sample_block)
|
||||
|
||||
assert result["valid"] is True
|
||||
assert len(result["errors"]) == 0
|
||||
|
||||
def test_add_block_to_chain(self, sample_block):
|
||||
"""Test adding block to blockchain"""
|
||||
block_service = BlockService()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.block_service.BlockService.add_block') as mock_add:
|
||||
mock_add.return_value = {"success": True, "block_hash": sample_block.hash}
|
||||
|
||||
result = block_service.add_block(sample_block)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["block_hash"] == sample_block.hash
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestTransactionPool:
|
||||
"""Test transaction pool management"""
|
||||
|
||||
def test_add_transaction(self, sample_transaction):
|
||||
"""Test adding transaction to pool"""
|
||||
tx_pool = TransactionPool()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.transaction_pool.TransactionPool.add_transaction') as mock_add:
|
||||
mock_add.return_value = {"success": True, "tx_hash": sample_transaction.hash}
|
||||
|
||||
result = tx_pool.add_transaction(sample_transaction)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
def test_get_pending_transactions(self):
|
||||
"""Test retrieving pending transactions"""
|
||||
tx_pool = TransactionPool()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.transaction_pool.TransactionPool.get_pending') as mock_pending:
|
||||
mock_pending.return_value = [
|
||||
{"hash": "0xtx123", "gas_price": 20},
|
||||
{"hash": "0xtx456", "gas_price": 25}
|
||||
]
|
||||
|
||||
pending = tx_pool.get_pending(limit=100)
|
||||
|
||||
assert len(pending) == 2
|
||||
assert pending[0]["gas_price"] == 20
|
||||
|
||||
def test_remove_transaction(self, sample_transaction):
|
||||
"""Test removing transaction from pool"""
|
||||
tx_pool = TransactionPool()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.transaction_pool.TransactionPool.remove_transaction') as mock_remove:
|
||||
mock_remove.return_value = True
|
||||
|
||||
result = tx_pool.remove_transaction(sample_transaction.hash)
|
||||
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConsensusService:
|
||||
"""Test consensus mechanism"""
|
||||
|
||||
def test_propose_block(self, validator_address, sample_block):
|
||||
"""Test block proposal"""
|
||||
consensus = ConsensusService()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.consensus.ConsensusService.propose_block') as mock_propose:
|
||||
mock_propose.return_value = {
|
||||
"proposal_id": "prop123",
|
||||
"block_hash": sample_block.hash,
|
||||
"votes_required": 3
|
||||
}
|
||||
|
||||
result = consensus.propose_block(sample_block, validator_address)
|
||||
|
||||
assert result["proposal_id"] == "prop123"
|
||||
assert result["votes_required"] == 3
|
||||
|
||||
def test_vote_on_proposal(self, validator_address):
|
||||
"""Test voting on block proposal"""
|
||||
consensus = ConsensusService()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.consensus.ConsensusService.vote') as mock_vote:
|
||||
mock_vote.return_value = {"vote_cast": True, "current_votes": 2}
|
||||
|
||||
result = consensus.vote(
|
||||
proposal_id="prop123",
|
||||
validator=validator_address,
|
||||
vote=True
|
||||
)
|
||||
|
||||
assert result["vote_cast"] is True
|
||||
|
||||
def test_check_consensus(self):
|
||||
"""Test consensus achievement check"""
|
||||
consensus = ConsensusService()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.consensus.ConsensusService.check_consensus') as mock_check:
|
||||
mock_check.return_value = {
|
||||
"achieved": True,
|
||||
"finalized": True,
|
||||
"block_hash": "0xfinalized123"
|
||||
}
|
||||
|
||||
result = consensus.check_consensus("prop123")
|
||||
|
||||
assert result["achieved"] is True
|
||||
assert result["finalized"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestP2PNetwork:
|
||||
"""Test P2P network functionality"""
|
||||
|
||||
def test_connect_to_peer(self):
|
||||
"""Test connecting to a peer"""
|
||||
network = P2PNetwork()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.p2p_network.P2PNetwork.connect') as mock_connect:
|
||||
mock_connect.return_value = {"connected": True, "peer_id": "peer123"}
|
||||
|
||||
result = network.connect("enode://123@192.168.1.100:30303")
|
||||
|
||||
assert result["connected"] is True
|
||||
|
||||
def test_broadcast_transaction(self, sample_transaction):
|
||||
"""Test broadcasting transaction to peers"""
|
||||
network = P2PNetwork()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.p2p_network.P2PNetwork.broadcast_transaction') as mock_broadcast:
|
||||
mock_broadcast.return_value = {"peers_notified": 5}
|
||||
|
||||
result = network.broadcast_transaction(sample_transaction)
|
||||
|
||||
assert result["peers_notified"] == 5
|
||||
|
||||
def test_sync_blocks(self):
|
||||
"""Test block synchronization"""
|
||||
network = P2PNetwork()
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.p2p_network.P2PNetwork.sync_blocks') as mock_sync:
|
||||
mock_sync.return_value = {
|
||||
"synced": True,
|
||||
"blocks_received": 10,
|
||||
"latest_block": 150
|
||||
}
|
||||
|
||||
result = network.sync_blocks(from_block=140)
|
||||
|
||||
assert result["synced"] is True
|
||||
assert result["blocks_received"] == 10
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSmartContracts:
|
||||
"""Test smart contract functionality"""
|
||||
|
||||
def test_deploy_contract(self, sample_account):
|
||||
"""Test deploying a smart contract"""
|
||||
contract_data = {
|
||||
"bytecode": "0x6060604052...",
|
||||
"abi": [{"type": "function", "name": "getValue"}],
|
||||
"args": []
|
||||
}
|
||||
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.contract_service.ContractService.deploy') as mock_deploy:
|
||||
mock_deploy.return_value = {
|
||||
"contract_address": "0xContract123",
|
||||
"transaction_hash": "0xTx456",
|
||||
"gas_used": 100000
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.contract_service import ContractService
|
||||
contract_service = ContractService()
|
||||
result = contract_service.deploy(contract_data, sample_account.address)
|
||||
|
||||
assert result["contract_address"] == "0xContract123"
|
||||
|
||||
def test_call_contract_method(self):
|
||||
"""Test calling smart contract method"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.contract_service.ContractService.call') as mock_call:
|
||||
mock_call.return_value = {
|
||||
"result": "42",
|
||||
"gas_used": 5000,
|
||||
"success": True
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.contract_service import ContractService
|
||||
contract_service = ContractService()
|
||||
result = contract_service.call_method(
|
||||
contract_address="0xContract123",
|
||||
method="getValue",
|
||||
args=[]
|
||||
)
|
||||
|
||||
assert result["result"] == "42"
|
||||
assert result["success"] is True
|
||||
|
||||
def test_estimate_contract_gas(self):
|
||||
"""Test gas estimation for contract interaction"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.contract_service.ContractService.estimate_gas') as mock_estimate:
|
||||
mock_estimate.return_value = {
|
||||
"gas_limit": 50000,
|
||||
"gas_price": 20,
|
||||
"total_cost": "0.001"
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.contract_service import ContractService
|
||||
contract_service = ContractService()
|
||||
result = contract_service.estimate_gas(
|
||||
contract_address="0xContract123",
|
||||
method="setValue",
|
||||
args=[42]
|
||||
)
|
||||
|
||||
assert result["gas_limit"] == 50000
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNodeManagement:
|
||||
"""Test node management operations"""
|
||||
|
||||
def test_start_node(self):
|
||||
"""Test starting blockchain node"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.node.BlockchainNode.start') as mock_start:
|
||||
mock_start.return_value = {"status": "running", "port": 30303}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.node import BlockchainNode
|
||||
node = BlockchainNode()
|
||||
result = node.start()
|
||||
|
||||
assert result["status"] == "running"
|
||||
|
||||
def test_stop_node(self):
|
||||
"""Test stopping blockchain node"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.node.BlockchainNode.stop') as mock_stop:
|
||||
mock_stop.return_value = {"status": "stopped"}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.node import BlockchainNode
|
||||
node = BlockchainNode()
|
||||
result = node.stop()
|
||||
|
||||
assert result["status"] == "stopped"
|
||||
|
||||
def test_get_node_info(self):
|
||||
"""Test getting node information"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.node.BlockchainNode.get_info') as mock_info:
|
||||
mock_info.return_value = {
|
||||
"version": "1.0.0",
|
||||
"chain_id": 1337,
|
||||
"block_number": 150,
|
||||
"peer_count": 5,
|
||||
"syncing": False
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.node import BlockchainNode
|
||||
node = BlockchainNode()
|
||||
result = node.get_info()
|
||||
|
||||
assert result["chain_id"] == 1337
|
||||
assert result["block_number"] == 150
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMining:
|
||||
"""Test mining operations"""
|
||||
|
||||
def test_start_mining(self, miner_address):
|
||||
"""Test starting mining process"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.mining_service.MiningService.start') as mock_mine:
|
||||
mock_mine.return_value = {
|
||||
"mining": True,
|
||||
"hashrate": "50 MH/s",
|
||||
"blocks_mined": 0
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.mining_service import MiningService
|
||||
mining = MiningService()
|
||||
result = mining.start(miner_address)
|
||||
|
||||
assert result["mining"] is True
|
||||
|
||||
def test_get_mining_stats(self):
|
||||
"""Test getting mining statistics"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.mining_service.MiningService.get_stats') as mock_stats:
|
||||
mock_stats.return_value = {
|
||||
"hashrate": "50 MH/s",
|
||||
"blocks_mined": 10,
|
||||
"difficulty": 1000000,
|
||||
"average_block_time": "12.5s"
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.mining_service import MiningService
|
||||
mining = MiningService()
|
||||
result = mining.get_stats()
|
||||
|
||||
assert result["blocks_mined"] == 10
|
||||
assert result["hashrate"] == "50 MH/s"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestChainData:
|
||||
"""Test blockchain data queries"""
|
||||
|
||||
def test_get_block_by_number(self):
|
||||
"""Test retrieving block by number"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.chain_data.ChainData.get_block') as mock_block:
|
||||
mock_block.return_value = {
|
||||
"number": 100,
|
||||
"hash": "0xblock123",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"transaction_count": 5
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.chain_data import ChainData
|
||||
chain_data = ChainData()
|
||||
result = chain_data.get_block(100)
|
||||
|
||||
assert result["number"] == 100
|
||||
assert result["transaction_count"] == 5
|
||||
|
||||
def test_get_transaction_by_hash(self):
|
||||
"""Test retrieving transaction by hash"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.chain_data.ChainData.get_transaction') as mock_tx:
|
||||
mock_tx.return_value = {
|
||||
"hash": "0xtx123",
|
||||
"block_number": 100,
|
||||
"from": "0xsender",
|
||||
"to": "0xreceiver",
|
||||
"value": "1000",
|
||||
"status": "confirmed"
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.chain_data import ChainData
|
||||
chain_data = ChainData()
|
||||
result = chain_data.get_transaction("0xtx123")
|
||||
|
||||
assert result["hash"] == "0xtx123"
|
||||
assert result["status"] == "confirmed"
|
||||
|
||||
def test_get_account_balance(self):
|
||||
"""Test getting account balance"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.chain_data.ChainData.get_balance') as mock_balance:
|
||||
mock_balance.return_value = {
|
||||
"balance": "1000000",
|
||||
"nonce": 25,
|
||||
"code_hash": "0xempty"
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.chain_data import ChainData
|
||||
chain_data = ChainData()
|
||||
result = chain_data.get_balance("0xaccount123")
|
||||
|
||||
assert result["balance"] == "1000000"
|
||||
assert result["nonce"] == 25
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestEventLogs:
|
||||
"""Test event log functionality"""
|
||||
|
||||
def test_get_logs(self):
|
||||
"""Test retrieving event logs"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.event_service.EventService.get_logs') as mock_logs:
|
||||
mock_logs.return_value = [
|
||||
{
|
||||
"address": "0xcontract123",
|
||||
"topics": ["0xevent123"],
|
||||
"data": "0xdata456",
|
||||
"block_number": 100,
|
||||
"transaction_hash": "0xtx789"
|
||||
}
|
||||
]
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.event_service import EventService
|
||||
event_service = EventService()
|
||||
result = event_service.get_logs(
|
||||
from_block=90,
|
||||
to_block=100,
|
||||
address="0xcontract123"
|
||||
)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["address"] == "0xcontract123"
|
||||
|
||||
def test_subscribe_to_events(self):
|
||||
"""Test subscribing to events"""
|
||||
with patch('apps.blockchain_node.src.aitbc_chain.services.event_service.EventService.subscribe') as mock_subscribe:
|
||||
mock_subscribe.return_value = {
|
||||
"subscription_id": "sub123",
|
||||
"active": True
|
||||
}
|
||||
|
||||
from apps.blockchain_node.src.aitbc_chain.services.event_service import EventService
|
||||
event_service = EventService()
|
||||
result = event_service.subscribe(
|
||||
address="0xcontract123",
|
||||
topics=["0xevent123"]
|
||||
)
|
||||
|
||||
assert result["subscription_id"] == "sub123"
|
||||
assert result["active"] is True
|
||||
@@ -1,944 +0,0 @@
|
||||
"""
|
||||
Unit tests for AITBC Coordinator API
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from apps.coordinator_api.src.app.main import app
|
||||
from apps.coordinator_api.src.app.models.job import Job, JobStatus
|
||||
from apps.coordinator_api.src.app.models.receipt import JobReceipt
|
||||
from apps.coordinator_api.src.app.services.job_service import JobService
|
||||
from apps.coordinator_api.src.app.services.receipt_service import ReceiptService
|
||||
from apps.coordinator_api.src.app.exceptions import JobError, ValidationError
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestJobEndpoints:
|
||||
"""Test job-related endpoints"""
|
||||
|
||||
def test_create_job_success(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test successful job creation"""
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] is not None
|
||||
assert data["status"] == "pending"
|
||||
assert data["job_type"] == sample_job_data["job_type"]
|
||||
assert data["tenant_id"] == sample_tenant.id
|
||||
|
||||
def test_create_job_invalid_data(self, coordinator_client):
|
||||
"""Test job creation with invalid data"""
|
||||
invalid_data = {
|
||||
"job_type": "invalid_type",
|
||||
"parameters": {},
|
||||
}
|
||||
|
||||
response = coordinator_client.post("/v1/jobs", json=invalid_data)
|
||||
assert response.status_code == 422
|
||||
assert "detail" in response.json()
|
||||
|
||||
def test_create_job_unauthorized(self, coordinator_client, sample_job_data):
|
||||
"""Test job creation without tenant ID"""
|
||||
response = coordinator_client.post("/v1/jobs", json=sample_job_data)
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_get_job_success(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test successful job retrieval"""
|
||||
# Create a job first
|
||||
create_response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id = create_response.json()["id"]
|
||||
|
||||
# Retrieve the job
|
||||
response = coordinator_client.get(
|
||||
f"/v1/jobs/{job_id}",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == job_id
|
||||
assert data["job_type"] == sample_job_data["job_type"]
|
||||
|
||||
def test_get_job_not_found(self, coordinator_client, sample_tenant):
|
||||
"""Test retrieving non-existent job"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs/non-existent",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_list_jobs_success(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test successful job listing"""
|
||||
# Create multiple jobs
|
||||
for i in range(5):
|
||||
coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
# List jobs
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) >= 5
|
||||
assert "total" in data
|
||||
assert "page" in data
|
||||
|
||||
def test_list_jobs_with_filters(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test job listing with filters"""
|
||||
# Create jobs with different statuses
|
||||
coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json={**sample_job_data, "priority": "high"},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
# Filter by priority
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs?priority=high",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert all(job["priority"] == "high" for job in data["items"])
|
||||
|
||||
def test_cancel_job_success(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test successful job cancellation"""
|
||||
# Create a job
|
||||
create_response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id = create_response.json()["id"]
|
||||
|
||||
# Cancel the job
|
||||
response = coordinator_client.patch(
|
||||
f"/v1/jobs/{job_id}/cancel",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "cancelled"
|
||||
|
||||
def test_cancel_completed_job(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test cancelling a completed job"""
|
||||
# Create and complete a job
|
||||
create_response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id = create_response.json()["id"]
|
||||
|
||||
# Mark as completed
|
||||
coordinator_client.patch(
|
||||
f"/v1/jobs/{job_id}",
|
||||
json={"status": "completed"},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
# Try to cancel
|
||||
response = coordinator_client.patch(
|
||||
f"/v1/jobs/{job_id}/cancel",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "cannot be cancelled" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestReceiptEndpoints:
|
||||
"""Test receipt-related endpoints"""
|
||||
|
||||
def test_get_receipts_success(self, coordinator_client, sample_job_data, sample_tenant, signed_receipt):
|
||||
"""Test successful receipt retrieval"""
|
||||
# Create a job
|
||||
create_response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id = create_response.json()["id"]
|
||||
|
||||
# Mock receipt storage
|
||||
with patch('apps.coordinator_api.src.app.services.receipt_service.ReceiptService.get_job_receipts') as mock_get:
|
||||
mock_get.return_value = [signed_receipt]
|
||||
|
||||
response = coordinator_client.get(
|
||||
f"/v1/jobs/{job_id}/receipts",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) > 0
|
||||
assert "signature" in data["items"][0]
|
||||
|
||||
def test_verify_receipt_success(self, coordinator_client, signed_receipt):
|
||||
"""Test successful receipt verification"""
|
||||
with patch('apps.coordinator_api.src.app.services.receipt_service.verify_receipt') as mock_verify:
|
||||
mock_verify.return_value = {"valid": True}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/receipts/verify",
|
||||
json={"receipt": signed_receipt}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["valid"] is True
|
||||
|
||||
def test_verify_receipt_invalid(self, coordinator_client):
|
||||
"""Test verification of invalid receipt"""
|
||||
invalid_receipt = {
|
||||
"job_id": "test",
|
||||
"signature": "invalid"
|
||||
}
|
||||
|
||||
with patch('apps.coordinator_api.src.app.services.receipt_service.verify_receipt') as mock_verify:
|
||||
mock_verify.return_value = {"valid": False, "error": "Invalid signature"}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/receipts/verify",
|
||||
json={"receipt": invalid_receipt}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["valid"] is False
|
||||
assert "error" in data
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMinerEndpoints:
|
||||
"""Test miner-related endpoints"""
|
||||
|
||||
def test_register_miner_success(self, coordinator_client, sample_tenant):
|
||||
"""Test successful miner registration"""
|
||||
miner_data = {
|
||||
"miner_id": "test-miner-123",
|
||||
"endpoint": "http://localhost:9000",
|
||||
"capabilities": ["ai_inference", "image_generation"],
|
||||
"resources": {
|
||||
"gpu_memory": "16GB",
|
||||
"cpu_cores": 8,
|
||||
}
|
||||
}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/miners/register",
|
||||
json=miner_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["miner_id"] == miner_data["miner_id"]
|
||||
assert data["status"] == "active"
|
||||
|
||||
def test_miner_heartbeat_success(self, coordinator_client, sample_tenant):
|
||||
"""Test successful miner heartbeat"""
|
||||
heartbeat_data = {
|
||||
"miner_id": "test-miner-123",
|
||||
"status": "active",
|
||||
"current_jobs": 2,
|
||||
"resources_used": {
|
||||
"gpu_memory": "8GB",
|
||||
"cpu_cores": 4,
|
||||
}
|
||||
}
|
||||
|
||||
with patch('apps.coordinator_api.src.app.services.miner_service.MinerService.update_heartbeat') as mock_heartbeat:
|
||||
mock_heartbeat.return_value = {"updated": True}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/miners/heartbeat",
|
||||
json=heartbeat_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["updated"] is True
|
||||
|
||||
def test_fetch_jobs_success(self, coordinator_client, sample_tenant):
|
||||
"""Test successful job fetching by miner"""
|
||||
with patch('apps.coordinator_api.src.app.services.job_service.JobService.get_available_jobs') as mock_fetch:
|
||||
mock_fetch.return_value = [
|
||||
{
|
||||
"id": "job-123",
|
||||
"job_type": "ai_inference",
|
||||
"requirements": {"gpu_memory": "8GB"}
|
||||
}
|
||||
]
|
||||
|
||||
response = coordinator_client.get(
|
||||
"/v1/miners/jobs",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMarketplaceEndpoints:
|
||||
"""Test marketplace-related endpoints"""
|
||||
|
||||
def test_create_offer_success(self, coordinator_client, sample_tenant):
|
||||
"""Test successful offer creation"""
|
||||
offer_data = {
|
||||
"service_type": "ai_inference",
|
||||
"pricing": {
|
||||
"per_hour": 0.50,
|
||||
"per_token": 0.0001,
|
||||
},
|
||||
"capacity": 100,
|
||||
"requirements": {
|
||||
"gpu_memory": "16GB",
|
||||
}
|
||||
}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/marketplace/offers",
|
||||
json=offer_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] is not None
|
||||
assert data["service_type"] == offer_data["service_type"]
|
||||
|
||||
def test_list_offers_success(self, coordinator_client, sample_tenant):
|
||||
"""Test successful offer listing"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/marketplace/offers",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert isinstance(data["items"], list)
|
||||
|
||||
def test_create_bid_success(self, coordinator_client, sample_tenant):
|
||||
"""Test successful bid creation"""
|
||||
bid_data = {
|
||||
"offer_id": "offer-123",
|
||||
"quantity": 10,
|
||||
"max_price": 1.00,
|
||||
}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/marketplace/bids",
|
||||
json=bid_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] is not None
|
||||
assert data["offer_id"] == bid_data["offer_id"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMultiTenancy:
|
||||
"""Test multi-tenancy features"""
|
||||
|
||||
def test_tenant_isolation(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test that tenants cannot access each other's data"""
|
||||
# Create job for tenant A
|
||||
response_a = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id_a = response_a.json()["id"]
|
||||
|
||||
# Try to access with different tenant ID
|
||||
response = coordinator_client.get(
|
||||
f"/v1/jobs/{job_id_a}",
|
||||
headers={"X-Tenant-ID": "different-tenant"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_quota_enforcement(self, coordinator_client, sample_job_data, sample_tenant, sample_tenant_quota):
|
||||
"""Test that quota limits are enforced"""
|
||||
# Mock quota service
|
||||
with patch('apps.coordinator_api.src.app.services.quota_service.QuotaService.check_quota') as mock_check:
|
||||
mock_check.return_value = False
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 429
|
||||
assert "quota" in response.json()["detail"].lower()
|
||||
|
||||
def test_tenant_metrics(self, coordinator_client, sample_tenant):
|
||||
"""Test tenant-specific metrics"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/metrics",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "tenant_id" in data
|
||||
assert data["tenant_id"] == sample_tenant.id
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestErrorHandling:
|
||||
"""Test error handling and edge cases"""
|
||||
|
||||
def test_validation_errors(self, coordinator_client):
|
||||
"""Test validation error responses"""
|
||||
# Send invalid JSON
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
data="invalid json",
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert "detail" in response.json()
|
||||
|
||||
def test_rate_limiting(self, coordinator_client, sample_tenant):
|
||||
"""Test rate limiting"""
|
||||
with patch('apps.coordinator_api.src.app.middleware.rate_limit.check_rate_limit') as mock_check:
|
||||
mock_check.return_value = False
|
||||
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 429
|
||||
assert "rate limit" in response.json()["detail"].lower()
|
||||
|
||||
def test_internal_server_error(self, coordinator_client, sample_tenant):
|
||||
"""Test internal server error handling"""
|
||||
with patch('apps.coordinator_api.src.app.services.job_service.JobService.create_job') as mock_create:
|
||||
mock_create.side_effect = Exception("Database error")
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json={"job_type": "test"},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "internal server error" in response.json()["detail"].lower()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWebhooks:
|
||||
"""Test webhook functionality"""
|
||||
|
||||
def test_webhook_signature_verification(self, coordinator_client):
|
||||
"""Test webhook signature verification"""
|
||||
webhook_data = {
|
||||
"event": "job.completed",
|
||||
"job_id": "test-123",
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Mock signature verification
|
||||
with patch('apps.coordinator_api.src.app.webhooks.verify_webhook_signature') as mock_verify:
|
||||
mock_verify.return_value = True
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/webhooks/job-status",
|
||||
json=webhook_data,
|
||||
headers={"X-Webhook-Signature": "test-signature"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_webhook_invalid_signature(self, coordinator_client):
|
||||
"""Test webhook with invalid signature"""
|
||||
webhook_data = {"event": "test"}
|
||||
|
||||
with patch('apps.coordinator_api.src.app.webhooks.verify_webhook_signature') as mock_verify:
|
||||
mock_verify.return_value = False
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/webhooks/job-status",
|
||||
json=webhook_data,
|
||||
headers={"X-Webhook-Signature": "invalid"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestHealthAndMetrics:
|
||||
"""Test health check and metrics endpoints"""
|
||||
|
||||
def test_health_check(self, coordinator_client):
|
||||
"""Test health check endpoint"""
|
||||
response = coordinator_client.get("/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "status" in data
|
||||
assert data["status"] == "healthy"
|
||||
|
||||
def test_metrics_endpoint(self, coordinator_client):
|
||||
"""Test Prometheus metrics endpoint"""
|
||||
response = coordinator_client.get("/metrics")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "text/plain" in response.headers["content-type"]
|
||||
|
||||
def test_readiness_check(self, coordinator_client):
|
||||
"""Test readiness check endpoint"""
|
||||
response = coordinator_client.get("/ready")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "ready" in data
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestJobExecution:
|
||||
"""Test job execution lifecycle"""
|
||||
|
||||
def test_job_execution_flow(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test complete job execution flow"""
|
||||
# Create job
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
job_id = response.json()["id"]
|
||||
|
||||
# Accept job
|
||||
response = coordinator_client.patch(
|
||||
f"/v1/jobs/{job_id}/accept",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "running"
|
||||
|
||||
# Complete job
|
||||
response = coordinator_client.patch(
|
||||
f"/v1/jobs/{job_id}/complete",
|
||||
json={"result": "Task completed successfully"},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "completed"
|
||||
|
||||
def test_job_retry_mechanism(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test job retry mechanism"""
|
||||
# Create job
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json={**sample_job_data, "max_retries": 3},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id = response.json()["id"]
|
||||
|
||||
# Fail job
|
||||
response = coordinator_client.patch(
|
||||
f"/v1/jobs/{job_id}/fail",
|
||||
json={"error": "Temporary failure"},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "failed"
|
||||
assert data["retry_count"] == 1
|
||||
|
||||
# Retry job
|
||||
response = coordinator_client.post(
|
||||
f"/v1/jobs/{job_id}/retry",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "pending"
|
||||
|
||||
def test_job_timeout_handling(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test job timeout handling"""
|
||||
with patch('apps.coordinator_api.src.app.services.job_service.JobService.check_timeout') as mock_timeout:
|
||||
mock_timeout.return_value = True
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs/timeout-check",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "timed_out" in response.json()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConfidentialTransactions:
|
||||
"""Test confidential transaction features"""
|
||||
|
||||
def test_create_confidential_job(self, coordinator_client, sample_tenant):
|
||||
"""Test creating a confidential job"""
|
||||
confidential_job = {
|
||||
"job_type": "confidential_inference",
|
||||
"parameters": {
|
||||
"encrypted_data": "encrypted_payload",
|
||||
"verification_key": "zk_proof_key"
|
||||
},
|
||||
"confidential": True
|
||||
}
|
||||
|
||||
with patch('apps.coordinator_api.src.app.services.zk_proofs.generate_proof') as mock_proof:
|
||||
mock_proof.return_value = "proof_hash"
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=confidential_job,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["confidential"] is True
|
||||
assert "proof_hash" in data
|
||||
|
||||
def test_verify_confidential_result(self, coordinator_client, sample_tenant):
|
||||
"""Test verification of confidential job results"""
|
||||
verification_data = {
|
||||
"job_id": "confidential-job-123",
|
||||
"result_hash": "result_hash",
|
||||
"zk_proof": "zk_proof_data"
|
||||
}
|
||||
|
||||
with patch('apps.coordinator_api.src.app.services.zk_proofs.verify_proof') as mock_verify:
|
||||
mock_verify.return_value = {"valid": True}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs/verify-result",
|
||||
json=verification_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["valid"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestBatchOperations:
|
||||
"""Test batch operations"""
|
||||
|
||||
def test_batch_job_creation(self, coordinator_client, sample_tenant):
|
||||
"""Test creating multiple jobs in batch"""
|
||||
batch_data = {
|
||||
"jobs": [
|
||||
{"job_type": "inference", "parameters": {"model": "gpt-4"}},
|
||||
{"job_type": "inference", "parameters": {"model": "claude-3"}},
|
||||
{"job_type": "image_gen", "parameters": {"prompt": "test image"}}
|
||||
]
|
||||
}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs/batch",
|
||||
json=batch_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "job_ids" in data
|
||||
assert len(data["job_ids"]) == 3
|
||||
|
||||
def test_batch_job_cancellation(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test cancelling multiple jobs"""
|
||||
# Create multiple jobs
|
||||
job_ids = []
|
||||
for i in range(3):
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_ids.append(response.json()["id"])
|
||||
|
||||
# Cancel all jobs
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs/batch-cancel",
|
||||
json={"job_ids": job_ids},
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["cancelled_count"] == 3
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestRealTimeFeatures:
|
||||
"""Test real-time features"""
|
||||
|
||||
def test_websocket_connection(self, coordinator_client):
|
||||
"""Test WebSocket connection for job updates"""
|
||||
with patch('fastapi.WebSocket') as mock_websocket:
|
||||
mock_websocket.accept.return_value = None
|
||||
|
||||
# Test WebSocket endpoint
|
||||
response = coordinator_client.get("/ws/jobs")
|
||||
# WebSocket connections use different protocol, so we test the endpoint exists
|
||||
assert response.status_code in [200, 401, 426] # 426 for upgrade required
|
||||
|
||||
def test_job_status_updates(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test real-time job status updates"""
|
||||
# Create job
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=sample_job_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
job_id = response.json()["id"]
|
||||
|
||||
# Subscribe to updates
|
||||
with patch('apps.coordinator_api.src.app.services.notification_service.NotificationService.subscribe') as mock_sub:
|
||||
mock_sub.return_value = "subscription_id"
|
||||
|
||||
response = coordinator_client.post(
|
||||
f"/v1/jobs/{job_id}/subscribe",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "subscription_id" in response.json()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAdvancedScheduling:
|
||||
"""Test advanced job scheduling features"""
|
||||
|
||||
def test_scheduled_job_creation(self, coordinator_client, sample_tenant):
|
||||
"""Test creating scheduled jobs"""
|
||||
scheduled_job = {
|
||||
"job_type": "inference",
|
||||
"parameters": {"model": "gpt-4"},
|
||||
"schedule": {
|
||||
"type": "cron",
|
||||
"expression": "0 2 * * *", # Daily at 2 AM
|
||||
"timezone": "UTC"
|
||||
}
|
||||
}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs/scheduled",
|
||||
json=scheduled_job,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "schedule_id" in data
|
||||
assert data["next_run"] is not None
|
||||
|
||||
def test_priority_queue_handling(self, coordinator_client, sample_job_data, sample_tenant):
|
||||
"""Test priority queue job handling"""
|
||||
# Create high priority job
|
||||
high_priority_job = {**sample_job_data, "priority": "urgent"}
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=high_priority_job,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
job_id = response.json()["id"]
|
||||
|
||||
# Check priority queue
|
||||
with patch('apps.coordinator_api.src.app.services.queue_service.QueueService.get_priority_queue') as mock_queue:
|
||||
mock_queue.return_value = [job_id]
|
||||
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs/queue/priority",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert job_id in data["jobs"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestResourceManagement:
|
||||
"""Test resource management and allocation"""
|
||||
|
||||
def test_resource_allocation(self, coordinator_client, sample_tenant):
|
||||
"""Test resource allocation for jobs"""
|
||||
resource_request = {
|
||||
"job_type": "gpu_inference",
|
||||
"requirements": {
|
||||
"gpu_memory": "16GB",
|
||||
"cpu_cores": 8,
|
||||
"ram": "32GB",
|
||||
"storage": "100GB"
|
||||
}
|
||||
}
|
||||
|
||||
with patch('apps.coordinator_api.src.app.services.resource_service.ResourceService.check_availability') as mock_check:
|
||||
mock_check.return_value = {"available": True, "estimated_wait": 0}
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/resources/check",
|
||||
json=resource_request,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["available"] is True
|
||||
|
||||
def test_resource_monitoring(self, coordinator_client, sample_tenant):
|
||||
"""Test resource usage monitoring"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/resources/usage",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "gpu_usage" in data
|
||||
assert "cpu_usage" in data
|
||||
assert "memory_usage" in data
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAPIVersioning:
|
||||
"""Test API versioning"""
|
||||
|
||||
def test_v1_api_compatibility(self, coordinator_client, sample_tenant):
|
||||
"""Test v1 API endpoints"""
|
||||
response = coordinator_client.get("/v1/version")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["version"] == "v1"
|
||||
|
||||
def test_deprecated_endpoint_warning(self, coordinator_client, sample_tenant):
|
||||
"""Test deprecated endpoint returns warning"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/legacy/jobs",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "X-Deprecated" in response.headers
|
||||
|
||||
def test_api_version_negotiation(self, coordinator_client, sample_tenant):
|
||||
"""Test API version negotiation"""
|
||||
response = coordinator_client.get(
|
||||
"/version",
|
||||
headers={"Accept-Version": "v1"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "API-Version" in response.headers
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSecurityFeatures:
|
||||
"""Test security features"""
|
||||
|
||||
def test_cors_headers(self, coordinator_client):
|
||||
"""Test CORS headers are set correctly"""
|
||||
response = coordinator_client.options("/v1/jobs")
|
||||
|
||||
assert "Access-Control-Allow-Origin" in response.headers
|
||||
assert "Access-Control-Allow-Methods" in response.headers
|
||||
|
||||
def test_request_size_limit(self, coordinator_client, sample_tenant):
|
||||
"""Test request size limits"""
|
||||
large_data = {"data": "x" * 10_000_000} # 10MB
|
||||
|
||||
response = coordinator_client.post(
|
||||
"/v1/jobs",
|
||||
json=large_data,
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 413
|
||||
|
||||
def test_sql_injection_protection(self, coordinator_client, sample_tenant):
|
||||
"""Test SQL injection protection"""
|
||||
malicious_input = "'; DROP TABLE jobs; --"
|
||||
|
||||
response = coordinator_client.get(
|
||||
f"/v1/jobs/{malicious_input}",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert response.status_code != 500
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPerformanceOptimizations:
|
||||
"""Test performance optimizations"""
|
||||
|
||||
def test_response_compression(self, coordinator_client):
|
||||
"""Test response compression for large payloads"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs",
|
||||
headers={"Accept-Encoding": "gzip"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Content-Encoding" in response.headers
|
||||
|
||||
def test_caching_headers(self, coordinator_client):
|
||||
"""Test caching headers are set"""
|
||||
response = coordinator_client.get("/v1/marketplace/offers")
|
||||
|
||||
assert "Cache-Control" in response.headers
|
||||
assert "ETag" in response.headers
|
||||
|
||||
def test_pagination_performance(self, coordinator_client, sample_tenant):
|
||||
"""Test pagination with large datasets"""
|
||||
response = coordinator_client.get(
|
||||
"/v1/jobs?page=1&size=100",
|
||||
headers={"X-Tenant-ID": sample_tenant.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["items"]) <= 100
|
||||
assert "next_page" in data or len(data["items"]) == 0
|
||||
@@ -1,511 +0,0 @@
|
||||
"""
|
||||
Unit tests for AITBC Wallet Daemon
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from apps.wallet_daemon.src.app.main import app
|
||||
from apps.wallet_daemon.src.app.models.wallet import Wallet, WalletStatus
|
||||
from apps.wallet_daemon.src.app.models.transaction import Transaction, TransactionStatus
|
||||
from apps.wallet_daemon.src.app.services.wallet_service import WalletService
|
||||
from apps.wallet_daemon.src.app.services.transaction_service import TransactionService
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWalletEndpoints:
|
||||
"""Test wallet-related endpoints"""
|
||||
|
||||
def test_create_wallet_success(self, wallet_client, sample_wallet_data, sample_user):
|
||||
"""Test successful wallet creation"""
|
||||
response = wallet_client.post(
|
||||
"/v1/wallets",
|
||||
json=sample_wallet_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] is not None
|
||||
assert data["address"] is not None
|
||||
assert data["status"] == "active"
|
||||
assert data["user_id"] == sample_user.id
|
||||
|
||||
def test_get_wallet_balance(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test getting wallet balance"""
|
||||
with patch('apps.wallet_daemon.src.app.services.wallet_service.WalletService.get_balance') as mock_balance:
|
||||
mock_balance.return_value = {
|
||||
"native": "1000.0",
|
||||
"tokens": {
|
||||
"AITBC": "500.0",
|
||||
"USDT": "100.0"
|
||||
}
|
||||
}
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/balance",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "native" in data
|
||||
assert "tokens" in data
|
||||
assert data["native"] == "1000.0"
|
||||
|
||||
def test_list_wallet_transactions(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test listing wallet transactions"""
|
||||
with patch('apps.wallet_daemon.src.app.services.transaction_service.TransactionService.get_wallet_transactions') as mock_txs:
|
||||
mock_txs.return_value = [
|
||||
{
|
||||
"id": "tx-123",
|
||||
"type": "send",
|
||||
"amount": "10.0",
|
||||
"status": "completed",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
]
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/transactions",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestTransactionEndpoints:
|
||||
"""Test transaction-related endpoints"""
|
||||
|
||||
def test_send_transaction(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test sending a transaction"""
|
||||
tx_data = {
|
||||
"to_address": "0x1234567890abcdef",
|
||||
"amount": "10.0",
|
||||
"token": "AITBC",
|
||||
"memo": "Test payment"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.transaction_service.TransactionService.send_transaction') as mock_send:
|
||||
mock_send.return_value = {
|
||||
"id": "tx-456",
|
||||
"hash": "0xabcdef1234567890",
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
"/v1/transactions/send",
|
||||
json=tx_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["id"] == "tx-456"
|
||||
assert data["status"] == "pending"
|
||||
|
||||
def test_sign_transaction(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test transaction signing"""
|
||||
unsigned_tx = {
|
||||
"to": "0x1234567890abcdef",
|
||||
"amount": "10.0",
|
||||
"nonce": 1
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.wallet_service.WalletService.sign_transaction') as mock_sign:
|
||||
mock_sign.return_value = {
|
||||
"signature": "0xsigned123456",
|
||||
"signed_transaction": unsigned_tx
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/sign",
|
||||
json=unsigned_tx,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "signature" in data
|
||||
assert data["signature"] == "0xsigned123456"
|
||||
|
||||
def test_estimate_gas(self, wallet_client, sample_user):
|
||||
"""Test gas estimation"""
|
||||
tx_data = {
|
||||
"to": "0x1234567890abcdef",
|
||||
"amount": "10.0",
|
||||
"data": "0x"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.transaction_service.TransactionService.estimate_gas') as mock_gas:
|
||||
mock_gas.return_value = {
|
||||
"gas_limit": "21000",
|
||||
"gas_price": "20",
|
||||
"total_cost": "0.00042"
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
"/v1/transactions/estimate-gas",
|
||||
json=tx_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "gas_limit" in data
|
||||
assert "gas_price" in data
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestStakingEndpoints:
|
||||
"""Test staking-related endpoints"""
|
||||
|
||||
def test_stake_tokens(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test token staking"""
|
||||
stake_data = {
|
||||
"amount": "100.0",
|
||||
"duration": 30, # days
|
||||
"validator": "validator-123"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.staking_service.StakingService.stake') as mock_stake:
|
||||
mock_stake.return_value = {
|
||||
"stake_id": "stake-789",
|
||||
"amount": "100.0",
|
||||
"apy": "5.5",
|
||||
"unlock_date": (datetime.utcnow() + timedelta(days=30)).isoformat()
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/stake",
|
||||
json=stake_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["stake_id"] == "stake-789"
|
||||
assert "apy" in data
|
||||
|
||||
def test_unstake_tokens(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test token unstaking"""
|
||||
with patch('apps.wallet_daemon.src.app.services.staking_service.StakingService.unstake') as mock_unstake:
|
||||
mock_unstake.return_value = {
|
||||
"unstake_id": "unstake-456",
|
||||
"amount": "100.0",
|
||||
"status": "pending",
|
||||
"release_date": (datetime.utcnow() + timedelta(days=7)).isoformat()
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/unstake",
|
||||
json={"stake_id": "stake-789"},
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "pending"
|
||||
|
||||
def test_get_staking_rewards(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test getting staking rewards"""
|
||||
with patch('apps.wallet_daemon.src.app.services.staking_service.StakingService.get_rewards') as mock_rewards:
|
||||
mock_rewards.return_value = {
|
||||
"total_rewards": "5.5",
|
||||
"daily_average": "0.183",
|
||||
"claimable": "5.5"
|
||||
}
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/rewards",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_rewards" in data
|
||||
assert data["claimable"] == "5.5"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestDeFiEndpoints:
|
||||
"""Test DeFi-related endpoints"""
|
||||
|
||||
def test_swap_tokens(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test token swapping"""
|
||||
swap_data = {
|
||||
"from_token": "AITBC",
|
||||
"to_token": "USDT",
|
||||
"amount": "100.0",
|
||||
"slippage": "0.5"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.defi_service.DeFiService.swap') as mock_swap:
|
||||
mock_swap.return_value = {
|
||||
"swap_id": "swap-123",
|
||||
"expected_output": "95.5",
|
||||
"price_impact": "0.1",
|
||||
"route": ["AITBC", "USDT"]
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/swap",
|
||||
json=swap_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "swap_id" in data
|
||||
assert "expected_output" in data
|
||||
|
||||
def test_add_liquidity(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test adding liquidity to pool"""
|
||||
liquidity_data = {
|
||||
"pool": "AITBC-USDT",
|
||||
"token_a": "AITBC",
|
||||
"token_b": "USDT",
|
||||
"amount_a": "100.0",
|
||||
"amount_b": "1000.0"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.defi_service.DeFiService.add_liquidity') as mock_add:
|
||||
mock_add.return_value = {
|
||||
"liquidity_id": "liq-456",
|
||||
"lp_tokens": "316.23",
|
||||
"share_percentage": "0.1"
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/add-liquidity",
|
||||
json=liquidity_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "lp_tokens" in data
|
||||
|
||||
def test_get_liquidity_positions(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test getting liquidity positions"""
|
||||
with patch('apps.wallet_daemon.src.app.services.defi_service.DeFiService.get_positions') as mock_positions:
|
||||
mock_positions.return_value = [
|
||||
{
|
||||
"pool": "AITBC-USDT",
|
||||
"lp_tokens": "316.23",
|
||||
"value_usd": "2000.0",
|
||||
"fees_earned": "10.5"
|
||||
}
|
||||
]
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/positions",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestNFTEndpoints:
|
||||
"""Test NFT-related endpoints"""
|
||||
|
||||
def test_mint_nft(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test NFT minting"""
|
||||
nft_data = {
|
||||
"collection": "aitbc-art",
|
||||
"metadata": {
|
||||
"name": "Test NFT",
|
||||
"description": "A test NFT",
|
||||
"image": "ipfs://QmHash",
|
||||
"attributes": [{"trait_type": "rarity", "value": "common"}]
|
||||
}
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.nft_service.NFTService.mint') as mock_mint:
|
||||
mock_mint.return_value = {
|
||||
"token_id": "123",
|
||||
"contract_address": "0xNFTContract",
|
||||
"token_uri": "ipfs://QmMetadata",
|
||||
"owner": sample_wallet.address
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/nft/mint",
|
||||
json=nft_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["token_id"] == "123"
|
||||
|
||||
def test_transfer_nft(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test NFT transfer"""
|
||||
transfer_data = {
|
||||
"token_id": "123",
|
||||
"to_address": "0xRecipient",
|
||||
"contract_address": "0xNFTContract"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.nft_service.NFTService.transfer') as mock_transfer:
|
||||
mock_transfer.return_value = {
|
||||
"transaction_id": "tx-nft-456",
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/nft/transfer",
|
||||
json=transfer_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "transaction_id" in data
|
||||
|
||||
def test_list_nfts(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test listing owned NFTs"""
|
||||
with patch('apps.wallet_daemon.src.app.services.nft_service.NFTService.list_nfts') as mock_list:
|
||||
mock_list.return_value = [
|
||||
{
|
||||
"token_id": "123",
|
||||
"collection": "aitbc-art",
|
||||
"name": "Test NFT",
|
||||
"image": "ipfs://QmHash"
|
||||
}
|
||||
]
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/nfts",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSecurityFeatures:
|
||||
"""Test wallet security features"""
|
||||
|
||||
def test_enable_2fa(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test enabling 2FA"""
|
||||
with patch('apps.wallet_daemon.src.app.services.security_service.SecurityService.enable_2fa') as mock_2fa:
|
||||
mock_2fa.return_value = {
|
||||
"secret": "JBSWY3DPEHPK3PXP",
|
||||
"qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
|
||||
"backup_codes": ["123456", "789012"]
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/security/2fa/enable",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "secret" in data
|
||||
assert "qr_code" in data
|
||||
|
||||
def test_verify_2fa(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test 2FA verification"""
|
||||
verify_data = {
|
||||
"code": "123456"
|
||||
}
|
||||
|
||||
with patch('apps.wallet_daemon.src.app.services.security_service.SecurityService.verify_2fa') as mock_verify:
|
||||
mock_verify.return_value = {"verified": True}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/security/2fa/verify",
|
||||
json=verify_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["verified"] is True
|
||||
|
||||
def test_whitelist_address(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test address whitelisting"""
|
||||
whitelist_data = {
|
||||
"address": "0xTrustedAddress",
|
||||
"label": "Exchange wallet",
|
||||
"daily_limit": "10000.0"
|
||||
}
|
||||
|
||||
response = wallet_client.post(
|
||||
f"/v1/wallets/{sample_wallet.id}/security/whitelist",
|
||||
json=whitelist_data,
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["address"] == whitelist_data["address"]
|
||||
assert data["status"] == "active"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAnalyticsEndpoints:
|
||||
"""Test analytics and reporting endpoints"""
|
||||
|
||||
def test_get_portfolio_summary(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test portfolio summary"""
|
||||
with patch('apps.wallet_daemon.src.app.services.analytics_service.AnalyticsService.get_portfolio') as mock_portfolio:
|
||||
mock_portfolio.return_value = {
|
||||
"total_value_usd": "5000.0",
|
||||
"assets": [
|
||||
{"symbol": "AITBC", "value": "3000.0", "percentage": 60},
|
||||
{"symbol": "USDT", "value": "2000.0", "percentage": 40}
|
||||
],
|
||||
"24h_change": "+2.5%",
|
||||
"profit_loss": "+125.0"
|
||||
}
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/analytics/portfolio",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_value_usd" in data
|
||||
assert "assets" in data
|
||||
|
||||
def test_get_transaction_history(self, wallet_client, sample_wallet, sample_user):
|
||||
"""Test transaction history analytics"""
|
||||
with patch('apps.wallet_daemon.src.app.services.analytics_service.AnalyticsService.get_transaction_history') as mock_history:
|
||||
mock_history.return_value = {
|
||||
"total_transactions": 150,
|
||||
"successful": 148,
|
||||
"failed": 2,
|
||||
"total_volume": "50000.0",
|
||||
"average_transaction": "333.33",
|
||||
"by_month": [
|
||||
{"month": "2024-01", "count": 45, "volume": "15000.0"},
|
||||
{"month": "2024-02", "count": 52, "volume": "17500.0"}
|
||||
]
|
||||
}
|
||||
|
||||
response = wallet_client.get(
|
||||
f"/v1/wallets/{sample_wallet.id}/analytics/transactions",
|
||||
headers={"X-User-ID": sample_user.id}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_transactions" in data
|
||||
assert "by_month" in data
|
||||
Reference in New Issue
Block a user