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/integration-test/java/org/neo4j/shell/MainIntegrationTest.java b/cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java index 34cda178..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 @@ -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,16 @@ private void ensureUser() throws Exception { } } + private void ensureDefaultDatabaseStarted() throws Exception { + 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 public void promptsOnWrongAuthenticationIfInteractive() throws Exception { // when @@ -239,7 +252,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 +387,173 @@ 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(); + } + } + + @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 4aad2001..1863fe8d 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,8 @@ 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; import org.neo4j.shell.commands.CommandExecutable; import org.neo4j.shell.commands.CommandHelper; @@ -98,7 +100,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 +162,7 @@ public Optional> commitTransaction() throws CommandException { lastNeo4jErrorCode = null; return results; } catch (Neo4jException e) { - lastNeo4jErrorCode = e.code(); + lastNeo4jErrorCode = getErrorCode(e); throw e; } } @@ -175,7 +177,7 @@ public boolean isTransactionOpen() { return boltStateHandler.isTransactionOpen(); } - void setCommandHelper(@Nonnull CommandHelper commandHelper) { + public void setCommandHelper(@Nonnull CommandHelper commandHelper) { this.commandHelper = commandHelper; } @@ -195,7 +197,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 +232,23 @@ public void changePassword(@Nonnull ConnectionConfig connectionConfig) { public void disconnect() { boltStateHandler.disconnect(); } + + private String getErrorCode(Neo4jException e) { + 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 s : suppressed) { + if (s instanceof Neo4jException) { + statusException = (Neo4jException) s; + break; + } + } + + if (statusException instanceof ServiceUnavailableException || statusException instanceof DiscoveryException) { + // Treat this the same way as a DatabaseUnavailable error for now. + return DATABASE_UNAVAILABLE_ERROR_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..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 @@ -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/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()); } } } 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..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); } } @@ -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); 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")); + } } 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); + } }