"""Tests for I2PTunnel config parser — TDD: tests before implementation.""" from pathlib import Path import pytest from i2p_apps.i2ptunnel.config import TunnelConfigParser, TunnelDefinition, TunnelType class TestTunnelType: """TunnelType enum covers all 12 Java I2P tunnel types.""" def test_all_types_present(self): expected = { "httpclient", "connectclient", "client", "server", "httpserver", "ircclient", "ircserver", "sockstunnel", "socksirctunnel", "httpbidirserver", "streamrclient", "streamrserver", } actual = {t.value for t in TunnelType} assert actual == expected def test_from_string(self): assert TunnelType("httpclient") == TunnelType.HTTPCLIENT assert TunnelType("server") == TunnelType.SERVER class TestTunnelDefinition: """TunnelDefinition dataclass.""" def test_defaults(self): td = TunnelDefinition( name="test", type=TunnelType.CLIENT, listen_port=1234, ) assert td.name == "test" assert td.interface == "127.0.0.1" assert td.start_on_load is False assert td.shared_client is False assert td.options == {} assert td.proxy_list == [] def test_is_client(self): td = TunnelDefinition(name="t", type=TunnelType.HTTPCLIENT, listen_port=4444) assert td.is_client is True def test_is_server(self): td = TunnelDefinition(name="t", type=TunnelType.SERVER, listen_port=0) assert td.is_server is True class TestConfigParserLoad: """Parse i2ptunnel.config Java Properties format.""" def test_load_sample_config(self, sample_config_path: Path): tunnels = TunnelConfigParser.load(sample_config_path) assert len(tunnels) == 7 def test_http_proxy_parsed(self, sample_config_path: Path): tunnels = TunnelConfigParser.load(sample_config_path) http = tunnels[0] assert http.name == "I2P HTTP Proxy" assert http.type == TunnelType.HTTPCLIENT assert http.interface == "127.0.0.1" assert http.listen_port == 4444 assert http.shared_client is True assert http.start_on_load is True assert http.proxy_list == ["false.i2p"] def test_options_extracted(self, sample_config_path: Path): tunnels = TunnelConfigParser.load(sample_config_path) http = tunnels[0] assert http.options["inbound.length"] == "3" assert http.options["outbound.length"] == "3" def test_server_tunnel_parsed(self, sample_config_path: Path): tunnels = TunnelConfigParser.load(sample_config_path) server = tunnels[3] assert server.name == "I2P Webserver" assert server.type == TunnelType.HTTPSERVER assert server.target_host == "127.0.0.1" assert server.target_port == 7658 assert server.spoofed_host == "mysite.i2p" assert server.priv_key_file == "eepsite/eepPriv.dat" def test_client_tunnel_destination(self, sample_config_path: Path): tunnels = TunnelConfigParser.load(sample_config_path) smtp = tunnels[4] assert smtp.type == TunnelType.CLIENT assert smtp.target_destination == "smtp.postman.i2p" assert smtp.listen_port == 7659 def test_start_on_load_false(self, sample_config_path: Path): tunnels = TunnelConfigParser.load(sample_config_path) irc = tunnels[2] assert irc.start_on_load is False def test_missing_properties_use_defaults(self, tmp_path: Path): p = tmp_path / "minimal.config" p.write_text("tunnel.0.name=Bare\ntunnel.0.type=client\n") tunnels = TunnelConfigParser.load(p) assert len(tunnels) == 1 assert tunnels[0].listen_port == 0 assert tunnels[0].interface == "127.0.0.1" def test_comments_and_blank_lines_skipped(self, tmp_path: Path): p = tmp_path / "comments.config" p.write_text("# comment line\n\ntunnel.0.name=T\ntunnel.0.type=client\n") tunnels = TunnelConfigParser.load(p) assert len(tunnels) == 1 def test_nonexistent_file_returns_empty(self, tmp_path: Path): tunnels = TunnelConfigParser.load(tmp_path / "nofile.config") assert tunnels == [] class TestConfigParserLoadDir: """Parse i2ptunnel.config.d/ directory.""" def test_load_dir(self, sample_config_dir: Path): tunnels = TunnelConfigParser.load_dir(sample_config_dir) assert len(tunnels) == 2 names = {t.name for t in tunnels} assert "HTTP Proxy" in names assert "My Server" in names def test_load_dir_nonexistent(self, tmp_path: Path): tunnels = TunnelConfigParser.load_dir(tmp_path / "nodir") assert tunnels == [] class TestConfigParserSave: """Round-trip: load -> save -> load produces identical result.""" def test_roundtrip(self, sample_config_path: Path, tmp_path: Path): original = TunnelConfigParser.load(sample_config_path) out_path = tmp_path / "output.config" TunnelConfigParser.save(out_path, original) reloaded = TunnelConfigParser.load(out_path) assert len(reloaded) == len(original) for orig, rel in zip(original, reloaded): assert orig.name == rel.name assert orig.type == rel.type assert orig.listen_port == rel.listen_port assert orig.interface == rel.interface assert orig.start_on_load == rel.start_on_load assert orig.options == rel.options def test_save_empty(self, tmp_path: Path): out_path = tmp_path / "empty.config" TunnelConfigParser.save(out_path, []) content = out_path.read_text() assert content.startswith("#") # header comment