refactor: reorganize aitbc core library into subpackages
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 17s
CLI Tests / test-cli (push) Failing after 3s
Cross-Node Transaction Testing / transaction-test (push) Successful in 3s
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Integration Tests / test-service-integration (push) Successful in 2m39s
Package Tests / Python package - aitbc-agent-sdk (push) Failing after 12s
Package Tests / Python package - aitbc-core (push) Successful in 12s
Package Tests / Python package - aitbc-crypto (push) Successful in 10s
Package Tests / Python package - aitbc-sdk (push) Failing after 7s
Package Tests / JavaScript package - aitbc-sdk-js (push) Successful in 6s
Package Tests / JavaScript package - aitbc-token (push) Successful in 14s
Python Tests / test-python (push) Failing after 9s
Security Scanning / security-scan (push) Successful in 15s
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 17s
CLI Tests / test-cli (push) Failing after 3s
Cross-Node Transaction Testing / transaction-test (push) Successful in 3s
Deploy to Testnet / deploy-testnet (push) Has been cancelled
Multi-Node Stress Testing / stress-test (push) Has been cancelled
Node Failover Simulation / failover-test (push) Has been cancelled
Integration Tests / test-service-integration (push) Successful in 2m39s
Package Tests / Python package - aitbc-agent-sdk (push) Failing after 12s
Package Tests / Python package - aitbc-core (push) Successful in 12s
Package Tests / Python package - aitbc-crypto (push) Successful in 10s
Package Tests / Python package - aitbc-sdk (push) Failing after 7s
Package Tests / JavaScript package - aitbc-sdk-js (push) Successful in 6s
Package Tests / JavaScript package - aitbc-token (push) Successful in 14s
Python Tests / test-python (push) Failing after 9s
Security Scanning / security-scan (push) Successful in 15s
- Create aitbc/crypto/ subpackage (crypto.py, security.py) - Create aitbc/utils/ subpackage (validation, time_utils, json_utils, paths, env) - Create aitbc/network/ subpackage (http_client, web3_utils) - Update all import statements across codebase - Maintain backward compatibility with __init__.py exports - Improve code organization and modularity
This commit is contained in:
370
tests/contract_tests/test_blockchain_rpc_contract.py
Normal file
370
tests/contract_tests/test_blockchain_rpc_contract.py
Normal file
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
Contract tests for AITBC blockchain RPC interactions.
|
||||
Tests verify that the blockchain RPC API maintains expected contracts and behaviors.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import httpx
|
||||
from typing import Dict, Any, Optional
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
|
||||
|
||||
class BlockchainRPCContract:
|
||||
"""
|
||||
Contract definition for blockchain RPC API.
|
||||
Defines expected behavior and response formats.
|
||||
"""
|
||||
|
||||
BASE_URL = "http://localhost:8006"
|
||||
|
||||
# Expected response structures
|
||||
BLOCK_RESPONSE_SCHEMA = {
|
||||
"height": int,
|
||||
"hash": str,
|
||||
"parent_hash": str,
|
||||
"timestamp": int,
|
||||
"transactions": list
|
||||
}
|
||||
|
||||
TRANSACTION_RESPONSE_SCHEMA = {
|
||||
"hash": str,
|
||||
"from": str,
|
||||
"to": str,
|
||||
"value": str,
|
||||
"nonce": int,
|
||||
"gas": int
|
||||
}
|
||||
|
||||
ACCOUNT_RESPONSE_SCHEMA = {
|
||||
"address": str,
|
||||
"balance": int,
|
||||
"nonce": int
|
||||
}
|
||||
|
||||
STATUS_RESPONSE_SCHEMA = {
|
||||
"syncing": bool,
|
||||
"current_block": int,
|
||||
"highest_block": int,
|
||||
"peers": int
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestBlockchainRPCContracts:
|
||||
"""Contract tests for blockchain RPC endpoints"""
|
||||
|
||||
@pytest.fixture
|
||||
def client(self):
|
||||
"""HTTP client for blockchain RPC"""
|
||||
return httpx.Client(timeout=30.0)
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_url(self):
|
||||
"""Blockchain RPC base URL"""
|
||||
return BlockchainRPCContract.BASE_URL
|
||||
|
||||
def test_get_block_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for getting a block by height.
|
||||
|
||||
Contract:
|
||||
- GET /rpc/blocks/{height} should return block data
|
||||
- Response should contain required fields: height, hash, parent_hash, timestamp, transactions
|
||||
- Height should be a non-negative integer
|
||||
- Hash should be a valid hex string
|
||||
"""
|
||||
try:
|
||||
response = client.get(f"{rpc_url}/rpc/blocks/0")
|
||||
|
||||
# Contract: Should return 200 for valid block height
|
||||
assert response.status_code in (200, 404), f"Expected 200 or 404, got {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Contract: Response should be a dictionary
|
||||
assert isinstance(data, dict), "Response should be a dictionary"
|
||||
|
||||
# Contract: Should contain required fields
|
||||
required_fields = ["height", "hash", "timestamp"]
|
||||
for field in required_fields:
|
||||
assert field in data, f"Missing required field: {field}"
|
||||
|
||||
# Contract: Height should be integer
|
||||
assert isinstance(data["height"], int), "Height should be an integer"
|
||||
assert data["height"] >= 0, "Height should be non-negative"
|
||||
|
||||
# Contract: Hash should be hex string
|
||||
assert isinstance(data["hash"], str), "Hash should be a string"
|
||||
assert data["hash"].startswith("0x"), "Hash should start with 0x"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_get_head_block_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for getting the head block.
|
||||
|
||||
Contract:
|
||||
- GET /rpc/head should return current head block
|
||||
- Response should contain height and hash
|
||||
- Head block should be the highest known block
|
||||
"""
|
||||
try:
|
||||
response = client.get(f"{rpc_url}/rpc/head")
|
||||
|
||||
# Contract: Should return 200
|
||||
assert response.status_code in (200, 404), f"Expected 200 or 404, got {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Contract: Should contain height and hash
|
||||
assert "height" in data, "Missing height field"
|
||||
assert "hash" in data, "Missing hash field"
|
||||
|
||||
# Contract: Height should be non-negative
|
||||
assert data["height"] >= 0, "Height should be non-negative"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_get_transaction_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for getting a transaction by hash.
|
||||
|
||||
Contract:
|
||||
- GET /rpc/transaction/{hash} should return transaction data
|
||||
- Response should contain: hash, from, to, value, nonce
|
||||
- Transaction hash should match request
|
||||
"""
|
||||
try:
|
||||
# Use a sample transaction hash (this may not exist)
|
||||
sample_hash = "0x" + "a" * 64
|
||||
response = client.get(f"{rpc_url}/rpc/transaction/{sample_hash}")
|
||||
|
||||
# Contract: Should return 200 if found, 404 if not found
|
||||
assert response.status_code in (200, 404), f"Expected 200 or 404, got {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Contract: Should contain transaction fields
|
||||
assert "hash" in data, "Missing hash field"
|
||||
assert data["hash"] == sample_hash, "Transaction hash should match request"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_get_account_balance_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for getting account balance.
|
||||
|
||||
Contract:
|
||||
- GET /rpc/account/{address} should return account data
|
||||
- Response should contain: address, balance, nonce
|
||||
- Address should match request
|
||||
- Balance should be non-negative integer
|
||||
"""
|
||||
try:
|
||||
# Use a sample address
|
||||
sample_address = "0x" + "a" * 40
|
||||
response = client.get(f"{rpc_url}/rpc/account/{sample_address}")
|
||||
|
||||
# Contract: Should return 200 or 404
|
||||
assert response.status_code in (200, 404), f"Expected 200 or 404, got {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Contract: Should contain account fields
|
||||
assert "address" in data, "Missing address field"
|
||||
assert data["address"] == sample_address, "Address should match request"
|
||||
|
||||
# Contract: Balance should be non-negative
|
||||
if "balance" in data:
|
||||
assert isinstance(data["balance"], (int, str)), "Balance should be integer or string"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_send_transaction_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for sending a transaction.
|
||||
|
||||
Contract:
|
||||
- POST /rpc/sendTx should accept transaction payload
|
||||
- Response should contain transaction hash if successful
|
||||
- Should return error if transaction is invalid
|
||||
"""
|
||||
try:
|
||||
# Create a sample transaction payload
|
||||
tx_payload = {
|
||||
"from": "0x" + "a" * 40,
|
||||
"to": "0x" + "b" * 40,
|
||||
"value": "1000000000000000000", # 1 ETH in wei
|
||||
"nonce": 0,
|
||||
"gas": 21000
|
||||
}
|
||||
|
||||
response = client.post(f"{rpc_url}/rpc/sendTx", json=tx_payload)
|
||||
|
||||
# Contract: Should return 200, 400, or 500
|
||||
assert response.status_code in (200, 400, 500), f"Unexpected status code: {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# Contract: Should contain transaction hash on success
|
||||
assert "hash" in data or "tx_hash" in data, "Missing transaction hash in response"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_get_peers_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for getting network peers.
|
||||
|
||||
Contract:
|
||||
- GET /rpc/peers should return peer information
|
||||
- Response should be a list of peers
|
||||
- Each peer should have at least an ID or address
|
||||
"""
|
||||
try:
|
||||
response = client.get(f"{rpc_url}/rpc/peers")
|
||||
|
||||
# Contract: Should return 200
|
||||
assert response.status_code in (200, 404), f"Expected 200 or 404, got {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Contract: Response should be a list
|
||||
assert isinstance(data, (list, dict)), "Response should be a list or dict"
|
||||
|
||||
if isinstance(data, list) and data:
|
||||
# Contract: Each peer should have identifier
|
||||
peer = data[0]
|
||||
assert isinstance(peer, dict), "Peer should be a dictionary"
|
||||
assert len(peer) > 0, "Peer should have at least one field"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_get_status_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for getting node status.
|
||||
|
||||
Contract:
|
||||
- GET /rpc/status should return node status
|
||||
- Response should contain: syncing status, current block, highest block
|
||||
- Current block should not exceed highest block
|
||||
"""
|
||||
try:
|
||||
response = client.get(f"{rpc_url}/rpc/status")
|
||||
|
||||
# Contract: Should return 200
|
||||
assert response.status_code in (200, 404), f"Expected 200 or 404, got {response.status_code}"
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
|
||||
# Contract: Should contain status fields
|
||||
assert isinstance(data, dict), "Response should be a dictionary"
|
||||
|
||||
# Contract: If syncing info present, validate it
|
||||
if "syncing" in data:
|
||||
assert isinstance(data["syncing"], bool), "Syncing should be boolean"
|
||||
|
||||
if "current_block" in data and "highest_block" in data:
|
||||
assert data["current_block"] <= data["highest_block"], \
|
||||
"Current block should not exceed highest block"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_rpc_response_format_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for RPC response format.
|
||||
|
||||
Contract:
|
||||
- All RPC responses should be JSON
|
||||
- Content-Type should be application/json
|
||||
- Response should be parseable as JSON
|
||||
"""
|
||||
try:
|
||||
# Test multiple endpoints
|
||||
endpoints = [
|
||||
"/rpc/head",
|
||||
"/rpc/status",
|
||||
"/rpc/peers"
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
response = client.get(f"{rpc_url}{endpoint}")
|
||||
|
||||
if response.status_code == 200:
|
||||
# Contract: Should have JSON content type
|
||||
assert "application/json" in response.headers.get("content-type", ""), \
|
||||
f"{endpoint} should return JSON content type"
|
||||
|
||||
# Contract: Should be parseable as JSON
|
||||
try:
|
||||
response.json()
|
||||
except Exception as e:
|
||||
pytest.fail(f"{endpoint} response should be valid JSON: {e}")
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
def test_rpc_error_handling_contract(self, client, rpc_url):
|
||||
"""
|
||||
Test contract for RPC error handling.
|
||||
|
||||
Contract:
|
||||
- Invalid endpoints should return 404
|
||||
- Invalid requests should return 400
|
||||
- Server errors should return 500
|
||||
- Error responses should contain error message
|
||||
"""
|
||||
try:
|
||||
# Test invalid endpoint
|
||||
response = client.get(f"{rpc_url}/rpc/invalid_endpoint")
|
||||
assert response.status_code == 404, "Invalid endpoint should return 404"
|
||||
|
||||
# Test invalid method
|
||||
response = client.post(f"{rpc_url}/rpc/head")
|
||||
assert response.status_code in (405, 404), "Invalid method should return 405 or 404"
|
||||
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
|
||||
|
||||
@pytest.mark.contract
|
||||
class TestBlockchainRPCTimeouts:
|
||||
"""Contract tests for RPC timeout behavior"""
|
||||
|
||||
@pytest.fixture
|
||||
def slow_client(self):
|
||||
"""HTTP client with short timeout for testing"""
|
||||
return httpx.Client(timeout=1.0)
|
||||
|
||||
def test_rpc_timeout_contract(self, slow_client, rpc_url):
|
||||
"""
|
||||
Test contract for RPC timeout handling.
|
||||
|
||||
Contract:
|
||||
- Requests should respect timeout settings
|
||||
- Timeout should raise appropriate exception
|
||||
- Client should handle timeouts gracefully
|
||||
"""
|
||||
try:
|
||||
# This should timeout if the endpoint is slow
|
||||
response = slow_client.get(f"{rpc_url}/rpc/head")
|
||||
# If it doesn't timeout, that's also valid
|
||||
assert response.status_code in (200, 404)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
# Contract: Timeout should raise TimeoutException
|
||||
pass # Expected behavior
|
||||
except httpx.ConnectError:
|
||||
pytest.skip("Blockchain RPC not available")
|
||||
@@ -8,7 +8,7 @@ from hypothesis import given, strategies as st, settings
|
||||
from hypothesis.strategies import text, binary, integers
|
||||
import json
|
||||
|
||||
from aitbc.crypto import (
|
||||
from aitbc.crypto.crypto import (
|
||||
derive_ethereum_address,
|
||||
sign_transaction_hash,
|
||||
verify_signature,
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
from hypothesis import given, strategies as st, settings
|
||||
from hypothesis.strategies import text, integers, email, uuid, ip_addresses
|
||||
|
||||
from aitbc.validation import (
|
||||
from aitbc.utils.validation import (
|
||||
validate_address,
|
||||
validate_hash,
|
||||
validate_url,
|
||||
|
||||
@@ -13,15 +13,14 @@ sys.path.insert(0, str(REPO_ROOT / "packages" / "py" / "aitbc-sdk" / "src"))
|
||||
import aitbc
|
||||
import aitbc_agent
|
||||
import aitbc_sdk
|
||||
import aitbc.paths as aitbc_paths
|
||||
|
||||
from aitbc.aitbc_logging import get_logger as direct_get_logger
|
||||
from aitbc.constants import BLOCKCHAIN_RPC_PORT, DATA_DIR, ENV_FILE, KEYSTORE_DIR, LOG_DIR, NODE_ENV_FILE, PACKAGE_VERSION
|
||||
from aitbc.exceptions import NetworkError, ValidationError
|
||||
from aitbc.http_client import AITBCHTTPClient
|
||||
from aitbc.paths import ensure_dir, get_keystore_path
|
||||
from aitbc.network.http_client import AITBCHTTPClient
|
||||
from aitbc.utils.paths import ensure_dir, get_keystore_path
|
||||
from aitbc.utils.validation import validate_address, validate_url
|
||||
from aitbc.testing import MockFactory
|
||||
from aitbc.validation import validate_address, validate_url
|
||||
|
||||
|
||||
def test_aitbc_root_exports_match_lightweight_submodules() -> None:
|
||||
|
||||
Reference in New Issue
Block a user