diff --git a/nutkit/protocol/responses.py b/nutkit/protocol/responses.py index cf4423ccb..b12f8f774 100644 --- a/nutkit/protocol/responses.py +++ b/nutkit/protocol/responses.py @@ -267,4 +267,4 @@ def __init__(self, msg): self.msg = msg def __str__(self): - return "BackendError : " + self.msg + return "BackendError : %s" % self.msg diff --git a/tests/neo4j/datatypes.py b/tests/neo4j/datatypes.py index 6dc8c5085..6b6b28438 100644 --- a/tests/neo4j/datatypes.py +++ b/tests/neo4j/datatypes.py @@ -104,6 +104,30 @@ def testShouldEchoNestedLists(self): self.createDriverAndSession() self.verifyCanEcho(types.CypherList(test_lists)) + def testShouldEchoListOfMaps(self): + test_list = [ + types.CypherMap({ + "a": types.CypherInt(1), + "b": types.CypherInt(2) + }), + types.CypherMap({ + "c": types.CypherInt(3), + "d": types.CypherInt(4) + }) + ] + + self.createDriverAndSession() + self.verifyCanEcho(types.CypherList(test_list)) + + def testShouldEchoMapOfLists(self): + test_map = { + 'a': types.CypherList([types.CypherInt(1)]), + 'b': types.CypherList([types.CypherInt(2)]) + } + + self.createDriverAndSession() + self.verifyCanEcho(types.CypherMap(test_map)) + def testShouldEchoNode(self): self.createDriverAndSession() diff --git a/tests/neo4j/txrun.py b/tests/neo4j/txrun.py index 0041a0fe1..fd279368c 100644 --- a/tests/neo4j/txrun.py +++ b/tests/neo4j/txrun.py @@ -40,3 +40,116 @@ def test_does_not_update_last_bookmark_on_rollback(self): tx.rollback() bookmarks = self._session.lastBookmarks() self.assertEqual(len(bookmarks), 0) + + def test_does_not_update_last_bookmark_on_failure(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + self.assertThrows(Exception, lambda: tx.run("RETURN").next()) + self.assertThrows(Exception, lambda: tx.commit()) + bookmarks = self._session.lastBookmarks() + self.assertEqual(len(bookmarks), 0) + + def test_should_be_able_to_rollback_a_failure(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + self.assertThrows(Exception, lambda: tx.run("RETURN").next()) + tx.rollback() + + def test_should_not_rollback_a_rollbacked_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + tx.run('CREATE (:TXNode1)').consume() + tx.rollback() + self.assertThrows( + Exception, + lambda: tx.rollback() + ) + + def test_should_not_commit_a_rollbacked_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + tx.run('CREATE (:TXNode1)').consume() + tx.rollback() + self.assertThrows( + Exception, + lambda: tx.commit() + ) + + def test_should_not_rollback_a_commited_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + tx.run('CREATE (:TXNode1)').consume() + tx.commit() + self.assertThrows( + Exception, + lambda: tx.rollback() + ) + + def test_should_not_commit_a_commited_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + tx.run('CREATE (:TXNode1)').consume() + tx.commit() + self.assertThrows( + Exception, + lambda: tx.commit() + ) + + def test_should_run_valid_query_in_invalid_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + self.assertThrows( + Exception, + lambda: tx.run("NOT CYPHER").consume() + ) + self.assertThrows( + Exception, + lambda: tx.run("RETURN 42").next() + ) + tx.rollback() + + def test_should_fail_run_in_a_commited_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + tx.commit() + self.assertThrows( + Exception, + lambda: tx.run("RETURN 42").consume() + ) + + def test_should_fail_run_in_a_rollbacked_tx(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + tx.rollback() + self.assertThrows( + Exception, + lambda: tx.run("RETURN 42").consume() + ) + + def test_should_throws_exception_when_invalid_tx_params(self): + self._session = self._driver.session("w") + tx = self._session.beginTransaction() + self.assertThrows( + Exception, + lambda: tx.run("RETURN $value", "invalid").next() + ) + tx.rollback() + + def test_should_fail_to_run_query_for_unreacheable_bookmark(self): + self._session = self._driver.session("w") + tx1 = self._session.beginTransaction() + result = tx1.run('CREATE ()') + result.consume() + tx1.commit() + unreachableBookmark = self._session.lastBookmarks()[0] + "0" + self._session.close() + self._session = self._driver.session( + "w", + bookmarks=[unreachableBookmark] + ) + tx2 = self._session.beginTransaction() + self.assertThrows( + Exception, + lambda: tx2.run("CREATE ()").consume() + ) + tx2.rollback() diff --git a/tests/shared.py b/tests/shared.py index 8ce4b65ff..c56ed4553 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -50,3 +50,12 @@ def setUp(self): except Exception: self._backend.close() raise + + def assertThrows(self, exception, callable): + try: + callable() + except Exception as e: + if not isinstance(e, exception): + self.fail("%s is not instance of %s" % (e, exception)) + else: + self.fail("Expect to throw an exception") diff --git a/tests/stub/bookmark.py b/tests/stub/bookmark.py index 607bfdca5..4060dfa6e 100644 --- a/tests/stub/bookmark.py +++ b/tests/stub/bookmark.py @@ -20,6 +20,64 @@ S: SUCCESS {"bookmark": "bm"} """ +send_and_receive_bookmark_read_tx = """ +!: BOLT 4 +!: AUTO HELLO +!: AUTO RESET +!: AUTO GOODBYE + +C: BEGIN {"bookmarks": ["neo4j:bookmark:v1:tx42"], "mode": "r"} +C: RUN "MATCH (n) RETURN n.name AS name" {} {} + PULL {"n": 1000} +S: SUCCESS {} + SUCCESS {"fields": ["name"]} + SUCCESS {} +C: COMMIT +S: SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} + +""" + +send_and_receive_bookmark_write_tx = """ +!: BOLT 4 +!: AUTO HELLO +!: AUTO RESET +!: AUTO GOODBYE + +C: BEGIN {"bookmarks": #BOOKMARKS# } +C: RUN "MATCH (n) RETURN n.name AS name" {} {} + PULL {"n": 1000} +S: SUCCESS {} + SUCCESS {"fields": ["name"]} + SUCCESS {} +C: COMMIT +S: SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} + +""" + +sequecing_writing_and_reading_tx = """ +!: BOLT 4 +!: AUTO HELLO +!: AUTO RESET +!: AUTO GOODBYE + +C: BEGIN {"bookmarks": ["neo4j:bookmark:v1:tx42"]} +C: RUN "MATCH (n) RETURN n.name AS name" {} {} + PULL {"n": 1000} +S: SUCCESS {} + SUCCESS {"fields": ["name"]} + SUCCESS {} +C: COMMIT +S: SUCCESS {"bookmark": "neo4j:bookmark:v1:tx4242"} + +C: BEGIN {"bookmarks": ["neo4j:bookmark:v1:tx4242"]} +C: RUN "MATCH (n) RETURN n.name AS name" {} {} + PULL {"n": 1000} +S: SUCCESS {} + SUCCESS {"fields": ["name"]} + SUCCESS {} +C: COMMIT +S: SUCCESS {"bookmark": "neo4j:bookmark:v1:tx424242"} +""" # Tests bookmarks from transaction class Tx(TestkitTestCase): @@ -50,3 +108,90 @@ def test_last_bookmark(self): self._server.done() self.assertEqual(bookmarks, ["bm"]) + + def test_send_and_receive_bookmarks_read_tx(self): + self._server.start( + script=send_and_receive_bookmark_read_tx + ) + session = self._driver.session( + accessMode="r", + bookmarks=["neo4j:bookmark:v1:tx42"] + ) + tx = session.beginTransaction() + result = tx.run('MATCH (n) RETURN n.name AS name') + result.next() + tx.commit() + bookmarks = session.lastBookmarks() + + self.assertEqual(bookmarks, ["neo4j:bookmark:v1:tx4242"]) + self._server.done() + + def test_send_and_receive_bookmarks_write_tx(self): + self._server.start( + script=send_and_receive_bookmark_write_tx, + vars={ + "#BOOKMARKS#": '["neo4j:bookmark:v1:tx42"]' + } + ) + session = self._driver.session( + accessMode="w", + bookmarks=["neo4j:bookmark:v1:tx42"] + ) + tx = session.beginTransaction() + result = tx.run('MATCH (n) RETURN n.name AS name') + result.next() + tx.commit() + bookmarks = session.lastBookmarks() + + self.assertEqual(bookmarks, ["neo4j:bookmark:v1:tx4242"]) + self._server.done() + + def test_sequece_of_writing_and_reading_tx(self): + self._server.start( + script=sequecing_writing_and_reading_tx + ) + session = self._driver.session( + accessMode="w", + bookmarks=["neo4j:bookmark:v1:tx42"] + ) + tx = session.beginTransaction() + result = tx.run('MATCH (n) RETURN n.name AS name') + result.next() + tx.commit() + + bookmarks = session.lastBookmarks() + self.assertEqual(bookmarks, ["neo4j:bookmark:v1:tx4242"]) + + txRead = session.beginTransaction() + result = txRead.run('MATCH (n) RETURN n.name AS name') + result.next() + txRead.commit() + + bookmarks = session.lastBookmarks() + self.assertEqual(bookmarks, ["neo4j:bookmark:v1:tx424242"]) + + self._server.done() + + def test_send_and_receive_multiple_bookmarks_write_tx(self): + self._server.start( + script=send_and_receive_bookmark_write_tx, + vars={ + "#BOOKMARKS#": + '["neo4j:bookmark:v1:tx42", "neo4j:bookmark:v1:tx43"]' + } + ) + session = self._driver.session( + accessMode="w", + bookmarks=[ + "neo4j:bookmark:v1:tx42", + "neo4j:bookmark:v1:tx43" + ] + ) + tx = session.beginTransaction() + result = tx.run('MATCH (n) RETURN n.name AS name') + result.next() + tx.commit() + bookmarks = session.lastBookmarks() + + self.assertEqual(bookmarks, ["neo4j:bookmark:v1:tx4242"]) + self._server.done() diff --git a/tests/stub/disconnected.py b/tests/stub/disconnected.py index e9acb7273..0dd519e00 100644 --- a/tests/stub/disconnected.py +++ b/tests/stub/disconnected.py @@ -35,6 +35,19 @@ S: """ +script_on_reset = """ +!: BOLT 4 +!: AUTO HELLO +!: AUTO GOODBYE + +C: RUN "RETURN 1 as n" {} {} + PULL {"n": 1000} +S: SUCCESS {"fields": ["n"]} + RECORD [1] + SUCCESS {} +C: RESET +S: FAILURE {"code": "Neo.TransientError.General.DatabaseUnavailable", "message": "Unable to reset"} +""" class SessionRunDisconnected(TestkitTestCase): def setUp(self): @@ -118,6 +131,20 @@ def test_disconnect_on_pull(self): expected_step = "after run" self.assertEqual(step, expected_step) + def test_fail_on_reset(self): + self._server.start(script=script_on_reset) + step = self._run() + self._session.close() + accept_count = self._server.count_responses("") + hangup_count = self._server.count_responses("") + active_connections = accept_count - hangup_count + self._driver.close() + self._server.done() + self.assertEqual(step, "success") + self.assertEqual(accept_count, 1) + self.assertEqual(hangup_count, 1) + self.assertEqual(active_connections, 0) + def get_vars(self): return { "#EXTRA_HELLO_PARAMS#": self.get_extra_hello_props() diff --git a/tests/stub/serversiderouting.py b/tests/stub/serversiderouting.py new file mode 100644 index 000000000..6b1408e3a --- /dev/null +++ b/tests/stub/serversiderouting.py @@ -0,0 +1,87 @@ +from nutkit.frontend import Driver +from nutkit.protocol import AuthorizationToken +import nutkit.protocol as types +from tests.shared import ( + get_driver_name, + TestkitTestCase, +) +from tests.stub.shared import StubServer + + +direct_connection_without_routing_ssr_script = """ +!: BOLT #VERSION# +!: AUTO RESET +!: AUTO GOODBYE + +{{ + C: HELLO {"scheme": "basic", "credentials": "c", "principal": "p", "user_agent": "007", "realm": "", "ticket": "" } +---- + C: HELLO {"scheme": "basic", "credentials": "c", "principal": "p", "user_agent": "007", "realm": "" } +---- + C: HELLO {"scheme": "basic", "credentials": "c", "principal": "p", "user_agent": "007" } +}} + +S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "bolt-123456789"} + +C: RUN "RETURN 1 AS n" {} {} + PULL {"n": 1000} +S: SUCCESS {"fields": ["n.name"]} + SUCCESS {"type": "w"} +""" + + +class ServerSideRouting(TestkitTestCase): + """ Verifies that the driver behaves as expected when + in Server Side Routing scenarios + """ + + def setUp(self): + super().setUp() + self._server = StubServer(9001) + self._auth = types.AuthorizationToken( + scheme="basic", principal="p", credentials="c") + self._userAgent = "007" + + def tearDown(self): + self._server.stop() + super().tearDown() + + def test_direct_connection_without_url_params(self): + """ When a direct driver is created without params, it should not send + any information about the routing context in the HELLO message + to not enable Server Side Routing + """ + uri = "bolt://%s" % self._server.address + self._server.start(script=direct_connection_without_routing_ssr_script, + vars={"#VERSION#": "4.1"}) + + + driver = Driver(self._backend, uri, self._auth, self._userAgent) + session = driver.session("w", fetchSize=1000) + result = session.run("RETURN 1 AS n") + # Otherwise the script will not fail when the protocol is not present + # (on backends where run is lazily evaluated) + result.next() + session.close() + driver.close() + self._server.done() + + def test_direct_connection_with_url_params(self): + """ When a direct driver is created without params, + it should throw an exception + """ + params = "region=china&policy=my_policy" + uri = "bolt://%s?%s" % (self._server.address, params) + self._server.start(script=direct_connection_without_routing_ssr_script, + vars={ + "#VERSION#": "4.1" + }) + try: + driver = Driver(self._backend, uri, self._auth, self._userAgent) + except Exception: + pass + else: + driver.close() + self.fail('Should not create the driver') + +