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:
57
aitbc/crypto/__init__.py
Normal file
57
aitbc/crypto/__init__.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Cryptographic utilities for AITBC
|
||||
Provides encryption, signing, hashing, and security-related functions
|
||||
"""
|
||||
|
||||
from .crypto import (
|
||||
derive_ethereum_address,
|
||||
sign_transaction_hash,
|
||||
verify_signature,
|
||||
encrypt_private_key,
|
||||
decrypt_private_key,
|
||||
generate_secure_random_bytes,
|
||||
keccak256_hash,
|
||||
sha256_hash,
|
||||
validate_ethereum_address,
|
||||
generate_ethereum_private_key
|
||||
)
|
||||
|
||||
from .security import (
|
||||
generate_token,
|
||||
generate_api_key,
|
||||
validate_token_format,
|
||||
validate_api_key,
|
||||
generate_secure_random_string,
|
||||
generate_secure_random_int,
|
||||
hash_password,
|
||||
verify_password,
|
||||
generate_nonce,
|
||||
generate_hmac,
|
||||
verify_hmac
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Crypto functions
|
||||
'derive_ethereum_address',
|
||||
'sign_transaction_hash',
|
||||
'verify_signature',
|
||||
'encrypt_private_key',
|
||||
'decrypt_private_key',
|
||||
'generate_secure_random_bytes',
|
||||
'keccak256_hash',
|
||||
'sha256_hash',
|
||||
'validate_ethereum_address',
|
||||
'generate_ethereum_private_key',
|
||||
# Security functions
|
||||
'generate_token',
|
||||
'generate_api_key',
|
||||
'validate_token_format',
|
||||
'validate_api_key',
|
||||
'generate_secure_random_string',
|
||||
'generate_secure_random_int',
|
||||
'hash_password',
|
||||
'verify_password',
|
||||
'generate_nonce',
|
||||
'generate_hmac',
|
||||
'verify_hmac'
|
||||
]
|
||||
19
aitbc/network/__init__.py
Normal file
19
aitbc/network/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Network utilities for AITBC
|
||||
Provides HTTP client and Web3 utilities
|
||||
"""
|
||||
|
||||
from .http_client import (
|
||||
AITBCHTTPClient,
|
||||
AsyncAITBCHTTPClient
|
||||
)
|
||||
|
||||
from .web3_utils import (
|
||||
create_web3_client
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'AITBCHTTPClient',
|
||||
'AsyncAITBCHTTPClient',
|
||||
'create_web3_client'
|
||||
]
|
||||
153
aitbc/utils/__init__.py
Normal file
153
aitbc/utils/__init__.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
Utility functions for AITBC
|
||||
Provides validation, time utilities, JSON utilities, path utilities, and environment variable utilities
|
||||
"""
|
||||
|
||||
from .validation import (
|
||||
validate_address,
|
||||
validate_hash,
|
||||
validate_url,
|
||||
validate_port,
|
||||
validate_email,
|
||||
validate_non_empty,
|
||||
validate_positive_number,
|
||||
validate_range,
|
||||
validate_chain_id,
|
||||
validate_uuid
|
||||
)
|
||||
|
||||
from .time_utils import (
|
||||
get_utc_now,
|
||||
get_timestamp_utc,
|
||||
format_iso8601,
|
||||
parse_iso8601,
|
||||
timestamp_to_iso,
|
||||
iso_to_timestamp,
|
||||
format_duration,
|
||||
format_duration_precise,
|
||||
parse_duration,
|
||||
add_duration,
|
||||
subtract_duration,
|
||||
get_time_until,
|
||||
get_time_since,
|
||||
calculate_deadline,
|
||||
is_deadline_passed,
|
||||
get_deadline_remaining,
|
||||
format_time_ago,
|
||||
format_time_in,
|
||||
to_timezone,
|
||||
get_timezone_offset,
|
||||
is_business_hours,
|
||||
get_start_of_day,
|
||||
get_end_of_day,
|
||||
get_start_of_week,
|
||||
get_end_of_week,
|
||||
get_start_of_month,
|
||||
get_end_of_month,
|
||||
sleep_until,
|
||||
retry_until_deadline
|
||||
)
|
||||
|
||||
from .json_utils import (
|
||||
load_json,
|
||||
save_json,
|
||||
merge_json,
|
||||
json_to_string,
|
||||
string_to_json,
|
||||
get_nested_value,
|
||||
set_nested_value,
|
||||
flatten_json
|
||||
)
|
||||
|
||||
from .paths import (
|
||||
get_data_path,
|
||||
get_config_path,
|
||||
get_log_path,
|
||||
get_repo_path,
|
||||
ensure_dir,
|
||||
ensure_file_dir,
|
||||
resolve_path,
|
||||
get_keystore_path,
|
||||
get_blockchain_data_path,
|
||||
get_marketplace_data_path
|
||||
)
|
||||
|
||||
from .env import (
|
||||
get_env_var,
|
||||
get_required_env_var,
|
||||
get_bool_env_var,
|
||||
get_int_env_var,
|
||||
get_float_env_var,
|
||||
get_list_env_var
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Validation
|
||||
'validate_address',
|
||||
'validate_hash',
|
||||
'validate_url',
|
||||
'validate_port',
|
||||
'validate_email',
|
||||
'validate_non_empty',
|
||||
'validate_positive_number',
|
||||
'validate_range',
|
||||
'validate_chain_id',
|
||||
'validate_uuid',
|
||||
# Time utils
|
||||
'get_utc_now',
|
||||
'get_timestamp_utc',
|
||||
'format_iso8601',
|
||||
'parse_iso8601',
|
||||
'timestamp_to_iso',
|
||||
'iso_to_timestamp',
|
||||
'format_duration',
|
||||
'format_duration_precise',
|
||||
'parse_duration',
|
||||
'add_duration',
|
||||
'subtract_duration',
|
||||
'get_time_until',
|
||||
'get_time_since',
|
||||
'calculate_deadline',
|
||||
'is_deadline_passed',
|
||||
'get_deadline_remaining',
|
||||
'format_time_ago',
|
||||
'format_time_in',
|
||||
'to_timezone',
|
||||
'get_timezone_offset',
|
||||
'is_business_hours',
|
||||
'get_start_of_day',
|
||||
'get_end_of_day',
|
||||
'get_start_of_week',
|
||||
'get_end_of_week',
|
||||
'get_start_of_month',
|
||||
'get_end_of_month',
|
||||
'sleep_until',
|
||||
'retry_until_deadline',
|
||||
# JSON utils
|
||||
'load_json',
|
||||
'save_json',
|
||||
'merge_json',
|
||||
'json_to_string',
|
||||
'string_to_json',
|
||||
'get_nested_value',
|
||||
'set_nested_value',
|
||||
'flatten_json',
|
||||
# Paths
|
||||
'get_data_path',
|
||||
'get_config_path',
|
||||
'get_log_path',
|
||||
'get_repo_path',
|
||||
'ensure_dir',
|
||||
'ensure_file_dir',
|
||||
'resolve_path',
|
||||
'get_keystore_path',
|
||||
'get_blockchain_data_path',
|
||||
'get_marketplace_data_path',
|
||||
# Environment
|
||||
'get_env_var',
|
||||
'get_required_env_var',
|
||||
'get_bool_env_var',
|
||||
'get_int_env_var',
|
||||
'get_float_env_var',
|
||||
'get_list_env_var'
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Agent daemon action handler for triggering autonomous agent responses."""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from aitbc.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.network.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Coordinator API action handler for triggering hermes agent actions."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from aitbc.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.network.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Marketplace action handler for triggering marketplace state updates."""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from aitbc.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.network.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
from aitbc.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.network.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import os
|
||||
import uuid
|
||||
import hashlib
|
||||
|
||||
from aitbc.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.network.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
import uvicorn
|
||||
|
||||
from aitbc.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.network.http_client import AsyncAITBCHTTPClient
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ from typing import Optional, Dict, Any, List
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.constants import BLOCKCHAIN_RPC_PORT, DATA_DIR, KEYSTORE_DIR
|
||||
from aitbc.exceptions import ConfigurationError, NetworkError, ValidationError
|
||||
from aitbc.http_client import AITBCHTTPClient
|
||||
from aitbc.paths import get_blockchain_data_path, get_data_path
|
||||
from aitbc.paths import ensure_dir, get_keystore_path
|
||||
from aitbc.validation import validate_address, validate_url
|
||||
from aitbc.network.http_client import AITBCHTTPClient
|
||||
from aitbc.utils.paths import get_blockchain_data_path, get_data_path
|
||||
from aitbc.utils.paths import ensure_dir, get_keystore_path
|
||||
from aitbc.utils.validation import validate_address, validate_url
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
import requests
|
||||
import getpass
|
||||
from aitbc.paths import get_keystore_path
|
||||
from aitbc.utils.paths import get_keystore_path
|
||||
|
||||
# Default paths
|
||||
DEFAULT_KEYSTORE_DIR = get_keystore_path()
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from aitbc.paths import get_data_path
|
||||
from aitbc.utils.paths import get_data_path
|
||||
|
||||
STATE_FILE = str(get_data_path("data/cli_extended_state.json"))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import json
|
||||
import requests
|
||||
import sys
|
||||
from aitbc.paths import get_data_path
|
||||
from aitbc.utils.paths import get_data_path
|
||||
|
||||
|
||||
def handle_wallet_create(args, create_wallet, read_password, first):
|
||||
|
||||
@@ -13,7 +13,7 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from aitbc.paths import get_keystore_path
|
||||
from aitbc.utils.paths import get_keystore_path
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
from aitbc.http_client import AITBCHTTPClient
|
||||
from aitbc.network.http_client import AITBCHTTPClient
|
||||
from aitbc_agent.contract_integration import (
|
||||
AgentContractIntegration,
|
||||
ContractClient,
|
||||
|
||||
@@ -14,7 +14,7 @@ from .agent import Agent, AgentCapabilities, AgentIdentity
|
||||
|
||||
from aitbc.aitbc_logging import get_logger
|
||||
from aitbc.exceptions import NetworkError
|
||||
from aitbc.http_client import AITBCHTTPClient
|
||||
from aitbc.network.http_client import AITBCHTTPClient
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any, Dict, Iterable, Iterator, List, Optional, cast
|
||||
import base64
|
||||
|
||||
from aitbc.exceptions import NetworkError
|
||||
from aitbc.http_client import AITBCHTTPClient
|
||||
from aitbc.network.http_client import AITBCHTTPClient
|
||||
from aitbc_crypto.signing import ReceiptVerifier
|
||||
|
||||
|
||||
|
||||
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