From eeadad1541444301c6144c3cbba8a1c0d75ded8e Mon Sep 17 00:00:00 2001 From: Henrik Nyman Date: Fri, 29 Nov 2019 16:57:19 +0100 Subject: [PATCH 1/5] Fix connection when default database is unavailable We should be able to connect to another database or system database even when the default database is stopped. So we no longer use the Java driver verifyConnectivity() method since that is hard-wired to check connectivity toward the default database only. Also adapt to that the DatabaseUnavailable error message could be masked by the driver. Treat any ServiceUnavailable the same in interactive mode. --- .../org/neo4j/shell/MainIntegrationTest.java | 110 +++++++++++++++++- .../java/org/neo4j/shell/CypherShell.java | 20 +++- .../neo4j/shell/state/BoltStateHandler.java | 6 - 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java index 34cda178..bc329bea 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java @@ -14,6 +14,7 @@ import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.TransientException; import org.neo4j.shell.cli.CliArgs; import org.neo4j.shell.commands.CommandHelper; import org.neo4j.shell.exception.CommandException; @@ -27,9 +28,11 @@ import static java.lang.String.format; import static org.hamcrest.CoreMatchers.isA; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -102,6 +105,20 @@ private void ensureUser() throws Exception { } } + private void ensureDefaultDatabaseStarted() throws Exception { + CypherShell shellToUse = shell; + if (!shellToUse.isConnected()) { + CliArgs cliArgs = new CliArgs(); + cliArgs.setUsername("neo4j", ""); + cliArgs.setPassword("neo", ""); + cliArgs.setDatabase("system"); + ShellAndConnection sac = getShell(cliArgs); + main.connectMaybeInteractively(sac.shell, sac.connectionConfig, true, false); + shellToUse = sac.shell; + } + shellToUse.execute("START DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + } + @Test public void promptsOnWrongAuthenticationIfInteractive() throws Exception { // when @@ -239,7 +256,7 @@ public void wrongPortWithNeo4j() throws Exception ConnectionConfig connectionConfig = sac.connectionConfig; exception.expect( ServiceUnavailableException.class ); - exception.expectMessage( "Unable to connect to database, ensure the database is running and that there is a working network connection to it" ); + // The error message here may be subject to change and is not stable across versions so let us not assert on it main.connectMaybeInteractively( shell, connectionConfig, true, true ); } @@ -374,6 +391,97 @@ public void shouldFailIfInputFileDoesntExistInteractively() throws Exception { shell.execute( ":source what.cypher" ); } + @Test + public void doesNotStartWhenDefaultDatabaseUnavailableIfInteractive() throws Exception { + shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell)); + inputBuffer.put(String.format("neo4j%nneo%n").getBytes()); + + assertEquals("", connectionConfig.username()); + assertEquals("", connectionConfig.password()); + + // when + main.connectMaybeInteractively(shell, connectionConfig, true, true); + + // Multiple databases are only available from 4.0 + assumeTrue( majorVersion( shell.getServerVersion() ) >= 4 ); + + // then + // should be connected + assertTrue(shell.isConnected()); + // should have prompted and set the username and password + String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" ); + assertEquals(expectedLoginOutput, baos.toString()); + assertEquals("neo4j", connectionConfig.username()); + assertEquals("neo", connectionConfig.password()); + + // Stop the default database + shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME); + shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + + try { + shell.disconnect(); + + // Should get exception that database is unavailable when trying to connect + exception.expect(TransientException.class); + exception.expectMessage("Database 'neo4j' is unavailable"); + main.connectMaybeInteractively(shell, connectionConfig, true, true); + + // then + assertFalse(shell.isConnected()); + } finally { + // Start the default database again + ensureDefaultDatabaseStarted(); + } + } + + @Test + public void startsAgainstSystemDatabaseWhenDefaultDatabaseUnavailableIfInteractive() throws Exception { + shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell)); + + assertEquals("", connectionConfig.username()); + assertEquals("", connectionConfig.password()); + + // when + main.connectMaybeInteractively(shell, connectionConfig, true, true); + + // Multiple databases are only available from 4.0 + assumeTrue( majorVersion( shell.getServerVersion() ) >= 4 ); + + // then + // should be connected + assertTrue(shell.isConnected()); + // should have prompted and set the username and password + String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" ); + assertEquals(expectedLoginOutput, baos.toString()); + assertEquals("neo4j", connectionConfig.username()); + assertEquals("neo", connectionConfig.password()); + + // Stop the default database + shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME); + shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + + try { + shell.disconnect(); + + // Connect to system database + CliArgs cliArgs = new CliArgs(); + cliArgs.setUsername("neo4j", ""); + cliArgs.setPassword("neo", ""); + cliArgs.setDatabase("system"); + ShellAndConnection sac = getShell(cliArgs); + // Use the new shell and connection config from here on + shell = sac.shell; + connectionConfig = sac.connectionConfig; + main.connectMaybeInteractively(shell, connectionConfig, true, false); + + // then + assertTrue(shell.isConnected()); + } finally { + // Start the default database again + ensureDefaultDatabaseStarted(); + } + } + private String executeFileNonInteractively(String filename) throws Exception { return executeFileNonInteractively(filename, mock(Logger.class)); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java index 4aad2001..1c5f2de4 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -1,6 +1,7 @@ package org.neo4j.shell; import org.neo4j.driver.exceptions.Neo4jException; +import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.shell.commands.Command; import org.neo4j.shell.commands.CommandExecutable; import org.neo4j.shell.commands.CommandHelper; @@ -98,7 +99,7 @@ private void executeCypher(@Nonnull final String cypher) throws CommandException }); lastNeo4jErrorCode = null; } catch (Neo4jException e) { - lastNeo4jErrorCode = e.code(); + lastNeo4jErrorCode = getErrorCode(e); throw e; } } @@ -160,7 +161,7 @@ public Optional> commitTransaction() throws CommandException { lastNeo4jErrorCode = null; return results; } catch (Neo4jException e) { - lastNeo4jErrorCode = e.code(); + lastNeo4jErrorCode = getErrorCode(e); throw e; } } @@ -195,7 +196,7 @@ public void setActiveDatabase(String databaseName) throws CommandException boltStateHandler.setActiveDatabase(databaseName); lastNeo4jErrorCode = null; } catch (Neo4jException e) { - lastNeo4jErrorCode = e.code(); + lastNeo4jErrorCode = getErrorCode(e); throw e; } } @@ -230,4 +231,17 @@ public void changePassword(@Nonnull ConnectionConfig connectionConfig) { public void disconnect() { boltStateHandler.disconnect(); } + + private String getErrorCode(Neo4jException e) { + if (e instanceof ServiceUnavailableException) { + // Unwrap possible transient Neo4jExceptions + Throwable cause = e.getCause(); + if (cause instanceof Neo4jException) { + return ((Neo4jException) cause).code(); + } + // Treat this the same way as a DatabaseUnavailable error for now. + return DATABASE_UNAVAILABLE_ERROR_CODE; + } + return e.code(); + } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java index 1d41684c..36c9c11b 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java @@ -174,9 +174,6 @@ private void reconnect() { } private void reconnect(boolean keepBookmark) { - // This will already throw an exception if there is no connectivity - driver.verifyConnectivity(); - SessionConfig.Builder builder = SessionConfig.builder(); builder.withDefaultAccessMode(AccessMode.WRITE); if (!ABSENT_DB_NAME.equals(activeDatabaseNameAsSetByUser)) { @@ -268,9 +265,6 @@ public void changePassword(@Nonnull ConnectionConfig connectionConfig) { try { driver = getDriver(connectionConfig, authToken); - // This will already throw an exception if there is no connectivity - driver.verifyConnectivity(); - SessionConfig.Builder builder = SessionConfig.builder() .withDefaultAccessMode(AccessMode.WRITE) .withDatabase(SYSTEM_DB_NAME); From a7580d706405a7eccab73d294762aabcab3f6731 Mon Sep 17 00:00:00 2001 From: Henrik Nyman Date: Mon, 2 Dec 2019 14:40:17 +0100 Subject: [PATCH 2/5] Improve error message and status when database switching fails --- .../main/java/org/neo4j/shell/CypherShell.java | 16 +++++++++++++--- .../neo4j/shell/cli/InteractiveShellRunner.java | 5 +---- .../org/neo4j/shell/state/BoltStateHandler.java | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java index 1c5f2de4..c6cdc846 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -233,15 +233,25 @@ public void disconnect() { } private String getErrorCode(Neo4jException e) { - if (e instanceof ServiceUnavailableException) { + Neo4jException statusException = e; + + // If we encountered a later suppressed Neo4jException we use that as the basis for the status instead + Throwable[] suppressed = e.getSuppressed(); + for (Throwable t : suppressed) { + if (t instanceof Neo4jException) { + statusException = (Neo4jException) t; + } + } + + if (statusException instanceof ServiceUnavailableException) { // Unwrap possible transient Neo4jExceptions - Throwable cause = e.getCause(); + Throwable cause = statusException.getCause(); if (cause instanceof Neo4jException) { return ((Neo4jException) cause).code(); } // Treat this the same way as a DatabaseUnavailable error for now. return DATABASE_UNAVAILABLE_ERROR_CODE; } - return e.code(); + return statusException.code(); } } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java b/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java index d85e61e0..da42e8e6 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java @@ -166,14 +166,11 @@ AnsiFormattedText updateAndGetPrompt() { } String databaseName = databaseManager.getActualDatabaseAsReportedByServer(); - if (databaseName == null) { + if (databaseName == null || ABSENT_DB_NAME.equals(databaseName)) { // We have failed to get a successful response from the connection ping query // Build the prompt from the db name as set by the user + a suffix indicating that we are in a disconnected state String dbNameSetByUser = databaseManager.getActiveDatabaseAsSetByUser(); databaseName = ABSENT_DB_NAME.equals(dbNameSetByUser)? UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT : dbNameSetByUser; - } else if (ABSENT_DB_NAME.equals(databaseName)) { - // The driver did not give us a database name in the response from the connection ping query - databaseName = UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT; } String errorSuffix = getErrorPrompt(executer.lastNeo4jErrorCode()); diff --git a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java index 36c9c11b..01382ad8 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/state/BoltStateHandler.java @@ -83,7 +83,7 @@ public void setActiveDatabase(String databaseName) throws CommandException try { reconnect(true); } - catch (ClientException e2) { + catch (Exception e2) { e.addSuppressed(e2); } } From d0c3e5a14a276c3a0eeae0961e9b45b175c0ca74 Mon Sep 17 00:00:00 2001 From: Henrik Nyman Date: Tue, 3 Dec 2019 00:48:03 +0100 Subject: [PATCH 3/5] Adapt error messages to Java driver 4.0.0-rc2 It will become possible to get the cause of ServiceUnavailableExceptions. --- .../org/neo4j/shell/MainIntegrationTest.java | 94 ++++++++++++++++--- .../java/org/neo4j/shell/CypherShell.java | 15 ++- .../java/org/neo4j/shell/log/AnsiLogger.java | 21 ++++- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java index bc329bea..03c1a203 100644 --- a/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java +++ b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java @@ -106,17 +106,13 @@ private void ensureUser() throws Exception { } private void ensureDefaultDatabaseStarted() throws Exception { - CypherShell shellToUse = shell; - if (!shellToUse.isConnected()) { - CliArgs cliArgs = new CliArgs(); - cliArgs.setUsername("neo4j", ""); - cliArgs.setPassword("neo", ""); - cliArgs.setDatabase("system"); - ShellAndConnection sac = getShell(cliArgs); - main.connectMaybeInteractively(sac.shell, sac.connectionConfig, true, false); - shellToUse = sac.shell; - } - shellToUse.execute("START DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + CliArgs cliArgs = new CliArgs(); + cliArgs.setUsername("neo4j", ""); + cliArgs.setPassword("neo", ""); + cliArgs.setDatabase("system"); + ShellAndConnection sac = getShell(cliArgs); + main.connectMaybeInteractively(sac.shell, sac.connectionConfig, true, false); + sac.shell.execute("START DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); } @Test @@ -482,6 +478,82 @@ public void startsAgainstSystemDatabaseWhenDefaultDatabaseUnavailableIfInteracti } } + @Test + public void switchingToUnavailableDatabaseIfInteractive() throws Exception { + shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell)); + inputBuffer.put(String.format("neo4j%nneo%n").getBytes()); + + assertEquals("", connectionConfig.username()); + assertEquals("", connectionConfig.password()); + + // when + main.connectMaybeInteractively(shell, connectionConfig, true, true); + + // Multiple databases are only available from 4.0 + assumeTrue(majorVersion( shell.getServerVersion() ) >= 4); + + // then + // should be connected + assertTrue(shell.isConnected()); + // should have prompted and set the username and password + String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" ); + assertEquals(expectedLoginOutput, baos.toString()); + assertEquals("neo4j", connectionConfig.username()); + assertEquals("neo", connectionConfig.password()); + + // Stop the default database + shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME); + shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + + try { + // Should get exception that database is unavailable when trying to connect + exception.expect(TransientException.class); + exception.expectMessage("Database 'neo4j' is unavailable"); + shell.execute(":use " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + } finally { + // Start the default database again + ensureDefaultDatabaseStarted(); + } + } + + @Test + public void switchingToUnavailableDefaultDatabaseIfInteractive() throws Exception { + shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell)); + inputBuffer.put(String.format("neo4j%nneo%n").getBytes()); + + assertEquals("", connectionConfig.username()); + assertEquals("", connectionConfig.password()); + + // when + main.connectMaybeInteractively(shell, connectionConfig, true, true); + + // Multiple databases are only available from 4.0 + assumeTrue(majorVersion( shell.getServerVersion() ) >= 4); + + // then + // should be connected + assertTrue(shell.isConnected()); + // should have prompted and set the username and password + String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" ); + assertEquals(expectedLoginOutput, baos.toString()); + assertEquals("neo4j", connectionConfig.username()); + assertEquals("neo", connectionConfig.password()); + + // Stop the default database + shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME); + shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME); + + try { + // Should get exception that database is unavailable when trying to connect + exception.expect(TransientException.class); + exception.expectMessage("Database 'neo4j' is unavailable"); + shell.execute(":use"); + } finally { + // Start the default database again + ensureDefaultDatabaseStarted(); + } + } + private String executeFileNonInteractively(String filename) throws Exception { return executeFileNonInteractively(filename, mock(Logger.class)); } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java index c6cdc846..9cd138f3 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -1,5 +1,6 @@ package org.neo4j.shell; +import org.neo4j.driver.exceptions.DiscoveryException; import org.neo4j.driver.exceptions.Neo4jException; import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.shell.commands.Command; @@ -237,18 +238,14 @@ private String getErrorCode(Neo4jException e) { // If we encountered a later suppressed Neo4jException we use that as the basis for the status instead Throwable[] suppressed = e.getSuppressed(); - for (Throwable t : suppressed) { - if (t instanceof Neo4jException) { - statusException = (Neo4jException) t; + for (Throwable s : suppressed) { + if (s instanceof Neo4jException) { + statusException = (Neo4jException) s; + break; } } - if (statusException instanceof ServiceUnavailableException) { - // Unwrap possible transient Neo4jExceptions - Throwable cause = statusException.getCause(); - if (cause instanceof Neo4jException) { - return ((Neo4jException) cause).code(); - } + if (statusException instanceof ServiceUnavailableException || statusException instanceof DiscoveryException) { // Treat this the same way as a DatabaseUnavailable error for now. return DATABASE_UNAVAILABLE_ERROR_CODE; } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java b/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java index 3e11d0d6..cbc29c7d 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java @@ -3,6 +3,8 @@ import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.exceptions.DiscoveryException; +import org.neo4j.driver.exceptions.ServiceUnavailableException; import org.neo4j.shell.cli.Format; import org.neo4j.shell.exception.AnsiFormattedException; @@ -132,10 +134,23 @@ String getFormattedMessage(@Nonnull final Throwable e) { .append("\nor as environment variable(s), NEO4J_USERNAME, and NEO4J_PASSWORD respectively.") .append("\nSee --help for more info."); } else { - if (e.getMessage() != null) { - msg = msg.append(e.getMessage()); + Throwable cause = e; + + // Get the suppressed root cause of ServiceUnavailableExceptions + if (e instanceof ServiceUnavailableException) { + Throwable[] suppressed = e.getSuppressed(); + for (Throwable s : suppressed) { + if (s instanceof DiscoveryException ) { + cause = getRootCause(s); + break; + } + } + } + + if (cause.getMessage() != null) { + msg = msg.append(cause.getMessage()); } else { - msg = msg.append(e.getClass().getSimpleName()); + msg = msg.append(cause.getClass().getSimpleName()); } } } From 1c82665a7d86a101be1fdedc5e4291f999c4a264 Mon Sep 17 00:00:00 2001 From: Henrik Nyman Date: Tue, 3 Dec 2019 16:54:59 +0100 Subject: [PATCH 4/5] Add more tests --- .../java/org/neo4j/shell/CypherShell.java | 2 +- .../shell/cli/InteractiveShellRunner.java | 4 +- .../org/neo4j/shell/OfflineTestShell.java | 3 +- .../shell/cli/InteractiveShellRunnerTest.java | 189 ++++++++++++++++++ 4 files changed, 193 insertions(+), 5 deletions(-) diff --git a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java index 9cd138f3..1863fe8d 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java @@ -177,7 +177,7 @@ public boolean isTransactionOpen() { return boltStateHandler.isTransactionOpen(); } - void setCommandHelper(@Nonnull CommandHelper commandHelper) { + public void setCommandHelper(@Nonnull CommandHelper commandHelper) { this.commandHelper = commandHelper; } diff --git a/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java b/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java index da42e8e6..8553286d 100644 --- a/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java +++ b/cypher-shell/src/main/java/org/neo4j/shell/cli/InteractiveShellRunner.java @@ -39,8 +39,8 @@ public class InteractiveShellRunner implements ShellRunner, SignalHandler { private final static String TRANSACTION_PROMPT = "# "; private final static String USERNAME_DB_DELIMITER = "@"; private final static int ONELINE_PROMPT_MAX_LENGTH = 50; - private static final String UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT = ""; - private static final String DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT = "[UNAVAILABLE]"; + static final String UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT = ""; + static final String DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT = "[UNAVAILABLE]"; // Need to know if we are currently executing when catch Ctrl-C, needs to be atomic due to // being called from different thread diff --git a/cypher-shell/src/test/java/org/neo4j/shell/OfflineTestShell.java b/cypher-shell/src/test/java/org/neo4j/shell/OfflineTestShell.java index 7b414756..7af1bfb7 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/OfflineTestShell.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/OfflineTestShell.java @@ -1,6 +1,5 @@ package org.neo4j.shell; - import org.neo4j.shell.log.Logger; import org.neo4j.shell.prettyprint.PrettyPrinter; import org.neo4j.shell.state.BoltStateHandler; @@ -12,7 +11,7 @@ */ public class OfflineTestShell extends CypherShell { - OfflineTestShell( Logger logger, BoltStateHandler boltStateHandler, PrettyPrinter prettyPrinter ) { + public OfflineTestShell(Logger logger, BoltStateHandler boltStateHandler, PrettyPrinter prettyPrinter) { super(logger, boltStateHandler, prettyPrinter, new ShellParameterMap()); } diff --git a/cypher-shell/src/test/java/org/neo4j/shell/cli/InteractiveShellRunnerTest.java b/cypher-shell/src/test/java/org/neo4j/shell/cli/InteractiveShellRunnerTest.java index c823ef2e..f789b800 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/cli/InteractiveShellRunnerTest.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/cli/InteractiveShellRunnerTest.java @@ -6,28 +6,36 @@ import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.exceptions.DiscoveryException; +import org.neo4j.driver.exceptions.ServiceUnavailableException; +import org.neo4j.driver.exceptions.TransientException; import org.neo4j.shell.ConnectionConfig; import org.neo4j.shell.CypherShell; import org.neo4j.shell.DatabaseManager; import org.neo4j.shell.Historian; +import org.neo4j.shell.OfflineTestShell; import org.neo4j.shell.ShellParameterMap; import org.neo4j.shell.StatementExecuter; import org.neo4j.shell.TransactionHandler; import org.neo4j.shell.UserMessagesHandler; +import org.neo4j.shell.commands.CommandHelper; import org.neo4j.shell.exception.CommandException; import org.neo4j.shell.exception.ExitException; import org.neo4j.shell.exception.NoMoreInputException; import org.neo4j.shell.log.AnsiFormattedText; +import org.neo4j.shell.log.AnsiLogger; import org.neo4j.shell.log.Logger; import org.neo4j.shell.parser.ShellStatementParser; import org.neo4j.shell.parser.StatementParser; import org.neo4j.shell.prettyprint.OutputFormatter; import org.neo4j.shell.prettyprint.PrettyPrinter; import org.neo4j.shell.state.BoltStateHandler; + import sun.misc.Signal; import javax.annotation.Nonnull; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -37,6 +45,7 @@ import java.util.concurrent.atomic.AtomicReference; import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; @@ -49,6 +58,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.neo4j.shell.cli.InteractiveShellRunner.DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT; public class InteractiveShellRunnerTest { @Rule @@ -293,6 +303,78 @@ public void testPrompt() throws Exception { assertEquals(OutputFormatter.repeat(' ', wantedPrompt.length()), prompt.plainString()); } + @Test + public void testPromptShowDatabaseAsSetByUserWhenServerReportNull() throws Exception { + // given + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + InteractiveShellRunner runner = new InteractiveShellRunner(cmdExecuter, txHandler, databaseManager, logger, statementParser, inputStream, + historyFile, userMessagesHandler, connectionConfig); + + // when + when(txHandler.isTransactionOpen()).thenReturn(false); + when(databaseManager.getActiveDatabaseAsSetByUser()).thenReturn("foo"); + when(databaseManager.getActualDatabaseAsReportedByServer()).thenReturn(null); + AnsiFormattedText prompt = runner.updateAndGetPrompt(); + + // then + String wantedPrompt = "myusername@foo> "; + assertEquals(wantedPrompt, prompt.plainString()); + } + + @Test + public void testPromptShowDatabaseAsSetByUserWhenServerReportAbsent() throws Exception { + // given + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + InteractiveShellRunner runner = new InteractiveShellRunner(cmdExecuter, txHandler, databaseManager, logger, statementParser, inputStream, + historyFile, userMessagesHandler, connectionConfig); + + // when + when(txHandler.isTransactionOpen()).thenReturn(false); + when(databaseManager.getActiveDatabaseAsSetByUser()).thenReturn("foo"); + when(databaseManager.getActualDatabaseAsReportedByServer()).thenReturn(DatabaseManager.ABSENT_DB_NAME); + AnsiFormattedText prompt = runner.updateAndGetPrompt(); + + // then + String wantedPrompt = "myusername@foo> "; + assertEquals(wantedPrompt, prompt.plainString()); + } + + @Test + public void testPromptShowUnresolvedDefaultDatabaseWhenServerReportNull() throws Exception { + // given + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + InteractiveShellRunner runner = new InteractiveShellRunner(cmdExecuter, txHandler, databaseManager, logger, statementParser, inputStream, + historyFile, userMessagesHandler, connectionConfig); + + // when + when(txHandler.isTransactionOpen()).thenReturn(false); + when(databaseManager.getActiveDatabaseAsSetByUser()).thenReturn(DatabaseManager.ABSENT_DB_NAME); + when(databaseManager.getActualDatabaseAsReportedByServer()).thenReturn(null); + AnsiFormattedText prompt = runner.updateAndGetPrompt(); + + // then + String wantedPrompt = format("myusername@%s> ", InteractiveShellRunner.UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT); + assertEquals(wantedPrompt, prompt.plainString()); + } + + @Test + public void testPromptShowUnresolvedDefaultDatabaseWhenServerReportAbsent() throws Exception { + // given + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + InteractiveShellRunner runner = new InteractiveShellRunner(cmdExecuter, txHandler, databaseManager, logger, statementParser, inputStream, + historyFile, userMessagesHandler, connectionConfig); + + // when + when(txHandler.isTransactionOpen()).thenReturn(false); + when(databaseManager.getActiveDatabaseAsSetByUser()).thenReturn(DatabaseManager.ABSENT_DB_NAME); + when(databaseManager.getActualDatabaseAsReportedByServer()).thenReturn(DatabaseManager.ABSENT_DB_NAME); + AnsiFormattedText prompt = runner.updateAndGetPrompt(); + + // then + String wantedPrompt = format("myusername@%s> ", InteractiveShellRunner.UNRESOLVED_DEFAULT_DB_PROPMPT_TEXT); + assertEquals(wantedPrompt, prompt.plainString()); + } + @Test public void testLongPrompt() throws Exception { // given @@ -499,4 +581,111 @@ public String getActualDatabaseAsReportedByServer() { return DEFAULT_DEFAULT_DB_NAME; } } + + private static class TestInteractiveShellRunner { + InteractiveShellRunner runner; + ByteArrayOutputStream output; + ByteArrayOutputStream error; + BoltStateHandler mockedBoltStateHandler; + + TestInteractiveShellRunner(InteractiveShellRunner runner, ByteArrayOutputStream output, + ByteArrayOutputStream error, BoltStateHandler mockedBoltStateHandler) { + this.runner = runner; + this.output = output; + this.error = error; + this.mockedBoltStateHandler = mockedBoltStateHandler; + } + } + + private TestInteractiveShellRunner setupInteractiveTestShellRunner(String input) throws Exception { + // NOTE: Tests using this will test a bit more of the stack using OfflineTestShell + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ByteArrayOutputStream error = new ByteArrayOutputStream(); + + BoltStateHandler mockedBoltStateHandler = mock(BoltStateHandler.class); + when(mockedBoltStateHandler.getServerVersion()).thenReturn(""); + + final PrettyPrinter mockedPrettyPrinter = mock(PrettyPrinter.class); + + Logger logger = new AnsiLogger(false, Format.VERBOSE, new PrintStream(output), new PrintStream(error)); + + OfflineTestShell offlineTestShell = new OfflineTestShell(logger, mockedBoltStateHandler, mockedPrettyPrinter); + CommandHelper commandHelper = new CommandHelper(logger, Historian.empty, offlineTestShell); + offlineTestShell.setCommandHelper(commandHelper); + + InputStream inputStream = new ByteArrayInputStream(input.getBytes()); + InteractiveShellRunner runner = new InteractiveShellRunner(offlineTestShell, offlineTestShell, offlineTestShell, logger, + new ShellStatementParser(), inputStream, historyFile, userMessagesHandler, connectionConfig); + + return new TestInteractiveShellRunner(runner, output, error, mockedBoltStateHandler); + } + + @Test + public void testSwitchToUnavailableDatabase1() throws Exception { + // given + String input = ":use foo;\n"; + TestInteractiveShellRunner sr = setupInteractiveTestShellRunner(input); + + // when + when(sr.mockedBoltStateHandler.getActualDatabaseAsReportedByServer()).thenReturn("foo"); + doThrow(new TransientException(DatabaseManager.DATABASE_UNAVAILABLE_ERROR_CODE, "Not available")) + .when(sr.mockedBoltStateHandler).setActiveDatabase("foo"); + + sr.runner.runUntilEnd(); + + // then + assertThat(sr.output.toString(), containsString(format("myusername@foo%s> ", DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT))); + assertThat(sr.error.toString(), containsString("Not available")); + } + + @Test + public void testSwitchToUnavailableDatabase2() throws Exception { + // given + String input = ":use foo;\n"; + TestInteractiveShellRunner sr = setupInteractiveTestShellRunner(input); + + // when + when(sr.mockedBoltStateHandler.getActualDatabaseAsReportedByServer()).thenReturn("foo"); + doThrow(new ServiceUnavailableException("Not available")).when(sr.mockedBoltStateHandler).setActiveDatabase("foo"); + + sr.runner.runUntilEnd(); + + // then + assertThat(sr.output.toString(), containsString(format("myusername@foo%s> ", DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT))); + assertThat(sr.error.toString(), containsString("Not available")); + } + + @Test + public void testSwitchToUnavailableDatabase3() throws Exception { + // given + String input = ":use foo;\n"; + TestInteractiveShellRunner sr = setupInteractiveTestShellRunner(input); + + // when + when(sr.mockedBoltStateHandler.getActualDatabaseAsReportedByServer()).thenReturn("foo"); + doThrow(new DiscoveryException("Not available", null)).when(sr.mockedBoltStateHandler).setActiveDatabase("foo"); + + sr.runner.runUntilEnd(); + + // then + assertThat(sr.output.toString(), containsString(format("myusername@foo%s> ", DATABASE_UNAVAILABLE_ERROR_PROMPT_TEXT))); + assertThat(sr.error.toString(), containsString("Not available")); + } + + @Test + public void testSwitchToNonExistingDatabase() throws Exception { + // given + String input = ":use foo;\n"; + TestInteractiveShellRunner sr = setupInteractiveTestShellRunner(input); + + // when + when(sr.mockedBoltStateHandler.getActualDatabaseAsReportedByServer()).thenReturn("mydb"); + doThrow(new ClientException("Non existing")).when(sr.mockedBoltStateHandler).setActiveDatabase("foo"); + + sr.runner.runUntilEnd(); + + // then + assertThat(sr.output.toString(), containsString("myusername@mydb> ")); + assertThat(sr.error.toString(), containsString("Non existing")); + } } From 2732f2ffcf6d2ffc3bb3f2043896bab48abeeb80 Mon Sep 17 00:00:00 2001 From: Henrik Nyman Date: Wed, 4 Dec 2019 16:49:54 +0100 Subject: [PATCH 5/5] Upgrade to Neo4j Java driver 4.0.0-rc2 --- build.gradle | 2 +- .../java/org/neo4j/shell/test/bolt/FakeDriver.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d2fac085..aaecfd8b 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { argparse4jVersion = '0.7.0' junitVersion = '4.12' evaluatorVersion = '3.5.4' - neo4jJavaDriverVersion = '4.0.0-rc1' + neo4jJavaDriverVersion = '4.0.0-rc2' findbugsVersion = '3.0.0' jansiVersion = '1.13' jlineVersion = '2.14.6' diff --git a/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeDriver.java b/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeDriver.java index 22c3b39c..845df647 100644 --- a/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeDriver.java +++ b/cypher-shell/src/test/java/org/neo4j/shell/test/bolt/FakeDriver.java @@ -11,6 +11,8 @@ import java.util.concurrent.CompletionStage; +import static java.util.concurrent.CompletableFuture.completedFuture; + public class FakeDriver implements Driver { @Override public boolean isEncrypted() { @@ -79,4 +81,16 @@ public void verifyConnectivity() { public CompletionStage verifyConnectivityAsync() { return null; } + + @Override + public boolean supportsMultiDb() + { + return true; + } + + @Override + public CompletionStage supportsMultiDbAsync() + { + return completedFuture(true); + } }