Files
tzst/tests/unit/test_security_and_errors.py
T
xixu-me cb5c198d16 Refactor tests and enhance coverage for conflict resolution and edge cases
- Removed outdated tests for missing lines in core.py.
- Added comprehensive tests for conflict resolution functionality, including various resolution strategies and edge cases.
- Improved error handling tests for archive creation and extraction processes.
- Enhanced unit tests for basic functionality and convenience functions with pytest markers.
- Introduced tests for unique filename generation and conflict handling scenarios.
- Added edge case tests for archive extraction and error conditions to improve overall test coverage.
2025-06-04 18:55:53 +08:00

435 lines
16 KiB
Python

"""Tests for security features and error handling."""
from unittest.mock import patch
import pytest
from tzst import TzstArchive, create_archive, extract_archive
from tzst import test_archive as tzst_test_archive
@pytest.mark.unit
class TestErrorHandling:
"""Test error handling."""
def test_invalid_archive_mode(self, sample_archive_path):
"""Test invalid archive mode."""
with pytest.raises(ValueError):
TzstArchive(sample_archive_path, "invalid")
def test_append_mode_not_supported(self, sample_archive_path):
"""Test that append mode raises NotImplementedError."""
with pytest.raises(NotImplementedError):
TzstArchive(sample_archive_path, "a")
def test_archive_not_open(self, sample_archive_path):
"""Test operations on non-open archive."""
archive = TzstArchive(sample_archive_path, "r")
with pytest.raises(RuntimeError):
archive.getnames()
def test_file_not_found(self, temp_dir):
"""Test handling of non-existent files."""
archive_path = temp_dir / "test.tzst"
fake_file = temp_dir / "fake.txt"
with pytest.raises(FileNotFoundError):
create_archive(archive_path, [fake_file])
@pytest.mark.unit
class TestSecurityFiltering:
"""Test security filtering mechanisms."""
def test_tar_filter_extraction(self, sample_files, temp_dir):
"""Test extraction with TAR security filter."""
file_paths = [f for f in sample_files if f.is_file()]
archive_path = temp_dir / "tar_filter_test.tzst"
# Create archive
create_archive(archive_path, file_paths)
# Extract with tar filter
extract_dir = temp_dir / "tar_filtered"
with patch("tzst.core.TzstArchive.extractall") as mock_extractall:
extract_archive(archive_path, extract_dir, filter="tar")
# Verify filter was passed
call_args = mock_extractall.call_args
assert call_args[1]["filter"] == "tar"
def test_data_filter_extraction(self, sample_files, temp_dir):
"""Test extraction with data security filter."""
file_paths = [f for f in sample_files if f.is_file()]
archive_path = temp_dir / "data_filter_test.tzst"
# Create archive
create_archive(archive_path, file_paths)
# Extract with data filter (default for security)
extract_dir = temp_dir / "data_filtered"
with patch("tzst.core.TzstArchive.extractall") as mock_extractall:
extract_archive(archive_path, extract_dir, filter="data")
# Verify filter was passed
call_args = mock_extractall.call_args
assert call_args[1]["filter"] == "data"
def test_invalid_filter_raises_error(self, sample_files, temp_dir):
"""Test that invalid filters raise appropriate errors."""
file_paths = [f for f in sample_files if f.is_file()]
archive_path = temp_dir / "invalid_filter_test.tzst"
# Create archive
create_archive(archive_path, file_paths)
# Try to extract with invalid filter
extract_dir = temp_dir / "invalid_filtered"
with pytest.raises(ValueError):
extract_archive(archive_path, extract_dir, filter="invalid")
class TestCompressionValidation:
"""Test compression level validation."""
def test_compression_level_boundary_values(self, sample_files, temp_dir):
"""Test boundary compression level values."""
file_paths = [f for f in sample_files if f.is_file()]
# Test level 1 (minimum valid)
archive_path_min = temp_dir / "min_compression.tzst"
create_archive(archive_path_min, file_paths, compression_level=1)
assert archive_path_min.exists()
assert tzst_test_archive(archive_path_min) is True
# Test level 22 (maximum valid)
archive_path_max = temp_dir / "max_compression.tzst"
create_archive(archive_path_max, file_paths, compression_level=22)
assert archive_path_max.exists()
assert tzst_test_archive(archive_path_max) is True
def test_invalid_compression_levels_raise_error(self, sample_files, temp_dir):
"""Test that invalid compression levels raise appropriate errors."""
file_paths = [f for f in sample_files if f.is_file()]
archive_path = temp_dir / "invalid_compression.tzst"
# Test invalid levels that should raise ValueError
for invalid_level in [0, 23, -1, 100]:
with pytest.raises(ValueError):
create_archive(
archive_path, file_paths, compression_level=invalid_level
)
def test_specific_compression_level_validation(self, temp_dir):
"""Test specific compression level validation to cover missing lines."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Test compression level too low (line 60)
with pytest.raises(ValueError, match="Invalid compression level '0'"):
TzstArchive(archive_path, mode="w", compression_level=0)
# Test compression level too high (line 66)
with pytest.raises(ValueError, match="Invalid compression level '23'"):
TzstArchive(archive_path, mode="w", compression_level=23)
finally:
archive_path.unlink(missing_ok=True)
def test_invalid_mode_validation(self, temp_dir):
"""Test invalid mode validation to cover missing lines."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Test invalid mode (line 54)
with pytest.raises(ValueError, match="Invalid mode 'x'"):
TzstArchive(archive_path, mode="x")
# Test invalid mode with additional characters
with pytest.raises(ValueError, match="Invalid mode 'rb'"):
TzstArchive(archive_path, mode="rb")
finally:
archive_path.unlink(missing_ok=True)
def test_runtime_errors_for_wrong_mode_operations(self, temp_dir):
"""Test RuntimeError for operations on wrong mode archives."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Create empty archive first
with TzstArchive(archive_path, mode="w") as archive:
pass
# Test read operations on write mode
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getmembers()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getnames()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.extractfile("test")
finally:
archive_path.unlink(missing_ok=True)
def test_operations_on_closed_archive(self, temp_dir):
"""Test operations on closed archive to cover missing lines."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Create and close archive
archive = TzstArchive(archive_path, mode="w")
archive.close()
# Test operations on closed archive
with pytest.raises(RuntimeError, match="Archive not open"):
archive.getmembers()
with pytest.raises(RuntimeError, match="Archive not open"):
archive.getnames()
with pytest.raises(RuntimeError, match="Archive not open"):
archive.extractfile("test")
finally:
archive_path.unlink(missing_ok=True)
def test_close_error_handling(self, temp_dir):
"""Test error handling in close method."""
from pathlib import Path
from unittest.mock import MagicMock
# Create a mock archive that will raise exceptions during close
archive = TzstArchive.__new__(TzstArchive)
archive.path = Path("test.tzst")
archive.mode = "w"
archive.compression_level = 3
# Create mock objects that raise exceptions when closed
mock_tarfile = MagicMock()
mock_tarfile.close.side_effect = Exception("Mock tarfile close error")
mock_stream = MagicMock()
mock_stream.close.side_effect = Exception("Mock stream close error")
mock_fileobj = MagicMock()
mock_fileobj.close.side_effect = Exception("Mock fileobj close error")
archive._tarfile = mock_tarfile
archive._compressed_stream = mock_stream
archive._fileobj = mock_fileobj
# This should not raise an exception despite the mock exceptions
archive.close()
# Verify all close methods were called
mock_tarfile.close.assert_called_once()
mock_stream.close.assert_called_once()
mock_fileobj.close.assert_called_once()
class TestSpecialFileTypes:
"""Test handling of special file types and edge cases."""
def test_empty_files(self, temp_dir):
"""Test archiving and extracting empty files."""
empty_file = temp_dir / "empty.txt"
empty_file.touch()
archive_path = temp_dir / "empty_file.tzst"
create_archive(archive_path, [empty_file])
assert archive_path.exists()
assert tzst_test_archive(archive_path) is True
# Test extraction
extract_dir = temp_dir / "empty_extracted"
extract_archive(archive_path, extract_dir)
extracted_file = extract_dir / "empty.txt"
assert extracted_file.exists()
assert extracted_file.stat().st_size == 0
def test_binary_files(self, temp_dir):
"""Test archiving binary files."""
binary_file = temp_dir / "binary.bin"
binary_content = bytes(range(256)) * 100 # 25.6KB of binary data
binary_file.write_bytes(binary_content)
archive_path = temp_dir / "binary_file.tzst"
create_archive(archive_path, [binary_file])
assert archive_path.exists()
assert tzst_test_archive(archive_path) is True
# Test extraction
extract_dir = temp_dir / "binary_extracted"
extract_archive(archive_path, extract_dir)
extracted_file = extract_dir / "binary.bin"
assert extracted_file.exists()
extracted_content = extracted_file.read_bytes()
assert extracted_content == binary_content
def test_files_with_special_characters(self, temp_dir):
"""Test files with special characters in names."""
special_chars_file = temp_dir / "file!@#$%^&()_+{}[]-',.txt"
special_chars_file.write_text("Special characters content")
archive_path = temp_dir / "special_chars.tzst"
create_archive(archive_path, [special_chars_file])
assert archive_path.exists()
assert tzst_test_archive(archive_path) is True
class TestAppendModeDocumentation:
"""Test that append mode provides helpful error messages."""
def test_append_mode_error_message(self, temp_dir):
"""Test that append mode raises informative error."""
archive_path = temp_dir / "append_test.tzst"
with pytest.raises(NotImplementedError) as exc_info:
TzstArchive(archive_path, "a")
error_msg = str(exc_info.value)
assert "append mode" in error_msg.lower()
assert "alternatives" in error_msg.lower() or "alternative" in error_msg.lower()
assert "decompressing" in error_msg.lower()
assert "recompressing" in error_msg.lower()
def test_append_mode_error_in_open(self, temp_dir):
"""Test append mode error when opening existing archive."""
archive_path = temp_dir / "append_open_test.tzst"
# Create an archive first
with TzstArchive(archive_path, "w"):
pass
# Try to open in append mode
with pytest.raises(NotImplementedError) as exc_info:
TzstArchive(archive_path, "a")
error_msg = str(exc_info.value)
assert (
"multiple archives" in error_msg.lower() or "recreate" in error_msg.lower()
)
class TestSpecificMissingLineCoverage:
"""Test specific missing lines from coverage report."""
def test_invalid_mode_validation_specific(self, temp_dir):
"""Test specific invalid mode validation to cover missing lines."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Test invalid mode (line 54)
with pytest.raises(ValueError, match="Invalid mode 'x'"):
TzstArchive(archive_path, mode="x")
# Test invalid mode with additional characters
with pytest.raises(ValueError, match="Invalid mode 'rb'"):
TzstArchive(archive_path, mode="rb")
finally:
archive_path.unlink(missing_ok=True)
def test_compression_level_validation_specific(self, temp_dir):
"""Test specific compression level validation to cover missing lines."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Test compression level too low (line 60)
with pytest.raises(ValueError, match="Invalid compression level '0'"):
TzstArchive(archive_path, mode="w", compression_level=0)
# Test compression level too high (line 66)
with pytest.raises(ValueError, match="Invalid compression level '23'"):
TzstArchive(archive_path, mode="w", compression_level=23)
finally:
archive_path.unlink(missing_ok=True)
def test_runtime_errors_for_wrong_mode_operations(self, temp_dir):
"""Test RuntimeError for operations on wrong mode archives."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Create empty archive first
with TzstArchive(archive_path, mode="w") as archive:
pass
# Test read operations on write mode
with TzstArchive(archive_path, mode="w") as archive:
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getmembers()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.getnames()
with pytest.raises(RuntimeError, match="Archive not open for reading"):
archive.extractfile("test")
finally:
archive_path.unlink(missing_ok=True)
def test_operations_on_closed_archive_specific(self, temp_dir):
"""Test operations on closed archive to cover missing lines."""
import tempfile
from pathlib import Path
with tempfile.NamedTemporaryFile(suffix=".tzst", delete=False) as f:
archive_path = Path(f.name)
try:
# Create and close archive
archive = TzstArchive(archive_path, mode="w")
archive.close()
# Test operations on closed archive
with pytest.raises(RuntimeError, match="Archive not open"):
archive.getmembers()
with pytest.raises(RuntimeError, match="Archive not open"):
archive.getnames()
with pytest.raises(RuntimeError, match="Archive not open"):
archive.extractfile("test")
finally:
archive_path.unlink(missing_ok=True)