diff --git a/boltstub/bolt_protocol.py b/boltstub/bolt_protocol.py index eb9a30556..025a79866 100644 --- a/boltstub/bolt_protocol.py +++ b/boltstub/bolt_protocol.py @@ -478,7 +478,6 @@ class Bolt5x1Protocol(Bolt5x0Protocol): version_aliases = set() # allow the server to negotiate other bolt versions equivalent_versions = set() - server_agent = "Neo4j/5.5.0" messages = { "C": { @@ -489,6 +488,8 @@ class Bolt5x1Protocol(Bolt5x0Protocol): "S": Bolt5x0Protocol.messages["S"], } + server_agent = "Neo4j/5.5.0" + class Bolt5x2Protocol(Bolt5x1Protocol): protocol_version = (5, 2) @@ -497,3 +498,12 @@ class Bolt5x2Protocol(Bolt5x1Protocol): equivalent_versions = set() server_agent = "Neo4j/5.7.0" + + +class Bolt5x3Protocol(Bolt5x2Protocol): + protocol_version = (5, 3) + version_aliases = set() + # allow the server to negotiate other bolt versions + equivalent_versions = set() + + server_agent = "Neo4j/5.9.0" diff --git a/boltstub/parsing.py b/boltstub/parsing.py index eb21fd61f..410640b34 100644 --- a/boltstub/parsing.py +++ b/boltstub/parsing.py @@ -54,7 +54,8 @@ def load_parser(): - with open(path.join(path.dirname(__file__), "grammar.lark"), "r") as fd: + grammar_path = path.join(path.dirname(__file__), "grammar.lark") + with open(grammar_path, "r", encoding="utf-8") as fd: return lark.Lark( fd, propagate_positions=True # , ambiguity="explicit" ) @@ -1557,7 +1558,7 @@ def parse(script: str, substitutions: Optional[dict] = None) -> Script: def parse_file(filename): - with open(filename) as fd: + with open(filename, encoding="utf-8") as fd: try: script = parse(fd.read()) except Exception: diff --git a/nutkit/protocol/feature.py b/nutkit/protocol/feature.py index 8634a4bf3..f8ab03dd5 100644 --- a/nutkit/protocol/feature.py +++ b/nutkit/protocol/feature.py @@ -103,6 +103,8 @@ class Feature(Enum): BOLT_5_1 = "Feature:Bolt:5.1" # The driver supports Bolt protocol version 5.2 BOLT_5_2 = "Feature:Bolt:5.2" + # The driver supports Bolt protocol version 5.3 + BOLT_5_3 = "Feature:Bolt:5.3" # The driver supports patching DateTimes to use UTC for Bolt 4.3 and 4.4 BOLT_PATCH_UTC = "Feature:Bolt:Patch:UTC" # The driver supports impersonation diff --git a/tests/stub/driver_parameters/scripts/router.script b/tests/stub/driver_parameters/scripts/v5x0/router.script similarity index 100% rename from tests/stub/driver_parameters/scripts/router.script rename to tests/stub/driver_parameters/scripts/v5x0/router.script diff --git a/tests/stub/driver_parameters/scripts/router_hello_delay.script b/tests/stub/driver_parameters/scripts/v5x0/router_hello_delay.script similarity index 100% rename from tests/stub/driver_parameters/scripts/router_hello_delay.script rename to tests/stub/driver_parameters/scripts/v5x0/router_hello_delay.script diff --git a/tests/stub/driver_parameters/scripts/router_route_delay.script b/tests/stub/driver_parameters/scripts/v5x0/router_route_delay.script similarity index 100% rename from tests/stub/driver_parameters/scripts/router_route_delay.script rename to tests/stub/driver_parameters/scripts/v5x0/router_route_delay.script diff --git a/tests/stub/driver_parameters/scripts/router_with_db_name.script b/tests/stub/driver_parameters/scripts/v5x0/router_with_db_name.script similarity index 100% rename from tests/stub/driver_parameters/scripts/router_with_db_name.script rename to tests/stub/driver_parameters/scripts/v5x0/router_with_db_name.script diff --git a/tests/stub/driver_parameters/scripts/session_run.script b/tests/stub/driver_parameters/scripts/v5x0/session_run.script similarity index 100% rename from tests/stub/driver_parameters/scripts/session_run.script rename to tests/stub/driver_parameters/scripts/v5x0/session_run.script diff --git a/tests/stub/driver_parameters/scripts/session_run_auth_delay.script b/tests/stub/driver_parameters/scripts/v5x0/session_run_auth_delay.script similarity index 100% rename from tests/stub/driver_parameters/scripts/session_run_auth_delay.script rename to tests/stub/driver_parameters/scripts/v5x0/session_run_auth_delay.script diff --git a/tests/stub/driver_parameters/scripts/session_run_bolt_handshake_delay.script b/tests/stub/driver_parameters/scripts/v5x0/session_run_bolt_handshake_delay.script similarity index 100% rename from tests/stub/driver_parameters/scripts/session_run_bolt_handshake_delay.script rename to tests/stub/driver_parameters/scripts/v5x0/session_run_bolt_handshake_delay.script diff --git a/tests/stub/driver_parameters/scripts/session_run_chaining.script b/tests/stub/driver_parameters/scripts/v5x0/session_run_chaining.script similarity index 100% rename from tests/stub/driver_parameters/scripts/session_run_chaining.script rename to tests/stub/driver_parameters/scripts/v5x0/session_run_chaining.script diff --git a/tests/stub/driver_parameters/scripts/transaction_chaining.script b/tests/stub/driver_parameters/scripts/v5x0/transaction_chaining.script similarity index 100% rename from tests/stub/driver_parameters/scripts/transaction_chaining.script rename to tests/stub/driver_parameters/scripts/v5x0/transaction_chaining.script diff --git a/tests/stub/driver_parameters/scripts/tx_without_commit_or_rollback.script b/tests/stub/driver_parameters/scripts/v5x0/tx_without_commit_or_rollback.script similarity index 100% rename from tests/stub/driver_parameters/scripts/tx_without_commit_or_rollback.script rename to tests/stub/driver_parameters/scripts/v5x0/tx_without_commit_or_rollback.script diff --git a/tests/stub/driver_parameters/scripts/v5x2/user_agent_custom.script b/tests/stub/driver_parameters/scripts/v5x2/user_agent_custom.script new file mode 100644 index 000000000..1285dae06 --- /dev/null +++ b/tests/stub/driver_parameters/scripts/v5x2/user_agent_custom.script @@ -0,0 +1,14 @@ +!: BOLT 5.2 + +C: HELLO {"[routing]": null, "user_agent": "Hello, I'm a banana 🍌!"} +S: SUCCESS {"server": "Neo4j/5.5.0", "connection_id": "bolt-1"} +C: LOGON {"{}": "*"} +S: SUCCESS {} +*: RESET +C: RUN "*" "*" "*" +S: SUCCESS {"fields": ["n"]} +C: PULL {"n": "*"} +S: RECORD [1] + SUCCESS {"type": "r"} +*: RESET +?: GOODBYE diff --git a/tests/stub/driver_parameters/scripts/v5x2/user_agent_default.script b/tests/stub/driver_parameters/scripts/v5x2/user_agent_default.script new file mode 100644 index 000000000..3e2e8e01b --- /dev/null +++ b/tests/stub/driver_parameters/scripts/v5x2/user_agent_default.script @@ -0,0 +1,14 @@ +!: BOLT 5.2 + +C: HELLO {"[routing]": null, "user_agent": {"U": "*"}} +S: SUCCESS {"server": "Neo4j/5.5.0", "connection_id": "bolt-1"} +C: LOGON {"{}": "*"} +S: SUCCESS {} +*: RESET +C: RUN "*" "*" "*" +S: SUCCESS {"fields": ["n"]} +C: PULL {"n": "*"} +S: RECORD [1] + SUCCESS {"type": "r"} +*: RESET +?: GOODBYE diff --git a/tests/stub/driver_parameters/scripts/v5x3/user_agent_custom.script b/tests/stub/driver_parameters/scripts/v5x3/user_agent_custom.script new file mode 100644 index 000000000..aceb70043 --- /dev/null +++ b/tests/stub/driver_parameters/scripts/v5x3/user_agent_custom.script @@ -0,0 +1,14 @@ +!: BOLT 5.3 + +C: HELLO {"[routing]": null, "user_agent": "Hello, I'm a banana 🍌!", "bolt_agent": {"product": {"U": "*"}, "[platform]": {"U": "*"}, "[language]": {"U": "*"}, "[language_details]": {"U": "*"}}} +S: SUCCESS {"server": "Neo4j/5.9.0", "connection_id": "bolt-1"} +C: LOGON {"{}": "*"} +S: SUCCESS {} +*: RESET +C: RUN "*" "*" "*" +S: SUCCESS {"fields": ["n"]} +C: PULL {"n": "*"} +S: RECORD [1] + SUCCESS {"type": "r"} +*: RESET +?: GOODBYE diff --git a/tests/stub/driver_parameters/scripts/v5x3/user_agent_default.script b/tests/stub/driver_parameters/scripts/v5x3/user_agent_default.script new file mode 100644 index 000000000..6a0b71f0a --- /dev/null +++ b/tests/stub/driver_parameters/scripts/v5x3/user_agent_default.script @@ -0,0 +1,14 @@ +!: BOLT 5.3 + +C: HELLO {"[routing]": null, "user_agent": {"U": "*"}, "bolt_agent": {"product": {"U": "*"}, "[platform]": {"U": "*"}, "[language]": {"U": "*"}, "[language_details]": {"U": "*"}}} +S: SUCCESS {"server": "Neo4j/5.9.0", "connection_id": "bolt-1"} +C: LOGON {"{}": "*"} +S: SUCCESS {} +*: RESET +C: RUN "*" "*" "*" +S: SUCCESS {"fields": ["n"]} +C: PULL {"n": "*"} +S: RECORD [1] + SUCCESS {"type": "r"} +*: RESET +?: GOODBYE diff --git a/tests/stub/driver_parameters/test_bookmark_manager.py b/tests/stub/driver_parameters/test_bookmark_manager.py index c8f5a9456..d1699856e 100644 --- a/tests/stub/driver_parameters/test_bookmark_manager.py +++ b/tests/stub/driver_parameters/test_bookmark_manager.py @@ -837,7 +837,7 @@ def test_multiple_bookmark_manager(self): ) def _start_server(self, server, script): - server.start(self.script_path(script), + server.start(self.script_path("v5x0", script), vars_={"#HOST#": self._router.host}) def assert_begin(self, line: str, bookmarks=None): diff --git a/tests/stub/driver_parameters/test_client_agent_strings.py b/tests/stub/driver_parameters/test_client_agent_strings.py new file mode 100644 index 000000000..0eb729b35 --- /dev/null +++ b/tests/stub/driver_parameters/test_client_agent_strings.py @@ -0,0 +1,123 @@ +import abc +import json +import re +from contextlib import contextmanager + +import nutkit.protocol as types +from nutkit.frontend import Driver +from tests.shared import TestkitTestCase +from tests.stub.shared import StubServer + + +class _ClientAgentStringsTestBase(TestkitTestCase, abc.ABC): + + @property + @abc.abstractmethod + def version_folder(self) -> str: + ... + + def setUp(self): + super().setUp() + self._server = StubServer(9000) + self._driver = None + self._session = None + + def tearDown(self): + self._server.reset() + if self._session: + self._session.close() + if self._driver: + self._driver.close() + return super().tearDown() + + def _start_server(self, script, version_folder=None, vars_=None): + if version_folder is None: + version_folder = self.version_folder + self._server.start(self.script_path(version_folder, script), + vars_=vars_) + + @contextmanager + def driver(self, **kwargs): + auth = types.AuthorizationToken("basic", principal="neo4j", + credentials="pass") + uri = "bolt://%s" % self._server.address + driver = Driver(self._backend, uri, auth, **kwargs) + try: + yield driver + finally: + driver.close() + + @contextmanager + def session(self, driver=None, **driver_kwargs): + if driver is None: + with self.driver(**driver_kwargs) as driver: + session = driver.session("r") + try: + yield session + finally: + session.close() + else: + session = driver.session("r") + try: + yield session + finally: + session.close() + + def _session_run_return_1(self, **driver_kwargs): + with self.session(**driver_kwargs) as session: + result = session.run("RETURN 1 AS n") + list(result) + + def _test_default_user_agent(self): + self._start_server("user_agent_default.script") + self._session_run_return_1() + + def _test_custom_user_agent(self): + self._start_server("user_agent_custom.script") + self._session_run_return_1(user_agent="Hello, I'm a banana 🍌!") + + +class TestClientAgentStringsV5x2(_ClientAgentStringsTestBase): + + version_folder = "v5x2" + required_features = (types.Feature.BOLT_5_2,) + + def test_default_user_agent(self): + super()._test_default_user_agent() + + def test_custom_user_agent(self): + super()._test_custom_user_agent() + + +class TestClientAgentStringsV5x3(_ClientAgentStringsTestBase): + + version_folder = "v5x3" + required_features = (types.Feature.BOLT_5_3,) + + def test_default_user_agent(self): + super()._test_default_user_agent() + + def test_custom_user_agent(self): + super()._test_custom_user_agent() + + def test_bolt_agent(self): + super()._test_default_user_agent() + + hellos = self._server.get_requests("HELLO") + assert len(hellos) == 1 + hello_extra = json.loads(hellos[0].split(maxsplit=1)[1]) + bolt_agent = hello_extra["{}"]["bolt_agent"]["{}"] + self._assert_bolt_agent_product_conforms_format(bolt_agent["product"]) + + self._server.reset() + super()._test_custom_user_agent() + + hellos = self._server.get_requests("HELLO") + assert len(hellos) == 1 + hello_extra = json.loads(hellos[0].split(maxsplit=1)[1]) + # asserts user agent is does not affect bolt agent + assert bolt_agent == hello_extra["{}"]["bolt_agent"]["{}"] + + @staticmethod + def _assert_bolt_agent_product_conforms_format(bolt_agent_product): + assert re.match(r"^.+/.+$", bolt_agent_product) diff --git a/tests/stub/driver_parameters/test_connection_acquisition_timeout_ms.py b/tests/stub/driver_parameters/test_connection_acquisition_timeout_ms.py index 029512b13..2abd67305 100644 --- a/tests/stub/driver_parameters/test_connection_acquisition_timeout_ms.py +++ b/tests/stub/driver_parameters/test_connection_acquisition_timeout_ms.py @@ -73,7 +73,7 @@ def _get_vars(self): def _start_server(self, server, script, vars_=None): if vars_ is None: vars_ = self._get_vars() - server.start(self.script_path(script), vars_=vars_) + server.start(self.script_path("v5x0", script), vars_=vars_) def test_should_work_when_every_step_is_done_in_time(self): """ diff --git a/tests/stub/driver_parameters/test_max_connection_pool_size.py b/tests/stub/driver_parameters/test_max_connection_pool_size.py index 49deb967e..b2702a67f 100644 --- a/tests/stub/driver_parameters/test_max_connection_pool_size.py +++ b/tests/stub/driver_parameters/test_max_connection_pool_size.py @@ -19,7 +19,7 @@ def setUp(self): # test for security reasons. self._server = StubServer(9999) self._server.start( - self.script_path("tx_without_commit_or_rollback.script") + self.script_path("v5x0", "tx_without_commit_or_rollback.script") ) self._driver = None self._sessions = [] diff --git a/tests/stub/shared.py b/tests/stub/shared.py index bdeb37bd3..ca5456707 100644 --- a/tests/stub/shared.py +++ b/tests/stub/shared.py @@ -80,14 +80,14 @@ def start(self, path=None, script=None, vars_=None): if path: self._last_rewritten_path = path script_fn = os.path.basename(path) - with open(path, "r") as f: + with open(path, "r", encoding="utf-8") as f: script = f.read() for v in vars_: script = script.replace(v, str(vars_[v])) if script: tempdir = tempfile.gettempdir() path = os.path.join(tempdir, script_fn) - with open(path, "w") as f: + with open(path, "w", encoding="utf-8") as f: f.write(script) f.flush() os.fsync(f) diff --git a/tests/stub/versions/scripts/v3_optional_hello.script b/tests/stub/versions/scripts/v3_and_up_optional_hello.script similarity index 100% rename from tests/stub/versions/scripts/v3_optional_hello.script rename to tests/stub/versions/scripts/v3_and_up_optional_hello.script diff --git a/tests/stub/versions/scripts/v5x1_optional_hello.script b/tests/stub/versions/scripts/v5x1_and_up_optional_hello.script similarity index 100% rename from tests/stub/versions/scripts/v5x1_optional_hello.script rename to tests/stub/versions/scripts/v5x1_and_up_optional_hello.script diff --git a/tests/stub/versions/scripts/v5x2_return_1.script b/tests/stub/versions/scripts/v5x2_return_1.script index daabd861c..c4b4ff671 100644 --- a/tests/stub/versions/scripts/v5x2_return_1.script +++ b/tests/stub/versions/scripts/v5x2_return_1.script @@ -1,4 +1,4 @@ -!: BOLT 5.1 +!: BOLT 5.2 C: HELLO {"{}": "*"} S: SUCCESS {"server": "#SERVER_AGENT#", "connection_id": "bolt-123456789"} diff --git a/tests/stub/versions/scripts/v5x3_return_1.script b/tests/stub/versions/scripts/v5x3_return_1.script new file mode 100644 index 000000000..27e1a8172 --- /dev/null +++ b/tests/stub/versions/scripts/v5x3_return_1.script @@ -0,0 +1,18 @@ +!: BOLT 5.3 + +C: HELLO {"{}": "*"} +S: SUCCESS {"server": "#SERVER_AGENT#", "connection_id": "bolt-123456789"} +A: LOGON {"{}": "*"} +*: RESET +{? + C: RUN {"U": "*"} {"{}": "*"} {"{}": "*"} + S: SUCCESS {"fields": ["n.name"]} + {{ + C: PULL {"n": {"Z": "*"}} + ---- + C: DISCARD {"n": {"Z": "*"}} + }} + S: SUCCESS {"type": "w"} +?} +*: RESET +?: GOODBYE diff --git a/tests/stub/versions/test_versions.py b/tests/stub/versions/test_versions.py index 6deef64d1..552deebce 100644 --- a/tests/stub/versions/test_versions.py +++ b/tests/stub/versions/test_versions.py @@ -145,15 +145,21 @@ def test_supports_bolt5x1(self): def test_supports_bolt5x2(self): self._run("5x2") + @driver_feature(types.Feature.BOLT_5_3) + def test_supports_bolt5x3(self): + self._run("5x3") + def test_server_version(self): - for version in ("5x2", "5x1", "5x0", "4x4", "4x3", "4x2", "4x1", "3"): + for version in ("5x3", "5x2", "5x1", "5x0", "4x4", "4x3", "4x2", "4x1", + "3"): if not self.driver_supports_bolt(version): continue with self.subTest(version=version): self._run(version, check_version=True) def test_server_agent(self): - for version in ("5x2", "5x1", "5x0", "4x4", "4x3", "4x2", "4x1", "3"): + for version in ("5x3", "5x2", "5x1", "5x0", "4x4", "4x3", "4x2", "4x1", + "3"): for agent, reject in ( ("Neo4j/4.3.0", False), ("Neo4j/4.1.0", False), @@ -185,7 +191,8 @@ def test_server_address_in_summary(self): # TODO: remove block when all drivers support the address field if get_driver_name() in ["javascript", "dotnet"]: self.skipTest("Backend doesn't support server address in summary") - for version in ("5x2", "5x1", "5x0", "4x4", "4x3", "4x2", "4x1", "3"): + for version in ("5x3", "5x2", "5x1", "5x0", "4x4", "4x3", "4x2", "4x1", + "3"): if not self.driver_supports_bolt(version): continue with self.subTest(version=version): @@ -217,7 +224,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_3x0(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="3", script="v3_optional_hello.script" + version="3", script="v3_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_4_1) @@ -226,7 +233,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_4x1(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="4.1", script="v3_optional_hello.script" + version="4.1", script="v3_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_4_2) @@ -235,7 +242,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_4x2(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="4.2", script="v3_optional_hello.script" + version="4.2", script="v3_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_4_3) @@ -244,7 +251,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_4x3(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="4.3", script="v3_optional_hello.script" + version="4.3", script="v3_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_4_4) @@ -253,7 +260,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_4x4(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="4.4", script="v3_optional_hello.script" + version="4.4", script="v3_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_5_0) @@ -262,7 +269,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_5x0(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="5.0", script="v3_optional_hello.script" + version="5.0", script="v3_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_5_1) @@ -271,7 +278,7 @@ def test_should_reject_server_using_verify_connectivity_bolt_5x1(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="5.1", script="v5x1_optional_hello.script" + version="5.1", script="v5x1_and_up_optional_hello.script" ) @driver_feature(types.Feature.BOLT_5_2) @@ -280,7 +287,16 @@ def test_should_reject_server_using_verify_connectivity_bolt_5x2(self): if get_driver_name() in ["dotnet", "go", "javascript"]: self.skipTest("Skipped because it needs investigation") self._test_should_reject_server_using_verify_connectivity( - version="5.2", script="v5x1_optional_hello.script" + version="5.2", script="v5x1_and_up_optional_hello.script" + ) + + @driver_feature(types.Feature.BOLT_5_3) + def test_should_reject_server_using_verify_connectivity_bolt_5x3(self): + # TODO remove this block once fixed + if get_driver_name() in ["dotnet", "go", "javascript"]: + self.skipTest("Skipped because it needs investigation") + self._test_should_reject_server_using_verify_connectivity( + version="5.3", script="v5x1_and_up_optional_hello.script" ) def _test_should_reject_server_using_verify_connectivity(