Research-Stack/4-Infrastructure/NoDupeLabs/tests/security_audit/test_security_logic_coverage.py

477 lines
20 KiB
Python

"""Test Security Logic Module - Coverage Completion.
Tests to achieve 100% coverage for security_logic.py
"""
import os
import tempfile
from pathlib import Path
from unittest.mock import patch
import pytest
from nodupe.tools.security_audit.security_logic import (
Security,
SecurityError,
)
class TestSecuritySanitizePathEdgeCases:
"""Test sanitize_path edge cases for full coverage."""
def test_sanitize_path_with_path_object(self):
"""Test sanitizing a Path object."""
path_obj = Path("test/file.txt")
result = Security.sanitize_path(path_obj)
assert "test" in result
assert "file.txt" in result
def test_sanitize_path_null_byte(self):
"""Test sanitizing path with null byte."""
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_path("test\x00file.txt")
assert "null bytes" in str(exc_info.value)
def test_sanitize_path_parent_not_allowed(self):
"""Test sanitizing path with .. when not allowed."""
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_path("../test/file.txt", allow_parent=False)
assert "parent directory" in str(exc_info.value)
def test_sanitize_path_parent_allowed(self):
"""Test sanitizing path with .. when allowed."""
result = Security.sanitize_path("../test/file.txt", allow_parent=True)
assert result is not None
def test_sanitize_path_backslash_conversion(self):
"""Test sanitizing path with backslashes."""
result = Security.sanitize_path("test\\file.txt")
assert "\\" not in result
assert "/" in result or "test" in result
def test_sanitize_path_multiple_slashes(self):
"""Test sanitizing path with multiple slashes."""
result = Security.sanitize_path("test//file.txt")
assert "//" not in result
def test_sanitize_path_absolute_not_allowed(self):
"""Test sanitizing absolute path when not allowed."""
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_path("/etc/passwd", allow_absolute=False)
assert "Absolute paths" in str(exc_info.value)
def test_sanitize_path_resolve_fallback(self):
"""Test sanitizing path when resolve fails."""
with patch('pathlib.Path.resolve', side_effect=OSError("Cannot resolve")):
result = Security.sanitize_path("test/file.txt")
assert result is not None
def test_sanitize_path_general_exception(self):
"""Test sanitizing path with general exception."""
with patch('pathlib.Path.resolve', side_effect=RuntimeError("Unexpected error")):
result = Security.sanitize_path("test/file.txt")
assert result is not None
class TestSecurityValidatePathEdgeCases:
"""Test validate_path edge cases for full coverage."""
def test_validate_path_string_input(self):
"""Test validating string path."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = os.path.join(tmpdir, "test.txt")
Path(test_file).touch()
result = Security.validate_path(test_file, must_exist=True)
assert result is True
def test_validate_path_path_object_input(self):
"""Test validating Path object."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
result = Security.validate_path(test_file, must_exist=True)
assert result is True
def test_validate_path_cannot_resolve(self):
"""Test validating path that cannot be resolved."""
with patch('pathlib.Path.resolve', side_effect=OSError("Cannot resolve")):
with pytest.raises(SecurityError) as exc_info:
Security.validate_path("test/file.txt", must_exist=True)
assert "Cannot resolve" in str(exc_info.value)
def test_validate_path_allowed_parent_string(self):
"""Test validating path with allowed parent as string."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
result = Security.validate_path(
str(test_file),
must_exist=True,
allowed_parent=tmpdir
)
assert result is True
def test_validate_path_outside_allowed_parent(self):
"""Test validating path outside allowed parent."""
with tempfile.TemporaryDirectory() as tmpdir:
other_dir = tempfile.mkdtemp()
test_file = Path(other_dir) / "test.txt"
test_file.touch()
with pytest.raises(SecurityError) as exc_info:
Security.validate_path(
str(test_file),
must_exist=True,
allowed_parent=tmpdir
)
assert "outside allowed directory" in str(exc_info.value)
def test_validate_path_allowed_parent_cannot_resolve(self):
"""Test validating path when allowed parent cannot be resolved."""
with patch('pathlib.Path.resolve', side_effect=OSError("Cannot resolve")):
with pytest.raises(SecurityError) as exc_info:
Security.validate_path(
"test/file.txt",
must_exist=True,
allowed_parent="/nonexistent"
)
assert "Cannot resolve" in str(exc_info.value)
def test_validate_path_must_exist_not_exists(self):
"""Test validating path that must exist but doesn't."""
with pytest.raises(SecurityError) as exc_info:
Security.validate_path("/nonexistent/file.txt", must_exist=True)
assert "does not exist" in str(exc_info.value)
def test_validate_path_must_be_file_not_file(self):
"""Test validating path that must be file but is directory."""
with tempfile.TemporaryDirectory() as tmpdir:
with pytest.raises(SecurityError) as exc_info:
Security.validate_path(tmpdir, must_be_file=True)
assert "not a file" in str(exc_info.value)
def test_validate_path_must_be_dir_not_dir(self):
"""Test validating path that must be directory but is file."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
with pytest.raises(SecurityError) as exc_info:
Security.validate_path(test_file, must_be_dir=True)
assert "not a directory" in str(exc_info.value)
def test_validate_path_general_exception(self):
"""Test validating path with general exception."""
with patch('pathlib.Path.resolve', side_effect=Exception("Unexpected")):
with pytest.raises(SecurityError) as exc_info:
Security.validate_path("test/file.txt")
assert "validation failed" in str(exc_info.value)
class TestSecuritySanitizeFilenameEdgeCases:
"""Test sanitize_filename edge cases for full coverage."""
def test_sanitize_filename_empty(self):
"""Test sanitizing empty filename."""
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_filename("")
assert "Invalid filename" in str(exc_info.value)
def test_sanitize_filename_dot(self):
"""Test sanitizing dot filename."""
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_filename(".")
assert "Invalid filename" in str(exc_info.value)
def test_sanitize_filename_double_dot(self):
"""Test sanitizing double dot filename."""
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_filename("..")
assert "Invalid filename" in str(exc_info.value)
def test_sanitize_filename_invalid_chars(self):
"""Test sanitizing filename with invalid characters."""
result = Security.sanitize_filename("test<file>.txt")
assert "<" not in result
assert ">" not in result
def test_sanitize_filename_reserved_name(self):
"""Test sanitizing reserved Windows name."""
result = Security.sanitize_filename("CON.txt")
assert result.startswith("_")
def test_sanitize_filename_truncate_with_extension(self):
"""Test sanitizing filename that needs truncation with extension."""
long_name = "a" * 300 + ".txt"
result = Security.sanitize_filename(long_name, max_length=50)
assert len(result) <= 50
assert result.endswith(".txt")
def test_sanitize_filename_truncate_without_extension(self):
"""Test sanitizing filename that needs truncation without extension."""
long_name = "a" * 300
result = Security.sanitize_filename(long_name, max_length=50)
assert len(result) <= 50
def test_sanitize_filename_becomes_empty(self):
"""Test sanitizing filename that becomes empty."""
with patch('re.sub', return_value=" "):
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_filename("test.txt")
assert "empty" in str(exc_info.value)
def test_sanitize_filename_general_exception(self):
"""Test sanitizing filename with general exception."""
with patch('os.path.basename', side_effect=Exception("Unexpected")):
with pytest.raises(SecurityError) as exc_info:
Security.sanitize_filename("test.txt")
assert "sanitization failed" in str(exc_info.value)
class TestSecurityIsSafePath:
"""Test is_safe_path method."""
def test_is_safe_path_true(self):
"""Test safe path within base."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
result = Security.is_safe_path(str(test_file), tmpdir)
assert result is True
def test_is_safe_path_false(self):
"""Test unsafe path outside base."""
result = Security.is_safe_path("/etc/passwd", "/home")
assert result is False
def test_is_safe_path_exception(self):
"""Test is_safe_path with exception."""
with patch('pathlib.Path.resolve', side_effect=OSError("Cannot resolve")):
result = Security.is_safe_path("/test", "/base")
assert result is False
class TestSecurityCheckPermissionsEdgeCases:
"""Test check_permissions edge cases."""
def test_check_permissions_path_object(self):
"""Test checking permissions with Path object."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
result = Security.check_permissions(test_file, readable=True)
assert result is True
def test_check_permissions_not_exists(self):
"""Test checking permissions on nonexistent path."""
with pytest.raises(SecurityError) as exc_info:
Security.check_permissions("/nonexistent/file.txt", readable=True)
assert "does not exist" in str(exc_info.value)
def test_check_permissions_not_readable(self):
"""Test checking permissions when not readable."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
# Make file unreadable
os.chmod(test_file, 0o000)
try:
with pytest.raises(SecurityError) as exc_info:
Security.check_permissions(str(test_file), readable=True)
assert "not readable" in str(exc_info.value)
finally:
os.chmod(test_file, 0o644)
def test_check_permissions_not_writable(self):
"""Test checking permissions when not writable."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
# Make file read-only
os.chmod(test_file, 0o444)
try:
with pytest.raises(SecurityError) as exc_info:
Security.check_permissions(str(test_file), writable=True)
assert "not writable" in str(exc_info.value)
finally:
os.chmod(test_file, 0o644)
def test_check_permissions_not_executable(self):
"""Test checking permissions when not executable."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
with pytest.raises(SecurityError) as exc_info:
Security.check_permissions(str(test_file), executable=True)
assert "not executable" in str(exc_info.value)
def test_check_permissions_general_exception(self):
"""Test checking permissions with general exception."""
with patch('pathlib.Path.exists', side_effect=Exception("Unexpected")):
with pytest.raises(SecurityError) as exc_info:
Security.check_permissions("/test", readable=True)
assert "Permission check failed" in str(exc_info.value)
class TestSecurityIsSymlink:
"""Test is_symlink method."""
def test_is_symlink_true(self):
"""Test detecting symlink."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
link = Path(tmpdir) / "link.txt"
link.symlink_to(test_file)
result = Security.is_symlink(str(link))
assert result is True
def test_is_symlink_false(self):
"""Test non-symlink."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
result = Security.is_symlink(str(test_file))
assert result is False
def test_is_symlink_exception(self):
"""Test is_symlink with exception."""
with patch('pathlib.Path.is_symlink', side_effect=OSError("Error")):
result = Security.is_symlink("/test")
assert result is False
class TestSecurityResolveSymlink:
"""Test resolve_symlink method."""
def test_resolve_symlink_follow(self):
"""Test resolving symlink with follow."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
link = Path(tmpdir) / "link.txt"
link.symlink_to(test_file)
result = Security.resolve_symlink(str(link), follow_symlinks=True)
assert str(test_file) in result
def test_resolve_symlink_no_follow(self):
"""Test resolving symlink without follow."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
link = Path(tmpdir) / "link.txt"
link.symlink_to(test_file)
result = Security.resolve_symlink(str(link), follow_symlinks=False)
assert str(link) in result
def test_resolve_symlink_path_object(self):
"""Test resolving symlink with Path object."""
with tempfile.TemporaryDirectory() as tmpdir:
test_file = Path(tmpdir) / "test.txt"
test_file.touch()
link = Path(tmpdir) / "link.txt"
link.symlink_to(test_file)
result = Security.resolve_symlink(link)
assert str(test_file) in result
def test_resolve_symlink_exception(self):
"""Test resolving symlink with exception."""
with patch('pathlib.Path.resolve', side_effect=OSError("Cannot resolve")):
with pytest.raises(SecurityError) as exc_info:
Security.resolve_symlink("/test")
assert "Cannot resolve" in str(exc_info.value)
class TestSecurityValidateExtension:
"""Test validate_extension method."""
def test_validate_extension_allowed_with_dot(self):
"""Test validating allowed extension with dot."""
result = Security.validate_extension("test.txt", [".txt", ".pdf"])
assert result is True
def test_validate_extension_allowed_without_dot(self):
"""Test validating allowed extension without dot."""
result = Security.validate_extension("test.txt", ["txt", "pdf"])
assert result is True
def test_validate_extension_not_allowed(self):
"""Test validating disallowed extension."""
with pytest.raises(SecurityError) as exc_info:
Security.validate_extension("test.exe", [".txt", ".pdf"])
assert "not in allowed list" in str(exc_info.value)
def test_validate_extension_case_insensitive(self):
"""Test validating extension is case insensitive."""
result = Security.validate_extension("test.TXT", [".txt"])
assert result is True
def test_validate_extension_general_exception(self):
"""Test validating extension with general exception."""
with patch('pathlib.Path.suffix', side_effect=Exception("Error")):
with pytest.raises(SecurityError) as exc_info:
Security.validate_extension("test.txt", [".txt"])
assert "validation failed" in str(exc_info.value)
class TestSecurityGenerateSafeFilename:
"""Test generate_safe_filename method."""
def test_generate_safe_filename_basic(self):
"""Test generating safe filename basic."""
result = Security.generate_safe_filename("test", ".txt")
assert "test" in result
assert result.endswith(".txt")
def test_generate_safe_filename_extension_with_dot(self):
"""Test generating safe filename with extension already having dot."""
result = Security.generate_safe_filename("test", ".txt")
assert result.endswith(".txt")
def test_generate_safe_filename_extension_without_dot(self):
"""Test generating safe filename with extension without dot."""
result = Security.generate_safe_filename("test", "txt")
assert result.endswith(".txt")
def test_generate_safe_filename_with_timestamp(self, monkeypatch):
"""Test generating safe filename with timestamp."""
# Mock datetime to return fixed timestamp
class MockDatetime:
"""Mock datetime class for testing."""
@staticmethod
def now():
"""Return mock current time."""
class MockNow:
"""Mock datetime.now() result class."""
@staticmethod
def strftime(fmt):
"""Format timestamp as string."""
return "20240101_120000"
return MockNow()
with patch('nodupe.tools.security_audit.security_logic.datetime', MockDatetime):
result = Security.generate_safe_filename("test", ".txt", add_timestamp=True)
assert "test" in result
assert "20240101_120000" in result
def test_generate_safe_filename_general_exception(self):
"""Test generating safe filename with general exception."""
with patch('nodupe.tools.security_audit.security_logic.Security.sanitize_filename',
side_effect=Exception("Error")):
with pytest.raises(SecurityError) as exc_info:
Security.generate_safe_filename("test", ".txt")
assert "generation failed" in str(exc_info.value)
class TestSecurityError:
"""Test SecurityError exception."""
def test_security_error_creation(self):
"""Test creating SecurityError."""
error = SecurityError("Test error")
assert str(error) == "Test error"
def test_security_error_with_cause(self):
"""Test creating SecurityError with cause."""
original_error = ValueError("Original error")
error = SecurityError("Security error")
error.__cause__ = original_error
assert error.__cause__ is not None