From e151fd448afa0f378cf891f481a274d8ac6e1f28 Mon Sep 17 00:00:00 2001 From: aitbc Date: Sat, 9 May 2026 12:19:10 +0200 Subject: [PATCH] test: add property-based tests for critical functions - 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 --- tests/property_tests/__init__.py | 1 + .../property_tests/test_crypto_properties.py | 152 ++++++++++++++++++ .../test_validation_properties.py | 145 +++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100644 tests/property_tests/__init__.py create mode 100644 tests/property_tests/test_crypto_properties.py create mode 100644 tests/property_tests/test_validation_properties.py diff --git a/tests/property_tests/__init__.py b/tests/property_tests/__init__.py new file mode 100644 index 00000000..f38d6a93 --- /dev/null +++ b/tests/property_tests/__init__.py @@ -0,0 +1 @@ +"""Property-based tests for AITBC critical functions""" diff --git a/tests/property_tests/test_crypto_properties.py b/tests/property_tests/test_crypto_properties.py new file mode 100644 index 00000000..da11693f --- /dev/null +++ b/tests/property_tests/test_crypto_properties.py @@ -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:]) diff --git a/tests/property_tests/test_validation_properties.py b/tests/property_tests/test_validation_properties.py new file mode 100644 index 00000000..a815cb10 --- /dev/null +++ b/tests/property_tests/test_validation_properties.py @@ -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)