package com.nulabinc.zxcvbn; import com.nulabinc.zxcvbn.guesses.*; import com.nulabinc.zxcvbn.matchers.Match; import com.nulabinc.zxcvbn.matchers.MatchFactory; import org.junit.Test; import org.junit.experimental.runners.Enclosed; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.lang.reflect.Method; import java.util.*; import static org.junit.Assert.assertEquals; @RunWith(Enclosed.class) public class ScoringTest { @RunWith(Parameterized.class) public static class NckTest { private int n; private int k; private int expected; public NckTest(int n, int k, int expected) { this.n = n; this.k = k; this.expected = expected; } @Test public void testNck() throws Exception { BaseGuess obj = new BaseGuess() { @Override public double exec(Match match) { return 0; } }; Method method = BaseGuess.class.getDeclaredMethod("nCk", int.class, int.class); method.setAccessible(true); String msg = String.format("nCk(%s, %s) == %s", n, k, expected); assertEquals(msg, expected, method.invoke(obj, n, k)); } @Parameterized.Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {0, 0, 1}, {1, 0, 1}, {5, 0, 1}, {0, 1, 0}, {0, 5, 0}, {2, 1, 2}, {4, 2, 6}, {33, 7, 4272048} }); } } @RunWith(Parameterized.class) public static class RepeatGuessesTest { private String token; private String baseToken; private int repeatCount; public RepeatGuessesTest(String token, String baseToken, int repeatCount) { this.token = token; this.baseToken = baseToken; this.repeatCount = repeatCount; } @Test public void testRepeatGuesses() throws Exception { double baseGuesses = Scoring.mostGuessableMatchSequence( baseToken, new Matching().omnimatch(baseToken)).getGuesses(); Match match = new Match.Builder(Pattern.Repeat, 0, 0, token) .baseToken(baseToken) .baseGuesses(baseGuesses) .repeatCount(repeatCount) .build(); double expectedGuesses = baseGuesses * repeatCount; String msg = String.format("the repeat pattern '%s' has guesses of %s", token, expectedGuesses); assertEquals(msg, expectedGuesses, new RepeatGuess().exec(match), 0.0); } @Parameterized.Parameters(name = "{0}") public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {"aa", "a", 2}, {"999", "9", 3}, {"$$$$", "$", 4}, {"abab", "ab", 2}, {"batterystaplebatterystaplebatterystaple", "batterystaple", 3} }); } } @RunWith(Parameterized.class) public static class SequenceGuessesTest { private String token; private boolean ascending; private int expectedGuesses; public SequenceGuessesTest(String token, boolean ascending, int expectedGuesses) { this.token = token; this.ascending = ascending; this.expectedGuesses = expectedGuesses; } @Test public void testSequenceGuesses() throws Exception { Match match = new Match.Builder(Pattern.Sequence, 0, 0, token).ascending(ascending).build(); String msg = String.format("the sequence pattern '%s' has guesses of %s", token, expectedGuesses); assertEquals(msg, expectedGuesses, new SequenceGuess().exec(match), 0.0); } @Parameterized.Parameters(name = "{0}") public static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {"ab", true, 4 * 2}, {"XYZ", true, 26 * 3}, {"4567", true, 10 * 4}, {"7654", false, 10 * 4 * 2}, {"ZYX", false, 4 * 3 * 2} }); } } public static class DictionaryGuessesTest { @Test public void testDictionaryGuessesSameWithRank() throws Exception { Match match = new Match.Builder(Pattern.Dictionary, 0, 0, "aaaa").rank(32).build(); String msg = "base guesses == the rank"; assertEquals(msg, 32, new DictionaryGuess().exec(match), 0.0); } @Test public void testDictionaryGuessesCapitalization() throws Exception { Match match = new Match.Builder(Pattern.Dictionary, 0, 0, "AAAaaa").rank(32).build(); String msg = "extra guesses are added for capitalization"; assertEquals(msg, 32 * new DictionaryGuess().uppercaseVariations(match), new DictionaryGuess().exec(match), 0.0); } @Test public void testDictionaryGuessesReverse() throws Exception { Match match = new Match.Builder(Pattern.Dictionary, 0, 0, "aaa").reversed(true).rank(32).build(); String msg = "guesses are doubled when word is reversed"; assertEquals(msg, 32 * 2, new DictionaryGuess().exec(match), 0.0); } @Test public void testDictionaryGuesses133t() throws Exception { Map<Character, Character> sub = new HashMap<>(); sub.put('@', 'a'); Match match = new Match.Builder(Pattern.Dictionary, 0, 0, "aaa@@@").sub(sub).l33t(true).rank(32).build(); String msg = "extra guesses are added for common l33t substitutions"; assertEquals(msg, 32 * new DictionaryGuess().l33tVariations(match), new DictionaryGuess().exec(match), 0.0); } @Test public void testDictionaryGuessesMixed() throws Exception { Map<Character, Character> sub = new HashMap<>(); sub.put('@', 'a'); Match match = new Match.Builder(Pattern.Dictionary, 0, 0, "AaA@@@").sub(sub).l33t(true).rank(32).build(); String msg = "extra guesses are added for both capitalization and common l33t substitutions"; int expected = 32 * new DictionaryGuess().l33tVariations(match) * new DictionaryGuess().uppercaseVariations(match); assertEquals(msg, expected, new DictionaryGuess().exec(match), 0.0); } } @RunWith(Parameterized.class) public static class UppercaseVariantsTest { private String word; private int variants; public UppercaseVariantsTest(String word, int variants) { this.word = word; this.variants = variants; } @Test public void testUppercaseVariants() throws Exception { DictionaryGuess dictionaryGuess = new DictionaryGuess(); Method uppercaseVariationsMethod = DictionaryGuess.class.getDeclaredMethod("uppercaseVariations", Match.class); uppercaseVariationsMethod.setAccessible(true); Match match = new Match.Builder(Pattern.Dictionary, 0, 0, word).sub(new HashMap<Character, Character>()).l33t(true).build(); String msg = String.format("guess multiplier of %s is %s", word, variants); assertEquals(msg, variants, uppercaseVariationsMethod.invoke(dictionaryGuess, match)); } @Parameterized.Parameters(name = "{0}") public static Collection<Object[]> data() throws Exception { BaseGuess baseGuess = new BaseGuess() { @Override public double exec(Match match) { return 0; } }; Method method = BaseGuess.class.getDeclaredMethod("nCk", int.class, int.class); method.setAccessible(true); return Arrays.asList(new Object[][]{ {"", 1}, {"a", 1}, {"A", 2}, {"abcdef", 1}, {"Abcdef", 2}, {"abcdeF", 2}, {"ABCDEF", 2}, {"aBcdef", method.invoke(baseGuess, 6, 1)}, {"aBcDef", (int) method.invoke(baseGuess, 6, 1) + (int) method.invoke(baseGuess, 6, 2)}, {"ABCDEf", method.invoke(baseGuess, 6, 1)}, {"aBCDEf", (int) method.invoke(baseGuess, 6, 1) + (int) method.invoke(baseGuess, 6, 2)}, {"ABCdef", (int) method.invoke(baseGuess, 6, 1) + (int) method.invoke(baseGuess, 6, 2) + (int) method.invoke(baseGuess, 6, 3)}, }); } } @RunWith(Parameterized.class) public static class L33tVariantsTest { private String word; private int variants; private Map<Character, Character> sub; public L33tVariantsTest(String word, int variants, Map<Character, Character> sub) { this.word = word; this.variants = variants; this.sub = sub; } @Test public void testL33tVariants() throws Exception { Match match = new Match.Builder(Pattern.Dictionary, 0, 0, word).sub(sub).l33t(!sub.isEmpty()).build(); String msg = String.format("extra l33t guesses of %s is %s", word, variants); assertEquals(msg, variants, new DictionaryGuess().l33tVariations(match)); } @Parameterized.Parameters(name = "{0}") public static Collection<Object[]> data() throws Exception { BaseGuess baseGuess = new BaseGuess() { @Override public double exec(Match match) { return 0; } }; Method method = BaseGuess.class.getDeclaredMethod("nCk", int.class, int.class); method.setAccessible(true); return Arrays.asList(new Object[][]{ {"", 1, Collections.emptyMap()}, {"a", 1, Collections.emptyMap()}, {"4", 2, Collections.singletonMap('4', 'a')}, {"4pple", 2, Collections.singletonMap('4', 'a')}, {"abcet", 1, Collections.emptyMap()}, {"4bcet", 2, Collections.singletonMap('4', 'a')}, {"a8cet", 2, Collections.singletonMap('8', 'b')}, {"abce+", 2, Collections.singletonMap('+', 't')}, {"48cet", 4, new HashMap<Character, Character>() {{ put('4', 'a'); put('8', 'b'); }}}, {"a4a4aa", (int) method.invoke(baseGuess, 6, 2) + (int) method.invoke(baseGuess, 6, 1), Collections.singletonMap('4', 'a')}, {"4a4a44", (int) method.invoke(baseGuess, 6, 2) + (int) method.invoke(baseGuess, 6, 1), Collections.singletonMap('4', 'a')}, {"a44att+", ((int) method.invoke(baseGuess, 4, 2) + (int) method.invoke(baseGuess, 4, 1)) * (int) method.invoke(baseGuess, 3, 1), new HashMap<Character, Character>() {{ put('4', 'a'); put('+', 't'); }}}, {"Aa44aA", (int) method.invoke(baseGuess, 6, 2) + (int) method.invoke(baseGuess, 6, 1), Collections.singletonMap('4', 'a')} }); } } public static class RestScoringTest { @Test public void testCalcGuessesPassword() throws Exception { Match match = new Match.Builder(Pattern.Dictionary, 0, 8, "password").guesses(1.0).build(); String msg = "estimate_guesses returns cached guesses when available"; assertEquals(msg, 1, new EstimateGuess("password").exec(match), 0.0); } @Test public void testCalcGuessesYear() throws Exception { Match match = MatchFactory.createDateMatch(0, 0, "1977", "/", 1977, 7, 14); String msg = "estimate_guesses delegates based on pattern"; assertEquals(msg, new EstimateGuess("1977").exec(match), new DateGuess().exec(match), 0.0); } @Test public void testL33tVariants() throws Exception { Match match = MatchFactory.createDictionaryMatch(0, 0, "", "", 0, ""); assertEquals("1 variant for non-l33t matches", 1.0, new DictionaryGuess().l33tVariations(match), 0.0); } } }