diff --git a/aitbc/crypto/__init__.py b/aitbc/crypto/__init__.py new file mode 100644 index 00000000..96242338 --- /dev/null +++ b/aitbc/crypto/__init__.py @@ -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' +] diff --git a/aitbc/crypto.py b/aitbc/crypto/crypto.py similarity index 100% rename from aitbc/crypto.py rename to aitbc/crypto/crypto.py diff --git a/aitbc/security.py b/aitbc/crypto/security.py similarity index 100% rename from aitbc/security.py rename to aitbc/crypto/security.py diff --git a/aitbc/network/__init__.py b/aitbc/network/__init__.py new file mode 100644 index 00000000..74f4bc97 --- /dev/null +++ b/aitbc/network/__init__.py @@ -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' +] diff --git a/aitbc/http_client.py b/aitbc/network/http_client.py similarity index 100% rename from aitbc/http_client.py rename to aitbc/network/http_client.py diff --git a/aitbc/web3_utils.py b/aitbc/network/web3_utils.py similarity index 100% rename from aitbc/web3_utils.py rename to aitbc/network/web3_utils.py diff --git a/aitbc/utils/__init__.py b/aitbc/utils/__init__.py new file mode 100644 index 00000000..99e886d3 --- /dev/null +++ b/aitbc/utils/__init__.py @@ -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' +] diff --git a/aitbc/env.py b/aitbc/utils/env.py similarity index 100% rename from aitbc/env.py rename to aitbc/utils/env.py diff --git a/aitbc/json_utils.py b/aitbc/utils/json_utils.py similarity index 100% rename from aitbc/json_utils.py rename to aitbc/utils/json_utils.py diff --git a/aitbc/paths.py b/aitbc/utils/paths.py similarity index 100% rename from aitbc/paths.py rename to aitbc/utils/paths.py diff --git a/aitbc/time_utils.py b/aitbc/utils/time_utils.py similarity index 100% rename from aitbc/time_utils.py rename to aitbc/utils/time_utils.py diff --git a/aitbc/validation.py b/aitbc/utils/validation.py similarity index 100% rename from aitbc/validation.py rename to aitbc/utils/validation.py diff --git a/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/agent_daemon.py b/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/agent_daemon.py index 920d1649..875abf27 100644 --- a/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/agent_daemon.py +++ b/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/agent_daemon.py @@ -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 diff --git a/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/coordinator_api.py b/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/coordinator_api.py index 222e4839..80d1f98d 100644 --- a/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/coordinator_api.py +++ b/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/coordinator_api.py @@ -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 diff --git a/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/marketplace.py b/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/marketplace.py index e4ebd549..ce8fab89 100644 --- a/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/marketplace.py +++ b/apps/blockchain-event-bridge/src/blockchain_event_bridge/action_handlers/marketplace.py @@ -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 diff --git a/apps/blockchain-event-bridge/src/blockchain_event_bridge/event_subscribers/contracts.py b/apps/blockchain-event-bridge/src/blockchain_event_bridge/event_subscribers/contracts.py index 9ddf987d..50c8fcff 100644 --- a/apps/blockchain-event-bridge/src/blockchain_event_bridge/event_subscribers/contracts.py +++ b/apps/blockchain-event-bridge/src/blockchain_event_bridge/event_subscribers/contracts.py @@ -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 diff --git a/apps/exchange/complete_cross_chain_exchange.py b/apps/exchange/complete_cross_chain_exchange.py index d97d8a14..523c92cb 100755 --- a/apps/exchange/complete_cross_chain_exchange.py +++ b/apps/exchange/complete_cross_chain_exchange.py @@ -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 diff --git a/apps/simple-explorer/main.py b/apps/simple-explorer/main.py index 25902180..7f301655 100644 --- a/apps/simple-explorer/main.py +++ b/apps/simple-explorer/main.py @@ -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 diff --git a/cli/aitbc_cli.py b/cli/aitbc_cli.py index 77b179d9..8b0b66ae 100755 --- a/cli/aitbc_cli.py +++ b/cli/aitbc_cli.py @@ -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__) diff --git a/cli/enterprise_cli.py b/cli/enterprise_cli.py index c1f36b1c..6462acb0 100755 --- a/cli/enterprise_cli.py +++ b/cli/enterprise_cli.py @@ -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() diff --git a/cli/extended_features.py b/cli/extended_features.py index f5defdac..3cbfabd5 100644 --- a/cli/extended_features.py +++ b/cli/extended_features.py @@ -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")) diff --git a/cli/handlers/wallet.py b/cli/handlers/wallet.py index ddbdee79..a4179d23 100644 --- a/cli/handlers/wallet.py +++ b/cli/handlers/wallet.py @@ -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): diff --git a/cli/keystore_auth.py b/cli/keystore_auth.py index 0415c326..a37a003a 100644 --- a/cli/keystore_auth.py +++ b/cli/keystore_auth.py @@ -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 diff --git a/packages/py/aitbc-agent-sdk/src/aitbc_agent/agent.py b/packages/py/aitbc-agent-sdk/src/aitbc_agent/agent.py index 79a1c776..e71d7577 100755 --- a/packages/py/aitbc-agent-sdk/src/aitbc_agent/agent.py +++ b/packages/py/aitbc-agent-sdk/src/aitbc_agent/agent.py @@ -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, diff --git a/packages/py/aitbc-agent-sdk/src/aitbc_agent/compute_provider.py b/packages/py/aitbc-agent-sdk/src/aitbc_agent/compute_provider.py index 5f50c626..6be437ba 100755 --- a/packages/py/aitbc-agent-sdk/src/aitbc_agent/compute_provider.py +++ b/packages/py/aitbc-agent-sdk/src/aitbc_agent/compute_provider.py @@ -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__) diff --git a/packages/py/aitbc-sdk/src/aitbc_sdk/receipts.py b/packages/py/aitbc-sdk/src/aitbc_sdk/receipts.py index 567a615b..79873883 100755 --- a/packages/py/aitbc-sdk/src/aitbc_sdk/receipts.py +++ b/packages/py/aitbc-sdk/src/aitbc_sdk/receipts.py @@ -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 diff --git a/tests/contract_tests/test_blockchain_rpc_contract.py b/tests/contract_tests/test_blockchain_rpc_contract.py new file mode 100644 index 00000000..c72d0529 --- /dev/null +++ b/tests/contract_tests/test_blockchain_rpc_contract.py @@ -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") diff --git a/tests/property_tests/test_crypto_properties.py b/tests/property_tests/test_crypto_properties.py index da11693f..7086d455 100644 --- a/tests/property_tests/test_crypto_properties.py +++ b/tests/property_tests/test_crypto_properties.py @@ -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, diff --git a/tests/property_tests/test_validation_properties.py b/tests/property_tests/test_validation_properties.py index a815cb10..18a51bc5 100644 --- a/tests/property_tests/test_validation_properties.py +++ b/tests/property_tests/test_validation_properties.py @@ -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, diff --git a/tests/verification/test_import_surface.py b/tests/verification/test_import_surface.py index 07871760..906433a0 100644 --- a/tests/verification/test_import_surface.py +++ b/tests/verification/test_import_surface.py @@ -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: