Release checklist and updated test scripts.

This commit is contained in:
Aaron D. Lee
2026-01-01 14:04:55 -05:00
parent a001f227ec
commit 12929bf326
8 changed files with 1635 additions and 575 deletions

View File

@@ -0,0 +1,525 @@
# Stegasoo v3.2.0 Release Checklist
## Overview
This checklist covers comprehensive functionality testing for the v3.2.0 release, which introduces breaking changes from v3.1.x.
### Breaking Changes in v3.2.0
| Change | v3.1.x | v3.2.0 |
|--------|--------|--------|
| Passphrase model | 7 daily phrases (`day_phrase`) | Single `passphrase` |
| Date parameter | Required `date_str` | Removed |
| Default words | 3 | 4 |
| Format version | 3 | 4 |
| Backward compatible | N/A | ❌ Cannot decode v3.1.x images |
---
## 1. Core Library Tests
### 1.1 Key Generation (`src/stegasoo/keygen.py`)
- [ ] **generate_pin()** - Default 6 digits, no leading zero
- [ ] **generate_pin(length=9)** - Custom length works
- [ ] **generate_phrase(words=4)** - Default 4 words
- [ ] **generate_phrase(words=6)** - Custom word count
- [ ] **generate_credentials(use_pin=True)** - Returns single passphrase
- [ ] **generate_credentials(use_rsa=True)** - RSA key generation
- [ ] **generate_credentials(use_pin=False, use_rsa=False)** - Raises error
- [ ] **Credentials.passphrase** - Single string, not dict
- [ ] **Credentials.passphrase_entropy** - Correct entropy (4 words = 44 bits)
- [ ] **Credentials.total_entropy** - Sum is correct
### 1.2 Encoding (`src/stegasoo/steganography.py`)
- [ ] **encode() with passphrase** - New parameter name works
- [ ] **encode() without date_str** - No date parameter needed
- [ ] **HEADER_OVERHEAD = 65** - Correct constant
- [ ] **LSB mode** - Default, full color PNG output
- [ ] **DCT mode** - Frequency domain embedding
- [ ] **DCT + JPEG output** - Works correctly
- [ ] **DCT + color mode** - Preserves colors
- [ ] **Capacity calculation** - Uses 65-byte overhead
### 1.3 Decoding (`src/stegasoo/steganography.py`)
- [ ] **decode() with passphrase** - New parameter name works
- [ ] **decode() without date_str** - No date parameter needed
- [ ] **Auto mode detection** - LSB vs DCT automatic
- [ ] **Wrong passphrase** - Raises DecryptionError
- [ ] **Wrong PIN** - Raises DecryptionError
- [ ] **Wrong reference photo** - Raises DecryptionError
### 1.4 DCT Steganography (`src/stegasoo/dct_steganography.py`)
- [ ] **Y channel extraction** - Uses correct formula (not just R channel)
- [ ] **Color mode encoding** - YCbCr conversion works
- [ ] **Grayscale mode** - Converts to grayscale
- [ ] **JPEG output** - Quality 95, proper format
- [ ] **PNG output** - Lossless DCT output
### 1.5 Batch Processing (`src/stegasoo/batch.py`)
- [ ] **BatchCredentials.passphrase** - Single field, not dict
- [ ] **BatchCredentials.from_dict()** - Accepts both old and new format
- [ ] **batch_encode()** - Uses passphrase parameter
- [ ] **batch_decode()** - Uses passphrase parameter
### 1.6 Validation
- [ ] **validate_passphrase()** - New function works
- [ ] **validate_passphrase() warning** - Warns if < 4 words
- [ ] **validate_pin()** - 6-9 digits, no leading zero
- [ ] **validate_message()** - Non-empty, within size limits
---
## 2. CLI Frontend Tests (`frontends/cli/main.py`)
### 2.1 Generate Command
```bash
# Test default generation (4 words, PIN)
stegasoo generate --pin
# Test custom word count
stegasoo generate --pin --words 6
# Test RSA generation
stegasoo generate --rsa
# Test JSON output
stegasoo generate --pin --json
```
- [ ] Output shows single `PASSPHRASE:` not daily phrases
- [ ] Default is 4 words
- [ ] JSON has `passphrase` field, not `phrases` dict
- [ ] Entropy shows `passphrase_entropy`
### 2.2 Encode Command
```bash
# Test basic encode
stegasoo encode -r ref.jpg -c carrier.png \
-p "word1 word2 word3 word4" --pin 123456 \
-m "Secret message"
# Test DCT mode
stegasoo encode -r ref.jpg -c carrier.png \
-p "word1 word2 word3 word4" --pin 123456 \
-m "Secret" --mode dct
# Test DCT + JPEG
stegasoo encode -r ref.jpg -c carrier.png \
-p "word1 word2 word3 word4" --pin 123456 \
-m "Secret" --mode dct --dct-format jpeg
```
- [ ] `-p` / `--passphrase` parameter works
- [ ] No `--date` parameter exists
- [ ] LSB mode produces PNG
- [ ] DCT mode works
- [ ] DCT + JPEG output works
- [ ] Output filename has no date suffix
### 2.3 Decode Command
```bash
# Test basic decode
stegasoo decode -r ref.jpg -s stego.png \
-p "word1 word2 word3 word4" --pin 123456
# Test auto mode detection
stegasoo decode -r ref.jpg -s stego.png \
-p "word1 word2 word3 word4" --pin 123456 --mode auto
```
- [ ] `-p` / `--passphrase` parameter works
- [ ] No `--date` parameter exists
- [ ] Auto-detects LSB vs DCT
- [ ] Outputs decoded message
### 2.4 Other Commands
```bash
# Verify command
stegasoo verify -s stego.png
# Compare command
stegasoo compare original.png stego.png
# Modes command
stegasoo modes
# Capacity command
stegasoo capacity carrier.png
```
- [ ] All commands work without errors
- [ ] No references to "day phrase" or dates
---
## 3. API Frontend Tests (`frontends/api/main.py`)
### 3.1 Status Endpoint
```bash
curl http://localhost:8000/
```
- [ ] Returns `version: "3.2.0"`
- [ ] Includes `breaking_changes` object
- [ ] No `day_names` field
### 3.2 Generate Endpoint
```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"use_pin": true, "words_per_passphrase": 4}'
```
- [ ] Parameter is `words_per_passphrase` (not `words_per_phrase`)
- [ ] Response has `passphrase` string field
- [ ] Response has `phrases: null`
- [ ] Entropy field is `passphrase` not `phrase`
### 3.3 Encode Endpoint
```bash
curl -X POST http://localhost:8000/encode \
-H "Content-Type: application/json" \
-d '{
"message": "Secret",
"passphrase": "word1 word2 word3 word4",
"pin": "123456",
"reference_photo_base64": "...",
"carrier_image_base64": "..."
}'
```
- [ ] Parameter is `passphrase` (not `day_phrase`)
- [ ] No `date_str` parameter accepted
- [ ] Response has `date_used: null`
- [ ] Response has `day_of_week: null`
### 3.4 Decode Endpoint
```bash
curl -X POST http://localhost:8000/decode \
-H "Content-Type: application/json" \
-d '{
"passphrase": "word1 word2 word3 word4",
"pin": "123456",
"stego_image_base64": "...",
"reference_photo_base64": "..."
}'
```
- [ ] Parameter is `passphrase` (not `day_phrase`)
- [ ] No `date_str` parameter needed
- [ ] Auto-detects embedding mode
### 3.5 Multipart Endpoints
```bash
# Encode multipart
curl -X POST http://localhost:8000/encode/multipart \
-F "passphrase=word1 word2 word3 word4" \
-F "pin=123456" \
-F "message=Secret" \
-F "reference_photo=@ref.jpg" \
-F "carrier=@carrier.png"
# Decode multipart
curl -X POST http://localhost:8000/decode/multipart \
-F "passphrase=word1 word2 word3 word4" \
-F "pin=123456" \
-F "reference_photo=@ref.jpg" \
-F "stego_image=@stego.png"
```
- [ ] Form field is `passphrase` (not `day_phrase`)
- [ ] No `date_str` field
- [ ] Headers include `X-Stegasoo-Version: 3.2.0`
- [ ] No date headers in response
---
## 4. Web Frontend Tests (`frontends/web/app.py`)
### 4.1 Generate Page (`/generate`)
- [ ] Form field is `words_per_passphrase`
- [ ] Default slider value is 4
- [ ] Output shows single passphrase, not 7 daily phrases
- [ ] Memory aid works with single passphrase
- [ ] Entropy display shows `passphrase_entropy`
- [ ] v3.2.0 badge visible
### 4.2 Encode Page (`/encode`)
- [ ] Form field is `passphrase`
- [ ] No date selection field
- [ ] v3.2.0 badge on passphrase label
- [ ] Passphrase validation warning works (< 4 words)
- [ ] DCT mode options work
- [ ] Success result shows no date info
### 4.3 Decode Page (`/decode`)
- [ ] Form field is `passphrase`
- [ ] No date input field
- [ ] No date detection from filename JavaScript
- [ ] Troubleshooting mentions v3.2.0 compatibility
- [ ] Auto mode detection works
### 4.4 Other Pages
- [ ] **Home** (`/`) - Shows v3.2.0 badge, passphrase terminology
- [ ] **About** (`/about`) - Updated terminology, v3.2.0 features
- [ ] **Footer** - Says "Passphrase" not "Day-Phrase"
---
## 5. Integration Tests
### 5.1 Full Roundtrip Tests
```bash
# Generate → Encode → Decode (LSB)
stegasoo generate --pin > creds.json
stegasoo encode -r ref.jpg -c carrier.png -p "..." --pin 123456 -m "Test" -o stego.png
stegasoo decode -r ref.jpg -s stego.png -p "..." --pin 123456
# Generate → Encode → Decode (DCT)
stegasoo encode -r ref.jpg -c carrier.png -p "..." --pin 123456 -m "Test" --mode dct -o stego_dct.png
stegasoo decode -r ref.jpg -s stego_dct.png -p "..." --pin 123456
```
- [ ] LSB roundtrip works
- [ ] DCT roundtrip works
- [ ] DCT + JPEG roundtrip works
- [ ] File embedding roundtrip works
### 5.2 Cross-Frontend Tests
- [ ] Encode via CLI, decode via API
- [ ] Encode via API, decode via Web
- [ ] Encode via Web, decode via CLI
### 5.3 Error Handling
- [ ] Wrong passphrase shows clear error
- [ ] Wrong PIN shows clear error
- [ ] Wrong reference photo shows clear error
- [ ] Capacity exceeded shows clear error
- [ ] Invalid image shows clear error
---
## 6. Documentation Tests
### 6.1 CLI Documentation (`frontends/CLI.md`)
- [ ] "What's New in v3.2.0" section exists
- [ ] All examples use 4-word passphrases
- [ ] No `--date` parameter in examples
- [ ] Command reference is complete
- [ ] Migration notes for v3.1.x users
### 6.2 API Documentation (`frontends/API.md`)
- [ ] "What's New in v3.2.0" section exists
- [ ] All request examples use `passphrase`
- [ ] No `date_str` in request models
- [ ] Response models show `date_used: null`
- [ ] Code examples updated
### 6.3 Web UI Documentation (`frontends/WEB_UI.md`)
- [ ] "What's New in v3.2.0" section exists
- [ ] Workflow examples use passphrase
- [ ] No date selection in screenshots/descriptions
- [ ] Troubleshooting updated
---
## 7. Backward Compatibility Tests
### 7.1 v3.1.x Image Decoding
- [ ] Attempting to decode v3.1.x image with v3.2.0 fails gracefully
- [ ] Error message mentions version incompatibility
- [ ] Suggests using v3.1.x for old images
### 7.2 Migration Path
- [ ] `BatchCredentials.from_dict()` accepts old `day_phrase` key
- [ ] `generate_credentials_legacy()` available if needed
- [ ] Documentation explains migration steps
---
## 8. Unit Test Updates
### 8.1 Test Files to Update
- [ ] `tests/test_stegasoo.py` - Use `passphrase` parameter
- [ ] `tests/test_batch.py` - Use `passphrase` in credentials
- [ ] `tests/test_compression.py` - No changes needed (compression unchanged)
### 8.2 New Tests Needed
- [ ] Test single passphrase generation
- [ ] Test `passphrase_words` parameter
- [ ] Test `validate_passphrase()` function
- [ ] Test DCT Y channel extraction
- [ ] Test 65-byte header overhead
---
## 9. Release Artifacts
### 9.1 Version Bumps
- [ ] `src/stegasoo/constants.py` - `__version__ = "3.2.0"`
- [ ] `pyproject.toml` or `setup.py` - version updated
- [ ] `CHANGELOG.md` - v3.2.0 section added
### 9.2 Documentation
- [ ] `README.md` - Updated for v3.2.0
- [ ] `frontends/CLI.md` - Complete
- [ ] `frontends/API.md` - Complete
- [ ] `frontends/WEB_UI.md` - Complete
### 9.3 Git
- [ ] All changes committed
- [ ] Tag created: `v3.2.0`
- [ ] Release notes written
---
## 10. Quick Smoke Test Script
```bash
#!/bin/bash
# v3.2.0 Smoke Test
set -e
echo "=== Stegasoo v3.2.0 Smoke Test ==="
# Check version
echo "1. Checking version..."
python -c "import stegasoo; print(f'Version: {stegasoo.__version__}')"
# Generate credentials
echo "2. Generating credentials..."
python -c "
from stegasoo import generate_credentials
creds = generate_credentials(use_pin=True, passphrase_words=4)
print(f'Passphrase: {creds.passphrase}')
print(f'PIN: {creds.pin}')
print(f'Entropy: {creds.total_entropy} bits')
assert ' ' in creds.passphrase, 'Passphrase should have spaces'
assert len(creds.passphrase.split()) == 4, 'Should have 4 words'
print('✓ Credentials OK')
"
# Test encode/decode roundtrip
echo "3. Testing encode/decode roundtrip..."
python -c "
from stegasoo import encode, decode
from PIL import Image
import io
# Create test image
img = Image.new('RGB', (200, 200), color='blue')
buf = io.BytesIO()
img.save(buf, format='PNG')
test_image = buf.getvalue()
# Encode
result = encode(
message='Hello v3.2.0!',
reference_photo=test_image,
carrier_image=test_image,
passphrase='test phrase four words',
pin='123456'
)
print(f'Encoded: {result.filename}')
# Decode
decoded = decode(
stego_image=result.stego_image,
reference_photo=test_image,
passphrase='test phrase four words',
pin='123456'
)
assert decoded.message == 'Hello v3.2.0!', 'Message mismatch'
print(f'Decoded: {decoded.message}')
print('✓ Roundtrip OK')
"
# Test DCT mode
echo "4. Testing DCT mode..."
python -c "
from stegasoo import encode, decode, has_dct_support
if has_dct_support():
from PIL import Image
import io
img = Image.new('RGB', (200, 200), color='green')
buf = io.BytesIO()
img.save(buf, format='PNG')
test_image = buf.getvalue()
result = encode(
message='DCT test',
reference_photo=test_image,
carrier_image=test_image,
passphrase='dct test phrase here',
pin='123456',
embed_mode='dct'
)
decoded = decode(
stego_image=result.stego_image,
reference_photo=test_image,
passphrase='dct test phrase here',
pin='123456'
)
assert decoded.message == 'DCT test'
print('✓ DCT Mode OK')
else:
print('⚠ DCT mode not available (scipy not installed)')
"
echo ""
echo "=== All smoke tests passed! ==="
```
---
## Sign-Off
| Area | Tested By | Date | Status |
|------|-----------|------|--------|
| Core Library | | | ☐ |
| CLI Frontend | | | ☐ |
| API Frontend | | | ☐ |
| Web Frontend | | | ☐ |
| Documentation | | | ☐ |
| Integration | | | ☐ |
**Release Approved:**
**Released By:** _________________
**Release Date:** _________________

View File

@@ -1,5 +1,10 @@
"""
Tests for Stegasoo batch processing module.
Tests for Stegasoo batch processing module (v3.2.0).
Updated for v3.2.0:
- Uses 'passphrase' instead of 'phrase' in credentials dict
- No date_str parameter
- BatchCredentials.passphrase is a single string
"""
import pytest
@@ -13,6 +18,7 @@ from stegasoo.batch import (
BatchResult,
BatchItem,
BatchStatus,
BatchCredentials,
batch_capacity_check,
print_batch_result,
)
@@ -41,6 +47,15 @@ def sample_images(temp_dir):
return images
@pytest.fixture
def sample_credentials():
"""Create sample v3.2.0 credentials dict."""
return {
"passphrase": "test phrase four words", # v3.2.0: single passphrase
"pin": "123456"
}
class TestBatchItem:
"""Tests for BatchItem dataclass."""
@@ -90,6 +105,50 @@ class TestBatchResult:
assert result.duration == 10.0
class TestBatchCredentials:
"""Tests for BatchCredentials dataclass (v3.2.0)."""
def test_from_dict_new_format(self):
"""Should parse v3.2.0 format with 'passphrase' key."""
data = {
"passphrase": "test phrase four words",
"pin": "123456"
}
creds = BatchCredentials.from_dict(data)
assert creds.passphrase == "test phrase four words"
assert creds.pin == "123456"
def test_from_dict_legacy_format(self):
"""Should parse legacy format with 'day_phrase' key for migration."""
data = {
"day_phrase": "legacy phrase here", # Old key name
"pin": "123456"
}
creds = BatchCredentials.from_dict(data)
# Should accept old key and map to passphrase
assert creds.passphrase == "legacy phrase here"
assert creds.pin == "123456"
def test_to_dict(self):
"""Should serialize to v3.2.0 format."""
creds = BatchCredentials(
passphrase="test phrase four words",
pin="123456"
)
result = creds.to_dict()
assert result['passphrase'] == "test phrase four words"
assert result['pin'] == "123456"
assert 'day_phrase' not in result # Old key should not be present
def test_passphrase_is_string(self):
"""Passphrase should be a string, not a dict."""
creds = BatchCredentials(
passphrase="test phrase four words",
pin="123456"
)
assert isinstance(creds.passphrase, str)
class TestBatchProcessor:
"""Tests for BatchProcessor class."""
@@ -145,13 +204,13 @@ class TestBatchProcessor:
results = list(processor.find_images([temp_dir], recursive=True))
assert any(p.name == "nested.png" for p in results)
def test_batch_encode_requires_message_or_file(self, sample_images):
def test_batch_encode_requires_message_or_file(self, sample_images, sample_credentials):
"""Should raise if neither message nor file provided."""
processor = BatchProcessor()
with pytest.raises(ValueError, match="message or file_payload"):
processor.batch_encode(
images=sample_images,
credentials={"phrase": "test", "pin": "123456"},
credentials=sample_credentials,
)
def test_batch_encode_requires_credentials(self, sample_images):
@@ -163,14 +222,28 @@ class TestBatchProcessor:
message="test",
)
def test_batch_encode_creates_result(self, sample_images, temp_dir):
def test_batch_encode_accepts_passphrase_credentials(self, sample_images, temp_dir, sample_credentials):
"""Should accept v3.2.0 format credentials with passphrase."""
processor = BatchProcessor()
result = processor.batch_encode(
images=sample_images,
message="Test message",
output_dir=temp_dir / "output",
credentials=sample_credentials, # Uses 'passphrase' key
)
assert isinstance(result, BatchResult)
assert result.operation == "encode"
assert result.total == 3
def test_batch_encode_creates_result(self, sample_images, temp_dir, sample_credentials):
"""Should return BatchResult with correct structure."""
processor = BatchProcessor()
result = processor.batch_encode(
images=sample_images,
message="Test message",
output_dir=temp_dir / "output",
credentials={"phrase": "test phrase", "pin": "123456"},
credentials=sample_credentials,
)
assert isinstance(result, BatchResult)
@@ -184,19 +257,31 @@ class TestBatchProcessor:
with pytest.raises(ValueError, match="Credentials"):
processor.batch_decode(images=sample_images)
def test_batch_decode_creates_result(self, sample_images):
"""Should return BatchResult with correct structure."""
def test_batch_decode_accepts_passphrase_credentials(self, sample_images, sample_credentials):
"""Should accept v3.2.0 format credentials with passphrase."""
processor = BatchProcessor()
result = processor.batch_decode(
images=sample_images,
credentials={"phrase": "test phrase", "pin": "123456"},
credentials=sample_credentials, # Uses 'passphrase' key
)
assert isinstance(result, BatchResult)
assert result.operation == "decode"
assert result.total == 3
def test_progress_callback_called(self, sample_images):
def test_batch_decode_creates_result(self, sample_images, sample_credentials):
"""Should return BatchResult with correct structure."""
processor = BatchProcessor()
result = processor.batch_decode(
images=sample_images,
credentials=sample_credentials,
)
assert isinstance(result, BatchResult)
assert result.operation == "decode"
assert result.total == 3
def test_progress_callback_called(self, sample_images, sample_credentials):
"""Progress callback should be called for each item."""
processor = BatchProcessor()
callback = Mock()
@@ -204,13 +289,13 @@ class TestBatchProcessor:
processor.batch_encode(
images=sample_images,
message="Test",
credentials={"phrase": "test", "pin": "123456"},
credentials=sample_credentials,
progress_callback=callback,
)
assert callback.call_count == 3
def test_custom_encode_func(self, sample_images, temp_dir):
def test_custom_encode_func(self, sample_images, temp_dir, sample_credentials):
"""Should use custom encode function if provided."""
processor = BatchProcessor()
encode_mock = Mock()
@@ -219,7 +304,7 @@ class TestBatchProcessor:
images=sample_images,
message="Test",
output_dir=temp_dir / "output",
credentials={"phrase": "test", "pin": "123456"},
credentials=sample_credentials,
encode_func=encode_mock,
)
@@ -289,3 +374,36 @@ class TestPrintBatchResult:
captured = capsys.readouterr()
assert "test.png" in captured.out
class TestCredentialsMigration:
"""Tests for v3.1.x to v3.2.0 credentials migration."""
def test_old_phrase_key_accepted(self):
"""Old 'phrase' key should be accepted for migration."""
old_format = {
"phrase": "old style phrase",
"pin": "123456"
}
# Should not raise
creds = BatchCredentials.from_dict(old_format)
assert creds.passphrase == "old style phrase"
def test_old_day_phrase_key_accepted(self):
"""Old 'day_phrase' key should be accepted for migration."""
old_format = {
"day_phrase": "old day phrase",
"pin": "123456"
}
creds = BatchCredentials.from_dict(old_format)
assert creds.passphrase == "old day phrase"
def test_new_passphrase_key_preferred(self):
"""New 'passphrase' key should take precedence if both present."""
mixed_format = {
"passphrase": "new style passphrase",
"day_phrase": "old day phrase",
"pin": "123456"
}
creds = BatchCredentials.from_dict(mixed_format)
assert creds.passphrase == "new style passphrase"

View File

@@ -1,7 +1,12 @@
"""
Stegasoo Tests
Stegasoo Tests (v3.2.0)
Tests for key generation, validation, encoding/decoding, and output formats.
Updated for v3.2.0:
- Single passphrase instead of daily phrases
- No date_str parameter
- passphrase_words parameter (default 4)
"""
import pytest
@@ -15,13 +20,13 @@ from stegasoo import (
generate_credentials,
validate_pin,
validate_message,
validate_passphrase,
encode,
decode,
decode_text,
DAY_NAMES,
__version__,
)
from stegasoo.steganography import get_output_format
from stegasoo.steganography import get_output_format, HEADER_OVERHEAD
# =============================================================================
@@ -38,6 +43,16 @@ def png_image():
return buf.getvalue()
@pytest.fixture
def large_png_image():
"""Create a larger test PNG image for DCT mode."""
img = Image.new('RGB', (400, 400), color='blue')
buf = io.BytesIO()
img.save(buf, format='PNG')
buf.seek(0)
return buf.getvalue()
@pytest.fixture
def bmp_image():
"""Create a test BMP image."""
@@ -69,100 +84,162 @@ def gif_image():
# =============================================================================
# Key Generation Tests
# 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_phrase_default(self):
"""Default phrase should have 4 words (v3.2.0 change)."""
phrase = generate_phrase()
words = phrase.split()
assert len(words) == 3
assert len(words) == 4 # Changed from 3 in v3.1.x
def test_generate_phrase_lengths(self):
for length in [3, 4, 5, 6]:
def test_generate_phrase_custom_length(self):
"""Phrase generation should work for custom lengths."""
for length in [3, 4, 5, 6, 8, 12]:
phrase = generate_phrase(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
assert len(creds.phrases) == 7
# 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):
"""Test that generating credentials with neither PIN nor RSA fails."""
# Code raises AssertionError from debug.validate before ValueError
"""Generating with neither PIN nor RSA should fail."""
with pytest.raises((ValueError, AssertionError)):
generate_credentials(use_pin=False, use_rsa=False)
def test_entropy_calculation(self):
creds = generate_credentials(use_pin=True, 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
# 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 is valid (RSA key might be used instead)
"""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
# Note: validate_message doesn't have a max length check by default
# This test is removed as it doesn't match the actual validation behavior
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
# =============================================================================
@@ -170,53 +247,76 @@ class TestValidation:
# =============================================================================
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'
# =============================================================================
# Encode/Decode Tests
# Header Overhead Test (v3.2.0)
# =============================================================================
class TestConstants:
"""Tests for constants and configuration."""
def test_header_overhead_value(self):
"""Header overhead should be 65 bytes (v3.2.0 fix)."""
assert HEADER_OVERHEAD == 65
# =============================================================================
# Encode/Decode Tests (v3.2.0 Updated)
# =============================================================================
class TestEncodeDecode:
"""Tests for encoding and decoding functions."""
def test_encode_decode_roundtrip(self, png_image):
"""Test full encode/decode cycle."""
"""Full encode/decode cycle should work."""
message = "Secret message!"
phrase = "apple forest thunder"
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,
day_phrase=phrase,
passphrase=passphrase,
pin=pin
)
@@ -224,27 +324,27 @@ class TestEncodeDecode:
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,
day_phrase=phrase,
passphrase=passphrase,
pin=pin
)
# decode() returns DecodeResult, not string
assert decoded.message == message
def test_decode_text_roundtrip(self, png_image):
"""Test decode_text convenience function."""
"""decode_text convenience function should work."""
message = "Secret message!"
phrase = "apple forest thunder"
passphrase = "apple forest thunder mountain"
pin = "123456"
result = encode(
message=message,
reference_photo=png_image,
carrier_image=png_image,
day_phrase=phrase,
passphrase=passphrase,
pin=pin
)
@@ -252,56 +352,56 @@ class TestEncodeDecode:
decoded_text = decode_text(
stego_image=result.stego_image,
reference_photo=png_image,
day_phrase=phrase,
passphrase=passphrase,
pin=pin
)
assert decoded_text == message
def test_png_carrier_produces_png(self, png_image):
"""Test that PNG carrier produces PNG output."""
"""PNG carrier should produce PNG output."""
result = encode(
message="Test",
reference_photo=png_image,
carrier_image=png_image,
day_phrase="test phrase",
passphrase="test phrase here now",
pin="123456"
)
assert result.filename.endswith('.png')
def test_bmp_carrier_produces_bmp(self, bmp_image, png_image):
"""Test that BMP carrier produces BMP output."""
"""BMP carrier should produce BMP output."""
result = encode(
message="Test",
reference_photo=png_image,
carrier_image=bmp_image,
day_phrase="test phrase",
passphrase="test phrase here now",
pin="123456"
)
assert result.filename.endswith('.bmp')
def test_jpeg_carrier_produces_png(self, jpeg_image, png_image):
"""Test that JPEG carrier produces PNG output (lossless)."""
"""JPEG carrier should produce PNG output (lossless)."""
result = encode(
message="Test",
reference_photo=png_image,
carrier_image=jpeg_image,
day_phrase="test phrase",
passphrase="test phrase here now",
pin="123456"
)
assert result.filename.endswith('.png')
def test_bmp_roundtrip(self, bmp_image, png_image):
"""Test full encode/decode cycle with BMP."""
"""Full encode/decode cycle with BMP should work."""
message = "BMP test message!"
phrase = "test phrase words"
passphrase = "test phrase words here"
pin = "123456"
result = encode(
message=message,
reference_photo=png_image,
carrier_image=bmp_image,
day_phrase=phrase,
passphrase=passphrase,
pin=pin
)
assert result.filename.endswith('.bmp')
@@ -309,65 +409,202 @@ class TestEncodeDecode:
decoded = decode(
stego_image=result.stego_image,
reference_photo=png_image,
day_phrase=phrase,
passphrase=passphrase,
pin=pin
)
# decode() returns DecodeResult, not string
assert decoded.message == message
def test_wrong_pin_fails(self, png_image):
"""Test that wrong PIN fails to decode."""
"""Wrong PIN should fail to decode."""
result = encode(
message="Secret",
reference_photo=png_image,
carrier_image=png_image,
day_phrase="test phrase here",
passphrase="test phrase here now",
pin="123456"
)
# Wrong PIN means wrong pixel key, so extraction fails before decryption
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
decode(
stego_image=result.stego_image,
reference_photo=png_image,
day_phrase="test phrase here",
passphrase="test phrase here now",
pin="654321" # Wrong PIN
)
def test_wrong_phrase_fails(self, png_image):
"""Test that wrong phrase fails to decode."""
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,
day_phrase="correct phrase here",
passphrase="correct phrase here now",
pin="123456"
)
# Wrong phrase means wrong pixel key, so extraction fails before decryption
with pytest.raises((stegasoo.DecryptionError, stegasoo.ExtractionError)):
decode(
stego_image=result.stego_image,
reference_photo=png_image,
day_phrase="wrong phrase here",
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_has_no_date(self, png_image):
"""v3.2.0: Output filename should not have date suffix."""
result = encode(
message="Test",
reference_photo=png_image,
carrier_image=png_image,
passphrase="test phrase here now",
pin="123456"
)
# Filename should be like "a1b2c3d4.png", not "a1b2c3d4_20251227.png"
# Check that there's no underscore followed by 8 digits
import re
assert not re.search(r'_\d{8}\.', 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__')
# Version should be a valid semver string
parts = stegasoo.__version__.split('.')
assert len(parts) >= 2
assert all(p.isdigit() for p in parts[:2])
def test_day_names(self):
assert len(DAY_NAMES) == 7
assert 'Monday' in DAY_NAMES
assert 'Sunday' in DAY_NAMES
def test_version_is_3_2_0(self):
"""Version should be 3.2.0 or higher."""
parts = stegasoo.__version__.split('.')
major = int(parts[0])
minor = int(parts[1])
assert major >= 3
if major == 3:
assert minor >= 2
# =============================================================================
# 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
)