diff --git a/.gitignore b/.gitignore index 397b4a7..5dc29e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,45 @@ *.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +*.py[cod] +__pycache__/ +.tox/ +.nox/ + +# Claude +.claude/* + +# Virtual environments +venv/ +.venv/ +env/ +ENV/ +.env + +# Poetry +poetry.lock + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +build/ +dist/ +*.egg-info/ +.eggs/ +*.egg + +# Pipenv (keeping lock file) +# Pipfile.lock is NOT ignored diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9d7f32a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,84 @@ +[tool.poetry] +name = "dnschef" +version = "0.4" +description = "A highly configurable DNS Proxy for Penetration Testers and Malware Analysts" +authors = ["Peter Kacherginsky", "Marcello Salvati"] +readme = "README" +homepage = "http://thesprawl.org/projects/dnschef/" +license = "BSD-3-Clause" + +[tool.poetry.dependencies] +python = "^3.7" +dnslib = "0.9.10" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.0" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--cov=dnschef", + "--cov-branch", + "--cov-report=term-missing:skip-covered", + "--cov-report=html", + "--cov-report=xml", + "--cov-fail-under=80" +] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Tests that take a long time to run" +] + +[tool.coverage.run] +source = ["dnschef"] +branch = true +omit = [ + "*/tests/*", + "*/test_*", + "*/__pycache__/*", + "*/site-packages/*", + "*/venv/*", + "*/.venv/*" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod" +] +show_missing = true +precision = 2 +fail_under = 80 + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..aee25d5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,163 @@ +import pytest +import tempfile +import shutil +import os +from pathlib import Path +from unittest.mock import Mock, MagicMock + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for test files.""" + temp_path = tempfile.mkdtemp() + yield Path(temp_path) + shutil.rmtree(temp_path) + + +@pytest.fixture +def temp_file(temp_dir): + """Create a temporary file within the temp directory.""" + def _temp_file(filename="test_file.txt", content=""): + file_path = temp_dir / filename + file_path.write_text(content) + return file_path + return _temp_file + + +@pytest.fixture +def mock_config(): + """Create a mock configuration object.""" + config = Mock() + config.interface = "127.0.0.1" + config.port = 53 + config.tcp = False + config.ipv6 = False + config.file = None + config.fakedomains = {} + config.truedomains = {} + config.nameservers = ["8.8.8.8", "8.8.4.4"] + return config + + +@pytest.fixture +def mock_dns_query(): + """Create a mock DNS query object.""" + query = Mock() + query.questions = [Mock()] + query.questions[0].qname = "example.com" + query.questions[0].qtype = 1 # A record + query.questions[0].qclass = 1 # IN class + return query + + +@pytest.fixture +def mock_socket(): + """Create a mock socket object.""" + socket_mock = MagicMock() + socket_mock.sendto = Mock(return_value=None) + socket_mock.recvfrom = Mock(return_value=(b"mock_response", ("127.0.0.1", 53))) + socket_mock.close = Mock(return_value=None) + return socket_mock + + +@pytest.fixture +def mock_logger(): + """Create a mock logger object.""" + logger = Mock() + logger.info = Mock() + logger.debug = Mock() + logger.warning = Mock() + logger.error = Mock() + logger.critical = Mock() + return logger + + +@pytest.fixture +def dns_server_config(): + """Create a basic DNS server configuration dictionary.""" + return { + "interface": "0.0.0.0", + "port": 53, + "tcp": False, + "ipv6": False, + "nameservers": ["8.8.8.8"], + "fakedomains": { + "example.com": "192.168.1.100" + }, + "truedomains": {} + } + + +@pytest.fixture +def sample_dns_records(): + """Provide sample DNS records for testing.""" + return { + "A": { + "example.com": "192.168.1.1", + "test.com": "10.0.0.1" + }, + "AAAA": { + "example.com": "2001:db8::1", + "test.com": "2001:db8::2" + }, + "MX": { + "example.com": "10 mail.example.com", + "test.com": "20 mail.test.com" + }, + "CNAME": { + "www.example.com": "example.com", + "alias.test.com": "test.com" + }, + "NS": { + "example.com": "ns1.example.com", + "test.com": "ns1.test.com" + }, + "TXT": { + "example.com": "v=spf1 include:_spf.example.com ~all", + "test.com": "test-txt-record" + } + } + + +@pytest.fixture(autouse=True) +def reset_environment(): + """Reset environment variables and state before each test.""" + original_env = os.environ.copy() + yield + os.environ.clear() + os.environ.update(original_env) + + +@pytest.fixture +def capture_stdout(monkeypatch): + """Capture stdout for testing print statements.""" + import io + import sys + + captured_output = io.StringIO() + monkeypatch.setattr(sys, 'stdout', captured_output) + yield captured_output + monkeypatch.undo() + + +@pytest.fixture +def mock_args(): + """Create mock command line arguments.""" + args = Mock() + args.interface = "127.0.0.1" + args.port = 53 + args.tcp = False + args.ipv6 = False + args.file = None + args.fakeip = None + args.fakeipv6 = None + args.fakedomains = None + args.fakealias = None + args.fakens = None + args.fakemx = None + args.faketxt = None + args.fakecname = None + args.truedomains = None + args.nameservers = "8.8.8.8" + args.q = False + return args \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_setup_validation.py b/tests/test_setup_validation.py new file mode 100644 index 0000000..40b4d68 --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,97 @@ +import pytest +import sys +from pathlib import Path + + +class TestSetupValidation: + """Validation tests to ensure the testing infrastructure is properly configured.""" + + def test_pytest_is_available(self): + """Test that pytest is importable and available.""" + import pytest + assert pytest.__version__ + + def test_pytest_cov_is_available(self): + """Test that pytest-cov plugin is available.""" + import pytest_cov + assert pytest_cov + + def test_pytest_mock_is_available(self): + """Test that pytest-mock plugin is available.""" + import pytest_mock + assert pytest_mock + + def test_project_root_in_path(self): + """Test that the project root is in Python path.""" + project_root = Path(__file__).parent.parent + assert str(project_root) in sys.path or str(project_root.absolute()) in sys.path + + def test_can_import_dnschef(self): + """Test that the main module can be imported.""" + try: + import dnschef + assert dnschef.DNSCHEF_VERSION + except ImportError: + # Add parent directory to path and try again + sys.path.insert(0, str(Path(__file__).parent.parent)) + import dnschef + assert dnschef.DNSCHEF_VERSION + + def test_fixtures_are_available(self, temp_dir, mock_config, mock_logger): + """Test that conftest fixtures are available and working.""" + assert temp_dir.exists() + assert temp_dir.is_dir() + + assert mock_config.interface == "127.0.0.1" + assert mock_config.port == 53 + + assert hasattr(mock_logger, 'info') + assert hasattr(mock_logger, 'error') + + def test_markers_are_defined(self, pytestconfig): + """Test that custom pytest markers are properly defined.""" + markers = pytestconfig.getini('markers') + marker_names = [m.split(':')[0].strip() for m in markers] + assert 'unit' in marker_names + assert 'integration' in marker_names + assert 'slow' in marker_names + + @pytest.mark.unit + def test_unit_marker_works(self): + """Test that the unit test marker can be used.""" + assert True + + @pytest.mark.integration + def test_integration_marker_works(self): + """Test that the integration test marker can be used.""" + assert True + + @pytest.mark.slow + def test_slow_marker_works(self): + """Test that the slow test marker can be used.""" + assert True + + def test_temp_file_fixture(self, temp_file): + """Test that the temp_file fixture works correctly.""" + test_file = temp_file("test.txt", "Hello, World!") + assert test_file.exists() + assert test_file.read_text() == "Hello, World!" + + def test_mock_dns_query_fixture(self, mock_dns_query): + """Test that the mock DNS query fixture is properly configured.""" + assert mock_dns_query.questions[0].qname == "example.com" + assert mock_dns_query.questions[0].qtype == 1 + assert mock_dns_query.questions[0].qclass == 1 + + def test_sample_dns_records_fixture(self, sample_dns_records): + """Test that the sample DNS records fixture provides expected data.""" + assert "A" in sample_dns_records + assert "AAAA" in sample_dns_records + assert "MX" in sample_dns_records + assert sample_dns_records["A"]["example.com"] == "192.168.1.1" + + def test_coverage_is_enabled(self): + """Test that coverage tracking is properly configured.""" + # This test will pass if pytest-cov is properly configured + # The actual coverage percentage will be checked by pytest-cov + assert True \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29