"""Tests for TunnelController lifecycle — TDD: tests before implementation.""" import asyncio import pytest from i2p_apps.i2ptunnel.config import TunnelDefinition, TunnelType from i2p_apps.i2ptunnel.controller import TunnelController, TunnelState def _make_def(**kw) -> TunnelDefinition: defaults = dict(name="test", type=TunnelType.CLIENT, listen_port=0) defaults.update(kw) return TunnelDefinition(**defaults) class MockSession: """Fake TunnelSession for controller tests.""" def __init__(self): self.closed = False self.destination = "AAAA" * 129 # fake base64 async def close(self): self.closed = True async def connect(self, dest): return None, None async def accept(self): return "", None, None async def lookup(self, name): return None class MockSessionFactory: """Factory that returns MockSession instances.""" def __init__(self): self.sessions: list[MockSession] = [] async def create(self, tunnel_def, priv_key=None): s = MockSession() self.sessions.append(s) return s async def release(self, session): session.closed = True class TestTunnelState: def test_enum_values(self): assert TunnelState.STOPPED.value == "stopped" assert TunnelState.STARTING.value == "starting" assert TunnelState.RUNNING.value == "running" assert TunnelState.STOPPING.value == "stopping" assert TunnelState.DESTROYED.value == "destroyed" class TestTunnelControllerLifecycle: def test_initial_state_is_stopped(self): tc = TunnelController(_make_def(), MockSessionFactory()) assert tc.state == TunnelState.STOPPED def test_start_transitions_to_running(self): tc = TunnelController(_make_def(), MockSessionFactory()) asyncio.run(tc.start()) assert tc.state == TunnelState.RUNNING def test_stop_transitions_to_stopped(self): tc = TunnelController(_make_def(), MockSessionFactory()) asyncio.run(tc.start()) asyncio.run(tc.stop()) assert tc.state == TunnelState.STOPPED def test_restart(self): tc = TunnelController(_make_def(), MockSessionFactory()) asyncio.run(tc.start()) asyncio.run(tc.restart()) assert tc.state == TunnelState.RUNNING def test_destroy_prevents_restart(self): tc = TunnelController(_make_def(), MockSessionFactory()) tc.destroy() assert tc.state == TunnelState.DESTROYED with pytest.raises(RuntimeError, match="destroyed"): asyncio.run(tc.start()) def test_stop_when_stopped_is_noop(self): tc = TunnelController(_make_def(), MockSessionFactory()) asyncio.run(tc.stop()) # should not raise assert tc.state == TunnelState.STOPPED def test_start_when_running_is_noop(self): tc = TunnelController(_make_def(), MockSessionFactory()) asyncio.run(tc.start()) asyncio.run(tc.start()) # second start is noop assert tc.state == TunnelState.RUNNING class TestTunnelControllerSession: def test_session_created_on_start(self): factory = MockSessionFactory() tc = TunnelController(_make_def(), factory) asyncio.run(tc.start()) assert len(factory.sessions) == 1 def test_session_released_on_stop(self): factory = MockSessionFactory() tc = TunnelController(_make_def(), factory) asyncio.run(tc.start()) asyncio.run(tc.stop()) assert factory.sessions[0].closed is True def test_config_accessible(self): td = _make_def(name="mytest") tc = TunnelController(td, MockSessionFactory()) assert tc.config.name == "mytest"