194 lines
5.2 KiB
Python
194 lines
5.2 KiB
Python
"""
|
|
Shared pytest fixtures and utilities for IMAP migration tools tests.
|
|
"""
|
|
|
|
import imaplib
|
|
import os
|
|
import socket
|
|
import sys
|
|
import time
|
|
from contextlib import contextmanager
|
|
|
|
import pytest
|
|
|
|
# Ensure src/tools are in path
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src")))
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../tools")))
|
|
|
|
from mock_imap_server import start_server_thread
|
|
from mock_oauth_server import start_server_thread as start_oauth_server_thread
|
|
|
|
|
|
def get_free_port():
|
|
"""Get a free port on localhost."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("", 0))
|
|
return s.getsockname()[1]
|
|
|
|
|
|
def create_server_pair(src_data=None, dest_data=None):
|
|
"""Create a pair of mock IMAP servers with given initial data."""
|
|
p1 = get_free_port()
|
|
p2 = get_free_port()
|
|
while p2 == p1:
|
|
p2 = get_free_port()
|
|
|
|
src_s, p1_actual = start_server_thread(p1, src_data)
|
|
dest_s, p2_actual = start_server_thread(p2, dest_data)
|
|
time.sleep(0.3)
|
|
|
|
# We don't have direct access to thread object anymore as it is managed internally by TCPServer/ThreadingMixin or start_server_thread wrapper?
|
|
# Actually start_server_thread in tools/mock_imap_server.py now returns (server, port).
|
|
# The server object is a ThreadingMixIn so it handles per-request threads, but the main serve_forever loop is in a thread we created.
|
|
# But wait, my modified start_server_thread does: t.start(), return server, actual_port.
|
|
# It dropped returning 't'. I need to fix that or update consumers to not need 't'.
|
|
# Shutdown() stops serve_forever loop. join() is nice but maybe not strictly required if we trust shutdown.
|
|
# However, to avoid ResourceWarnings, we might want 't'.
|
|
|
|
return (None, src_s, p1_actual), (None, dest_s, p2_actual)
|
|
|
|
|
|
def shutdown_server_pair(src_tuple, dest_tuple):
|
|
"""Shutdown a pair of mock servers."""
|
|
src_t, src_s, _ = src_tuple
|
|
dest_t, dest_s, _ = dest_tuple
|
|
src_s.shutdown()
|
|
src_s.server_close() # Explicitly close socket
|
|
dest_s.shutdown()
|
|
dest_s.server_close()
|
|
if src_t:
|
|
src_t.join(timeout=2)
|
|
if dest_t:
|
|
dest_t.join(timeout=2)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_server_factory():
|
|
"""
|
|
Factory fixture that creates mock IMAP server pairs.
|
|
Automatically cleans up all servers after the test.
|
|
"""
|
|
servers = []
|
|
|
|
def _create(src_data=None, dest_data=None):
|
|
src_tuple, dest_tuple = create_server_pair(src_data, dest_data)
|
|
servers.append((src_tuple, dest_tuple))
|
|
src_server = src_tuple[1]
|
|
dest_server = dest_tuple[1]
|
|
src_port = src_tuple[2]
|
|
dest_port = dest_tuple[2]
|
|
return src_server, dest_server, src_port, dest_port
|
|
|
|
yield _create
|
|
|
|
for src_tuple, dest_tuple in servers:
|
|
shutdown_server_pair(src_tuple, dest_tuple)
|
|
|
|
|
|
@pytest.fixture
|
|
def single_mock_server():
|
|
"""
|
|
Creates a single mock IMAP server for testing scripts that only need one server.
|
|
"""
|
|
servers = []
|
|
|
|
def _create(initial_data=None):
|
|
port = get_free_port()
|
|
server, actual_port = start_server_thread(port, initial_data)
|
|
time.sleep(0.3)
|
|
servers.append(server)
|
|
return server, actual_port
|
|
|
|
yield _create
|
|
|
|
for server in servers:
|
|
server.shutdown()
|
|
server.server_close()
|
|
|
|
|
|
@contextmanager
|
|
def temp_env(env):
|
|
original = os.environ.copy()
|
|
os.environ.clear()
|
|
os.environ.update(env)
|
|
try:
|
|
yield
|
|
finally:
|
|
os.environ.clear()
|
|
os.environ.update(original)
|
|
|
|
|
|
@contextmanager
|
|
def temp_argv(args):
|
|
original = sys.argv[:]
|
|
sys.argv = list(args)
|
|
try:
|
|
yield
|
|
finally:
|
|
sys.argv = original
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clean_sys_argv():
|
|
"""Ensure sys.argv is clean for all tests."""
|
|
original = sys.argv[:]
|
|
sys.argv = ["test_script.py"]
|
|
yield
|
|
sys.argv = original
|
|
|
|
|
|
def make_mock_connection(src_port, dest_port, src_user="src_user", dest_user="dest_user"):
|
|
"""
|
|
Creates a mock connection function that routes to the correct server based on username.
|
|
"""
|
|
|
|
def mock_conn(host, user, pwd, oauth2_token=None):
|
|
if user == src_user:
|
|
port = src_port
|
|
elif user == dest_user:
|
|
port = dest_port
|
|
else:
|
|
raise ValueError(f"Unknown user: {user}")
|
|
c = imaplib.IMAP4("localhost", port)
|
|
c.login(user, pwd or "")
|
|
return c
|
|
|
|
return mock_conn
|
|
|
|
|
|
def make_single_mock_connection(port):
|
|
"""
|
|
Creates a mock connection function for a single server.
|
|
"""
|
|
|
|
def mock_conn(host, user, pwd, oauth2_token=None):
|
|
c = imaplib.IMAP4("localhost", port)
|
|
c.login(user, pwd or "")
|
|
return c
|
|
|
|
return mock_conn
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_oauth_server():
|
|
"""Starts a mock OAuth2 server for token and discovery endpoints."""
|
|
thread, server = start_oauth_server_thread(0)
|
|
host, port = server.server_address
|
|
base_url = f"http://{host}:{port}"
|
|
|
|
yield base_url
|
|
|
|
server.shutdown()
|
|
thread.join(timeout=2)
|
|
|
|
|
|
__all__ = [
|
|
"mock_server_factory",
|
|
"single_mock_server",
|
|
"make_mock_connection",
|
|
"make_single_mock_connection",
|
|
"mock_oauth_server",
|
|
"temp_env",
|
|
"temp_argv",
|
|
]
|