""" Unit tests for SHA256 hash calculation and deduplication logic. Tests cover: - Hash computation produces consistent results - Hash is always 64 character lowercase hexadecimal - Different content produces different hashes - Binary content handling - Large file handling (streaming) """ import pytest import hashlib import io from tests.conftest import ( create_test_file, compute_sha256, TEST_CONTENT_HELLO, TEST_HASH_HELLO, TEST_CONTENT_BINARY, TEST_HASH_BINARY, ) class TestHashComputation: """Unit tests for hash calculation functionality.""" @pytest.mark.unit def test_sha256_consistent_results(self): """Test SHA256 hash produces consistent results for identical content.""" content = b"test content for hashing" # Compute hash multiple times hash1 = compute_sha256(content) hash2 = compute_sha256(content) hash3 = compute_sha256(content) assert hash1 == hash2 == hash3 @pytest.mark.unit def test_sha256_different_content_different_hash(self): """Test SHA256 produces different hashes for different content.""" content1 = b"content version 1" content2 = b"content version 2" hash1 = compute_sha256(content1) hash2 = compute_sha256(content2) assert hash1 != hash2 @pytest.mark.unit def test_sha256_format_64_char_hex(self): """Test SHA256 hash is always 64 character lowercase hexadecimal.""" test_cases = [ b"", # Empty b"a", # Single char b"Hello, World!", # Normal string bytes(range(256)), # All byte values b"x" * 10000, # Larger content ] for content in test_cases: hash_value = compute_sha256(content) # Check length assert len(hash_value) == 64, ( f"Hash length should be 64, got {len(hash_value)}" ) # Check lowercase assert hash_value == hash_value.lower(), "Hash should be lowercase" # Check hexadecimal assert all(c in "0123456789abcdef" for c in hash_value), ( "Hash should be hex" ) @pytest.mark.unit def test_sha256_known_value(self): """Test SHA256 produces expected hash for known input.""" assert compute_sha256(TEST_CONTENT_HELLO) == TEST_HASH_HELLO @pytest.mark.unit def test_sha256_binary_content(self): """Test SHA256 handles binary content correctly.""" assert compute_sha256(TEST_CONTENT_BINARY) == TEST_HASH_BINARY # Test with null bytes content_with_nulls = b"\x00\x00test\x00\x00" hash_value = compute_sha256(content_with_nulls) assert len(hash_value) == 64 @pytest.mark.unit def test_sha256_streaming_computation(self): """Test SHA256 can be computed in chunks (streaming).""" # Large content chunk_size = 8192 total_size = chunk_size * 10 # 80KB content = b"x" * total_size # Direct computation direct_hash = compute_sha256(content) # Streaming computation hasher = hashlib.sha256() for i in range(0, total_size, chunk_size): hasher.update(content[i : i + chunk_size]) streaming_hash = hasher.hexdigest() assert direct_hash == streaming_hash @pytest.mark.unit def test_sha256_order_matters(self): """Test that content order affects hash (not just content set).""" content1 = b"AB" content2 = b"BA" assert compute_sha256(content1) != compute_sha256(content2) class TestStorageHashComputation: """Tests for hash computation in the storage layer.""" @pytest.mark.unit def test_storage_computes_sha256(self, mock_storage): """Test storage layer correctly computes SHA256 hash.""" content = TEST_CONTENT_HELLO file_obj = io.BytesIO(content) result = mock_storage._store_simple(file_obj) assert result.sha256 == TEST_HASH_HELLO @pytest.mark.unit def test_storage_computes_md5(self, mock_storage): """Test storage layer also computes MD5 hash.""" content = TEST_CONTENT_HELLO file_obj = io.BytesIO(content) result = mock_storage._store_simple(file_obj) expected_md5 = hashlib.md5(content).hexdigest() assert result.md5 == expected_md5 @pytest.mark.unit def test_storage_computes_sha1(self, mock_storage): """Test storage layer also computes SHA1 hash.""" content = TEST_CONTENT_HELLO file_obj = io.BytesIO(content) result = mock_storage._store_simple(file_obj) expected_sha1 = hashlib.sha1(content).hexdigest() assert result.sha1 == expected_sha1 @pytest.mark.unit def test_storage_returns_correct_size(self, mock_storage): """Test storage layer returns correct file size.""" content = b"test content with known size" file_obj = io.BytesIO(content) result = mock_storage._store_simple(file_obj) assert result.size == len(content) @pytest.mark.unit def test_storage_generates_correct_s3_key(self, mock_storage): """Test storage layer generates correct S3 key pattern.""" content = TEST_CONTENT_HELLO file_obj = io.BytesIO(content) result = mock_storage._store_simple(file_obj) # Key should be: fruits/{hash[:2]}/{hash[2:4]}/{hash} expected_key = ( f"fruits/{TEST_HASH_HELLO[:2]}/{TEST_HASH_HELLO[2:4]}/{TEST_HASH_HELLO}" ) assert result.s3_key == expected_key class TestHashEdgeCases: """Edge case tests for hash computation.""" @pytest.mark.unit def test_hash_empty_content_rejected(self, mock_storage): """Test that empty content is rejected.""" from app.storage import HashComputationError file_obj = io.BytesIO(b"") with pytest.raises(HashComputationError): mock_storage._store_simple(file_obj) @pytest.mark.unit def test_hash_large_file_streaming(self, mock_storage): """Test hash computation for large files uses streaming.""" # Create a 10MB file size = 10 * 1024 * 1024 content = b"x" * size file_obj = io.BytesIO(content) result = mock_storage._store_simple(file_obj) expected_hash = compute_sha256(content) assert result.sha256 == expected_hash @pytest.mark.unit def test_hash_special_bytes(self): """Test hash handles all byte values correctly.""" # All possible byte values content = bytes(range(256)) hash_value = compute_sha256(content) assert len(hash_value) == 64 assert hash_value == TEST_HASH_BINARY