diff --git a/.gitignore b/.gitignore index 94487b9..0ca1887 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,71 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class *.pyc + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.tox/ +.nox/ + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ +.env + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Claude settings +.claude/* + +# Poetry +poetry.lock + +# Logs +*.log + +# Local env files +.env.local +.env.*.local + +# Temporary files +tmp/ +temp/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0d48a34 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,86 @@ +[tool.poetry] +name = "redwarden" +version = "0.1.0" +description = "A security proxy tool for red team operations" +authors = ["RedWarden Team"] +readme = "README.md" +packages = [{include = "lib"}, {include = "plugins"}] + +[tool.poetry.dependencies] +python = "^3.8" +brotli = "*" +requests = "*" +PyYaml = "*" +sqlitedict = "*" +tornado = "*" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.1" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[tool.pytest.ini_options] +minversion = "7.0" +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--cov=lib", + "--cov=plugins", + "--cov-branch", + "--cov-report=term-missing:skip-covered", + "--cov-report=html:htmlcov", + "--cov-report=xml:coverage.xml", + "--cov-fail-under=0", # Set to 0 for infrastructure setup, change to 80 when writing actual tests +] +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 = ["lib", "plugins"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/migrations/*", + "*/.venv/*", + "*/venv/*", +] + +[tool.coverage.report] +precision = 2 +show_missing = true +skip_covered = false +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", +] + +[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..fa2f890 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,139 @@ +"""Shared pytest fixtures and configuration for all tests.""" + +import os +import sys +import tempfile +from pathlib import Path +from typing import Generator, Dict, Any +from unittest.mock import Mock, MagicMock + +import pytest +import yaml + +# Add project root to Python path +PROJECT_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Create a temporary directory for test files.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +@pytest.fixture +def mock_config() -> Dict[str, Any]: + """Provide a mock configuration dictionary.""" + return { + "proxy": { + "host": "127.0.0.1", + "port": 8080, + "ssl": False, + }, + "logging": { + "level": "DEBUG", + "file": None, + }, + "plugins": { + "enabled": ["malleable_redirector"], + "config": {} + } + } + + +@pytest.fixture +def mock_yaml_config(temp_dir: Path, mock_config: Dict[str, Any]) -> Path: + """Create a temporary YAML config file.""" + config_file = temp_dir / "test_config.yaml" + with open(config_file, 'w') as f: + yaml.dump(mock_config, f) + return config_file + + +@pytest.fixture +def mock_logger() -> Mock: + """Provide a mock logger object.""" + logger = Mock() + logger.debug = MagicMock() + logger.info = MagicMock() + logger.warning = MagicMock() + logger.error = MagicMock() + logger.critical = MagicMock() + return logger + + +@pytest.fixture +def mock_request() -> Mock: + """Provide a mock HTTP request object.""" + request = Mock() + request.method = "GET" + request.path = "/test" + request.headers = {"User-Agent": "TestAgent/1.0"} + request.body = b"" + request.remote_addr = ("127.0.0.1", 12345) + return request + + +@pytest.fixture +def mock_response() -> Mock: + """Provide a mock HTTP response object.""" + response = Mock() + response.status_code = 200 + response.headers = {"Content-Type": "text/html"} + response.body = b"Test" + return response + + +@pytest.fixture +def mock_plugin() -> Mock: + """Provide a mock plugin object.""" + plugin = Mock() + plugin.name = "test_plugin" + plugin.enabled = True + plugin.process_request = MagicMock(return_value=None) + plugin.process_response = MagicMock(return_value=None) + return plugin + + +@pytest.fixture(autouse=True) +def reset_environment(): + """Reset environment variables before each test.""" + original_env = os.environ.copy() + yield + os.environ.clear() + os.environ.update(original_env) + + +@pytest.fixture +def sample_malleable_profile(temp_dir: Path) -> Path: + """Create a sample Malleable C2 profile for testing.""" + profile_content = """ + set useragent "Mozilla/5.0 Test Browser"; + + http-get { + set uri "/test.php"; + + client { + header "Accept" "text/html"; + } + + server { + header "Server" "TestServer/1.0"; + } + } + """ + profile_file = temp_dir / "test.profile" + profile_file.write_text(profile_content) + return profile_file + + +@pytest.fixture +def isolated_imports(): + """Ensure imports are isolated between tests.""" + original_modules = sys.modules.copy() + yield + # Remove any modules imported during the test + for module in list(sys.modules.keys()): + if module not in original_modules: + del sys.modules[module] \ 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_infrastructure.py b/tests/test_infrastructure.py new file mode 100644 index 0000000..7d21f86 --- /dev/null +++ b/tests/test_infrastructure.py @@ -0,0 +1,145 @@ +"""Validation tests to verify the testing infrastructure is working correctly.""" + +import sys +from pathlib import Path + +import pytest + + +class TestInfrastructureSetup: + """Test class to validate the testing infrastructure.""" + + def test_pytest_is_working(self): + """Verify that pytest is running correctly.""" + assert True + + def test_project_imports(self): + """Verify that project modules can be imported.""" + # Test importing main modules + import lib + import plugins + + assert lib is not None + assert plugins is not None + + def test_fixtures_available(self, temp_dir, mock_config, mock_logger): + """Verify that custom fixtures are available and working.""" + assert temp_dir.exists() + assert isinstance(mock_config, dict) + assert hasattr(mock_logger, 'info') + + def test_coverage_is_tracked(self): + """Verify that coverage tracking is enabled.""" + # This test will be included in coverage reports + result = 1 + 1 + assert result == 2 + + @pytest.mark.unit + def test_unit_marker(self): + """Verify that the unit test marker works.""" + assert True + + @pytest.mark.integration + def test_integration_marker(self): + """Verify that the integration test marker works.""" + assert True + + @pytest.mark.slow + def test_slow_marker(self): + """Verify that the slow test marker works.""" + import time + # Not actually slow for validation purposes + assert True + + def test_temp_dir_fixture(self, temp_dir): + """Verify the temp_dir fixture creates a usable directory.""" + test_file = temp_dir / "test.txt" + test_file.write_text("test content") + + assert test_file.exists() + assert test_file.read_text() == "test content" + + def test_mock_yaml_config_fixture(self, mock_yaml_config): + """Verify the mock_yaml_config fixture creates a valid YAML file.""" + import yaml + + assert mock_yaml_config.exists() + + with open(mock_yaml_config, 'r') as f: + config = yaml.safe_load(f) + + assert 'proxy' in config + assert config['proxy']['port'] == 8080 + + def test_mock_request_fixture(self, mock_request): + """Verify the mock_request fixture provides expected attributes.""" + assert mock_request.method == "GET" + assert mock_request.path == "/test" + assert "User-Agent" in mock_request.headers + + def test_mock_response_fixture(self, mock_response): + """Verify the mock_response fixture provides expected attributes.""" + assert mock_response.status_code == 200 + assert "Content-Type" in mock_response.headers + assert b"" in mock_response.body + + def test_environment_isolation(self): + """Verify that environment variables are isolated between tests.""" + import os + + # Set a test variable + os.environ['TEST_VAR'] = 'test_value' + assert os.environ.get('TEST_VAR') == 'test_value' + + # The reset_environment fixture should clean this up after the test + + def test_python_path_includes_project_root(self): + """Verify that the project root is in the Python path.""" + project_root = str(Path(__file__).parent.parent) + assert project_root in sys.path + + +class TestProjectStructure: + """Test class to validate the project structure is set up correctly.""" + + def test_test_directories_exist(self): + """Verify that all test directories exist.""" + test_root = Path(__file__).parent + + assert test_root.exists() + assert (test_root / "unit").exists() + assert (test_root / "integration").exists() + assert (test_root / "__init__.py").exists() + assert (test_root / "unit" / "__init__.py").exists() + assert (test_root / "integration" / "__init__.py").exists() + + def test_conftest_exists(self): + """Verify that conftest.py exists and is importable.""" + conftest_path = Path(__file__).parent / "conftest.py" + assert conftest_path.exists() + + # Verify it can be imported (pytest does this automatically) + import tests.conftest + assert tests.conftest is not None + + def test_pyproject_toml_exists(self): + """Verify that pyproject.toml exists with proper configuration.""" + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + assert pyproject_path.exists() + + import tomllib + + with open(pyproject_path, 'rb') as f: + config = tomllib.load(f) + + # Check Poetry configuration + assert 'tool' in config + assert 'poetry' in config['tool'] + assert 'pytest' in config['tool'] + assert 'coverage' in config['tool'] + + # Check test dependencies + dev_deps = config['tool']['poetry']['group']['dev']['dependencies'] + assert 'pytest' in dev_deps + assert 'pytest-cov' in dev_deps + assert 'pytest-mock' in dev_deps \ 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