- 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.
435 lines
16 KiB
Python
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)
|