from __future__ import annotations import unittest from vpn_egressctl.errors import ValidationError from vpn_egressctl.uri import parse_hysteria2_uri class UriParserTests(unittest.TestCase): def test_minimal_uri(self) -> None: endpoint = parse_hysteria2_uri("hysteria2://secret@example.com/") self.assertEqual(endpoint.server, "example.com") self.assertEqual(endpoint.server_port, 443) self.assertEqual(endpoint.password, "secret") self.assertEqual(endpoint.sni, "example.com") def test_short_scheme_and_percent_encoding(self) -> None: endpoint = parse_hysteria2_uri("hy2://user%3Apass@EXAMPLE.com:8443?sni=t%C3%A9st.example#Moscow") self.assertEqual(endpoint.password, "user:pass") self.assertEqual(endpoint.server, "example.com") self.assertEqual(endpoint.server_port, 8443) self.assertEqual(endpoint.sni, "xn--tst-bma.example") self.assertEqual(endpoint.display_name, "Moscow") def test_salamander(self) -> None: endpoint = parse_hysteria2_uri( "hysteria2://secret@example.com:443/?insecure=1&obfs=salamander&obfs-password=o%40p" ) self.assertTrue(endpoint.insecure) self.assertEqual(endpoint.obfs_type, "salamander") self.assertEqual(endpoint.obfs_password, "o@p") def test_ipv6(self) -> None: endpoint = parse_hysteria2_uri("hysteria2://secret@[2001:db8::1]:444/") self.assertEqual(endpoint.server, "2001:db8::1") self.assertEqual(endpoint.server_port, 444) self.assertEqual(endpoint.endpoint_label(), "[2001:db8::1]:444") def test_multi_port(self) -> None: endpoint = parse_hysteria2_uri("hysteria2://secret@example.com:443,5000-6000/") self.assertIsNone(endpoint.server_port) self.assertEqual(endpoint.server_ports, ("443", "5000:6000")) def test_range_only_uses_server_ports(self) -> None: endpoint = parse_hysteria2_uri("hysteria2://secret@example.com:5000-6000/") self.assertIsNone(endpoint.server_port) self.assertEqual(endpoint.server_ports, ("5000:6000",)) def test_userpass_is_preserved(self) -> None: endpoint = parse_hysteria2_uri("hysteria2://alice%3Acorrect%20horse@example.com") self.assertEqual(endpoint.password, "alice:correct horse") def test_unicode_host_is_idna(self) -> None: endpoint = parse_hysteria2_uri("hysteria2://secret@пример.рф") self.assertEqual(endpoint.server, "xn--e1afmkfd.xn--p1ai") def test_repr_hides_secrets(self) -> None: endpoint = parse_hysteria2_uri( "hysteria2://TOPSECRET@example.com?obfs=salamander&obfs-password=OBFSSECRET" ) text = repr(endpoint) self.assertNotIn("TOPSECRET", text) self.assertNotIn("OBFSSECRET", text) def assert_invalid(self, uri: str, marker: str | None = None) -> None: with self.assertRaises(ValidationError) as caught: parse_hysteria2_uri(uri) if marker: self.assertIn(marker, str(caught.exception)) def test_rejections(self) -> None: cases = [ ("http://secret@example.com", "scheme"), ("hysteria2://example.com", "authentication"), ("hysteria2://@example.com", "authentication"), ("hysteria2://secret@", "server"), ("hysteria2://secret@example.com:0", "1..65535"), ("hysteria2://secret@example.com:65536", "1..65535"), ("hysteria2://secret@example.com:100-99", "range"), ("hysteria2://secret@example.com:100,100", "Overlapping"), ("hysteria2://secret@2001:db8::1", "brackets"), ("hysteria2://secret@example.com/path", "paths"), ("hysteria2://secret@example.com?unknown=x", "Unsupported"), ("hysteria2://secret@example.com?sni=a&sni=b", "Duplicate"), ("hysteria2://secret@example.com?insecure=true", "exactly"), ("hysteria2://secret@example.com?obfs=gecko", "1.13.19"), ("hysteria2://secret@example.com?obfs=salamander", "obfs-password"), ("hysteria2://secret@example.com?obfs-password=x", "requires"), ("hysteria2://secret@example.com?pinSHA256=x", "safely"), ("hysteria2://secret@example.com?ech=x", "safely"), ("hysteria2://sec%ZZret@example.com", "percent"), ("hysteria2://secret@example.com?sni=%FF", "UTF-8"), ] for uri, marker in cases: with self.subTest(uri=uri): self.assert_invalid(uri, marker) if __name__ == "__main__": unittest.main()