diff --git a/build.gradle b/build.gradle index a54e995a..5a883ebe 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ dependencies { testCompile "junit:junit:4.11" testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' - testCompile "org.mockito:mockito-core:1.9.5" + testCompile "org.mockito:mockito-core:2.8.47" testCompile "org.apache.sshd:sshd-core:1.2.0" testRuntime "ch.qos.logback:logback-classic:1.1.2" testCompile 'org.glassfish.grizzly:grizzly-http-server:2.3.17' diff --git a/src/main/java/net/schmizz/sshj/userauth/password/ConsolePasswordFinder.java b/src/main/java/net/schmizz/sshj/userauth/password/ConsolePasswordFinder.java index f7456907..44b109e9 100644 --- a/src/main/java/net/schmizz/sshj/userauth/password/ConsolePasswordFinder.java +++ b/src/main/java/net/schmizz/sshj/userauth/password/ConsolePasswordFinder.java @@ -16,37 +16,96 @@ package net.schmizz.sshj.userauth.password; import java.io.Console; +import java.util.IllegalFormatException; /** A PasswordFinder that reads a password from a console */ public class ConsolePasswordFinder implements PasswordFinder { private final Console console; + private final String promptFormat; + private final int maxTries; - /** - * Initializes with the System Console, which will be null if not run from an interactive shell. - */ - public ConsolePasswordFinder() { - this(System.console()); + private int numTries; + + public static ConsolePasswordFinderBuilder builder() { + return new ConsolePasswordFinderBuilder(); } - /** - * @param console the console to read the password from. May be null. - */ - public ConsolePasswordFinder(Console console) { + public ConsolePasswordFinder(Console console, String promptFormat, int maxTries) { this.console = console; + this.promptFormat = promptFormat; + this.maxTries = maxTries; + this.numTries = 0; } @Override public char[] reqPassword(Resource resource) { + numTries++; if (console == null) { // the request cannot be serviced return null; } - return console.readPassword("Enter passphrase for %s:", resource.toString()); + return console.readPassword(promptFormat, resource.toString()); } @Override public boolean shouldRetry(Resource resource) { - return true; + return numTries < maxTries; + } + + public static class ConsolePasswordFinderBuilder { + private Console console; + private String promptFormat; + private int maxTries; + + /** Builder constructor should only be called from parent class */ + private ConsolePasswordFinderBuilder() { + console = System.console(); + promptFormat = "Enter passphrase for %s:"; + maxTries = 3; + } + + public ConsolePasswordFinder build() { + return new ConsolePasswordFinder(console, promptFormat, maxTries); + } + + public ConsolePasswordFinderBuilder setConsole(Console console) { + this.console = console; + return this; + } + + public Console getConsole() { + return console; + } + + /** + * @param promptFormat a StringFormatter string that may contain up to one "%s" + */ + public ConsolePasswordFinderBuilder setPromptFormat(String promptFormat) { + checkFormatString(promptFormat); + this.promptFormat = promptFormat; + return this; + } + + public String getPromptFormat() { + return promptFormat; + } + + public ConsolePasswordFinderBuilder setMaxTries(int maxTries) { + this.maxTries = maxTries; + return this; + } + + public int getMaxTries() { + return maxTries; + } + + private static void checkFormatString(String promptFormat) { + try { + String.format(promptFormat, ""); + } catch (IllegalFormatException e) { + throw new IllegalArgumentException("promptFormat must have no more than one %s and no other markers", e); + } + } } } diff --git a/src/test/java/net/schmizz/sshj/userauth/password/TestConsolePasswordFinder.java b/src/test/java/net/schmizz/sshj/userauth/password/TestConsolePasswordFinder.java index 3af5e204..8d27ca20 100644 --- a/src/test/java/net/schmizz/sshj/userauth/password/TestConsolePasswordFinder.java +++ b/src/test/java/net/schmizz/sshj/userauth/password/TestConsolePasswordFinder.java @@ -19,18 +19,69 @@ import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; +import java.io.Console; + public class TestConsolePasswordFinder { - /* - * Note that Mockito 1.9 cannot mock Console because it is a final class, - * so there are no other tests. - */ + @Test + public void testReqPassword() { + char[] expectedPassword = "password".toCharArray(); + + Console console = Mockito.mock(Console.class); + Mockito.when(console.readPassword(Mockito.anyString(), Mockito.any())) + .thenReturn(expectedPassword); + + Resource resource = Mockito.mock(Resource.class); + char[] password = ConsolePasswordFinder.builder() + .setConsole(console) + .build() + .reqPassword(resource); + + Assert.assertArrayEquals("Password should match mocked return value", + expectedPassword, password); + Mockito.verifyNoMoreInteractions(resource); + } @Test public void testReqPasswordNullConsole() { - char[] password = new ConsolePasswordFinder(null) - .reqPassword(Mockito.mock(Resource.class)); + Resource resource = Mockito.mock(Resource.class); + char[] password = ConsolePasswordFinder.builder() + .setConsole(null) + .build() + .reqPassword(resource); + Assert.assertNull("Password should be null with null console", password); + Mockito.verifyNoMoreInteractions(resource); + } + + @Test + public void testShouldRetry() { + Resource resource = new PrivateKeyStringResource(""); + ConsolePasswordFinder finder = ConsolePasswordFinder.builder() + .setConsole(null) + .setMaxTries(1) + .build(); + Assert.assertTrue("Should allow a retry at first", finder.shouldRetry(resource)); + + finder.reqPassword(resource); + Assert.assertFalse("Should stop allowing retries after one interaction", finder.shouldRetry(resource)); + } + + @Test + public void testPromptFormat() { + // expecting no Exceptions + ConsolePasswordFinder.builder().setPromptFormat(""); + ConsolePasswordFinder.builder().setPromptFormat("%s"); + } + + @Test(expected = IllegalArgumentException.class) + public void testPromptFormatTooManyMarkers() { + ConsolePasswordFinder.builder().setPromptFormat("%s%s"); + } + + @Test(expected = IllegalArgumentException.class) + public void testPromptFormatWrongMarkerType() { + ConsolePasswordFinder.builder().setPromptFormat("%d"); } } diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ef9230d2 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1,2 @@ +# incubating feature to allow mocking final classes +mock-maker-inline