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
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:
1
tests/property_tests/__init__.py
Normal file
1
tests/property_tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Property-based tests for AITBC critical functions"""
|
||||
152
tests/property_tests/test_crypto_properties.py
Normal file
152
tests/property_tests/test_crypto_properties.py
Normal 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:])
|
||||
145
tests/property_tests/test_validation_properties.py
Normal file
145
tests/property_tests/test_validation_properties.py
Normal 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)
|
||||
Reference in New Issue
Block a user