zxcvbn-ruby 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +23 -0
- data/Rakefile +18 -0
- data/data/adjacency_graphs.json +9 -0
- data/data/frequency_lists.yaml +85094 -0
- data/lib/zxcvbn.rb +37 -0
- data/lib/zxcvbn/crack_time.rb +51 -0
- data/lib/zxcvbn/dictionary_ranker.rb +23 -0
- data/lib/zxcvbn/entropy.rb +151 -0
- data/lib/zxcvbn/match.rb +13 -0
- data/lib/zxcvbn/matchers/date.rb +134 -0
- data/lib/zxcvbn/matchers/dictionary.rb +34 -0
- data/lib/zxcvbn/matchers/digits.rb +18 -0
- data/lib/zxcvbn/matchers/l33t.rb +127 -0
- data/lib/zxcvbn/matchers/new_l33t.rb +120 -0
- data/lib/zxcvbn/matchers/regex_helpers.rb +21 -0
- data/lib/zxcvbn/matchers/repeat.rb +32 -0
- data/lib/zxcvbn/matchers/sequences.rb +64 -0
- data/lib/zxcvbn/matchers/spatial.rb +79 -0
- data/lib/zxcvbn/matchers/year.rb +18 -0
- data/lib/zxcvbn/math.rb +63 -0
- data/lib/zxcvbn/omnimatch.rb +49 -0
- data/lib/zxcvbn/password_strength.rb +21 -0
- data/lib/zxcvbn/score.rb +15 -0
- data/lib/zxcvbn/scorer.rb +84 -0
- data/lib/zxcvbn/version.rb +3 -0
- data/spec/matchers/date_spec.rb +109 -0
- data/spec/matchers/dictionary_spec.rb +14 -0
- data/spec/matchers/digits_spec.rb +15 -0
- data/spec/matchers/l33t_spec.rb +85 -0
- data/spec/matchers/repeat_spec.rb +18 -0
- data/spec/matchers/sequences_spec.rb +16 -0
- data/spec/matchers/spatial_spec.rb +20 -0
- data/spec/matchers/year_spec.rb +15 -0
- data/spec/omnimatch_spec.rb +24 -0
- data/spec/scorer_spec.rb +5 -0
- data/spec/scoring/crack_time_spec.rb +106 -0
- data/spec/scoring/entropy_spec.rb +213 -0
- data/spec/scoring/math_spec.rb +131 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/support/js_helpers.rb +35 -0
- data/spec/support/js_source/adjacency_graphs.js +8 -0
- data/spec/support/js_source/compiled.js +1188 -0
- data/spec/support/js_source/frequency_lists.js +10 -0
- data/spec/support/js_source/init.coffee +63 -0
- data/spec/support/js_source/init.js +95 -0
- data/spec/support/js_source/matching.coffee +444 -0
- data/spec/support/js_source/matching.js +685 -0
- data/spec/support/js_source/scoring.coffee +270 -0
- data/spec/support/js_source/scoring.js +390 -0
- data/spec/support/matcher.rb +35 -0
- data/spec/zxcvbn_spec.rb +49 -0
- data/zxcvbn-ruby.gemspec +20 -0
- metadata +167 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Matchers::Repeat do
|
4
|
+
let(:matcher) { subject }
|
5
|
+
let(:matches) { matcher.matches('bbbbbtestingaaa') }
|
6
|
+
|
7
|
+
it 'sets the pattern name' do
|
8
|
+
matches.all? { |m| m.pattern == 'repeat' }.should be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'finds the repeated patterns' do
|
12
|
+
matches.count.should eq 2
|
13
|
+
matches[0].token.should eq 'bbbbb'
|
14
|
+
matches[0].repeated_char.should eq 'b'
|
15
|
+
matches[1].token.should eq 'aaa'
|
16
|
+
matches[1].repeated_char.should eq 'a'
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Matchers::Sequences do
|
4
|
+
let(:matcher) { subject }
|
5
|
+
let(:matches) { matcher.matches('abcde87654') }
|
6
|
+
|
7
|
+
it 'sets the pattern name' do
|
8
|
+
matches.all? { |m| m.pattern == 'sequence' }.should be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'finds the correct matches' do
|
12
|
+
matches.count.should == 2
|
13
|
+
matches[0].token.should eq 'abcde'
|
14
|
+
matches[1].token.should eq '87654'
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Matchers::Spatial do
|
4
|
+
let(:matcher) { Zxcvbn::Matchers::Spatial.new(graphs) }
|
5
|
+
let(:graphs) { Zxcvbn::ADJACENCY_GRAPHS }
|
6
|
+
|
7
|
+
describe '#matches' do
|
8
|
+
let(:matches) { matcher.matches('rtyikm') }
|
9
|
+
|
10
|
+
it 'finds the correct of matches' do
|
11
|
+
matches.count.should eq 3
|
12
|
+
matches[0].token.should eq 'rty'
|
13
|
+
matches[0].graph.should eq 'qwerty'
|
14
|
+
matches[1].token.should eq 'ikm'
|
15
|
+
matches[1].graph.should eq 'qwerty'
|
16
|
+
matches[2].token.should eq 'yik'
|
17
|
+
matches[2].graph.should eq 'dvorak'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Matchers::Year do
|
4
|
+
let(:matcher) { subject }
|
5
|
+
let(:matches) { matcher.matches('testing1998') }
|
6
|
+
|
7
|
+
it 'sets the pattern name' do
|
8
|
+
matches.all? { |m| m.pattern == 'year' }.should be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'finds the correct matches' do
|
12
|
+
matches.count.should == 1
|
13
|
+
matches[0].token.should eq '1998'
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Omnimatch do
|
4
|
+
before(:all) do
|
5
|
+
@omnimatch = described_class.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def omnimatch(password)
|
9
|
+
@omnimatch.matches(password)
|
10
|
+
end
|
11
|
+
|
12
|
+
def js_omnimatch(password)
|
13
|
+
run_js(%'omnimatch("#{password}")')
|
14
|
+
end
|
15
|
+
|
16
|
+
TEST_PASSWORDS.each do |password|
|
17
|
+
it "gives back the same results for #{password}" do
|
18
|
+
js_results = js_omnimatch(password)
|
19
|
+
ruby_results = omnimatch(password)
|
20
|
+
|
21
|
+
ruby_results.should match_js_results js_results
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/spec/scorer_spec.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::CrackTime do
|
4
|
+
include Zxcvbn::CrackTime
|
5
|
+
|
6
|
+
describe '#entropy_to_crack_time' do
|
7
|
+
specify do
|
8
|
+
entropy_to_crack_time(15.433976574415976).should eq 2.2134000000000014
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#crack_time_to_score' do
|
13
|
+
context 'crack time less than 10 to the power 2' do
|
14
|
+
it 'returns 0' do
|
15
|
+
crack_time_to_score(90).should eq 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'crack time in between 10**2 and 10**4' do
|
20
|
+
it 'returns 1' do
|
21
|
+
crack_time_to_score(5000).should eq 1
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'crack time in between 10**4 and 10**6' do
|
26
|
+
it 'returns 2' do
|
27
|
+
crack_time_to_score(500_000).should eq 2
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'crack time in between 10**6 and 10**8' do
|
32
|
+
it 'returns 3' do
|
33
|
+
crack_time_to_score(50_000_000).should eq 3
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'crack time above 10**8' do
|
38
|
+
it 'returns 4' do
|
39
|
+
crack_time_to_score(110_000_000).should eq 4
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#display_time' do
|
45
|
+
let(:minute_to_seconds) { 60 }
|
46
|
+
let(:hour_to_seconds) { minute_to_seconds * 60 }
|
47
|
+
let(:day_to_seconds) { hour_to_seconds * 24 }
|
48
|
+
let(:month_to_seconds) { day_to_seconds * 31 }
|
49
|
+
let(:year_to_seconds) { month_to_seconds * 12 }
|
50
|
+
let(:century_to_seconds) { year_to_seconds * 100 }
|
51
|
+
|
52
|
+
context 'when less than a minute' do
|
53
|
+
it 'should return instant' do
|
54
|
+
[0, minute_to_seconds - 1].each do |seconds|
|
55
|
+
display_time(seconds).should eql 'instant'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'when less than an hour' do
|
61
|
+
it 'should return a readable time in minutes' do
|
62
|
+
[60, (hour_to_seconds - 1)].each do |seconds|
|
63
|
+
display_time(seconds).should =~ /[0-9]+ minutes$/
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when less than a day' do
|
69
|
+
it 'should return a readable time in hours' do
|
70
|
+
[hour_to_seconds, (day_to_seconds - 1)].each do |seconds|
|
71
|
+
display_time(seconds).should =~ /[0-9]+ hours$/
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when less than 31 days' do
|
77
|
+
it 'should return a readable time in days' do
|
78
|
+
[day_to_seconds, month_to_seconds - 1].each do |seconds|
|
79
|
+
display_time(seconds).should =~ /[0-9]+ days$/
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when less than 1 year' do
|
85
|
+
it 'should return a readable time in days' do
|
86
|
+
[month_to_seconds, (year_to_seconds - 1)].each do |seconds|
|
87
|
+
display_time(seconds).should =~ /[0-9]+ months$/
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when less than a century' do
|
93
|
+
it 'should return a readable time in days' do
|
94
|
+
[year_to_seconds, (century_to_seconds - 1)].each do |seconds|
|
95
|
+
display_time(seconds).should =~ /[0-9]+ years$/
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when a century or more' do
|
101
|
+
it 'should return centuries' do
|
102
|
+
display_time(century_to_seconds).should eql 'centuries'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Entropy do
|
4
|
+
include Zxcvbn::Math
|
5
|
+
|
6
|
+
let(:entropy) {
|
7
|
+
Class.new do
|
8
|
+
include Zxcvbn::Entropy
|
9
|
+
end.new
|
10
|
+
}
|
11
|
+
|
12
|
+
describe '#repeat_entropy' do
|
13
|
+
it 'returns the correct value' do
|
14
|
+
match = Zxcvbn::Match.new(:token => '2222')
|
15
|
+
entropy.repeat_entropy(match).should eq 5.321928094887363
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#sequence_entropy' do
|
20
|
+
let(:match) { Zxcvbn::Match.new(:token => token, :ascending => true) }
|
21
|
+
|
22
|
+
{'a' => 'abcdefg', '1' => '1234567'}.each do |first_char, token|
|
23
|
+
context "when the first char is #{first_char}" do
|
24
|
+
let(:token) { token }
|
25
|
+
|
26
|
+
it 'returns the correct value' do
|
27
|
+
entropy.sequence_entropy(match).should eq 3.807354922057604
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'when the first character is a digit' do
|
33
|
+
let(:token) { '23456' }
|
34
|
+
|
35
|
+
it 'returns the correct value' do
|
36
|
+
entropy.sequence_entropy(match).should eq 5.643856189774725
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when the first character is a lowercase letter' do
|
41
|
+
let(:token) { 'bcdef' }
|
42
|
+
|
43
|
+
it 'returns the correct value' do
|
44
|
+
entropy.sequence_entropy(match).should eq 7.022367813028454
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when the first character is an uppercase letter' do
|
49
|
+
let(:token) { 'BCDEF' }
|
50
|
+
|
51
|
+
it 'returns the correct value' do
|
52
|
+
entropy.sequence_entropy(match).should eq 8.022367813028454
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when the match is ascending' do
|
57
|
+
before { match.ascending = false }
|
58
|
+
let(:token) { 'bcdef' }
|
59
|
+
|
60
|
+
it 'returns the correct value' do
|
61
|
+
entropy.sequence_entropy(match).should eq 8.022367813028454
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#digits_entropy' do
|
67
|
+
it 'returns the correct value' do
|
68
|
+
match = Zxcvbn::Match.new(:token => '12345678')
|
69
|
+
entropy.digits_entropy(match).should eq 26.5754247590989
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#year_entropy' do
|
74
|
+
it 'returns the correct value' do
|
75
|
+
entropy.year_entropy(nil).should eq 6.894817763307944
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#date_entropy' do
|
80
|
+
context 'with a two digit year' do
|
81
|
+
it 'returns the correct value' do
|
82
|
+
match = Zxcvbn::Match.new(:year => 98)
|
83
|
+
entropy.date_entropy(match).should eq 15.183015000882756
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'with a four digit year' do
|
88
|
+
it 'returns the correct value' do
|
89
|
+
match = Zxcvbn::Match.new(:year => 2012)
|
90
|
+
entropy.date_entropy(match).should eq 15.433976574415976
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'with a separator' do
|
95
|
+
it 'returns the correct value' do
|
96
|
+
match = Zxcvbn::Match.new(:year => 2012, :separator => '/')
|
97
|
+
entropy.date_entropy(match).should eq 17.433976574415976
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#dictionary_entropy' do
|
103
|
+
let(:match) { Zxcvbn::Match.new(:token => token, :rank => rank, :l33t => l33t, :sub => sub) }
|
104
|
+
let(:l33t) { false }
|
105
|
+
let(:sub) { {} }
|
106
|
+
let(:calculated_entropy) { entropy.dictionary_entropy(match) }
|
107
|
+
|
108
|
+
context 'a simple dictionary word, all lower case and no l33t subs' do
|
109
|
+
let(:token) { 'you' }
|
110
|
+
let(:rank) { 1 }
|
111
|
+
|
112
|
+
specify { calculated_entropy.should eq 0 }
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'with all upper case characters' do
|
116
|
+
let(:token) { 'YOU' }
|
117
|
+
let(:rank) { 1 }
|
118
|
+
|
119
|
+
specify { calculated_entropy.should eq 1 }
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'starting with uppercase' do
|
123
|
+
let(:token) { 'You' }
|
124
|
+
let(:rank) { 1 }
|
125
|
+
|
126
|
+
specify { calculated_entropy.should eq 1 }
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'starting with uppercase' do
|
130
|
+
let(:token) { 'yoU' }
|
131
|
+
let(:rank) { 1 }
|
132
|
+
|
133
|
+
specify { calculated_entropy.should eq 1 }
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'mixed upper and lower' do
|
137
|
+
let(:token) { 'tEsTiNg' }
|
138
|
+
let(:rank) { 1 }
|
139
|
+
|
140
|
+
specify { calculated_entropy.should eq 6 }
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'starting with digits' do
|
144
|
+
let(:token) { '12345' }
|
145
|
+
let(:rank) { 1 }
|
146
|
+
|
147
|
+
specify { calculated_entropy.should eq 0 }
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'extra l33t entropy' do
|
151
|
+
let(:token) { 'p3rs0n' }
|
152
|
+
let(:rank) { 1 }
|
153
|
+
let(:l33t) { true }
|
154
|
+
let(:sub) { {'3' => 'e', '0' => 'o'} }
|
155
|
+
|
156
|
+
specify { calculated_entropy.should eq 1 }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#spatial_entropy' do
|
161
|
+
let(:match) { Zxcvbn::Match.new(:token => '123wsclf', :turns => 1) }
|
162
|
+
|
163
|
+
context 'when keyboard is qwerty' do
|
164
|
+
it 'should return the correct entropy' do
|
165
|
+
match.graph = 'qwerty'
|
166
|
+
|
167
|
+
entropy.spatial_entropy(match).should eql 11.562242424221074
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'when keyboard is dvorak' do
|
172
|
+
it 'should return the correct entropy' do
|
173
|
+
match.graph = 'dvorak'
|
174
|
+
|
175
|
+
entropy.spatial_entropy(match).should eql 11.562242424221074
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'when keyboard is not qwerty or dvorak' do
|
180
|
+
it 'should return the correct entropy' do
|
181
|
+
match.graph = 'keypad'
|
182
|
+
|
183
|
+
entropy.spatial_entropy(match).should eql 9.05528243550119
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'when match includes several turns' do
|
188
|
+
it 'should return the correct entropy' do
|
189
|
+
match.turns = 5
|
190
|
+
|
191
|
+
entropy.spatial_entropy(match).should eql 21.761397858718993
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'when match includes shifted count' do
|
196
|
+
it 'should return the correct entropy' do
|
197
|
+
match.shiffted_count = 5
|
198
|
+
|
199
|
+
entropy.spatial_entropy(match).should eql 9.05528243550119
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context 'when match includes shifted count and several turns' do
|
204
|
+
it 'should return the correct entropy' do
|
205
|
+
match.shiffted_count = 5
|
206
|
+
match.turns = 5
|
207
|
+
|
208
|
+
entropy.spatial_entropy(match).should eql 21.761397858718993
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Zxcvbn::Math do
|
4
|
+
include Zxcvbn::Math
|
5
|
+
|
6
|
+
describe '#bruteforce_cardinality' do
|
7
|
+
context 'when empty password' do
|
8
|
+
it 'should return 0 if empty password' do
|
9
|
+
bruteforce_cardinality('').should eql 0
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when password is one character long' do
|
14
|
+
context 'and a digit' do
|
15
|
+
it 'should return 10' do
|
16
|
+
(0..9).each do |digit|
|
17
|
+
bruteforce_cardinality(digit.to_s).should eql 10
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'and an upper case character' do
|
23
|
+
it 'should return 26' do
|
24
|
+
('A'..'Z').each do |character|
|
25
|
+
bruteforce_cardinality(character).should eql 26
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'and a lower case character' do
|
31
|
+
it 'should return 26' do
|
32
|
+
('a'..'z').each do |character|
|
33
|
+
bruteforce_cardinality(character).should eql 26
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'and a symbol' do
|
39
|
+
it 'should return 33' do
|
40
|
+
%w|/ [ ` {|.each do |symbol|
|
41
|
+
bruteforce_cardinality(symbol).should eql 33
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when password is more than one character long' do
|
48
|
+
context 'and only digits' do
|
49
|
+
it 'should return 10' do
|
50
|
+
bruteforce_cardinality('123456789').should eql 10
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'and only lowercase characters' do
|
55
|
+
it 'should return 26' do
|
56
|
+
bruteforce_cardinality('password').should eql 26
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'and only uppercase characters' do
|
61
|
+
it 'should return 26' do
|
62
|
+
bruteforce_cardinality('PASSWORD').should eql 26
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'and only symbols' do
|
67
|
+
it 'should return 33' do
|
68
|
+
bruteforce_cardinality('/ [ ` {').should eql 33
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'and a mixed of character types' do
|
73
|
+
it 'should add up every character type cardinality' do
|
74
|
+
bruteforce_cardinality('p1SsWorD!').should eql 95
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#average_degree_for_graph' do
|
81
|
+
context 'when keyboard is qwerty' do
|
82
|
+
it 'returns the correct average degree over all keys' do
|
83
|
+
average_degree_for_graph('qwerty').should eql 4.595744680851064
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'when keyboard is dvorak' do
|
88
|
+
it 'returns the correct average degree over all keys' do
|
89
|
+
average_degree_for_graph('dvorak').should eql 4.595744680851064
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'when keyboard is keypad' do
|
94
|
+
it 'returns the correct average degree over all keys' do
|
95
|
+
average_degree_for_graph('keypad').should eql 5.066666666666666
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when keyboard is mac keypad' do
|
100
|
+
it 'returns the correct average degree over all keys' do
|
101
|
+
average_degree_for_graph('mac_keypad').should eql 5.25
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '#starting_positions_for_graph' do
|
107
|
+
context 'when keyboard is qwerty' do
|
108
|
+
it 'returns the correct average degree over all keys' do
|
109
|
+
starting_positions_for_graph('qwerty').should eql 94
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'when keyboard is dvorak' do
|
114
|
+
it 'returns the correct average degree over all keys' do
|
115
|
+
starting_positions_for_graph('dvorak').should eql 94
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when keyboard is keypad' do
|
120
|
+
it 'returns the correct average degree over all keys' do
|
121
|
+
starting_positions_for_graph('keypad').should eql 15
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'when keyboard is mac keypad' do
|
126
|
+
it 'returns the correct average degree over all keys' do
|
127
|
+
starting_positions_for_graph('mac_keypad').should eql 16
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|