Clean up repo structure
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,785 +0,0 @@
|
||||
"""
|
||||
Stegasoo Tests (v4.0.0)
|
||||
|
||||
Tests for key generation, validation, encoding/decoding, output formats,
|
||||
and channel key functionality.
|
||||
|
||||
Updated for v4.0.0:
|
||||
- Same API as v3.2.0 (passphrase, no date_str)
|
||||
- Channel key support for deployment/group isolation
|
||||
- HEADER_OVERHEAD increased to 66 bytes (flags byte added)
|
||||
- Python 3.12 recommended (3.13 not supported)
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
import stegasoo
|
||||
from stegasoo import (
|
||||
decode,
|
||||
decode_text,
|
||||
encode,
|
||||
generate_channel_key,
|
||||
generate_credentials,
|
||||
generate_passphrase,
|
||||
generate_pin,
|
||||
get_channel_fingerprint,
|
||||
validate_channel_key,
|
||||
validate_message,
|
||||
validate_passphrase,
|
||||
validate_pin,
|
||||
)
|
||||
from stegasoo.steganography import get_output_format
|
||||
|
||||
# =============================================================================
|
||||
# Fixtures
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def png_image():
|
||||
"""Create a test PNG image."""
|
||||
img = Image.new("RGB", (100, 100), color="red")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
buf.seek(0)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def large_png_image():
|
||||
"""Create a larger test PNG image for DCT mode.
|
||||
|
||||
Uses noise instead of solid color to ensure DCT color mode works.
|
||||
Solid colors cause coefficient drift during RGB conversion that
|
||||
can exceed the quantization step and corrupt embedded data.
|
||||
"""
|
||||
import numpy as np
|
||||
# Create random noise image (ensures varied Y channel values)
|
||||
np.random.seed(42) # Reproducible
|
||||
data = np.random.randint(0, 256, (400, 400, 3), dtype=np.uint8)
|
||||
img = Image.fromarray(data, 'RGB')
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
buf.seek(0)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bmp_image():
|
||||
"""Create a test BMP image."""
|
||||
img = Image.new("RGB", (100, 100), color="blue")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="BMP")
|
||||
buf.seek(0)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def jpeg_image():
|
||||
"""Create a test JPEG image."""
|
||||
img = Image.new("RGB", (100, 100), color="green")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG")
|
||||
buf.seek(0)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gif_image():
|
||||
"""Create a test GIF image."""
|
||||
img = Image.new("RGB", (100, 100), color="yellow")
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="GIF")
|
||||
buf.seek(0)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Key Generation Tests (v3.2.0 Updated)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestKeygen:
|
||||
"""Tests for key generation functions."""
|
||||
|
||||
def test_generate_pin_default(self):
|
||||
"""Default PIN should be 6 digits, no leading zero."""
|
||||
pin = generate_pin()
|
||||
assert len(pin) == 6
|
||||
assert pin.isdigit()
|
||||
assert pin[0] != "0"
|
||||
|
||||
def test_generate_pin_lengths(self):
|
||||
"""PIN generation should work for all valid lengths."""
|
||||
for length in [6, 7, 8, 9]:
|
||||
pin = generate_pin(length)
|
||||
assert len(pin) == length
|
||||
assert pin.isdigit()
|
||||
|
||||
def test_generate_passphrase_default(self):
|
||||
"""Default passphrase should have 4 words (v3.2.0 change)."""
|
||||
phrase = generate_passphrase()
|
||||
words = phrase.split()
|
||||
assert len(words) == 4 # Changed from 3 in v3.1.x
|
||||
|
||||
def test_generate_passphrase_custom_length(self):
|
||||
"""Passphrase generation should work for custom lengths."""
|
||||
for length in [3, 4, 5, 6, 8, 12]:
|
||||
phrase = generate_passphrase(length)
|
||||
words = phrase.split()
|
||||
assert len(words) == length
|
||||
|
||||
def test_generate_credentials_pin_only(self):
|
||||
"""PIN-only credentials should have single passphrase."""
|
||||
creds = generate_credentials(use_pin=True, use_rsa=False)
|
||||
assert creds.pin is not None
|
||||
assert creds.rsa_key_pem is None
|
||||
# v3.2.0: Single passphrase instead of 7 daily phrases
|
||||
assert creds.passphrase is not None
|
||||
assert isinstance(creds.passphrase, str)
|
||||
assert " " in creds.passphrase # Should have multiple words
|
||||
|
||||
def test_generate_credentials_rsa_only(self):
|
||||
"""RSA-only credentials should have single passphrase."""
|
||||
creds = generate_credentials(use_pin=False, use_rsa=True)
|
||||
assert creds.pin is None
|
||||
assert creds.rsa_key_pem is not None
|
||||
assert creds.passphrase is not None
|
||||
|
||||
def test_generate_credentials_both(self):
|
||||
"""Both PIN and RSA should work together."""
|
||||
creds = generate_credentials(use_pin=True, use_rsa=True)
|
||||
assert creds.pin is not None
|
||||
assert creds.rsa_key_pem is not None
|
||||
assert creds.passphrase is not None
|
||||
|
||||
def test_generate_credentials_neither_fails(self):
|
||||
"""Generating with neither PIN nor RSA should fail."""
|
||||
with pytest.raises((ValueError, AssertionError)):
|
||||
generate_credentials(use_pin=False, use_rsa=False)
|
||||
|
||||
def test_generate_credentials_custom_words(self):
|
||||
"""Custom passphrase_words parameter should work."""
|
||||
creds = generate_credentials(use_pin=True, passphrase_words=6)
|
||||
words = creds.passphrase.split()
|
||||
assert len(words) == 6
|
||||
|
||||
def test_generate_credentials_default_words(self):
|
||||
"""Default should be 4 words (v3.2.0)."""
|
||||
creds = generate_credentials(use_pin=True)
|
||||
words = creds.passphrase.split()
|
||||
assert len(words) == 4
|
||||
|
||||
def test_passphrase_entropy_calculation(self):
|
||||
"""Passphrase entropy should be calculated correctly."""
|
||||
creds = generate_credentials(use_pin=True, passphrase_words=4)
|
||||
# 4 words × 11 bits/word = 44 bits
|
||||
assert creds.passphrase_entropy == 44
|
||||
|
||||
def test_total_entropy_calculation(self):
|
||||
"""Total entropy should sum all components."""
|
||||
creds = generate_credentials(use_pin=True, use_rsa=False, passphrase_words=4)
|
||||
# 44 bits (passphrase) + ~20 bits (PIN)
|
||||
assert creds.total_entropy > 0
|
||||
assert creds.total_entropy >= creds.passphrase_entropy
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Validation Tests (v3.2.0 Updated)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestValidation:
|
||||
"""Tests for validation functions."""
|
||||
|
||||
def test_validate_pin_valid(self):
|
||||
"""Valid PIN should pass validation."""
|
||||
result = validate_pin("123456")
|
||||
assert result.is_valid
|
||||
|
||||
def test_validate_pin_empty_ok(self):
|
||||
"""Empty PIN should be valid (RSA key might be used instead)."""
|
||||
result = validate_pin("")
|
||||
assert result.is_valid
|
||||
|
||||
def test_validate_pin_too_short(self):
|
||||
"""PIN shorter than 6 digits should fail."""
|
||||
result = validate_pin("12345")
|
||||
assert not result.is_valid
|
||||
|
||||
def test_validate_pin_too_long(self):
|
||||
"""PIN longer than 9 digits should fail."""
|
||||
result = validate_pin("1234567890")
|
||||
assert not result.is_valid
|
||||
|
||||
def test_validate_pin_leading_zero(self):
|
||||
"""PIN with leading zero should fail."""
|
||||
result = validate_pin("012345")
|
||||
assert not result.is_valid
|
||||
|
||||
def test_validate_pin_non_digits(self):
|
||||
"""PIN with non-digit characters should fail."""
|
||||
result = validate_pin("12345a")
|
||||
assert not result.is_valid
|
||||
|
||||
def test_validate_message_valid(self):
|
||||
"""Valid message should pass validation."""
|
||||
result = validate_message("Hello, World!")
|
||||
assert result.is_valid
|
||||
|
||||
def test_validate_message_empty(self):
|
||||
"""Empty message should fail validation."""
|
||||
result = validate_message("")
|
||||
assert not result.is_valid
|
||||
|
||||
def test_validate_passphrase_valid(self):
|
||||
"""Valid passphrase should pass validation."""
|
||||
result = validate_passphrase("word1 word2 word3 word4")
|
||||
assert result.is_valid
|
||||
|
||||
def test_validate_passphrase_empty(self):
|
||||
"""Empty passphrase should fail validation."""
|
||||
result = validate_passphrase("")
|
||||
assert not result.is_valid
|
||||
|
||||
def test_validate_passphrase_short_warning(self):
|
||||
"""Short passphrase should have warning but still be valid."""
|
||||
result = validate_passphrase("word1 word2 word3") # Only 3 words
|
||||
assert result.is_valid
|
||||
assert result.warning is not None # Should warn about short passphrase
|
||||
|
||||
def test_validate_passphrase_recommended_no_warning(self):
|
||||
"""Recommended length passphrase should have no warning."""
|
||||
result = validate_passphrase("word1 word2 word3 word4") # 4 words
|
||||
assert result.is_valid
|
||||
# May or may not have warning depending on implementation
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Output Format Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestOutputFormat:
|
||||
"""Tests for output format handling."""
|
||||
|
||||
def test_png_stays_png(self):
|
||||
"""PNG input should produce PNG output."""
|
||||
fmt, ext = get_output_format("PNG")
|
||||
assert fmt == "PNG"
|
||||
assert ext == "png"
|
||||
|
||||
def test_bmp_stays_bmp(self):
|
||||
"""BMP input should produce BMP output."""
|
||||
fmt, ext = get_output_format("BMP")
|
||||
assert fmt == "BMP"
|
||||
assert ext == "bmp"
|
||||
|
||||
def test_jpeg_becomes_png(self):
|
||||
"""JPEG input should produce PNG output (lossless)."""
|
||||
fmt, ext = get_output_format("JPEG")
|
||||
assert fmt == "PNG"
|
||||
assert ext == "png"
|
||||
|
||||
def test_gif_becomes_png(self):
|
||||
"""GIF input should produce PNG output."""
|
||||
fmt, ext = get_output_format("GIF")
|
||||
assert fmt == "PNG"
|
||||
assert ext == "png"
|
||||
|
||||
def test_none_becomes_png(self):
|
||||
"""None format should default to PNG."""
|
||||
fmt, ext = get_output_format(None)
|
||||
assert fmt == "PNG"
|
||||
assert ext == "png"
|
||||
|
||||
def test_unknown_becomes_png(self):
|
||||
"""Unknown format should default to PNG."""
|
||||
fmt, ext = get_output_format("UNKNOWN")
|
||||
assert fmt == "PNG"
|
||||
assert ext == "png"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Header Overhead Test (v4.0.0)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestConstants:
|
||||
"""Tests for constants and configuration."""
|
||||
|
||||
def test_header_overhead_value(self):
|
||||
"""Header overhead should be 66 bytes (v4.0.0: added flags byte)."""
|
||||
from stegasoo.steganography import HEADER_OVERHEAD
|
||||
|
||||
assert HEADER_OVERHEAD == 66
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Encode/Decode Tests (v4.0.0 Updated)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestEncodeDecode:
|
||||
"""Tests for encoding and decoding functions."""
|
||||
|
||||
def test_encode_decode_roundtrip(self, png_image):
|
||||
"""Full encode/decode cycle should work."""
|
||||
message = "Secret message!"
|
||||
passphrase = "apple forest thunder mountain" # 4 words
|
||||
pin = "123456"
|
||||
|
||||
# v3.2.0: Use passphrase parameter, no date_str
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
assert result.stego_image is not None
|
||||
assert len(result.stego_image) > 0
|
||||
assert result.filename.endswith(".png")
|
||||
|
||||
# v3.2.0: Use passphrase parameter, no date_str
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_decode_text_roundtrip(self, png_image):
|
||||
"""decode_text convenience function should work."""
|
||||
message = "Secret message!"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
# decode_text returns string directly
|
||||
decoded_text = decode_text(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
assert decoded_text == message
|
||||
|
||||
def test_png_carrier_produces_png(self, png_image):
|
||||
"""PNG carrier should produce PNG output."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="123456",
|
||||
)
|
||||
assert result.filename.endswith(".png")
|
||||
|
||||
def test_bmp_carrier_produces_bmp(self, bmp_image, png_image):
|
||||
"""BMP carrier should produce BMP output."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=bmp_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="123456",
|
||||
)
|
||||
assert result.filename.endswith(".bmp")
|
||||
|
||||
def test_jpeg_carrier_produces_png(self, jpeg_image, png_image):
|
||||
"""JPEG carrier should produce PNG output (lossless)."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=jpeg_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="123456",
|
||||
)
|
||||
assert result.filename.endswith(".png")
|
||||
|
||||
def test_bmp_roundtrip(self, bmp_image, png_image):
|
||||
"""Full encode/decode cycle with BMP should work."""
|
||||
message = "BMP test message!"
|
||||
passphrase = "test phrase words here"
|
||||
pin = "123456"
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=bmp_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
assert result.filename.endswith(".bmp")
|
||||
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_wrong_pin_fails(self, png_image):
|
||||
"""Wrong PIN should fail to decode."""
|
||||
result = encode(
|
||||
message="Secret",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="123456",
|
||||
)
|
||||
|
||||
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="654321", # Wrong PIN
|
||||
)
|
||||
|
||||
def test_wrong_passphrase_fails(self, png_image):
|
||||
"""Wrong passphrase should fail to decode."""
|
||||
result = encode(
|
||||
message="Secret",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase="correct phrase here now",
|
||||
pin="123456",
|
||||
)
|
||||
|
||||
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase="wrong phrase here now", # Wrong passphrase
|
||||
pin="123456",
|
||||
)
|
||||
|
||||
def test_unicode_message(self, png_image):
|
||||
"""Unicode messages should encode/decode correctly."""
|
||||
message = "Hello, 世界! 🎉 Émojis and ümlauts"
|
||||
passphrase = "unicode test phrase here"
|
||||
pin = "123456"
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_filename_format(self, png_image):
|
||||
"""Output filename should have random hex and date suffix."""
|
||||
result = encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="123456",
|
||||
)
|
||||
# Filename format: {random_hex}_{YYYYMMDD}.{ext}
|
||||
# e.g., "a1b2c3d4_20251227.png"
|
||||
import re
|
||||
|
||||
assert re.search(r"^[a-f0-9]{8}_\d{8}\.png$", result.filename)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DCT Mode Tests (v3.2.0)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestDCTMode:
|
||||
"""Tests for DCT steganography mode."""
|
||||
|
||||
@pytest.fixture
|
||||
def skip_if_no_dct(self):
|
||||
"""Skip test if DCT support not available."""
|
||||
if not stegasoo.has_dct_support():
|
||||
pytest.skip("DCT support not available (scipy not installed)")
|
||||
|
||||
def test_dct_encode_decode_roundtrip(self, large_png_image, skip_if_no_dct):
|
||||
"""DCT mode encode/decode should work."""
|
||||
message = "DCT test"
|
||||
passphrase = "dct test phrase here"
|
||||
pin = "123456"
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=large_png_image,
|
||||
carrier_image=large_png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
embed_mode="dct",
|
||||
)
|
||||
|
||||
assert result.stego_image is not None
|
||||
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=large_png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_dct_auto_detection(self, large_png_image, skip_if_no_dct):
|
||||
"""Auto mode should detect DCT encoding."""
|
||||
message = "Auto detect DCT"
|
||||
passphrase = "auto detect test here"
|
||||
pin = "123456"
|
||||
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=large_png_image,
|
||||
carrier_image=large_png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
embed_mode="dct",
|
||||
)
|
||||
|
||||
# Decode with auto mode (default)
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=large_png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
embed_mode="auto",
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Version Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestVersion:
|
||||
"""Tests for version information."""
|
||||
|
||||
def test_version_exists(self):
|
||||
"""Version string should exist and be valid."""
|
||||
assert hasattr(stegasoo, "__version__")
|
||||
parts = stegasoo.__version__.split(".")
|
||||
assert len(parts) >= 2
|
||||
assert all(p.isdigit() for p in parts[:2])
|
||||
|
||||
def test_version_is_4_0_0(self):
|
||||
"""Version should be 4.0.0 or higher."""
|
||||
parts = stegasoo.__version__.split(".")
|
||||
major = int(parts[0])
|
||||
assert major >= 4
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Backward Compatibility Tests
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestBackwardCompatibility:
|
||||
"""Tests for backward compatibility handling."""
|
||||
|
||||
def test_old_day_phrase_parameter_raises(self, png_image):
|
||||
"""Using old day_phrase parameter should raise TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
day_phrase="old style phrase", # Old parameter name
|
||||
pin="123456",
|
||||
)
|
||||
|
||||
def test_old_date_str_parameter_raises(self, png_image):
|
||||
"""Using old date_str parameter should raise TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
encode(
|
||||
message="Test",
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase="test phrase here now",
|
||||
pin="123456",
|
||||
date_str="2025-01-01", # Removed parameter
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Channel Key Tests (v4.0.0)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestChannelKey:
|
||||
"""Tests for channel key functionality (v4.0.0)."""
|
||||
|
||||
def test_generate_channel_key_format(self):
|
||||
"""Generated channel key should have correct format."""
|
||||
key = generate_channel_key()
|
||||
# Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (8 groups of 4)
|
||||
assert len(key) == 39
|
||||
parts = key.split("-")
|
||||
assert len(parts) == 8
|
||||
for part in parts:
|
||||
assert len(part) == 4
|
||||
assert part.isalnum()
|
||||
|
||||
def test_validate_channel_key_valid(self):
|
||||
"""Valid channel key should pass validation."""
|
||||
key = generate_channel_key()
|
||||
assert validate_channel_key(key)
|
||||
|
||||
def test_validate_channel_key_invalid(self):
|
||||
"""Invalid channel key should fail validation."""
|
||||
assert not validate_channel_key("")
|
||||
assert not validate_channel_key("invalid")
|
||||
assert not validate_channel_key("ABCD-1234") # Too short
|
||||
|
||||
def test_channel_fingerprint_format(self):
|
||||
"""Channel fingerprint should mask middle sections."""
|
||||
key = "ABCD-1234-EFGH-5678-IJKL-9012-MNOP-3456"
|
||||
fingerprint = get_channel_fingerprint(key)
|
||||
assert fingerprint is not None
|
||||
# First and last groups visible, middle masked
|
||||
assert fingerprint.startswith("ABCD-")
|
||||
assert fingerprint.endswith("-3456")
|
||||
assert "••••" in fingerprint
|
||||
|
||||
def test_encode_decode_with_channel_key(self, png_image):
|
||||
"""Encode/decode should work with explicit channel key."""
|
||||
message = "Secret with channel key!"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
channel_key = generate_channel_key()
|
||||
|
||||
# Encode with channel key
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key,
|
||||
)
|
||||
|
||||
assert result.stego_image is not None
|
||||
|
||||
# Decode with same channel key
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key,
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_decode_wrong_channel_key_fails(self, png_image):
|
||||
"""Decoding with wrong channel key should fail."""
|
||||
message = "Secret message"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
channel_key1 = generate_channel_key()
|
||||
channel_key2 = generate_channel_key()
|
||||
|
||||
# Encode with one channel key
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key1,
|
||||
)
|
||||
|
||||
# Decode with different channel key should fail
|
||||
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key2,
|
||||
)
|
||||
|
||||
def test_encode_decode_public_mode(self, png_image):
|
||||
"""Encode/decode should work without channel key (public mode)."""
|
||||
message = "Public message!"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
|
||||
# Encode without channel key (explicit public mode)
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key="", # Explicit public mode
|
||||
)
|
||||
|
||||
# Decode without channel key
|
||||
decoded = decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key="", # Explicit public mode
|
||||
)
|
||||
|
||||
assert decoded.message == message
|
||||
|
||||
def test_channel_key_mismatch_public_vs_private(self, png_image):
|
||||
"""Decoding public message with channel key should fail."""
|
||||
message = "Public message"
|
||||
passphrase = "apple forest thunder mountain"
|
||||
pin = "123456"
|
||||
|
||||
# Encode without channel key (public)
|
||||
result = encode(
|
||||
message=message,
|
||||
reference_photo=png_image,
|
||||
carrier_image=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key="", # Public mode
|
||||
)
|
||||
|
||||
# Decode with channel key should fail
|
||||
channel_key = generate_channel_key()
|
||||
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
|
||||
decode(
|
||||
stego_image=result.stego_image,
|
||||
reference_photo=png_image,
|
||||
passphrase=passphrase,
|
||||
pin=pin,
|
||||
channel_key=channel_key,
|
||||
)
|
||||
Reference in New Issue
Block a user