diff --git a/src/neo4j/_async/io/_bolt.py b/src/neo4j/_async/io/_bolt.py index 1af01ba88..fd4afbc69 100644 --- a/src/neo4j/_async/io/_bolt.py +++ b/src/neo4j/_async/io/_bolt.py @@ -34,7 +34,7 @@ BoltError, BoltHandshakeError, ) -from ..._meta import get_user_agent +from ..._meta import USER_AGENT from ...addressing import ResolvedAddress from ...api import ( ServerInfo, @@ -154,7 +154,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *, if user_agent: self.user_agent = user_agent else: - self.user_agent = get_user_agent() + self.user_agent = USER_AGENT self.auth = auth self.auth_dict = self._to_auth_dict(auth) @@ -263,6 +263,7 @@ def protocol_handlers(cls, protocol_version=None): AsyncBolt5x0, AsyncBolt5x1, AsyncBolt5x2, + AsyncBolt5x3, ) handlers = { @@ -275,6 +276,7 @@ def protocol_handlers(cls, protocol_version=None): AsyncBolt5x0.PROTOCOL_VERSION: AsyncBolt5x0, AsyncBolt5x1.PROTOCOL_VERSION: AsyncBolt5x1, AsyncBolt5x2.PROTOCOL_VERSION: AsyncBolt5x2, + AsyncBolt5x3.PROTOCOL_VERSION: AsyncBolt5x3, } if protocol_version is None: @@ -389,7 +391,10 @@ async def open( # Carry out Bolt subclass imports locally to avoid circular dependency # issues. - if protocol_version == (5, 2): + if protocol_version == (5, 3): + from ._bolt5 import AsyncBolt5x3 + bolt_cls = AsyncBolt5x3 + elif protocol_version == (5, 2): from ._bolt5 import AsyncBolt5x2 bolt_cls = AsyncBolt5x2 elif protocol_version == (5, 1): diff --git a/src/neo4j/_async/io/_bolt5.py b/src/neo4j/_async/io/_bolt5.py index 25c8521a1..4502a14a2 100644 --- a/src/neo4j/_async/io/_bolt5.py +++ b/src/neo4j/_async/io/_bolt5.py @@ -23,6 +23,7 @@ from ..._codec.hydration import v2 as hydration_v2 from ..._exceptions import BoltProtocolError +from ..._meta import BOLT_AGENT_DICT from ...api import ( READ_ACCESS, Version, @@ -618,3 +619,13 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, self._append(b"\x11", (extra,), Response(self, "begin", hydration_hooks, **handlers), dehydration_hooks=dehydration_hooks) + + +class AsyncBolt5x3(AsyncBolt5x2): + + PROTOCOL_VERSION = Version(5, 3) + + def get_base_headers(self): + headers = super().get_base_headers() + headers["bolt_agent"] = BOLT_AGENT_DICT + return headers diff --git a/src/neo4j/_conf.py b/src/neo4j/_conf.py index d00b21d98..b9b1d35f1 100644 --- a/src/neo4j/_conf.py +++ b/src/neo4j/_conf.py @@ -26,7 +26,6 @@ deprecation_warn, experimental_warn, ExperimentalWarning, - get_user_agent, ) from .api import ( DEFAULT_DATABASE, @@ -400,7 +399,7 @@ class PoolConfig(Config): # The use of this option is strongly discouraged. #: User Agent (Python Driver Specific) - user_agent = get_user_agent() + user_agent = None # Specify the client agent name. #: Socket Keep Alive (Python and .NET Driver Specific) diff --git a/src/neo4j/_meta.py b/src/neo4j/_meta.py index 48b751a36..868982914 100644 --- a/src/neo4j/_meta.py +++ b/src/neo4j/_meta.py @@ -19,6 +19,8 @@ from __future__ import annotations import asyncio +import platform +import sys import tracemalloc import typing as t from functools import wraps @@ -35,17 +37,43 @@ deprecated_package = False +def _compute_bolt_agent() -> t.Dict[str, str]: + def format_version_info(version_info): + return "{}.{}.{}-{}-{}".format(*version_info) + + return { + "product": f"neo4j-python/{version}", + "platform": + f"{platform.system() or 'Unknown'} " + f"{platform.release() or 'unknown'}; " + f"{platform.machine() or 'unknown'}", + "language": f"Python/{format_version_info(sys.version_info)}", + "language_details": + f"{platform.python_implementation()}; " + f"{format_version_info(sys.implementation.version)} " + f"({', '.join(platform.python_build())}) " + f"[{platform.python_compiler()}]" + } + + +BOLT_AGENT_DICT = _compute_bolt_agent() + + +def _compute_user_agent() -> str: + template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})" + fields = (version,) + tuple(sys.version_info) + (sys.platform,) + return template.format(*fields) + + +USER_AGENT = _compute_user_agent() + + +# TODO: 6.0 - remove this function def get_user_agent(): """ Obtain the default user agent string sent to the server after a successful handshake. """ - from sys import ( - platform, - version_info, - ) - template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})" - fields = (version,) + tuple(version_info) + (platform,) - return template.format(*fields) + return USER_AGENT def _id(x): diff --git a/src/neo4j/_sync/io/_bolt.py b/src/neo4j/_sync/io/_bolt.py index f8e026512..abba0dce6 100644 --- a/src/neo4j/_sync/io/_bolt.py +++ b/src/neo4j/_sync/io/_bolt.py @@ -34,7 +34,7 @@ BoltError, BoltHandshakeError, ) -from ..._meta import get_user_agent +from ..._meta import USER_AGENT from ...addressing import ResolvedAddress from ...api import ( ServerInfo, @@ -154,7 +154,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *, if user_agent: self.user_agent = user_agent else: - self.user_agent = get_user_agent() + self.user_agent = USER_AGENT self.auth = auth self.auth_dict = self._to_auth_dict(auth) @@ -263,6 +263,7 @@ def protocol_handlers(cls, protocol_version=None): Bolt5x0, Bolt5x1, Bolt5x2, + Bolt5x3, ) handlers = { @@ -275,6 +276,7 @@ def protocol_handlers(cls, protocol_version=None): Bolt5x0.PROTOCOL_VERSION: Bolt5x0, Bolt5x1.PROTOCOL_VERSION: Bolt5x1, Bolt5x2.PROTOCOL_VERSION: Bolt5x2, + Bolt5x3.PROTOCOL_VERSION: Bolt5x3, } if protocol_version is None: @@ -389,7 +391,10 @@ def open( # Carry out Bolt subclass imports locally to avoid circular dependency # issues. - if protocol_version == (5, 2): + if protocol_version == (5, 3): + from ._bolt5 import Bolt5x3 + bolt_cls = Bolt5x3 + elif protocol_version == (5, 2): from ._bolt5 import Bolt5x2 bolt_cls = Bolt5x2 elif protocol_version == (5, 1): diff --git a/src/neo4j/_sync/io/_bolt5.py b/src/neo4j/_sync/io/_bolt5.py index a511d1d89..1740d22d9 100644 --- a/src/neo4j/_sync/io/_bolt5.py +++ b/src/neo4j/_sync/io/_bolt5.py @@ -23,6 +23,7 @@ from ..._codec.hydration import v2 as hydration_v2 from ..._exceptions import BoltProtocolError +from ..._meta import BOLT_AGENT_DICT from ...api import ( READ_ACCESS, Version, @@ -618,3 +619,13 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, self._append(b"\x11", (extra,), Response(self, "begin", hydration_hooks, **handlers), dehydration_hooks=dehydration_hooks) + + +class Bolt5x3(Bolt5x2): + + PROTOCOL_VERSION = Version(5, 3) + + def get_base_headers(self): + headers = super().get_base_headers() + headers["bolt_agent"] = BOLT_AGENT_DICT + return headers diff --git a/testkitbackend/test_config.json b/testkitbackend/test_config.json index 48f24e645..c2c911a59 100644 --- a/testkitbackend/test_config.json +++ b/testkitbackend/test_config.json @@ -48,6 +48,7 @@ "Feature:Bolt:5.0": true, "Feature:Bolt:5.1": true, "Feature:Bolt:5.2": true, + "Feature:Bolt:5.3": true, "Feature:Bolt:Patch:UTC": true, "Feature:Impersonation": true, "Feature:TLS:1.1": "Driver blocks TLS 1.1 for security reasons.", diff --git a/tests/unit/async_/io/test_class_bolt.py b/tests/unit/async_/io/test_class_bolt.py index 735e7fd55..eea7bde04 100644 --- a/tests/unit/async_/io/test_class_bolt.py +++ b/tests/unit/async_/io/test_class_bolt.py @@ -39,7 +39,7 @@ def test_class_method_protocol_handlers(): expected_handlers = { (3, 0), (4, 1), (4, 2), (4, 3), (4, 4), - (5, 0), (5, 1), (5, 2), + (5, 0), (5, 1), (5, 2), (5, 3), } protocol_handlers = AsyncBolt.protocol_handlers() @@ -64,7 +64,8 @@ def test_class_method_protocol_handlers(): ((5, 0), 1), ((5, 1), 1), ((5, 2), 1), - ((5, 3), 0), + ((5, 3), 1), + ((5, 4), 0), ((6, 0), 0), ] ) @@ -84,7 +85,7 @@ def test_class_method_protocol_handlers_with_invalid_protocol_version(): # [bolt-version-bump] search tag when changing bolt version support def test_class_method_get_handshake(): handshake = AsyncBolt.get_handshake() - assert (b"\x00\x02\x02\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03" + assert (b"\x00\x03\x03\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03" == handshake) @@ -130,6 +131,7 @@ async def test_cancel_hello_in_open(mocker, none_auth): ((5, 0), "neo4j._async.io._bolt5.AsyncBolt5x0"), ((5, 1), "neo4j._async.io._bolt5.AsyncBolt5x1"), ((5, 2), "neo4j._async.io._bolt5.AsyncBolt5x2"), + ((5, 3), "neo4j._async.io._bolt5.AsyncBolt5x3"), ), ) @mark_async_test @@ -162,13 +164,13 @@ async def test_version_negotiation( (2, 0), (4, 0), (3, 1), - (5, 3), + (5, 4), (6, 0), )) @mark_async_test async def test_failing_version_negotiation(mocker, bolt_version, none_auth): supported_protocols = \ - "('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2')" + "('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2', '5.3')" address = ("localhost", 7687) socket_mock = mocker.AsyncMock(spec=AsyncBoltSocket) diff --git a/tests/unit/async_/io/test_class_bolt3.py b/tests/unit/async_/io/test_class_bolt3.py index e0954f473..56b400c68 100644 --- a/tests/unit/async_/io/test_class_bolt3.py +++ b/tests/unit/async_/io/test_class_bolt3.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt3 import AsyncBolt3 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -249,3 +250,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt3.PACKER_CLS, + unpacker_cls=AsyncBolt3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt3.PACKER_CLS, + unpacker_cls=AsyncBolt3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt4x0.py b/tests/unit/async_/io/test_class_bolt4x0.py index ff1edcbdc..d194f12a0 100644 --- a/tests/unit/async_/io/test_class_bolt4x0.py +++ b/tests/unit/async_/io/test_class_bolt4x0.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt4 import AsyncBolt4x0 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -345,3 +346,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x0.PACKER_CLS, + unpacker_cls=AsyncBolt4x0.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x0.PACKER_CLS, + unpacker_cls=AsyncBolt4x0.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt4x1.py b/tests/unit/async_/io/test_class_bolt4x1.py index 7766c5592..7a1fa2d24 100644 --- a/tests/unit/async_/io/test_class_bolt4x1.py +++ b/tests/unit/async_/io/test_class_bolt4x1.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt4 import AsyncBolt4x1 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -362,3 +363,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x1.PACKER_CLS, + unpacker_cls=AsyncBolt4x1.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x1.PACKER_CLS, + unpacker_cls=AsyncBolt4x1.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt4x2.py b/tests/unit/async_/io/test_class_bolt4x2.py index 0547606a1..f9af5ea20 100644 --- a/tests/unit/async_/io/test_class_bolt4x2.py +++ b/tests/unit/async_/io/test_class_bolt4x2.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt4 import AsyncBolt4x2 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -363,3 +364,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x2.PACKER_CLS, + unpacker_cls=AsyncBolt4x2.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x2.PACKER_CLS, + unpacker_cls=AsyncBolt4x2.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt4x3.py b/tests/unit/async_/io/test_class_bolt4x3.py index dca811442..f09e1f724 100644 --- a/tests/unit/async_/io/test_class_bolt4x3.py +++ b/tests/unit/async_/io/test_class_bolt4x3.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt4 import AsyncBolt4x3 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -390,3 +391,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x3.PACKER_CLS, + unpacker_cls=AsyncBolt4x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x3.PACKER_CLS, + unpacker_cls=AsyncBolt4x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt4x4.py b/tests/unit/async_/io/test_class_bolt4x4.py index 19cf8b5cc..135790540 100644 --- a/tests/unit/async_/io/test_class_bolt4x4.py +++ b/tests/unit/async_/io/test_class_bolt4x4.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt4 import AsyncBolt4x4 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -403,3 +404,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x4.PACKER_CLS, + unpacker_cls=AsyncBolt4x4.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x4( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt4x4.PACKER_CLS, + unpacker_cls=AsyncBolt4x4.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt4x4( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt5x0.py b/tests/unit/async_/io/test_class_bolt5x0.py index 2937d442e..30b42963c 100644 --- a/tests/unit/async_/io/test_class_bolt5x0.py +++ b/tests/unit/async_/io/test_class_bolt5x0.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt5 import AsyncBolt5x0 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -403,3 +404,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x0.PACKER_CLS, + unpacker_cls=AsyncBolt5x0.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x0.PACKER_CLS, + unpacker_cls=AsyncBolt5x0.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt5x1.py b/tests/unit/async_/io/test_class_bolt5x1.py index ab4855d96..831a7d9dd 100644 --- a/tests/unit/async_/io/test_class_bolt5x1.py +++ b/tests/unit/async_/io/test_class_bolt5x1.py @@ -24,6 +24,7 @@ import neo4j.exceptions from neo4j._async.io._bolt5 import AsyncBolt5x1 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j.exceptions import ConfigurationError from ...._async_compat import mark_async_test @@ -457,3 +458,50 @@ async def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): await connection.hello() + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x1.PACKER_CLS, + unpacker_cls=AsyncBolt5x1.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x1.PACKER_CLS, + unpacker_cls=AsyncBolt5x1.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt5x2.py b/tests/unit/async_/io/test_class_bolt5x2.py index 7c9a2064c..770e3016b 100644 --- a/tests/unit/async_/io/test_class_bolt5x2.py +++ b/tests/unit/async_/io/test_class_bolt5x2.py @@ -24,6 +24,7 @@ import neo4j from neo4j._async.io._bolt5 import AsyncBolt5x2 from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from ...._async_compat import mark_async_test @@ -406,7 +407,6 @@ def _assert_notifications_in_extra(extra, expected): assert extra[key] == expected[key] - @pytest.mark.parametrize(("method", "args", "extra_idx"), ( ("run", ("RETURN 1",), 2), ("begin", (), 0), @@ -476,3 +476,50 @@ async def test_hello_supports_notification_filters( if dis_cats is not None: expected["notifications_disabled_categories"] = dis_cats _assert_notifications_in_extra(extra, expected) + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x2.PACKER_CLS, + unpacker_cls=AsyncBolt5x2.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x2.PACKER_CLS, + unpacker_cls=AsyncBolt5x2.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/async_/io/test_class_bolt5x3.py b/tests/unit/async_/io/test_class_bolt5x3.py new file mode 100644 index 000000000..ad671583c --- /dev/null +++ b/tests/unit/async_/io/test_class_bolt5x3.py @@ -0,0 +1,436 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# This file is part of Neo4j. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import itertools +import logging + +import pytest + +import neo4j +from neo4j._async.io._bolt5 import AsyncBolt5x3 +from neo4j._conf import PoolConfig +from neo4j._meta import ( + BOLT_AGENT_DICT, + USER_AGENT, +) + +from ...._async_compat import mark_async_test + + +@pytest.mark.parametrize("set_stale", (True, False)) +def test_conn_is_stale(fake_socket, set_stale): + address = neo4j.Address(("127.0.0.1", 7687)) + max_connection_lifetime = 0 + connection = AsyncBolt5x3(address, fake_socket(address), max_connection_lifetime) + if set_stale: + connection.set_stale() + assert connection.stale() is True + + +@pytest.mark.parametrize("set_stale", (True, False)) +def test_conn_is_not_stale_if_not_enabled(fake_socket, set_stale): + address = neo4j.Address(("127.0.0.1", 7687)) + max_connection_lifetime = -1 + connection = AsyncBolt5x3(address, fake_socket(address), max_connection_lifetime) + if set_stale: + connection.set_stale() + assert connection.stale() is set_stale + + +@pytest.mark.parametrize("set_stale", (True, False)) +def test_conn_is_not_stale(fake_socket, set_stale): + address = neo4j.Address(("127.0.0.1", 7687)) + max_connection_lifetime = 999999999 + connection = AsyncBolt5x3(address, fake_socket(address), max_connection_lifetime) + if set_stale: + connection.set_stale() + assert connection.stale() is set_stale + + +@pytest.mark.parametrize(("args", "kwargs", "expected_fields"), ( + (("", {}), {"db": "something"}, ({"db": "something"},)), + (("", {}), {"imp_user": "imposter"}, ({"imp_user": "imposter"},)), + ( + ("", {}), + {"db": "something", "imp_user": "imposter"}, + ({"db": "something", "imp_user": "imposter"},) + ), +)) +@mark_async_test +async def test_extra_in_begin(fake_socket, args, kwargs, expected_fields): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.begin(*args, **kwargs) + await connection.send_all() + tag, is_fields = await socket.pop_message() + assert tag == b"\x11" + assert tuple(is_fields) == expected_fields + + +@pytest.mark.parametrize(("args", "kwargs", "expected_fields"), ( + (("", {}), {"db": "something"}, ("", {}, {"db": "something"})), + (("", {}), {"imp_user": "imposter"}, ("", {}, {"imp_user": "imposter"})), + ( + ("", {}), + {"db": "something", "imp_user": "imposter"}, + ("", {}, {"db": "something", "imp_user": "imposter"}) + ), +)) +@mark_async_test +async def test_extra_in_run(fake_socket, args, kwargs, expected_fields): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.run(*args, **kwargs) + await connection.send_all() + tag, is_fields = await socket.pop_message() + assert tag == b"\x10" + assert tuple(is_fields) == expected_fields + + +@mark_async_test +async def test_n_extra_in_discard(fake_socket): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.discard(n=666) + await connection.send_all() + tag, fields = await socket.pop_message() + assert tag == b"\x2F" + assert len(fields) == 1 + assert fields[0] == {"n": 666} + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (666, {"n": -1, "qid": 666}), + (-1, {"n": -1}), + ] +) +@mark_async_test +async def test_qid_extra_in_discard(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.discard(qid=test_input) + await connection.send_all() + tag, fields = await socket.pop_message() + assert tag == b"\x2F" + assert len(fields) == 1 + assert fields[0] == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (777, {"n": 666, "qid": 777}), + (-1, {"n": 666}), + ] +) +@mark_async_test +async def test_n_and_qid_extras_in_discard(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.discard(n=666, qid=test_input) + await connection.send_all() + tag, fields = await socket.pop_message() + assert tag == b"\x2F" + assert len(fields) == 1 + assert fields[0] == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (666, {"n": 666}), + (-1, {"n": -1}), + ] +) +@mark_async_test +async def test_n_extra_in_pull(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.pull(n=test_input) + await connection.send_all() + tag, fields = await socket.pop_message() + assert tag == b"\x3F" + assert len(fields) == 1 + assert fields[0] == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (777, {"n": -1, "qid": 777}), + (-1, {"n": -1}), + ] +) +@mark_async_test +async def test_qid_extra_in_pull(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.pull(qid=test_input) + await connection.send_all() + tag, fields = await socket.pop_message() + assert tag == b"\x3F" + assert len(fields) == 1 + assert fields[0] == expected + + +@mark_async_test +async def test_n_and_qid_extras_in_pull(fake_socket): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.pull(n=666, qid=777) + await connection.send_all() + tag, fields = await socket.pop_message() + assert tag == b"\x3F" + assert len(fields) == 1 + assert fields[0] == {"n": 666, "qid": 777} + + +@mark_async_test +async def test_hello_passes_routing_metadata(fake_socket_pair): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x3.PACKER_CLS, + unpacker_cls=AsyncBolt5x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/4.4.0"}) + await sockets.server.send_message(b"\x70", {}) + connection = AsyncBolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime, + routing_context={"foo": "bar"} + ) + await connection.hello() + tag, fields = await sockets.server.pop_message() + assert tag == b"\x01" + assert len(fields) == 1 + assert fields[0]["routing"] == {"foo": "bar"} + + +@pytest.mark.parametrize(("hints", "valid"), ( + ({"connection.recv_timeout_seconds": 1}, True), + ({"connection.recv_timeout_seconds": 42}, True), + ({}, True), + ({"whatever_this_is": "ignore me!"}, True), + ({"connection.recv_timeout_seconds": -1}, False), + ({"connection.recv_timeout_seconds": 0}, False), + ({"connection.recv_timeout_seconds": 2.5}, False), + ({"connection.recv_timeout_seconds": None}, False), + ({"connection.recv_timeout_seconds": False}, False), + ({"connection.recv_timeout_seconds": "1"}, False), +)) +@mark_async_test +async def test_hint_recv_timeout_seconds( + fake_socket_pair, hints, valid, caplog, mocker +): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x3.PACKER_CLS, + unpacker_cls=AsyncBolt5x3.UNPACKER_CLS) + sockets.client.settimeout = mocker.Mock() + await sockets.server.send_message( + b"\x70", {"server": "Neo4j/4.3.4", "hints": hints} + ) + await sockets.server.send_message(b"\x70", {}) + connection = AsyncBolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime + ) + with caplog.at_level(logging.INFO): + await connection.hello() + if valid: + if "connection.recv_timeout_seconds" in hints: + sockets.client.settimeout.assert_called_once_with( + hints["connection.recv_timeout_seconds"] + ) + else: + sockets.client.settimeout.assert_not_called() + assert not any("recv_timeout_seconds" in msg + and "invalid" in msg + for msg in caplog.messages) + else: + sockets.client.settimeout.assert_not_called() + assert any(repr(hints["connection.recv_timeout_seconds"]) in msg + and "recv_timeout_seconds" in msg + and "invalid" in msg + for msg in caplog.messages) + + +CREDENTIALS = "+++super-secret-sauce+++" + + +@pytest.mark.parametrize("auth", ( + ("user", CREDENTIALS), + neo4j.basic_auth("user", CREDENTIALS), + neo4j.kerberos_auth(CREDENTIALS), + neo4j.bearer_auth(CREDENTIALS), + neo4j.custom_auth("user", CREDENTIALS, "realm", "scheme"), + neo4j.Auth("scheme", "principal", CREDENTIALS, "realm", foo="bar"), +)) +@mark_async_test +async def test_credentials_are_not_logged(auth, fake_socket_pair, caplog): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x3.PACKER_CLS, + unpacker_cls=AsyncBolt5x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/4.3.4"}) + await sockets.server.send_message(b"\x70", {}) + connection = AsyncBolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime, auth=auth + ) + with caplog.at_level(logging.DEBUG): + await connection.hello() + + if isinstance(auth, tuple): + auth = neo4j.basic_auth(*auth) + for field in ("scheme", "principal", "realm", "parameters"): + value = getattr(auth, field, None) + if value: + assert repr(value) in caplog.text + assert CREDENTIALS not in caplog.text + + +def _assert_notifications_in_extra(extra, expected): + for key in expected: + assert key in extra + assert extra[key] == expected[key] + + + +@pytest.mark.parametrize(("method", "args", "extra_idx"), ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), +)) +@pytest.mark.parametrize( + ("cls_min_sev", "method_min_sev"), + itertools.product((None, "WARNING", "OFF"), repeat=2) +) +@pytest.mark.parametrize( + ("cls_dis_cats", "method_dis_cats"), + itertools.product((None, [], ["HINT"], ["HINT", "DEPRECATION"]), repeat=2) +) +@mark_async_test +async def test_supports_notification_filters( + fake_socket, method, args, extra_idx, cls_min_sev, method_min_sev, + cls_dis_cats, method_dis_cats +): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, AsyncBolt5x3.UNPACKER_CLS) + connection = AsyncBolt5x3( + address, socket, PoolConfig.max_connection_lifetime, + notifications_min_severity=cls_min_sev, + notifications_disabled_categories=cls_dis_cats + ) + method = getattr(connection, method) + + method(*args, notifications_min_severity=method_min_sev, + notifications_disabled_categories=method_dis_cats) + await connection.send_all() + + _, fields = await socket.pop_message() + extra = fields[extra_idx] + expected = {} + if method_min_sev is not None: + expected["notifications_minimum_severity"] = method_min_sev + if method_dis_cats is not None: + expected["notifications_disabled_categories"] = method_dis_cats + _assert_notifications_in_extra(extra, expected) + + +@pytest.mark.parametrize("min_sev", (None, "WARNING", "OFF")) +@pytest.mark.parametrize("dis_cats", + (None, [], ["HINT"], ["HINT", "DEPRECATION"])) +@mark_async_test +async def test_hello_supports_notification_filters( + fake_socket_pair, min_sev, dis_cats +): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x3.PACKER_CLS, + unpacker_cls=AsyncBolt5x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + connection = AsyncBolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime, + notifications_min_severity=min_sev, + notifications_disabled_categories=dis_cats + ) + + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + expected = {} + if min_sev is not None: + expected["notifications_minimum_severity"] = min_sev + if dis_cats is not None: + expected["notifications_disabled_categories"] = dis_cats + _assert_notifications_in_extra(extra, expected) + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x3.PACKER_CLS, + unpacker_cls=AsyncBolt5x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_async_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +async def test_sends_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=AsyncBolt5x3.PACKER_CLS, + unpacker_cls=AsyncBolt5x3.UNPACKER_CLS) + await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + await sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = AsyncBolt5x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + await connection.hello() + + tag, fields = await sockets.server.pop_message() + extra = fields[0] + assert extra["bolt_agent"] == BOLT_AGENT_DICT diff --git a/tests/unit/async_/io/test_direct.py b/tests/unit/async_/io/test_direct.py index 953287c6c..6be98a2e2 100644 --- a/tests/unit/async_/io/test_direct.py +++ b/tests/unit/async_/io/test_direct.py @@ -18,6 +18,7 @@ import pytest +import neo4j from neo4j import PreviewWarning from neo4j._async.io import AsyncBolt from neo4j._async.io._pool import AsyncIOPool @@ -161,7 +162,7 @@ def assert_pool_size( address, expected_active, expected_inactive, pool): @mark_async_test async def test_pool_can_acquire(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection = await pool._acquire(address, None, Deadline(3), None) assert connection.address == address assert_pool_size(address, 1, 0, pool) @@ -169,7 +170,7 @@ async def test_pool_can_acquire(pool): @mark_async_test async def test_pool_can_acquire_twice(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection_1 = await pool._acquire(address, None, Deadline(3), None) connection_2 = await pool._acquire(address, None, Deadline(3), None) assert connection_1.address == address @@ -180,8 +181,8 @@ async def test_pool_can_acquire_twice(pool): @mark_async_test async def test_pool_can_acquire_two_addresses(pool): - address_1 = ("127.0.0.1", 7687) - address_2 = ("127.0.0.1", 7474) + address_1 = neo4j.Address(("127.0.0.1", 7687)) + address_2 = neo4j.Address(("127.0.0.1", 7474)) connection_1 = await pool._acquire(address_1, None, Deadline(3), None) connection_2 = await pool._acquire(address_2, None, Deadline(3), None) assert connection_1.address == address_1 @@ -192,7 +193,7 @@ async def test_pool_can_acquire_two_addresses(pool): @mark_async_test async def test_pool_can_acquire_and_release(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection = await pool._acquire(address, None, Deadline(3), None) assert_pool_size(address, 1, 0, pool) await pool.release(connection) @@ -201,7 +202,7 @@ async def test_pool_can_acquire_and_release(pool): @mark_async_test async def test_pool_releasing_twice(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection = await pool._acquire(address, None, Deadline(3), None) await pool.release(connection) assert_pool_size(address, 0, 1, pool) @@ -211,7 +212,7 @@ async def test_pool_releasing_twice(pool): @mark_async_test async def test_pool_in_use_count(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) assert pool.in_use_connection_count(address) == 0 connection = await pool._acquire(address, None, Deadline(3), None) assert pool.in_use_connection_count(address) == 1 @@ -222,7 +223,7 @@ async def test_pool_in_use_count(pool): @mark_async_test async def test_pool_max_conn_pool_size(pool): async with AsyncFakeBoltPool((), max_connection_pool_size=1) as pool: - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) await pool._acquire(address, None, Deadline(0), None) assert pool.in_use_connection_count(address) == 1 with pytest.raises(ClientError): @@ -233,7 +234,7 @@ async def test_pool_max_conn_pool_size(pool): @pytest.mark.parametrize("is_reset", (True, False)) @mark_async_test async def test_pool_reset_when_released(is_reset, pool, mocker): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) quick_connection_name = AsyncQuickConnection.__name__ is_reset_mock = mocker.patch( f"{__name__}.{quick_connection_name}.is_reset", diff --git a/tests/unit/common/work/test_summary.py b/tests/unit/common/work/test_summary.py index d92fc742c..3d3049356 100644 --- a/tests/unit/common/work/test_summary.py +++ b/tests/unit/common/work/test_summary.py @@ -307,6 +307,7 @@ def test_summary_result_counters(summary_args_kwargs, counters_set) -> None: ((5, 0), "t_first"), ((5, 1), "t_first"), ((5, 2), "t_first"), + ((5, 3), "t_first"), )) def test_summary_result_available_after( summary_args_kwargs, exists, bolt_version, meta_name @@ -336,6 +337,7 @@ def test_summary_result_available_after( ((5, 0), "t_last"), ((5, 1), "t_last"), ((5, 2), "t_last"), + ((5, 3), "t_last"), )) def test_summary_result_consumed_after( summary_args_kwargs, exists, bolt_version, meta_name diff --git a/tests/unit/sync/io/test_class_bolt.py b/tests/unit/sync/io/test_class_bolt.py index f0306f968..9c74816d3 100644 --- a/tests/unit/sync/io/test_class_bolt.py +++ b/tests/unit/sync/io/test_class_bolt.py @@ -39,7 +39,7 @@ def test_class_method_protocol_handlers(): expected_handlers = { (3, 0), (4, 1), (4, 2), (4, 3), (4, 4), - (5, 0), (5, 1), (5, 2), + (5, 0), (5, 1), (5, 2), (5, 3), } protocol_handlers = Bolt.protocol_handlers() @@ -64,7 +64,8 @@ def test_class_method_protocol_handlers(): ((5, 0), 1), ((5, 1), 1), ((5, 2), 1), - ((5, 3), 0), + ((5, 3), 1), + ((5, 4), 0), ((6, 0), 0), ] ) @@ -84,7 +85,7 @@ def test_class_method_protocol_handlers_with_invalid_protocol_version(): # [bolt-version-bump] search tag when changing bolt version support def test_class_method_get_handshake(): handshake = Bolt.get_handshake() - assert (b"\x00\x02\x02\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03" + assert (b"\x00\x03\x03\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03" == handshake) @@ -130,6 +131,7 @@ def test_cancel_hello_in_open(mocker, none_auth): ((5, 0), "neo4j._sync.io._bolt5.Bolt5x0"), ((5, 1), "neo4j._sync.io._bolt5.Bolt5x1"), ((5, 2), "neo4j._sync.io._bolt5.Bolt5x2"), + ((5, 3), "neo4j._sync.io._bolt5.Bolt5x3"), ), ) @mark_sync_test @@ -162,13 +164,13 @@ def test_version_negotiation( (2, 0), (4, 0), (3, 1), - (5, 3), + (5, 4), (6, 0), )) @mark_sync_test def test_failing_version_negotiation(mocker, bolt_version, none_auth): supported_protocols = \ - "('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2')" + "('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2', '5.3')" address = ("localhost", 7687) socket_mock = mocker.MagicMock(spec=BoltSocket) diff --git a/tests/unit/sync/io/test_class_bolt3.py b/tests/unit/sync/io/test_class_bolt3.py index 73999db0b..8c209434b 100644 --- a/tests/unit/sync/io/test_class_bolt3.py +++ b/tests/unit/sync/io/test_class_bolt3.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt3 import Bolt3 from neo4j.exceptions import ConfigurationError @@ -249,3 +250,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt3.PACKER_CLS, + unpacker_cls=Bolt3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt3.PACKER_CLS, + unpacker_cls=Bolt3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt4x0.py b/tests/unit/sync/io/test_class_bolt4x0.py index 6fb416e18..e1831cb2e 100644 --- a/tests/unit/sync/io/test_class_bolt4x0.py +++ b/tests/unit/sync/io/test_class_bolt4x0.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt4 import Bolt4x0 from neo4j.exceptions import ConfigurationError @@ -345,3 +346,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x0.PACKER_CLS, + unpacker_cls=Bolt4x0.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x0.PACKER_CLS, + unpacker_cls=Bolt4x0.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt4x1.py b/tests/unit/sync/io/test_class_bolt4x1.py index 3ed433d90..132212915 100644 --- a/tests/unit/sync/io/test_class_bolt4x1.py +++ b/tests/unit/sync/io/test_class_bolt4x1.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt4 import Bolt4x1 from neo4j.exceptions import ConfigurationError @@ -362,3 +363,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x1.PACKER_CLS, + unpacker_cls=Bolt4x1.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x1.PACKER_CLS, + unpacker_cls=Bolt4x1.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt4x2.py b/tests/unit/sync/io/test_class_bolt4x2.py index d9172badf..ed19ee522 100644 --- a/tests/unit/sync/io/test_class_bolt4x2.py +++ b/tests/unit/sync/io/test_class_bolt4x2.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt4 import Bolt4x2 from neo4j.exceptions import ConfigurationError @@ -363,3 +364,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x2.PACKER_CLS, + unpacker_cls=Bolt4x2.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x2.PACKER_CLS, + unpacker_cls=Bolt4x2.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt4x3.py b/tests/unit/sync/io/test_class_bolt4x3.py index c89d31d91..0721172a0 100644 --- a/tests/unit/sync/io/test_class_bolt4x3.py +++ b/tests/unit/sync/io/test_class_bolt4x3.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt4 import Bolt4x3 from neo4j.exceptions import ConfigurationError @@ -390,3 +391,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x3.PACKER_CLS, + unpacker_cls=Bolt4x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x3.PACKER_CLS, + unpacker_cls=Bolt4x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt4x4.py b/tests/unit/sync/io/test_class_bolt4x4.py index be7cc7543..b33eaaf54 100644 --- a/tests/unit/sync/io/test_class_bolt4x4.py +++ b/tests/unit/sync/io/test_class_bolt4x4.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt4 import Bolt4x4 from neo4j.exceptions import ConfigurationError @@ -403,3 +404,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x4.PACKER_CLS, + unpacker_cls=Bolt4x4.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x4( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt4x4.PACKER_CLS, + unpacker_cls=Bolt4x4.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt4x4( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt5x0.py b/tests/unit/sync/io/test_class_bolt5x0.py index cf9dab995..9ae410f18 100644 --- a/tests/unit/sync/io/test_class_bolt5x0.py +++ b/tests/unit/sync/io/test_class_bolt5x0.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt5 import Bolt5x0 from neo4j.exceptions import ConfigurationError @@ -403,3 +404,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x0.PACKER_CLS, + unpacker_cls=Bolt5x0.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x0.PACKER_CLS, + unpacker_cls=Bolt5x0.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x0( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt5x1.py b/tests/unit/sync/io/test_class_bolt5x1.py index 783638a25..68eaf7b33 100644 --- a/tests/unit/sync/io/test_class_bolt5x1.py +++ b/tests/unit/sync/io/test_class_bolt5x1.py @@ -23,6 +23,7 @@ import neo4j import neo4j.exceptions from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt5 import Bolt5x1 from neo4j.exceptions import ConfigurationError @@ -457,3 +458,50 @@ def test_hello_does_not_support_notification_filters( ) with pytest.raises(ConfigurationError, match="Notification filtering"): connection.hello() + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x1.PACKER_CLS, + unpacker_cls=Bolt5x1.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x1.PACKER_CLS, + unpacker_cls=Bolt5x1.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x1( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt5x2.py b/tests/unit/sync/io/test_class_bolt5x2.py index 79e9cace6..69384b9c4 100644 --- a/tests/unit/sync/io/test_class_bolt5x2.py +++ b/tests/unit/sync/io/test_class_bolt5x2.py @@ -23,6 +23,7 @@ import neo4j from neo4j._conf import PoolConfig +from neo4j._meta import USER_AGENT from neo4j._sync.io._bolt5 import Bolt5x2 from ...._async_compat import mark_sync_test @@ -406,7 +407,6 @@ def _assert_notifications_in_extra(extra, expected): assert extra[key] == expected[key] - @pytest.mark.parametrize(("method", "args", "extra_idx"), ( ("run", ("RETURN 1",), 2), ("begin", (), 0), @@ -476,3 +476,50 @@ def test_hello_supports_notification_filters( if dis_cats is not None: expected["notifications_disabled_categories"] = dis_cats _assert_notifications_in_extra(extra, expected) + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x2.PACKER_CLS, + unpacker_cls=Bolt5x2.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_does_not_send_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x2.PACKER_CLS, + unpacker_cls=Bolt5x2.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x2( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert "bolt_agent" not in extra diff --git a/tests/unit/sync/io/test_class_bolt5x3.py b/tests/unit/sync/io/test_class_bolt5x3.py new file mode 100644 index 000000000..f2a5fae4d --- /dev/null +++ b/tests/unit/sync/io/test_class_bolt5x3.py @@ -0,0 +1,436 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# This file is part of Neo4j. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import itertools +import logging + +import pytest + +import neo4j +from neo4j._conf import PoolConfig +from neo4j._meta import ( + BOLT_AGENT_DICT, + USER_AGENT, +) +from neo4j._sync.io._bolt5 import Bolt5x3 + +from ...._async_compat import mark_sync_test + + +@pytest.mark.parametrize("set_stale", (True, False)) +def test_conn_is_stale(fake_socket, set_stale): + address = neo4j.Address(("127.0.0.1", 7687)) + max_connection_lifetime = 0 + connection = Bolt5x3(address, fake_socket(address), max_connection_lifetime) + if set_stale: + connection.set_stale() + assert connection.stale() is True + + +@pytest.mark.parametrize("set_stale", (True, False)) +def test_conn_is_not_stale_if_not_enabled(fake_socket, set_stale): + address = neo4j.Address(("127.0.0.1", 7687)) + max_connection_lifetime = -1 + connection = Bolt5x3(address, fake_socket(address), max_connection_lifetime) + if set_stale: + connection.set_stale() + assert connection.stale() is set_stale + + +@pytest.mark.parametrize("set_stale", (True, False)) +def test_conn_is_not_stale(fake_socket, set_stale): + address = neo4j.Address(("127.0.0.1", 7687)) + max_connection_lifetime = 999999999 + connection = Bolt5x3(address, fake_socket(address), max_connection_lifetime) + if set_stale: + connection.set_stale() + assert connection.stale() is set_stale + + +@pytest.mark.parametrize(("args", "kwargs", "expected_fields"), ( + (("", {}), {"db": "something"}, ({"db": "something"},)), + (("", {}), {"imp_user": "imposter"}, ({"imp_user": "imposter"},)), + ( + ("", {}), + {"db": "something", "imp_user": "imposter"}, + ({"db": "something", "imp_user": "imposter"},) + ), +)) +@mark_sync_test +def test_extra_in_begin(fake_socket, args, kwargs, expected_fields): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.begin(*args, **kwargs) + connection.send_all() + tag, is_fields = socket.pop_message() + assert tag == b"\x11" + assert tuple(is_fields) == expected_fields + + +@pytest.mark.parametrize(("args", "kwargs", "expected_fields"), ( + (("", {}), {"db": "something"}, ("", {}, {"db": "something"})), + (("", {}), {"imp_user": "imposter"}, ("", {}, {"imp_user": "imposter"})), + ( + ("", {}), + {"db": "something", "imp_user": "imposter"}, + ("", {}, {"db": "something", "imp_user": "imposter"}) + ), +)) +@mark_sync_test +def test_extra_in_run(fake_socket, args, kwargs, expected_fields): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.run(*args, **kwargs) + connection.send_all() + tag, is_fields = socket.pop_message() + assert tag == b"\x10" + assert tuple(is_fields) == expected_fields + + +@mark_sync_test +def test_n_extra_in_discard(fake_socket): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.discard(n=666) + connection.send_all() + tag, fields = socket.pop_message() + assert tag == b"\x2F" + assert len(fields) == 1 + assert fields[0] == {"n": 666} + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (666, {"n": -1, "qid": 666}), + (-1, {"n": -1}), + ] +) +@mark_sync_test +def test_qid_extra_in_discard(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.discard(qid=test_input) + connection.send_all() + tag, fields = socket.pop_message() + assert tag == b"\x2F" + assert len(fields) == 1 + assert fields[0] == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (777, {"n": 666, "qid": 777}), + (-1, {"n": 666}), + ] +) +@mark_sync_test +def test_n_and_qid_extras_in_discard(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.discard(n=666, qid=test_input) + connection.send_all() + tag, fields = socket.pop_message() + assert tag == b"\x2F" + assert len(fields) == 1 + assert fields[0] == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (666, {"n": 666}), + (-1, {"n": -1}), + ] +) +@mark_sync_test +def test_n_extra_in_pull(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.pull(n=test_input) + connection.send_all() + tag, fields = socket.pop_message() + assert tag == b"\x3F" + assert len(fields) == 1 + assert fields[0] == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + (777, {"n": -1, "qid": 777}), + (-1, {"n": -1}), + ] +) +@mark_sync_test +def test_qid_extra_in_pull(fake_socket, test_input, expected): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.pull(qid=test_input) + connection.send_all() + tag, fields = socket.pop_message() + assert tag == b"\x3F" + assert len(fields) == 1 + assert fields[0] == expected + + +@mark_sync_test +def test_n_and_qid_extras_in_pull(fake_socket): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3(address, socket, PoolConfig.max_connection_lifetime) + connection.pull(n=666, qid=777) + connection.send_all() + tag, fields = socket.pop_message() + assert tag == b"\x3F" + assert len(fields) == 1 + assert fields[0] == {"n": 666, "qid": 777} + + +@mark_sync_test +def test_hello_passes_routing_metadata(fake_socket_pair): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x3.PACKER_CLS, + unpacker_cls=Bolt5x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/4.4.0"}) + sockets.server.send_message(b"\x70", {}) + connection = Bolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime, + routing_context={"foo": "bar"} + ) + connection.hello() + tag, fields = sockets.server.pop_message() + assert tag == b"\x01" + assert len(fields) == 1 + assert fields[0]["routing"] == {"foo": "bar"} + + +@pytest.mark.parametrize(("hints", "valid"), ( + ({"connection.recv_timeout_seconds": 1}, True), + ({"connection.recv_timeout_seconds": 42}, True), + ({}, True), + ({"whatever_this_is": "ignore me!"}, True), + ({"connection.recv_timeout_seconds": -1}, False), + ({"connection.recv_timeout_seconds": 0}, False), + ({"connection.recv_timeout_seconds": 2.5}, False), + ({"connection.recv_timeout_seconds": None}, False), + ({"connection.recv_timeout_seconds": False}, False), + ({"connection.recv_timeout_seconds": "1"}, False), +)) +@mark_sync_test +def test_hint_recv_timeout_seconds( + fake_socket_pair, hints, valid, caplog, mocker +): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x3.PACKER_CLS, + unpacker_cls=Bolt5x3.UNPACKER_CLS) + sockets.client.settimeout = mocker.Mock() + sockets.server.send_message( + b"\x70", {"server": "Neo4j/4.3.4", "hints": hints} + ) + sockets.server.send_message(b"\x70", {}) + connection = Bolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime + ) + with caplog.at_level(logging.INFO): + connection.hello() + if valid: + if "connection.recv_timeout_seconds" in hints: + sockets.client.settimeout.assert_called_once_with( + hints["connection.recv_timeout_seconds"] + ) + else: + sockets.client.settimeout.assert_not_called() + assert not any("recv_timeout_seconds" in msg + and "invalid" in msg + for msg in caplog.messages) + else: + sockets.client.settimeout.assert_not_called() + assert any(repr(hints["connection.recv_timeout_seconds"]) in msg + and "recv_timeout_seconds" in msg + and "invalid" in msg + for msg in caplog.messages) + + +CREDENTIALS = "+++super-secret-sauce+++" + + +@pytest.mark.parametrize("auth", ( + ("user", CREDENTIALS), + neo4j.basic_auth("user", CREDENTIALS), + neo4j.kerberos_auth(CREDENTIALS), + neo4j.bearer_auth(CREDENTIALS), + neo4j.custom_auth("user", CREDENTIALS, "realm", "scheme"), + neo4j.Auth("scheme", "principal", CREDENTIALS, "realm", foo="bar"), +)) +@mark_sync_test +def test_credentials_are_not_logged(auth, fake_socket_pair, caplog): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x3.PACKER_CLS, + unpacker_cls=Bolt5x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/4.3.4"}) + sockets.server.send_message(b"\x70", {}) + connection = Bolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime, auth=auth + ) + with caplog.at_level(logging.DEBUG): + connection.hello() + + if isinstance(auth, tuple): + auth = neo4j.basic_auth(*auth) + for field in ("scheme", "principal", "realm", "parameters"): + value = getattr(auth, field, None) + if value: + assert repr(value) in caplog.text + assert CREDENTIALS not in caplog.text + + +def _assert_notifications_in_extra(extra, expected): + for key in expected: + assert key in extra + assert extra[key] == expected[key] + + + +@pytest.mark.parametrize(("method", "args", "extra_idx"), ( + ("run", ("RETURN 1",), 2), + ("begin", (), 0), +)) +@pytest.mark.parametrize( + ("cls_min_sev", "method_min_sev"), + itertools.product((None, "WARNING", "OFF"), repeat=2) +) +@pytest.mark.parametrize( + ("cls_dis_cats", "method_dis_cats"), + itertools.product((None, [], ["HINT"], ["HINT", "DEPRECATION"]), repeat=2) +) +@mark_sync_test +def test_supports_notification_filters( + fake_socket, method, args, extra_idx, cls_min_sev, method_min_sev, + cls_dis_cats, method_dis_cats +): + address = neo4j.Address(("127.0.0.1", 7687)) + socket = fake_socket(address, Bolt5x3.UNPACKER_CLS) + connection = Bolt5x3( + address, socket, PoolConfig.max_connection_lifetime, + notifications_min_severity=cls_min_sev, + notifications_disabled_categories=cls_dis_cats + ) + method = getattr(connection, method) + + method(*args, notifications_min_severity=method_min_sev, + notifications_disabled_categories=method_dis_cats) + connection.send_all() + + _, fields = socket.pop_message() + extra = fields[extra_idx] + expected = {} + if method_min_sev is not None: + expected["notifications_minimum_severity"] = method_min_sev + if method_dis_cats is not None: + expected["notifications_disabled_categories"] = method_dis_cats + _assert_notifications_in_extra(extra, expected) + + +@pytest.mark.parametrize("min_sev", (None, "WARNING", "OFF")) +@pytest.mark.parametrize("dis_cats", + (None, [], ["HINT"], ["HINT", "DEPRECATION"])) +@mark_sync_test +def test_hello_supports_notification_filters( + fake_socket_pair, min_sev, dis_cats +): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x3.PACKER_CLS, + unpacker_cls=Bolt5x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + connection = Bolt5x3( + address, sockets.client, PoolConfig.max_connection_lifetime, + notifications_min_severity=min_sev, + notifications_disabled_categories=dis_cats + ) + + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + expected = {} + if min_sev is not None: + expected["notifications_minimum_severity"] = min_sev + if dis_cats is not None: + expected["notifications_disabled_categories"] = dis_cats + _assert_notifications_in_extra(extra, expected) + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_user_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x3.PACKER_CLS, + unpacker_cls=Bolt5x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + if not user_agent: + assert extra["user_agent"] == USER_AGENT + else: + assert extra["user_agent"] == user_agent + + +@mark_sync_test +@pytest.mark.parametrize( + "user_agent", (None, "test user agent", "", USER_AGENT) +) +def test_sends_bolt_agent(fake_socket_pair, user_agent): + address = neo4j.Address(("127.0.0.1", 7687)) + sockets = fake_socket_pair(address, + packer_cls=Bolt5x3.PACKER_CLS, + unpacker_cls=Bolt5x3.UNPACKER_CLS) + sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"}) + sockets.server.send_message(b"\x70", {}) + max_connection_lifetime = 0 + connection = Bolt5x3( + address, sockets.client, max_connection_lifetime, user_agent=user_agent + ) + connection.hello() + + tag, fields = sockets.server.pop_message() + extra = fields[0] + assert extra["bolt_agent"] == BOLT_AGENT_DICT diff --git a/tests/unit/sync/io/test_direct.py b/tests/unit/sync/io/test_direct.py index 5349f5a21..aa294c2fb 100644 --- a/tests/unit/sync/io/test_direct.py +++ b/tests/unit/sync/io/test_direct.py @@ -18,6 +18,7 @@ import pytest +import neo4j from neo4j import PreviewWarning from neo4j._conf import ( Config, @@ -161,7 +162,7 @@ def assert_pool_size( address, expected_active, expected_inactive, pool): @mark_sync_test def test_pool_can_acquire(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection = pool._acquire(address, None, Deadline(3), None) assert connection.address == address assert_pool_size(address, 1, 0, pool) @@ -169,7 +170,7 @@ def test_pool_can_acquire(pool): @mark_sync_test def test_pool_can_acquire_twice(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection_1 = pool._acquire(address, None, Deadline(3), None) connection_2 = pool._acquire(address, None, Deadline(3), None) assert connection_1.address == address @@ -180,8 +181,8 @@ def test_pool_can_acquire_twice(pool): @mark_sync_test def test_pool_can_acquire_two_addresses(pool): - address_1 = ("127.0.0.1", 7687) - address_2 = ("127.0.0.1", 7474) + address_1 = neo4j.Address(("127.0.0.1", 7687)) + address_2 = neo4j.Address(("127.0.0.1", 7474)) connection_1 = pool._acquire(address_1, None, Deadline(3), None) connection_2 = pool._acquire(address_2, None, Deadline(3), None) assert connection_1.address == address_1 @@ -192,7 +193,7 @@ def test_pool_can_acquire_two_addresses(pool): @mark_sync_test def test_pool_can_acquire_and_release(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection = pool._acquire(address, None, Deadline(3), None) assert_pool_size(address, 1, 0, pool) pool.release(connection) @@ -201,7 +202,7 @@ def test_pool_can_acquire_and_release(pool): @mark_sync_test def test_pool_releasing_twice(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) connection = pool._acquire(address, None, Deadline(3), None) pool.release(connection) assert_pool_size(address, 0, 1, pool) @@ -211,7 +212,7 @@ def test_pool_releasing_twice(pool): @mark_sync_test def test_pool_in_use_count(pool): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) assert pool.in_use_connection_count(address) == 0 connection = pool._acquire(address, None, Deadline(3), None) assert pool.in_use_connection_count(address) == 1 @@ -222,7 +223,7 @@ def test_pool_in_use_count(pool): @mark_sync_test def test_pool_max_conn_pool_size(pool): with FakeBoltPool((), max_connection_pool_size=1) as pool: - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) pool._acquire(address, None, Deadline(0), None) assert pool.in_use_connection_count(address) == 1 with pytest.raises(ClientError): @@ -233,7 +234,7 @@ def test_pool_max_conn_pool_size(pool): @pytest.mark.parametrize("is_reset", (True, False)) @mark_sync_test def test_pool_reset_when_released(is_reset, pool, mocker): - address = ("127.0.0.1", 7687) + address = neo4j.Address(("127.0.0.1", 7687)) quick_connection_name = QuickConnection.__name__ is_reset_mock = mocker.patch( f"{__name__}.{quick_connection_name}.is_reset",