Repository: aaugustin/websockets
Pull request #1042 (2021-09-06 19:50:48)

Expect a WebSocketURI in ClientConnection.


Patch:
diff --git a/docs/reference/utilities.rst b/docs/reference/utilities.rst
index dc63338..e3e53f2 100644
--- a/docs/reference/utilities.rst
+++ b/docs/reference/utilities.rst
@@ -35,3 +35,12 @@ HTTP events
         .. automethod:: raw_items
 
     .. autoexception:: MultipleValuesError
+
+URIs
+----
+
+.. automodule:: websockets.uri
+
+    .. autofunction:: parse_uri
+
+    .. autoclass:: WebSocketURI
diff --git a/src/websockets/client.py b/src/websockets/client.py
index 13c8a8a..42c7aee 100644
--- a/src/websockets/client.py
+++ b/src/websockets/client.py
@@ -32,7 +32,7 @@ from .typing import (
     Subprotocol,
     UpgradeProtocol,
 )
-from .uri import parse_uri
+from .uri import WebSocketURI
 from .utils import accept_key, generate_key
 
 
@@ -46,7 +46,7 @@ __all__ = ["ClientConnection"]
 class ClientConnection(Connection):
     def __init__(
         self,
-        uri: str,
+        wsuri: WebSocketURI,
         origin: Optional[Origin] = None,
         extensions: Optional[Sequence[ClientExtensionFactory]] = None,
         subprotocols: Optional[Sequence[Subprotocol]] = None,
@@ -60,7 +60,7 @@ class ClientConnection(Connection):
             max_size=max_size,
             logger=logger,
         )
-        self.wsuri = parse_uri(uri)
+        self.wsuri = wsuri
         self.origin = origin
         self.available_extensions = extensions
         self.available_subprotocols = subprotocols
diff --git a/src/websockets/frames.py b/src/websockets/frames.py
index 9a97f25..c31796c 100644
--- a/src/websockets/frames.py
+++ b/src/websockets/frames.py
@@ -99,9 +99,9 @@ class Frame:
     """
     WebSocket frame.
 
-    Args:
-        opcode: opcode.
-        data: payload data.
+    Attributes:
+        opcode: Opcode.
+        data: Payload data.
         fin: FIN bit.
         rsv1: RSV1 bit.
         rsv2: RSV2 bit.
@@ -368,6 +368,10 @@ class Close:
     """
     WebSocket close code and reason.
 
+    Attributes:
+        code: Close code.
+        reason: Close reason.
+
     """
 
     code: int
diff --git a/src/websockets/uri.py b/src/websockets/uri.py
index 3d8f7cd..fff0c38 100644
--- a/src/websockets/uri.py
+++ b/src/websockets/uri.py
@@ -17,11 +17,12 @@ class WebSocketURI:
 
     Attributes:
         secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI.
-        host: Host, normalized to lower case.
-        port: Port, always set even if it's the default.
-        resource_name: Path and optional query.
-        user_info: ``(username, password)`` when the URI contains
-            `User Information`_, else :obj:`None`.
+        host: Normalized to lower case.
+        port: Always set even if it's the default.
+        path: May be empty.
+        query: May be empty if the URI doesn't include a query component.
+        username: Available when the URI contains `User Information`_.
+        password: Available when the URI contains `User Information`_.
 
     .. _User Information: https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.1
 
@@ -30,8 +31,27 @@ class WebSocketURI:
     secure: bool
     host: str
     port: int
-    resource_name: str
-    user_info: Optional[Tuple[str, str]] = None
+    path: str
+    query: str
+    username: Optional[str]
+    password: Optional[str]
+
+    @property
+    def resource_name(self) -> str:
+        if self.path:
+            resource_name = self.path
+        else:
+            resource_name = "/"
+        if self.query:
+            resource_name += "?" + self.query
+        return resource_name
+
+    @property
+    def user_info(self) -> Optional[Tuple[str, str]]:
+        if self.username is None:
+            return None
+        assert self.password is not None
+        return (self.username, self.password)
 
 
 # All characters from the gen-delims and sub-delims sets in RFC 3987.
@@ -45,6 +65,9 @@ def parse_uri(uri: str) -> WebSocketURI:
     Args:
         uri: WebSocket URI.
 
+    Returns:
+        WebSocketURI: Parsed WebSocket URI.
+
     Raises:
         InvalidURI: if ``uri`` isn't a valid WebSocket URI.
 
@@ -60,16 +83,14 @@ def parse_uri(uri: str) -> WebSocketURI:
     secure = parsed.scheme == "wss"
     host = parsed.hostname
     port = parsed.port or (443 if secure else 80)
-    resource_name = parsed.path or "/"
-    if parsed.query:
-        resource_name += "?" + parsed.query
-    user_info = None
-    if parsed.username is not None:
-        # urllib.parse.urlparse accepts URLs with a username but without a
-        # password. This doesn't make sense for HTTP Basic Auth credentials.
-        if parsed.password is None:
-            raise exceptions.InvalidURI(uri, "username provided without password")
-        user_info = (parsed.username, parsed.password)
+    path = parsed.path
+    query = parsed.query
+    username = parsed.username
+    password = parsed.password
+    # urllib.parse.urlparse accepts URLs with a username but without a
+    # password. This doesn't make sense for HTTP Basic Auth credentials.
+    if username is not None and password is None:
+        raise exceptions.InvalidURI(uri, "username provided without password")
 
     try:
         uri.encode("ascii")
@@ -77,11 +98,11 @@ def parse_uri(uri: str) -> WebSocketURI:
         # Input contains non-ASCII characters.
         # It must be an IRI. Convert it to a URI.
         host = host.encode("idna").decode()
-        resource_name = urllib.parse.quote(resource_name, safe=DELIMS)
-        if user_info is not None:
-            user_info = (
-                urllib.parse.quote(user_info[0], safe=DELIMS),
-                urllib.parse.quote(user_info[1], safe=DELIMS),
-            )
-
-    return WebSocketURI(secure, host, port, resource_name, user_info)
+        path = urllib.parse.quote(path, safe=DELIMS)
+        query = urllib.parse.quote(query, safe=DELIMS)
+        if username is not None:
+            assert password is not None
+            username = urllib.parse.quote(username, safe=DELIMS)
+            password = urllib.parse.quote(password, safe=DELIMS)
+
+    return WebSocketURI(secure, host, port, path, query, username, password)


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 015b93b..1c1452d 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -9,6 +9,7 @@ from websockets.exceptions import InvalidHandshake, InvalidHeader
 from websockets.frames import OP_TEXT, Frame
 from websockets.http import USER_AGENT
 from websockets.http11 import Request, Response
+from websockets.uri import parse_uri
 from websockets.utils import accept_key
 
 from .extensions.utils import (
@@ -24,7 +25,7 @@ from .utils import DATE
 class ConnectTests(unittest.TestCase):
     def test_send_connect(self):
         with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
-            client = ClientConnection("wss://example.com/test")
+            client = ClientConnection(parse_uri("wss://example.com/test"))
         request = client.connect()
         self.assertIsInstance(request, Request)
         client.send_request(request)
@@ -44,7 +45,7 @@ class ConnectTests(unittest.TestCase):
 
     def test_connect_request(self):
         with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
-            client = ClientConnection("wss://example.com/test")
+            client = ClientConnection(parse_uri("wss://example.com/test"))
         request = client.connect()
         self.assertEqual(request.path, "/test")
         self.assertEqual(
@@ -62,7 +63,7 @@ class ConnectTests(unittest.TestCase):
         )
 
     def test_path(self):
-        client = ClientConnection("wss://example.com/endpoint?test=1")
+        client = ClientConnection(parse_uri("wss://example.com/endpoint?test=1"))
         request = client.connect()
 
         self.assertEqual(request.path, "/endpoint?test=1")
@@ -77,33 +78,40 @@ class ConnectTests(unittest.TestCase):
             ("wss://example.com:8443/", "example.com:8443"),
         ]:
             with self.subTest(uri=uri):
-                client = ClientConnection(uri)
+                client = ClientConnection(parse_uri(uri))
                 request = client.connect()
 
                 self.assertEqual(request.headers["Host"], host)
 
     def test_user_info(self):
-        client = ClientConnection("wss://hello:iloveyou@example.com/")
+        client = ClientConnection(parse_uri("wss://hello:iloveyou@example.com/"))
         request = client.connect()
 
         self.assertEqual(request.headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=")
 
     def test_origin(self):
-        client = ClientConnection("wss://example.com/", origin="https://example.com")
+        client = ClientConnection(
+            parse_uri("wss://example.com/"),
+            origin="https://example.com",
+        )
         request = client.connect()
 
         self.assertEqual(request.headers["Origin"], "https://example.com")
 
     def test_extensions(self):
         client = ClientConnection(
-            "wss://example.com/", extensions=[ClientOpExtensionFactory()]
+            parse_uri("wss://example.com/"),
+            extensions=[ClientOpExtensionFactory()],
         )
         request = client.connect()
 
         self.assertEqual(request.headers["Sec-WebSocket-Extensions"], "x-op; op")
 
     def test_subprotocols(self):
-        client = ClientConnection("wss://example.com/", subprotocols=["chat"])
+        client = ClientConnection(
+            parse_uri("wss://example.com/"),
+            subprotocols=["chat"],
+        )
         request = client.connect()
 
         self.assertEqual(request.headers["Sec-WebSocket-Protocol"], "chat")
@@ -112,7 +120,7 @@ class ConnectTests(unittest.TestCase):
 class AcceptRejectTests(unittest.TestCase):
     def test_receive_accept(self):
         with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
-            client = ClientConnection("ws://example.com/test")
+            client = ClientConnection(parse_uri("ws://example.com/test"))
         client.connect()
         client.receive_data(
             (
@@ -131,7 +139,7 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_receive_reject(self):
         with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
-            client = ClientConnection("ws://example.com/test")
+            client = ClientConnection(parse_uri("ws://example.com/test"))
         client.connect()
         client.receive_data(
             (
@@ -151,7 +159,7 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_accept_response(self):
         with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
-            client = ClientConnection("ws://example.com/test")
+            client = ClientConnection(parse_uri("ws://example.com/test"))
         client.connect()
         client.receive_data(
             (
@@ -183,7 +191,7 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_reject_response(self):
         with unittest.mock.patch("websockets.client.generate_key", return_value=KEY):
-            client = ClientConnection("ws://example.com/test")
+            client = ClientConnection(parse_uri("ws://example.com/test"))
         client.connect()
         client.receive_data(
             (
@@ -231,7 +239,7 @@ class AcceptRejectTests(unittest.TestCase):
         )
 
     def test_basic(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         client.receive_data(response.serialize())
         [response] = client.events_received()
@@ -239,7 +247,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(client.state, OPEN)
 
     def test_missing_connection(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         del response.headers["Connection"]
         client.receive_data(response.serialize())
@@ -251,7 +259,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(str(raised.exception), "missing Connection header")
 
     def test_invalid_connection(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         del response.headers["Connection"]
         response.headers["Connection"] = "close"
@@ -264,7 +272,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(str(raised.exception), "invalid Connection header: close")
 
     def test_missing_upgrade(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         del response.headers["Upgrade"]
         client.receive_data(response.serialize())
@@ -276,7 +284,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(str(raised.exception), "missing Upgrade header")
 
     def test_invalid_upgrade(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         del response.headers["Upgrade"]
         response.headers["Upgrade"] = "h2c"
@@ -289,7 +297,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c")
 
     def test_missing_accept(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         del response.headers["Sec-WebSocket-Accept"]
         client.receive_data(response.serialize())
@@ -301,7 +309,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Accept header")
 
     def test_multiple_accept(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Accept"] = ACCEPT
         client.receive_data(response.serialize())
@@ -317,7 +325,7 @@ class AcceptRejectTests(unittest.TestCase):
         )
 
     def test_invalid_accept(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         del response.headers["Sec-WebSocket-Accept"]
         response.headers["Sec-WebSocket-Accept"] = ACCEPT
@@ -332,7 +340,7 @@ class AcceptRejectTests(unittest.TestCase):
         )
 
     def test_no_extensions(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         client.receive_data(response.serialize())
         [response] = client.events_received()
@@ -342,7 +350,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_no_extension(self):
         client = ClientConnection(
-            "wss://example.com/", extensions=[ClientOpExtensionFactory()]
+            parse_uri("wss://example.com/"),
+            extensions=[ClientOpExtensionFactory()],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
@@ -354,7 +363,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_extension(self):
         client = ClientConnection(
-            "wss://example.com/", extensions=[ClientRsv2ExtensionFactory()]
+            parse_uri("wss://example.com/"),
+            extensions=[ClientRsv2ExtensionFactory()],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Extensions"] = "x-rsv2"
@@ -365,7 +375,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(client.extensions, [Rsv2Extension()])
 
     def test_unexpected_extension(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
         client.receive_data(response.serialize())
@@ -378,7 +388,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_unsupported_extension(self):
         client = ClientConnection(
-            "wss://example.com/", extensions=[ClientRsv2ExtensionFactory()]
+            parse_uri("wss://example.com/"),
+            extensions=[ClientRsv2ExtensionFactory()],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Extensions"] = "x-op; op"
@@ -395,7 +406,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_supported_extension_parameters(self):
         client = ClientConnection(
-            "wss://example.com/", extensions=[ClientOpExtensionFactory("this")]
+            parse_uri("wss://example.com/"),
+            extensions=[ClientOpExtensionFactory("this")],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Extensions"] = "x-op; op=this"
@@ -407,7 +419,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_unsupported_extension_parameters(self):
         client = ClientConnection(
-            "wss://example.com/", extensions=[ClientOpExtensionFactory("this")]
+            parse_uri("wss://example.com/"),
+            extensions=[ClientOpExtensionFactory("this")],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that"
@@ -424,7 +437,7 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_multiple_supported_extension_parameters(self):
         client = ClientConnection(
-            "wss://example.com/",
+            parse_uri("wss://example.com/"),
             extensions=[
                 ClientOpExtensionFactory("this"),
                 ClientOpExtensionFactory("that"),
@@ -440,7 +453,7 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_multiple_extensions(self):
         client = ClientConnection(
-            "wss://example.com/",
+            parse_uri("wss://example.com/"),
             extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()],
         )
         response = self.make_accept_response(client)
@@ -454,7 +467,7 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_multiple_extensions_order(self):
         client = ClientConnection(
-            "wss://example.com/",
+            parse_uri("wss://example.com/"),
             extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()],
         )
         response = self.make_accept_response(client)
@@ -467,7 +480,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(client.extensions, [Rsv2Extension(), OpExtension()])
 
     def test_no_subprotocols(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         client.receive_data(response.serialize())
         [response] = client.events_received()
@@ -476,7 +489,9 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertIsNone(client.subprotocol)
 
     def test_no_subprotocol(self):
-        client = ClientConnection("wss://example.com/", subprotocols=["chat"])
+        client = ClientConnection(
+            parse_uri("wss://example.com/"), subprotocols=["chat"]
+        )
         response = self.make_accept_response(client)
         client.receive_data(response.serialize())
         [response] = client.events_received()
@@ -485,7 +500,9 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertIsNone(client.subprotocol)
 
     def test_subprotocol(self):
-        client = ClientConnection("wss://example.com/", subprotocols=["chat"])
+        client = ClientConnection(
+            parse_uri("wss://example.com/"), subprotocols=["chat"]
+        )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Protocol"] = "chat"
         client.receive_data(response.serialize())
@@ -495,7 +512,7 @@ class AcceptRejectTests(unittest.TestCase):
         self.assertEqual(client.subprotocol, "chat")
 
     def test_unexpected_subprotocol(self):
-        client = ClientConnection("wss://example.com/")
+        client = ClientConnection(parse_uri("wss://example.com/"))
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Protocol"] = "chat"
         client.receive_data(response.serialize())
@@ -508,7 +525,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_multiple_subprotocols(self):
         client = ClientConnection(
-            "wss://example.com/", subprotocols=["superchat", "chat"]
+            parse_uri("wss://example.com/"),
+            subprotocols=["superchat", "chat"],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Protocol"] = "superchat"
@@ -525,7 +543,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_supported_subprotocol(self):
         client = ClientConnection(
-            "wss://example.com/", subprotocols=["superchat", "chat"]
+            parse_uri("wss://example.com/"),
+            subprotocols=["superchat", "chat"],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Protocol"] = "chat"
@@ -537,7 +556,8 @@ class AcceptRejectTests(unittest.TestCase):
 
     def test_unsupported_subprotocol(self):
         client = ClientConnection(
-            "wss://example.com/", subprotocols=["superchat", "chat"]
+            parse_uri("wss://example.com/"),
+            subprotocols=["superchat", "chat"],
         )
         response = self.make_accept_response(client)
         response.headers["Sec-WebSocket-Protocol"] = "otherchat"
@@ -552,7 +572,7 @@ class AcceptRejectTests(unittest.TestCase):
 
 class MiscTests(unittest.TestCase):
     def test_bypass_handshake(self):
-        client = ClientConnection("ws://example.com/test", state=OPEN)
+        client = ClientConnection(parse_uri("ws://example.com/test"), state=OPEN)
         client.receive_data(b"\x81\x06Hello!")
         [frame] = client.events_received()
         self.assertEqual(frame, Frame(OP_TEXT, b"Hello!"))
@@ -560,5 +580,5 @@ class MiscTests(unittest.TestCase):
     def test_custom_logger(self):
         logger = logging.getLogger("test")
         with self.assertLogs("test", logging.DEBUG) as logs:
-            ClientConnection("wss://example.com/test", logger=logger)
+            ClientConnection(parse_uri("wss://example.com/test"), logger=logger)
         self.assertEqual(len(logs.records), 1)
diff --git a/tests/test_uri.py b/tests/test_uri.py
index f937d29..8acc01c 100644
--- a/tests/test_uri.py
+++ b/tests/test_uri.py
@@ -7,33 +7,46 @@ from websockets.uri import *
 VALID_URIS = [
     (
         "ws://localhost/",
-        WebSocketURI(False, "localhost", 80, "/", None),
+        WebSocketURI(False, "localhost", 80, "/", "", None, None),
     ),
     (
         "wss://localhost/",
-        WebSocketURI(True, "localhost", 443, "/", None),
+        WebSocketURI(True, "localhost", 443, "/", "", None, None),
+    ),
+    (
+        "ws://localhost",
+        WebSocketURI(False, "localhost", 80, "", "", None, None),
     ),
     (
         "ws://localhost/path?query",
-        WebSocketURI(False, "localhost", 80, "/path?query", None),
+        WebSocketURI(False, "localhost", 80, "/path", "query", None, None),
     ),
     (
         "ws://localhost/path;params",
-        WebSocketURI(False, "localhost", 80, "/path;params", None),
+        WebSocketURI(False, "localhost", 80, "/path;params", "", None, None),
     ),
     (
         "WS://LOCALHOST/PATH?QUERY",
-        WebSocketURI(False, "localhost", 80, "/PATH?QUERY", None),
+        WebSocketURI(False, "localhost", 80, "/PATH", "QUERY", None, None),
     ),
     (
         "ws://user:pass@localhost/",
-        WebSocketURI(False, "localhost", 80, "/", ("user", "pass")),
+        WebSocketURI(False, "localhost", 80, "/", "", "user", "pass"),
+    ),
+    (
+        "ws://høst/",
+        WebSocketURI(False, "xn--hst-0na", 80, "/", "", None, None),
     ),
-    ("ws://høst/", WebSocketURI(False, "xn--hst-0na", 80, "/", None)),
     (
-        "ws://üser:påss@høst/πass",
+        "ws://üser:påss@høst/πass?qùéry",
         WebSocketURI(
-            False, "xn--hst-0na", 80, "/%CF%80ass", ("%C3%BCser", "p%C3%A5ss")
+            False,
+            "xn--hst-0na",
+            80,
+            "/%CF%80ass",
+            "q%C3%B9%C3%A9ry",
+            "%C3%BCser",
+            "p%C3%A5ss",
         ),
     ),
 ]
@@ -46,6 +59,19 @@ INVALID_URIS = [
     "ws:///path",
 ]
 
+RESOURCE_NAMES = [
+    ("ws://localhost/", "/"),
+    ("ws://localhost", "/"),
+    ("ws://localhost/path?query", "/path?query"),
+    ("ws://høst/πass?qùéry", "/%CF%80ass?q%C3%B9%C3%A9ry"),
+]
+
+USER_INFOS = [
+    ("ws://localhost/", None),
+    ("ws://user:pass@localhost/", ("user", "pass")),
+    ("ws://üser:påss@høst/", ("%C3%BCser", "p%C3%A5ss")),
+]
+
 
 class URITests(unittest.TestCase):
     def test_success(self):
@@ -58,3 +84,13 @@ class URITests(unittest.TestCase):
             with self.subTest(uri=uri):
                 with self.assertRaises(InvalidURI):
                     parse_uri(uri)
+
+    def test_resource_name(self):
+        for uri, resource_name in RESOURCE_NAMES:
+            with self.subTest(uri=uri):
+                self.assertEqual(parse_uri(uri).resource_name, resource_name)
+
+    def test_user_info(self):
+        for uri, user_info in USER_INFOS:
+            with self.subTest(uri=uri):
+                self.assertEqual(parse_uri(uri).user_info, user_info)