"""Tests for Router lifecycle — state transitions, start, shutdown, status.""" import time import pytest from i2p_router.router import Router, RouterState from i2p_router.config import RouterConfig class TestInitialState: """Router initial state is STOPPED.""" def test_initial_state_is_stopped(self): router = Router() assert router.state == RouterState.STOPPED def test_initial_uptime_is_zero(self): router = Router() assert router.uptime_seconds == 0.0 def test_initial_config_is_default(self): router = Router() assert router.config.router_name == "i2p-python-router" assert router.config.listen_port == 9700 def test_custom_config(self): cfg = RouterConfig(router_name="test-router", listen_port=8080) router = Router(config=cfg) assert router.config.router_name == "test-router" assert router.config.listen_port == 8080 class TestStartTransition: """start() transitions to STARTING then RUNNING.""" def test_start_reaches_running(self): router = Router() router.start() assert router.state == RouterState.RUNNING def test_start_sets_uptime(self): router = Router() router.start() # Uptime should be very small but non-negative assert router.uptime_seconds >= 0.0 def test_start_creates_context(self): router = Router() router.start() assert router._context is not None def test_start_with_invalid_config_raises(self): cfg = RouterConfig(listen_port=-1) router = Router(config=cfg) with pytest.raises(ValueError): router.start() # State should remain STOPPED on validation failure assert router.state == RouterState.STOPPED class TestShutdownTransition: """shutdown() transitions to SHUTTING_DOWN then STOPPED.""" def test_shutdown_reaches_stopped(self): router = Router() router.start() assert router.state == RouterState.RUNNING router.shutdown() assert router.state == RouterState.STOPPED def test_shutdown_clears_uptime(self): router = Router() router.start() router.shutdown() assert router.uptime_seconds == 0.0 def test_shutdown_when_already_stopped_is_noop(self): router = Router() router.shutdown() # Should not raise assert router.state == RouterState.STOPPED class TestGetStatus: """get_status() returns correct state info.""" def test_status_when_stopped(self): router = Router() status = router.get_status() assert status["state"] == "stopped" assert status["uptime_seconds"] == 0.0 assert "config" in status def test_status_when_running(self): router = Router() router.start() status = router.get_status() assert status["state"] == "running" assert status["uptime_seconds"] >= 0.0 assert status["config"]["router_name"] == "i2p-python-router" router.shutdown() def test_status_config_reflects_custom_config(self): cfg = RouterConfig(router_name="custom", listen_port=7000) router = Router(config=cfg) status = router.get_status() assert status["config"]["router_name"] == "custom" assert status["config"]["listen_port"] == 7000 class TestIdempotentStart: """Double start() is idempotent.""" def test_double_start_stays_running(self): router = Router() router.start() router.start() # Should not raise assert router.state == RouterState.RUNNING router.shutdown() def test_double_start_preserves_context(self): router = Router() router.start() ctx1 = router._context router.start() ctx2 = router._context # Context should be the same object (not recreated) assert ctx1 is ctx2 router.shutdown() class TestRestart: """start after shutdown works (restart).""" def test_restart_reaches_running(self): router = Router() router.start() router.shutdown() assert router.state == RouterState.STOPPED router.start() assert router.state == RouterState.RUNNING router.shutdown() def test_restart_resets_uptime(self): router = Router() router.start() time.sleep(0.01) uptime1 = router.uptime_seconds router.shutdown() router.start() uptime2 = router.uptime_seconds # New uptime should be less than old (fresh start) assert uptime2 < uptime1 router.shutdown() def test_restart_creates_new_context(self): router = Router() router.start() ctx1 = router._context router.shutdown() router.start() ctx2 = router._context # After restart, new context is created assert ctx1 is not ctx2 router.shutdown()