A Python port of the Invisible Internet Project (I2P)
at main 119 lines 3.8 kB view raw
1"""Tests for TunnelController lifecycle — TDD: tests before implementation.""" 2 3import asyncio 4 5import pytest 6 7from i2p_apps.i2ptunnel.config import TunnelDefinition, TunnelType 8from i2p_apps.i2ptunnel.controller import TunnelController, TunnelState 9 10 11def _make_def(**kw) -> TunnelDefinition: 12 defaults = dict(name="test", type=TunnelType.CLIENT, listen_port=0) 13 defaults.update(kw) 14 return TunnelDefinition(**defaults) 15 16 17class MockSession: 18 """Fake TunnelSession for controller tests.""" 19 20 def __init__(self): 21 self.closed = False 22 self.destination = "AAAA" * 129 # fake base64 23 24 async def close(self): 25 self.closed = True 26 27 async def connect(self, dest): 28 return None, None 29 30 async def accept(self): 31 return "", None, None 32 33 async def lookup(self, name): 34 return None 35 36 37class MockSessionFactory: 38 """Factory that returns MockSession instances.""" 39 40 def __init__(self): 41 self.sessions: list[MockSession] = [] 42 43 async def create(self, tunnel_def, priv_key=None): 44 s = MockSession() 45 self.sessions.append(s) 46 return s 47 48 async def release(self, session): 49 session.closed = True 50 51 52class TestTunnelState: 53 def test_enum_values(self): 54 assert TunnelState.STOPPED.value == "stopped" 55 assert TunnelState.STARTING.value == "starting" 56 assert TunnelState.RUNNING.value == "running" 57 assert TunnelState.STOPPING.value == "stopping" 58 assert TunnelState.DESTROYED.value == "destroyed" 59 60 61class TestTunnelControllerLifecycle: 62 def test_initial_state_is_stopped(self): 63 tc = TunnelController(_make_def(), MockSessionFactory()) 64 assert tc.state == TunnelState.STOPPED 65 66 def test_start_transitions_to_running(self): 67 tc = TunnelController(_make_def(), MockSessionFactory()) 68 asyncio.run(tc.start()) 69 assert tc.state == TunnelState.RUNNING 70 71 def test_stop_transitions_to_stopped(self): 72 tc = TunnelController(_make_def(), MockSessionFactory()) 73 asyncio.run(tc.start()) 74 asyncio.run(tc.stop()) 75 assert tc.state == TunnelState.STOPPED 76 77 def test_restart(self): 78 tc = TunnelController(_make_def(), MockSessionFactory()) 79 asyncio.run(tc.start()) 80 asyncio.run(tc.restart()) 81 assert tc.state == TunnelState.RUNNING 82 83 def test_destroy_prevents_restart(self): 84 tc = TunnelController(_make_def(), MockSessionFactory()) 85 tc.destroy() 86 assert tc.state == TunnelState.DESTROYED 87 with pytest.raises(RuntimeError, match="destroyed"): 88 asyncio.run(tc.start()) 89 90 def test_stop_when_stopped_is_noop(self): 91 tc = TunnelController(_make_def(), MockSessionFactory()) 92 asyncio.run(tc.stop()) # should not raise 93 assert tc.state == TunnelState.STOPPED 94 95 def test_start_when_running_is_noop(self): 96 tc = TunnelController(_make_def(), MockSessionFactory()) 97 asyncio.run(tc.start()) 98 asyncio.run(tc.start()) # second start is noop 99 assert tc.state == TunnelState.RUNNING 100 101 102class TestTunnelControllerSession: 103 def test_session_created_on_start(self): 104 factory = MockSessionFactory() 105 tc = TunnelController(_make_def(), factory) 106 asyncio.run(tc.start()) 107 assert len(factory.sessions) == 1 108 109 def test_session_released_on_stop(self): 110 factory = MockSessionFactory() 111 tc = TunnelController(_make_def(), factory) 112 asyncio.run(tc.start()) 113 asyncio.run(tc.stop()) 114 assert factory.sessions[0].closed is True 115 116 def test_config_accessible(self): 117 td = _make_def(name="mytest") 118 tc = TunnelController(td, MockSessionFactory()) 119 assert tc.config.name == "mytest"