Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,38 @@ crashlytics.properties
crashlytics-build.properties
fabric.properties
/slaver_singlefile.py

# Testing related
.pytest_cache/
.coverage
.coverage.*
htmlcov/
coverage.xml
*.cover
.hypothesis/
pytest_cache/
.tox/

# Claude settings
.claude/*

# Poetry
# Note: poetry.lock should be committed to version control
dist/
*.egg-info/

# Virtual environments
.venv/
venv/
ENV/
env/
.env

# IDE specific files
.idea/
.vscode/
*.swp
*.swo
*~
.project
.pydevproject
283 changes: 283 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
[tool.poetry]
name = "shootback"
version = "1.0.0"
description = "A reverse TCP tunnel with Python, using only standard library."
authors = ["Shootback Contributors"]
readme = "README.md"
license = "MIT"
package-mode = false

[tool.poetry.dependencies]
python = "^3.7"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.1"

[tool.poetry.scripts]
test = "pytest"
tests = "pytest"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--tb=short",
"--cov=.",
"--cov-report=html",
"--cov-report=xml",
"--cov-report=term-missing",
"--cov-fail-under=80"
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow tests"
]

[tool.coverage.run]
source = ["."]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/venv/*",
"*/env/*",
".venv/*",
"build_singlefile_slaver.py",
"setup.py",
"conftest.py"
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
fail_under = 80

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
134 changes: 134 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Shared pytest fixtures and configuration for shootback tests."""

import os
import sys
import tempfile
import shutil
import socket
import threading
import time
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))


@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files."""
temp_path = tempfile.mkdtemp()
yield temp_path
shutil.rmtree(temp_path, ignore_errors=True)


@pytest.fixture
def mock_socket():
"""Create a mock socket object."""
mock_sock = Mock(spec=socket.socket)
mock_sock.fileno.return_value = 1
mock_sock.recv.return_value = b"test data"
mock_sock.send.return_value = 9
return mock_sock


@pytest.fixture
def mock_ssl_context():
"""Create a mock SSL context."""
with patch('ssl.SSLContext') as mock_ctx:
yield mock_ctx


@pytest.fixture
def free_port():
"""Find and return a free port number."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
s.listen(1)
port = s.getsockname()[1]
return port


@pytest.fixture
def mock_config():
"""Create a mock configuration dictionary."""
return {
'master': {
'listen-on': '0.0.0.0:10000',
'communicate-url': 'https://localhost:10443',
'ssl': False,
'ssl-cert': None,
'ssl-key': None
},
'slaver': {
'communicate-url': 'https://localhost:10443',
'target': 'localhost:22',
'ssl': False,
'ssl-cert': None
}
}


@pytest.fixture
def mock_logger():
"""Create a mock logger."""
logger = Mock()
logger.info = Mock()
logger.error = Mock()
logger.warning = Mock()
logger.debug = Mock()
logger.exception = Mock()
return logger


@pytest.fixture
def thread_cleanup():
"""Ensure threads are cleaned up after tests."""
threads_before = threading.active_count()
yield
max_wait = 5
start_time = time.time()
while threading.active_count() > threads_before and time.time() - start_time < max_wait:
time.sleep(0.1)


@pytest.fixture
def mock_threading_event():
"""Create a mock threading Event."""
event = Mock(spec=threading.Event)
event.is_set.return_value = False
event.wait.return_value = True
return event


@pytest.fixture
def sample_binary_data():
"""Provide sample binary data for testing."""
return b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09'


@pytest.fixture
def sample_text_data():
"""Provide sample text data for testing."""
return "Hello, this is test data!"


@pytest.fixture
def mock_server_socket():
"""Create a mock server socket that accepts connections."""
server_sock = Mock(spec=socket.socket)
client_sock = Mock(spec=socket.socket)
client_sock.recv.return_value = b"client data"
server_sock.accept.return_value = (client_sock, ('127.0.0.1', 12345))
return server_sock


@pytest.fixture(autouse=True)
def reset_modules():
"""Reset singleton modules between tests."""
modules_to_reset = ['master', 'slaver', 'common_func']
for module in modules_to_reset:
if module in sys.modules:
del sys.modules[module]
yield
Empty file added tests/integration/__init__.py
Empty file.
67 changes: 67 additions & 0 deletions tests/test_setup_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Validation tests to ensure the testing infrastructure is properly set up."""

import pytest
import sys
import os


class TestSetupValidation:
"""Validate that the testing infrastructure is properly configured."""

def test_pytest_is_available(self):
"""Test that pytest is available and can be imported."""
import pytest
assert pytest is not None

def test_pytest_cov_is_available(self):
"""Test that pytest-cov is available for coverage reporting."""
import pytest_cov
assert pytest_cov is not None

def test_pytest_mock_is_available(self):
"""Test that pytest-mock is available for mocking."""
import pytest_mock
assert pytest_mock is not None

def test_project_modules_can_be_imported(self):
"""Test that main project modules can be imported."""
import common_func
import master
import slaver

assert common_func is not None
assert master is not None
assert slaver is not None

def test_conftest_fixtures_are_available(self, temp_dir, mock_socket, free_port):
"""Test that conftest fixtures are properly loaded."""
assert os.path.exists(temp_dir)
assert os.path.isdir(temp_dir)
assert hasattr(mock_socket, 'recv')
assert isinstance(free_port, int)
assert 1024 < free_port < 65536

@pytest.mark.unit
def test_unit_marker_works(self):
"""Test that the unit test marker is properly configured."""
assert True

@pytest.mark.integration
def test_integration_marker_works(self):
"""Test that the integration test marker is properly configured."""
assert True

@pytest.mark.slow
def test_slow_marker_works(self):
"""Test that the slow test marker is properly configured."""
assert True

def test_coverage_configuration(self):
"""Test that coverage is properly configured."""
import coverage
assert coverage is not None

def test_python_path_includes_project_root(self):
"""Test that the project root is in Python path."""
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
assert project_root in sys.path or os.path.normpath(project_root) in [os.path.normpath(p) for p in sys.path]
Empty file added tests/unit/__init__.py
Empty file.