From fc6e4eb8053a347846e2ac4fe491b5a996de2104 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Mon, 5 Jan 2026 21:53:42 -0500 Subject: [PATCH] Add comprehensive pytest unit tests for stegasoo library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests cover: - Version info - Credential generation (passphrase, PIN, channel key) - Validation functions (passphrase, PIN, message, image) - LSB encode/decode roundtrip and failure cases - DCT encode/decode roundtrip and JPEG output - Channel key encode/decode and wrong key rejection - Compression of long messages - Edge cases: Unicode, special chars, minimum passphrase 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- tests/test_stegasoo.py | 456 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 tests/test_stegasoo.py diff --git a/tests/test_stegasoo.py b/tests/test_stegasoo.py new file mode 100644 index 0000000..08f67f8 --- /dev/null +++ b/tests/test_stegasoo.py @@ -0,0 +1,456 @@ +""" +Stegasoo Library Unit Tests + +Tests core functionality: encode/decode, LSB/DCT modes, channel keys, validation. +""" + +import io +import pytest +from pathlib import Path +from PIL import Image + +import stegasoo +from stegasoo import ( + encode, + decode, + decode_text, + generate_passphrase, + generate_pin, + generate_channel_key, + validate_passphrase, + validate_pin, + validate_image, + validate_message, + has_dct_support, + EncodeResult, + DecodeResult, + ValidationError, + CapacityError, +) + +# Test data paths +TEST_DATA = Path(__file__).parent.parent / "test_data" +CARRIER_PATH = TEST_DATA / "carrier.jpg" +REF_PATH = TEST_DATA / "ref.jpg" + +# Test credentials +TEST_PASSPHRASE = "tower booty sunny windy toasty spicy" +TEST_PIN = "727643678" +TEST_MESSAGE = "Hello, Stegasoo!" + + +@pytest.fixture +def carrier_bytes(): + """Load carrier image as bytes.""" + return CARRIER_PATH.read_bytes() + + +@pytest.fixture +def ref_bytes(): + """Load reference image as bytes.""" + return REF_PATH.read_bytes() + + +@pytest.fixture +def small_image(): + """Create a small test image in memory.""" + img = Image.new("RGB", (200, 200), color="blue") + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + +class TestVersion: + """Test version info.""" + + def test_version_exists(self): + assert hasattr(stegasoo, "__version__") + assert stegasoo.__version__ + + def test_version_format(self): + parts = stegasoo.__version__.split(".") + assert len(parts) >= 2 + assert all(p.isdigit() for p in parts[:2]) + + +class TestGeneration: + """Test credential generation.""" + + def test_generate_passphrase_default(self): + passphrase = generate_passphrase() + words = passphrase.split() + assert len(words) == stegasoo.DEFAULT_PASSPHRASE_WORDS + + def test_generate_passphrase_custom_length(self): + passphrase = generate_passphrase(words=8) + words = passphrase.split() + assert len(words) == 8 + + def test_generate_pin_default(self): + pin = generate_pin() + assert pin.isdigit() + assert len(pin) == 6 # Default is 6 digits + + def test_generate_pin_custom_length(self): + pin = generate_pin(length=9) + assert pin.isdigit() + assert len(pin) == 9 + + def test_generate_channel_key(self): + key = generate_channel_key() + # Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (39 chars) + assert len(key) == 39 + assert key.count("-") == 7 + + +class TestValidation: + """Test validation functions.""" + + def test_validate_passphrase_valid(self): + result = validate_passphrase(TEST_PASSPHRASE) + assert result.is_valid + + def test_validate_passphrase_too_short(self): + result = validate_passphrase("one two") + assert not result.is_valid + + def test_validate_pin_valid(self): + result = validate_pin(TEST_PIN) + assert result.is_valid + + def test_validate_pin_too_short(self): + result = validate_pin("123") + assert not result.is_valid + + def test_validate_pin_non_numeric(self): + result = validate_pin("abc123") + assert not result.is_valid + + def test_validate_message_valid(self): + result = validate_message("Hello world") + assert result.is_valid + + def test_validate_message_empty(self): + result = validate_message("") + assert not result.is_valid + + def test_validate_image_valid(self, carrier_bytes): + result = validate_image(carrier_bytes) + assert result.is_valid + + def test_validate_image_invalid(self): + result = validate_image(b"not an image") + assert not result.is_valid + + +class TestLSBMode: + """Test LSB (Least Significant Bit) encoding/decoding.""" + + def test_encode_decode_roundtrip(self, carrier_bytes, ref_bytes): + """Basic encode/decode roundtrip.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert result.stego_image + assert len(result.stego_image) > 0 + + decoded = decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert decoded.message == TEST_MESSAGE + + def test_decode_text_helper(self, carrier_bytes, ref_bytes): + """Test decode_text convenience function.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + text = decode_text( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert text == TEST_MESSAGE + + def test_wrong_passphrase_fails(self, carrier_bytes, ref_bytes): + """Decoding with wrong passphrase should fail.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + with pytest.raises(Exception): + decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase="wrong passphrase words here now", + pin=TEST_PIN, + embed_mode="lsb", + ) + + def test_wrong_pin_fails(self, carrier_bytes, ref_bytes): + """Decoding with wrong PIN should fail.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + with pytest.raises(Exception): + decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin="999999999", + embed_mode="lsb", + ) + + def test_wrong_reference_fails(self, carrier_bytes, ref_bytes, small_image): + """Decoding with wrong reference should fail.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + with pytest.raises(Exception): + decode( + stego_image=result.stego_image, + reference_photo=small_image, # Wrong reference + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + +class TestDCTMode: + """Test DCT (Discrete Cosine Transform) encoding/decoding.""" + + @pytest.fixture(autouse=True) + def check_dct_support(self): + """Skip DCT tests if not supported.""" + if not has_dct_support(): + pytest.skip("DCT support not available") + + def test_encode_decode_roundtrip(self, carrier_bytes, ref_bytes): + """Basic DCT encode/decode roundtrip.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="dct", + ) + + assert result.stego_image + + decoded = decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="dct", + ) + + assert decoded.message == TEST_MESSAGE + + def test_dct_jpeg_output(self, carrier_bytes, ref_bytes): + """Test DCT mode with JPEG output.""" + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="dct", + dct_output_format="jpeg", + ) + + assert result.stego_image + # Verify it's JPEG by checking magic bytes + assert result.stego_image[:2] == b"\xff\xd8" + + +class TestChannelKey: + """Test channel key functionality.""" + + def test_encode_with_channel_key(self, carrier_bytes, ref_bytes): + """Encode with channel key.""" + channel_key = generate_channel_key() + + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + channel_key=channel_key, + embed_mode="lsb", + ) + + assert result.stego_image + + # Decode with same channel key + decoded = decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + channel_key=channel_key, + embed_mode="lsb", + ) + + assert decoded.message == TEST_MESSAGE + + def test_wrong_channel_key_fails(self, carrier_bytes, ref_bytes): + """Decoding with wrong channel key should fail.""" + channel_key = generate_channel_key() + wrong_key = generate_channel_key() + + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + channel_key=channel_key, + embed_mode="lsb", + ) + + with pytest.raises(Exception): + decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + channel_key=wrong_key, + embed_mode="lsb", + ) + + +class TestCompression: + """Test message compression.""" + + def test_long_message_compresses(self, carrier_bytes, ref_bytes): + """Long messages should be compressed.""" + long_message = "A" * 1000 + + result = encode( + message=long_message, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert result.stego_image + + decoded = decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert decoded.message == long_message + + +class TestEdgeCases: + """Test edge cases and error handling.""" + + def test_unicode_message(self, carrier_bytes, ref_bytes): + """Test encoding Unicode messages.""" + unicode_msg = "Hello 🦖 Stegasoo! 日本語 émojis" + + result = encode( + message=unicode_msg, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert result.stego_image + + decoded = decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert decoded.message == unicode_msg + + def test_minimum_passphrase(self, carrier_bytes, ref_bytes): + """Test with minimum valid passphrase.""" + min_passphrase = "one two three four" # 4 words minimum + + result = encode( + message=TEST_MESSAGE, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=min_passphrase, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert result.stego_image + + def test_special_characters_in_message(self, carrier_bytes, ref_bytes): + """Test special characters in message.""" + special_msg = "Line1\nLine2\tTab\r\nCRLF" + + result = encode( + message=special_msg, + reference_photo=ref_bytes, + carrier_image=carrier_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert result.stego_image + + decoded = decode( + stego_image=result.stego_image, + reference_photo=ref_bytes, + passphrase=TEST_PASSPHRASE, + pin=TEST_PIN, + embed_mode="lsb", + ) + + assert decoded.message == special_msg