"""TLS transport tests. Generates self-signed certs for testing.""" import os import shutil import subprocess import sys import tempfile import pytest sys.path.insert(0, os.path.dirname(__file__)) from common import ( PROJECT_ROOT, BUILD_DIR, SERVER_CMD, TEST_DATA_DIR, ServerManager, run_client, generate_test_files, verify_transfer, clean_dir, make_result, get_dest_received_dir, _find_free_port, _wait_proc, ) SOURCE_DIR = os.path.join(TEST_DATA_DIR, "tls_source") DEST_DIR = os.path.join(TEST_DATA_DIR, "tls_dest") CERT_DIR = os.path.join(TEST_DATA_DIR, "tls_certs") def _generate_certs(cert_dir): """Generate a self-signed CA, server cert, and client cert for testing.""" os.makedirs(cert_dir, exist_ok=True) ca_key = os.path.join(cert_dir, "ca.key") ca_cert = os.path.join(cert_dir, "ca.pem") server_key = os.path.join(cert_dir, "server.key") server_cert = os.path.join(cert_dir, "server.pem") client_key = os.path.join(cert_dir, "client.key") client_cert = os.path.join(cert_dir, "client.pem") # CA key + cert subprocess.run([ "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", ca_key, "-out", ca_cert, "-days", "1", "-subj", "/CN=FastSync Test CA", ], check=True, capture_output=True) # Server key + CSR + cert (signed by CA) subprocess.run([ "openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", server_key, "-out", os.path.join(cert_dir, "server.csr"), "-subj", "/CN=localhost", ], check=True, capture_output=True) subprocess.run([ "openssl", "x509", "-req", "-in", os.path.join(cert_dir, "server.csr"), "-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial", "-out", server_cert, "-days", "1", ], check=True, capture_output=True) # Client key + CSR + cert (signed by CA) subprocess.run([ "openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", client_key, "-out", os.path.join(cert_dir, "client.csr"), "-subj", "/CN=fastsync-client", ], check=True, capture_output=True) subprocess.run([ "openssl", "x509", "-req", "-in", os.path.join(cert_dir, "client.csr"), "-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial", "-out", client_cert, "-days", "1", ], check=True, capture_output=True) return { "ca": ca_cert, "server_cert": server_cert, "server_key": server_key, "client_cert": client_cert, "client_key": client_key, } @pytest.fixture(scope="module") def certs(): """Generate test certificates once per test module.""" if os.path.exists(CERT_DIR): shutil.rmtree(CERT_DIR) c = _generate_certs(CERT_DIR) yield c shutil.rmtree(CERT_DIR, ignore_errors=True) @pytest.fixture(scope="module", autouse=True) def setup_test_data(): generate_test_files(SOURCE_DIR, full=False) clean_dir(DEST_DIR) yield shutil.rmtree(TEST_DATA_DIR, ignore_errors=True) class TestTLSBasic: def test_tls_server_client(self, certs): """Basic TLS: server with cert/key, client with cert/key + CA.""" clean_dir(DEST_DIR) with ServerManager() as server: server.start(extra_args=[ "--tls", "--cert", certs["server_cert"], "--key", certs["server_key"], ]) result, dur = run_client( SOURCE_DIR, DEST_DIR, flags=["--tls", "--cert", certs["client_cert"], "--key", certs["client_key"], "--ca", certs["ca"]], port=server.port, ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, received) assert not missing, f"Missing files: {missing}" assert not mismatches, f"Mismatched files: {mismatches}" def test_tls_with_compression(self, certs): """TLS + compression.""" clean_dir(DEST_DIR) with ServerManager() as server: server.start(extra_args=[ "--tls", "--cert", certs["server_cert"], "--key", certs["server_key"], ]) result, dur = run_client( SOURCE_DIR, DEST_DIR, flags=["-c", "--tls", "--cert", certs["client_cert"], "--key", certs["client_key"], "--ca", certs["ca"]], port=server.port, ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, received) assert not missing, f"Missing files: {missing}" assert not mismatches, f"Mismatched files: {mismatches}" def test_tls_with_multithreading(self, certs): """TLS + multithreading.""" clean_dir(DEST_DIR) with ServerManager() as server: server.start(extra_args=[ "--tls", "--cert", certs["server_cert"], "--key", certs["server_key"], ]) result, dur = run_client( SOURCE_DIR, DEST_DIR, flags=["-m", "--tls", "--cert", certs["client_cert"], "--key", certs["client_key"], "--ca", certs["ca"]], port=server.port, ) if result.returncode != 0: pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}") received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, received) assert not missing, f"Missing files: {missing}" assert not mismatches, f"Mismatched files: {mismatches}" class TestTLSErrorCases: def test_server_tls_missing_cert_key(self): """Server should fail if --tls is given without --cert/--key.""" proc = subprocess.Popen( SERVER_CMD + ["--tls"], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, ) _, stderr = proc.communicate(timeout=5) assert proc.returncode != 0, "Server should fail with --tls but no cert/key" def test_client_tls_missing_key(self): """Client should fail if --tls is given without --key.""" clean_dir(DEST_DIR) with ServerManager() as server: result, _ = run_client( SOURCE_DIR, DEST_DIR, flags=["--tls", "--cert", "/nonexistent/cert.pem"], port=server.port, ) assert result.returncode != 0, "Client should fail with --tls but no --key"