test: add property-based tests for critical functions
Some checks failed
Cross-Node Transaction Testing / transaction-test (push) Has been cancelled
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
Python Tests / test-python (push) Failing after 1m5s

- Add property-based tests for cryptographic functions using hypothesis
- Test crypto operations: address derivation, signing, encryption, hashing
- Add property-based tests for validation functions
- Test validation: addresses, hashes, URLs, ports, emails, UUIDs
- Ensure cryptographic determinism and validation correctness
- Add 100+ property-based test cases for critical functions
This commit is contained in:
aitbc
2026-05-09 12:19:10 +02:00
parent 62e65bc88f
commit e151fd448a
3 changed files with 298 additions and 0 deletions

View File

@@ -0,0 +1 @@
"""Property-based tests for AITBC critical functions"""

View File

@@ -0,0 +1,152 @@
"""
Property-based tests for critical AITBC cryptographic functions using hypothesis.
Tests ensure that cryptographic operations maintain expected properties across random inputs.
"""
import pytest
from hypothesis import given, strategies as st, settings
from hypothesis.strategies import text, binary, integers
import json
from aitbc.crypto import (
derive_ethereum_address,
sign_transaction_hash,
verify_signature,
encrypt_private_key,
decrypt_private_key,
keccak256_hash,
sha256_hash,
generate_secure_random_bytes,
validate_ethereum_address,
generate_ethereum_private_key
)
class TestCryptoProperties:
"""Property-based tests for cryptographic functions"""
@given(st.binary(min_size=32, max_size=32))
@settings(max_examples=100)
def test_derive_address_deterministic(self, private_key_bytes):
"""Test that address derivation is deterministic for same private key"""
# Convert bytes to hex string
private_key_hex = private_key_bytes.hex()
# Derive address twice
address1 = derive_ethereum_address(private_key_hex)
address2 = derive_ethereum_address(private_key_hex)
# Should be identical
assert address1 == address2
@given(st.binary(min_size=32, max_size=32))
@settings(max_examples=50)
def test_derived_address_format(self, private_key_bytes):
"""Test that derived addresses have correct format"""
private_key_hex = private_key_bytes.hex()
address = derive_ethereum_address(private_key_hex)
# Address should be 42 characters (0x + 40 hex chars)
assert len(address) == 42
assert address.startswith('0x')
assert all(c in '0123456789abcdef' for c in address[2:])
@given(st.binary(min_size=32, max_size=32), st.binary(min_size=32, max_size=32))
@settings(max_examples=50)
def test_sign_verify_roundtrip(self, private_key_bytes, message_bytes):
"""Test that signing and verification are consistent"""
private_key_hex = private_key_bytes.hex()
message_hash = message_bytes.hex()
# Sign message
signature = sign_transaction_hash(message_hash, private_key_hex)
# Derive address from private key
address = derive_ethereum_address(private_key_hex)
# Verify signature
assert verify_signature(message_hash, signature, address)
@given(st.text(min_size=8, max_size=64), st.text(min_size=8, max_size=64))
@settings(max_examples=50)
def test_encrypt_decrypt_roundtrip(self, password, private_key):
"""Test that encryption and decryption are reversible"""
# Ensure private key is valid hex
private_key_hex = private_key.encode('utf-8').hex()[:64].ljust(64, '0')
# Encrypt
encrypted = encrypt_private_key(private_key_hex, password)
# Decrypt
decrypted = decrypt_private_key(encrypted, password)
# Should match original
assert decrypted == private_key_hex
@given(st.binary(min_size=1, max_size=1024))
@settings(max_examples=50)
def test_keccak256_deterministic(self, data):
"""Test that Keccak-256 hashing is deterministic"""
hash1 = keccak256_hash(data.hex())
hash2 = keccak256_hash(data.hex())
assert hash1 == hash2
@given(st.binary(min_size=1, max_size=1024))
@settings(max_examples=50)
def test_sha256_deterministic(self, data):
"""Test that SHA-256 hashing is deterministic"""
hash1 = sha256_hash(data.hex())
hash2 = sha256_hash(data.hex())
assert hash1 == hash2
@given(st.integers(min_value=16, max_value=128))
@settings(max_examples=50)
def test_random_bytes_length(self, length):
"""Test that random byte generation produces correct length"""
random_bytes = generate_secure_random_bytes(length)
# Should be 2*length hex characters
assert len(random_bytes) == length * 2
@given(st.integers(min_value=16, max_value=128))
@settings(max_examples=50)
def test_random_bytes_uniqueness(self, length):
"""Test that random byte generation produces unique values"""
random_bytes1 = generate_secure_random_bytes(length)
random_bytes2 = generate_secure_random_bytes(length)
# Should be different (extremely unlikely to be same)
assert random_bytes1 != random_bytes2
@given(st.binary(min_size=32, max_size=32))
@settings(max_examples=50)
def test_address_validation(self, private_key_bytes):
"""Test that derived addresses pass validation"""
private_key_hex = private_key_bytes.hex()
address = derive_ethereum_address(private_key_hex)
assert validate_ethereum_address(address)
@given(st.text(alphabet='0123456789abcdef', min_size=40, max_size=40))
@settings(max_examples=50)
def test_address_validation_format(self, hex_string):
"""Test address validation with various formats"""
# Valid format with 0x prefix
valid_address = '0x' + hex_string
assert validate_ethereum_address(valid_address)
# Invalid format without 0x prefix
invalid_address = hex_string
assert not validate_ethereum_address(invalid_address)
@settings(max_examples=10)
def test_private_key_generation_format(self):
"""Test that generated private keys have correct format"""
private_key = generate_ethereum_private_key()
# Should be 66 characters (0x + 64 hex chars)
assert len(private_key) == 66
assert private_key.startswith('0x')
assert all(c in '0123456789abcdef' for c in private_key[2:])

View File

@@ -0,0 +1,145 @@
"""
Property-based tests for AITBC validation functions using hypothesis.
Tests ensure that validation functions maintain expected properties across random inputs.
"""
import pytest
from hypothesis import given, strategies as st, settings
from hypothesis.strategies import text, integers, email, uuid, ip_addresses
from aitbc.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
)
class TestValidationProperties:
"""Property-based tests for validation functions"""
@given(st.text(min_size=1, max_size=100))
@settings(max_examples=50)
def test_validate_non_empty_strings(self, text):
"""Test that non-empty strings pass validation"""
assert validate_non_empty(text)
@given(st.just(""))
@settings(max_examples=10)
def test_validate_empty_strings(self, empty_string):
"""Test that empty strings fail validation"""
assert not validate_non_empty(empty_string)
@given(st.integers(min_value=1, max_value=1000000))
@settings(max_examples=50)
def test_validate_positive_numbers(self, number):
"""Test that positive numbers pass validation"""
assert validate_positive_number(number)
@given(st.integers(max_value=0))
@settings(max_examples=50)
def test_validate_non_positive_numbers(self, number):
"""Test that non-positive numbers fail validation"""
assert not validate_positive_number(number)
@given(st.integers(min_value=0, max_value=100), st.integers(min_value=101, max_value=200))
@settings(max_examples=50)
def test_validate_range_in_bounds(self, value, max_val):
"""Test that values in range pass validation"""
assert validate_range(value, 0, max_val)
@given(st.integers(min_value=-100, max_value=-1))
@settings(max_examples=50)
def test_validate_range_out_of_bounds(self, value):
"""Test that values out of range fail validation"""
assert not validate_range(value, 0, 100)
@given(st.integers(min_value=1, max_value=65535))
@settings(max_examples=50)
def test_validate_valid_ports(self, port):
"""Test that valid ports pass validation"""
assert validate_port(port)
@given(st.integers(min_value=65536, max_value=100000))
@settings(max_examples=50)
def test_validate_invalid_ports(self, port):
"""Test that invalid ports fail validation"""
assert not validate_port(port)
@given(st.emails())
@settings(max_examples=50)
def test_validate_valid_emails(self, email_addr):
"""Test that valid email addresses pass validation"""
assert validate_email(email_addr)
@given(st.text(min_size=1, max_size=50).filter(lambda x: '@' not in x))
@settings(max_examples=50)
def test_validate_invalid_emails(self, text):
"""Test that invalid email addresses fail validation"""
assert not validate_email(text)
@given(st.just("0x" + "a" * 40))
@settings(max_examples=10)
def test_validate_valid_address(self, address):
"""Test that valid Ethereum addresses pass validation"""
assert validate_address(address)
@given(st.text(min_size=1, max_size=50).filter(lambda x: not x.startswith('0x')))
@settings(max_examples=50)
def test_validate_invalid_address_format(self, text):
"""Test that invalid address formats fail validation"""
assert not validate_address(text)
@given(st.just("0x" + "a" * 64))
@settings(max_examples=10)
def test_validate_valid_hash(self, hash_str):
"""Test that valid hashes pass validation"""
assert validate_hash(hash_str)
@given(st.text(min_size=1, max_size=50).filter(lambda x: not x.startswith('0x')))
@settings(max_examples=50)
def test_validate_invalid_hash_format(self, text):
"""Test that invalid hash formats fail validation"""
assert not validate_hash(text)
@given(st.just("ait-mainnet"))
@settings(max_examples=10)
def test_validate_valid_chain_id(self, chain_id):
"""Test that valid chain IDs pass validation"""
assert validate_chain_id(chain_id)
@given(st.text(min_size=1, max_size=50).filter(lambda x: 'ait-' not in x))
@settings(max_examples=50)
def test_validate_invalid_chain_id(self, text):
"""Test that invalid chain IDs fail validation"""
assert not validate_chain_id(text)
@given(st.uuids())
@settings(max_examples=50)
def test_validate_valid_uuid(self, uuid_obj):
"""Test that valid UUIDs pass validation"""
assert validate_uuid(str(uuid_obj))
@given(st.text(min_size=1, max_size=50).filter(lambda x: '-' not in x))
@settings(max_examples=50)
def test_validate_invalid_uuid(self, text):
"""Test that invalid UUIDs fail validation"""
assert not validate_uuid(text)
@given(st.just("http://localhost:8000"))
@settings(max_examples=10)
def test_validate_valid_url(self, url):
"""Test that valid URLs pass validation"""
assert validate_url(url)
@given(st.text(min_size=1, max_size=50).filter(lambda x: 'http' not in x and 'https' not in x))
@settings(max_examples=50)
def test_validate_invalid_url(self, text):
"""Test that invalid URLs fail validation"""
assert not validate_url(text)