cipher_bureau 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +160 -0
  3. data/Rakefile +38 -0
  4. data/db/seeds/norwegian/wordloader.rb +47 -0
  5. data/lib/cipher_bureau.rb +59 -0
  6. data/lib/cipher_bureau/data_loader.rb +65 -0
  7. data/lib/cipher_bureau/dictionary.rb +63 -0
  8. data/lib/cipher_bureau/exceptions.rb +25 -0
  9. data/lib/cipher_bureau/password.rb +114 -0
  10. data/lib/cipher_bureau/password_meter.rb +183 -0
  11. data/lib/cipher_bureau/statistic.rb +48 -0
  12. data/lib/cipher_bureau/version.rb +3 -0
  13. data/lib/generators/cipher_bureau/create_migration_generator.rb +40 -0
  14. data/lib/generators/cipher_bureau/load_data_generator.rb +36 -0
  15. data/lib/generators/templates/create_cipher_bureau_dictionaries.rb +41 -0
  16. data/lib/generators/templates/create_cipher_bureau_statistics.rb +39 -0
  17. data/lib/tasks/cipher_bureau_tasks.rake +40 -0
  18. data/test/dummy/README.rdoc +261 -0
  19. data/test/dummy/Rakefile +7 -0
  20. data/test/dummy/app/assets/javascripts/application.js +15 -0
  21. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  22. data/test/dummy/app/controllers/application_controller.rb +3 -0
  23. data/test/dummy/app/helpers/application_helper.rb +2 -0
  24. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  25. data/test/dummy/config.ru +4 -0
  26. data/test/dummy/config/application.rb +59 -0
  27. data/test/dummy/config/boot.rb +10 -0
  28. data/test/dummy/config/database.yml +25 -0
  29. data/test/dummy/config/environment.rb +5 -0
  30. data/test/dummy/config/environments/development.rb +37 -0
  31. data/test/dummy/config/environments/production.rb +67 -0
  32. data/test/dummy/config/environments/test.rb +37 -0
  33. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  34. data/test/dummy/config/initializers/inflections.rb +15 -0
  35. data/test/dummy/config/initializers/mime_types.rb +5 -0
  36. data/test/dummy/config/initializers/secret_token.rb +7 -0
  37. data/test/dummy/config/initializers/session_store.rb +8 -0
  38. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  39. data/test/dummy/config/locales/en.yml +5 -0
  40. data/test/dummy/config/routes.rb +58 -0
  41. data/test/dummy/db/migrate/201301290819401_create_cipher_bureau_dictionaries.rb +19 -0
  42. data/test/dummy/db/migrate/201301290819402_create_cipher_bureau_statistics.rb +17 -0
  43. data/test/dummy/db/schema.rb +46 -0
  44. data/test/dummy/public/404.html +26 -0
  45. data/test/dummy/public/422.html +26 -0
  46. data/test/dummy/public/500.html +25 -0
  47. data/test/dummy/public/favicon.ico +0 -0
  48. data/test/dummy/script/rails +6 -0
  49. data/test/fixtures/cipher_bureau_dictionaries.yml +704 -0
  50. data/test/fixtures/cipher_bureau_statistics.yml +40 -0
  51. data/test/test_helper.rb +15 -0
  52. data/test/units/cipher_bureau/dictionary_test.rb +74 -0
  53. data/test/units/cipher_bureau/password_meter_test.rb +128 -0
  54. data/test/units/cipher_bureau/password_test.rb +188 -0
  55. data/test/units/cipher_bureau/statistic_test.rb +78 -0
  56. data/test/units/cipher_bureau_test.rb +45 -0
  57. metadata +197 -0
@@ -0,0 +1,40 @@
1
+ noun_4_ascii:
2
+ id: 13
3
+ country_code: 47
4
+ grammar: noun
5
+ name_type:
6
+ ascii: true
7
+ length: 4
8
+ word_count: 20
9
+ noun_4:
10
+ id: 66
11
+ country_code: 47
12
+ grammar: noun
13
+ name_type:
14
+ ascii: false
15
+ length: 4
16
+ word_count: 20
17
+ surname_5:
18
+ id: 308
19
+ country_code: 47
20
+ grammar: name
21
+ name_type: surname
22
+ ascii: false
23
+ length: 5
24
+ word_count: 20
25
+ boysname_5:
26
+ id: 320
27
+ country_code: 47
28
+ grammar: name
29
+ name_type: boysname
30
+ ascii: false
31
+ length: 5
32
+ word_count: 20
33
+ girlsname_5:
34
+ id: 332
35
+ country_code: 47
36
+ grammar: name
37
+ name_type: girlsname
38
+ ascii: false
39
+ length: 5
40
+ word_count: 20
@@ -0,0 +1,15 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
5
+ require "rails/test_help"
6
+
7
+ Rails.backtrace_cleaner.remove_silencers!
8
+
9
+ # Load support files
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
11
+
12
+ # Load fixtures from the engine
13
+ if ActiveSupport::TestCase.method_defined?(:fixture_path=)
14
+ ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
15
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ # Copyright (c) 2013 Dynamic Project Management AS
3
+ # Copyright (c) 2013 Knut I. Stenmark
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'test_helper'
25
+
26
+ class CipherBureau::DictionaryTest < ActiveSupport::TestCase
27
+ fixtures :all
28
+
29
+ context 'validation' do
30
+ should validate_presence_of :word
31
+ should validate_presence_of :grammar
32
+ should validate_presence_of :country_code
33
+ end
34
+ context 'saving' do
35
+ setup do
36
+ @word = CipherBureau::Dictionary.new :word => 'gurba', :grammar => 'bullshit', :country_code => '999'
37
+ end
38
+ should 'be valid' do
39
+ assert @word.valid?, "Test record was invalid"
40
+ end
41
+ should 'set length' do
42
+ @word.save!
43
+ assert_equal 5, @word.length
44
+ end
45
+ should 'set ascii' do
46
+ @word.word = 'heihåpp'
47
+ @word.save!
48
+ assert !@word.ascii
49
+ end
50
+ should 'register statistics' do
51
+ CipherBureau::Statistic.expects(:register).with(@word)
52
+ @word.save
53
+ end
54
+ end
55
+
56
+ context 'class method' do
57
+ context 'random' do
58
+ should 'check statistics before fetching data' do
59
+ criteria = {:country_code => 47, :grammar => 'noun'}
60
+ CipherBureau::Statistic.expects(:word_count).with(criteria.merge(:length => 4)).returns(3)
61
+ CipherBureau::Dictionary.expects(:randomized_offset).with(3).returns(1)
62
+ assert_equal 'kjel', CipherBureau::Dictionary.random(4, criteria)
63
+ end
64
+ should 'support camelize' do
65
+ CipherBureau::Dictionary.expects(:fetch_random_word).with(:length => 4).returns(stub(:word => 'halo'))
66
+ assert_equal 'Halo', CipherBureau::Dictionary.random(4, :camelize => true)
67
+ CipherBureau::Dictionary.expects(:fetch_random_word).with(:length => 4).returns(stub(:word => 'øyne'))
68
+ assert_equal 'Øyne', CipherBureau::Dictionary.random(4, :camelize => true)
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ end
@@ -0,0 +1,128 @@
1
+ # Copyright (c) 2013 Dynamic Project Management AS
2
+ # Copyright (c) 2013 Knut I. Stenmark
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ # encoding: utf-8
24
+ require 'test_helper'
25
+
26
+ class CipherBureau::PasswordMeterTest < ActiveSupport::TestCase
27
+ # Algorihm:
28
+ # See http://www.passwordmeter.com
29
+ setup do
30
+ @m = CipherBureau::PasswordMeter.new('12345678')
31
+ end
32
+
33
+ def t(method, expected, password = nil)
34
+ @m.password = password if password
35
+ actual = @m.send(method)
36
+ assert_equal expected, actual, "Method: #{method} with password '#{@m.password}' expected to return #{expected}, was #{actual}"
37
+ end
38
+
39
+ context 'strength' do
40
+ should 'calculate weak' do
41
+ @m.password = 'aaaaaaaaaaaa'
42
+ assert_equal 48 - 12 - 124 - 22, @m.absolute_score
43
+ assert_equal 0, @m.strength
44
+ end
45
+ should 'calculate strong' do
46
+ @m.password = 'acgHY79-|/'
47
+ assert_equal 40 + 16 + 14 + 8 + 18 + 8 + 10 - 2 - 4 - 2, @m.absolute_score
48
+ assert_equal 100, @m.strength
49
+ end
50
+ should 'also be a class method' do
51
+ assert_equal 100, CipherBureau::PasswordMeter.strength( 'acgHY79-|/')
52
+ end
53
+ end
54
+
55
+
56
+ context 'addition' do
57
+ should 'return the expected results' do
58
+ t :number_of_characters, 8 * 4
59
+ t :uppercase_letters, (7 - 3 ) * 2, 'AbCdefG'
60
+ t :uppercase_letters, 0, 'aaaaaaaaaaa'
61
+ t :lowercase_letters, (7 - 4) * 2, 'AbCdefG'
62
+ t :lowercase_letters, 0, 'aaaaaaaaaaa'
63
+ t :lowercase_letters, 2, 'aaaaaaaaaaA'
64
+ t :numbers, 0, '12345678'
65
+ t :numbers, 8 * 4, 'a12345678'
66
+ t :symbols, 3 * 6, 'abcdef[]|'
67
+ t :middle_numbers_or_symbols, 0, '11'
68
+ t :middle_numbers_or_symbols, 2 * 2, 'ab12a'
69
+ t :middle_numbers_or_symbols, 2 * 2, 'ab[]a'
70
+ t :middle_numbers_or_symbols, 2 * 4, 'ab[12]a'
71
+ t :requirements, 0, '12'
72
+ t :requirements, 0, 'abcdefghi'
73
+ t :requirements, 0, 'abcde1234'
74
+ t :requirements, 0, 'abcde[]&:'
75
+ t :requirements, 10, 'AbB=-123i'
76
+ t :requirements, 8, 'AbB12345'
77
+ end
78
+ end
79
+
80
+ context 'deduction' do
81
+ should 'return representative values for letters only' do
82
+ t :letters_only, 5, 'abcde'
83
+ t :letters_only, 5, 'ABCDE'
84
+ t :letters_only, 5, 'AbCdE'
85
+ t :letters_only, 0, 'AbCd1'
86
+ t :letters_only, 0, '12345'
87
+ t :letters_only, 0, 'AbCd['
88
+ t :letters_only, 0, ';:`?=)'
89
+ end
90
+ should 'return representative values for numbers only' do
91
+ t :numbers_only, 5, '54321'
92
+ t :numbers_only, 0, '5432a'
93
+ t :numbers_only, 0, ';:`?=)'
94
+ t :numbers_only, 0, 'ABCDE'
95
+ end
96
+ should 'return representative values for repeated_characters' do
97
+ t :repeated_characters, 37, '11111'
98
+ t :repeated_characters, 15, '11211'
99
+ t :repeated_characters, 3, 'aa2b1'
100
+ end
101
+ should 'return representative values for sequential_letters' do
102
+ t :sequential_letters, 3, 'ABC'
103
+ t :sequential_letters, 3, 'BCD'
104
+ t :sequential_letters, 6, 'ABCD'
105
+ t :sequential_letters, 6, 'DCBA'
106
+ end
107
+ should 'return representative values for sequential_numbers' do
108
+ t :sequential_numbers, 6, '1234'
109
+ t :sequential_numbers, 6, '4321'
110
+ t :sequential_numbers, 9, '54321'
111
+ end
112
+ should 'return representative values for sequential_symbols' do
113
+ t :sequential_symbols, 21, '!@#$%^&*()'
114
+ end
115
+ should 'return representative values for consecutive_uppercase' do
116
+ t :consecutive_uppercase, 6, 'ACEG'
117
+ t :consecutive_uppercase, 12, 'ACEGsKJSNks'
118
+ end
119
+ should 'return representative values for consecutive_lowercase' do
120
+ t :consecutive_lowercase, 6, 'aceg'
121
+ t :consecutive_lowercase, 10, 'acegABkjjJ'
122
+ end
123
+ should 'return representative values for consecutive_numbers' do
124
+ t :consecutive_numbers, 6, '1357'
125
+ t :consecutive_numbers, 8, 'ab10357CD'
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,188 @@
1
+ # Copyright (c) 2013 Dynamic Project Management AS
2
+ # Copyright (c) 2013 Knut I. Stenmark
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ # encoding: utf-8
24
+ require 'test_helper'
25
+
26
+ class CipherBureau::PasswordTest < ActiveSupport::TestCase
27
+ fixtures :all
28
+
29
+ setup do
30
+ CipherBureau.default_memorable_options = {}
31
+ end
32
+
33
+ context 'the interface' do
34
+ context 'memorable' do
35
+ should 'generate correct length' do
36
+ @pwd = CipherBureau::Password.memorable(:length => 10)
37
+ assert_equal 10, @pwd.length
38
+ end
39
+ should 'have a default option' do
40
+ end
41
+ should 'support grammar selection' do
42
+ @pwd = CipherBureau::Password.memorable(:length => 10)
43
+ @meter = CipherBureau::PasswordMeter.new(@pwd)
44
+ end
45
+ should 'support camelize option' do
46
+ @pwd = CipherBureau::Password.memorable(:length => 10, :camelize => true)
47
+ assert_equal @pwd[0].upcase, @pwd[0]
48
+ assert_equal @pwd[6].upcase, @pwd[6]
49
+ end
50
+ should 'support middle_numbers option' do
51
+ CipherBureau::Dictionary.expects(:random).twice.with(4, {}).returns('abcd','efgh')
52
+ @pwd = CipherBureau::Password.memorable(:length => 10, :middle_numbers => true)
53
+ assert @pwd.match( /^\w{4,4}\d{2,2}\w{4,4}$/)
54
+ end
55
+ should 'embed numbers and symbols in middle' do
56
+ @pwd = CipherBureau::Password.memorable(:length => 10)
57
+ @meter = CipherBureau::PasswordMeter.new(@pwd)
58
+ assert @meter.middle_numbers_or_symbols > 2
59
+ end
60
+
61
+ context 'scope' do
62
+ context 'with default values' do
63
+ setup do
64
+ CipherBureau.default_memorable_options = {
65
+ :length => 10,
66
+ :country_code => 47,
67
+ :ascii => false,
68
+ :grammar => 'name',
69
+ :name_type => 'girlsname'
70
+ }
71
+ end
72
+ should 'initialize with them' do
73
+ @pwd = CipherBureau::Password.new :length => 10
74
+ assert_equal ({:country_code => 47, :ascii => false, :grammar => 'name', :name_type => 'girlsname'}), @pwd.send(:scope)
75
+ end
76
+ should 'override, by setting initializer' do
77
+ opts = {:length => 8, :country_code => 44, :ascii => true, :grammar => 'noun'}
78
+ @pwd = CipherBureau::Password.new opts
79
+ opts.delete(:length)
80
+ assert_equal opts, @pwd.send(:scope)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ context 'letters_and_numbers' do
86
+ should 'contain only letters and numbers' do
87
+ 10.times do
88
+ assert_match /^[A-Za-z0-9]{12,12}$/, CipherBureau::Password.letters_and_numbers(:length => 12)
89
+ end
90
+ end
91
+ end
92
+ context 'numbers_only' do
93
+ should 'contain only numbers only' do
94
+ 10.times do
95
+ assert_match /^[0-9]{10,10}$/, CipherBureau::Password.numbers_only(:length => 10)
96
+ end
97
+ end
98
+ end
99
+ context 'random' do
100
+ should 'contain any random character' do
101
+ 50.times do
102
+ illegal = CipherBureau::Password.random(1, :length => 12).bytes.select { |b| b < 32 || b > 126 }
103
+ assert_equal 0, illegal.size
104
+ end
105
+ end
106
+ should 'generate the correct length' do
107
+ pwd = CipherBureau::Password.random(1, :length => 14)
108
+ assert_equal 14, pwd.length
109
+ end
110
+ should 'really be random' do
111
+ assert_not_equal CipherBureau::Password.random(1, :length => 14), CipherBureau::Password.random(1, :length => 14)
112
+ end
113
+ end
114
+ context 'fips-181' do
115
+ # http://www.itl.nist.gov/fipspubs/fip181.htm
116
+ should 'never be used' do
117
+ assert_raises NoMethodError do
118
+ pwd = CipherBureau::Password.fips_181(1, :length => 14)
119
+ end
120
+ end
121
+ end
122
+ context 'strength' do
123
+ should 'delegate to PasswordMeter' do
124
+ CipherBureau::PasswordMeter.expects(:strength).with('abcde').returns(1)
125
+ assert_equal 1, CipherBureau::Password.strength('abcde')
126
+ end
127
+ end
128
+ end
129
+
130
+ context 'initializing' do
131
+ should 'validate length' do
132
+ assert_raises ArgumentError do
133
+ pwd = CipherBureau::Password.new
134
+ end
135
+ end
136
+ should 'set options' do
137
+ pwd = CipherBureau::Password.new({:length => 12, :country_code => 99})
138
+ assert_equal 12, pwd.length
139
+ assert_equal ({:country_code => 99}), pwd.options
140
+ end
141
+ should 'set defaults' do
142
+ CipherBureau.default_memorable_options = {
143
+ :length => 12,
144
+ :country_code => 98
145
+ }
146
+ pwd = CipherBureau::Password.new
147
+ assert_equal 12, pwd.length
148
+ assert_equal ({:country_code => 98}), pwd.options
149
+ end
150
+ end
151
+
152
+ context 'collecting and enumerating' do
153
+ context 'single record' do
154
+ should 'return sigle object' do
155
+ assert CipherBureau::Password.random(1, :length => 10).is_a?(String)
156
+ assert CipherBureau::Password.random(:length => 10).is_a?(String)
157
+ end
158
+ should 'have the abilty to include strength' do
159
+ assert CipherBureau::Password.random(:length => 10, :strength => true).is_a?(Array)
160
+ CipherBureau::Password.expects(:strength).returns(10)
161
+ CipherBureau::Password.any_instance.expects(:random).returns('abcdefgh')
162
+ assert_equal ['abcdefgh', 10], CipherBureau::Password.random(:length => 10, :strength => true)
163
+ end
164
+ end
165
+ context 'multiple records' do
166
+ should 'return array' do
167
+ assert CipherBureau::Password.random(2, :length => 10).is_a?(Array)
168
+ assert_equal 2, CipherBureau::Password.random(2, :length => 10).size
169
+ assert_equal 3, CipherBureau::Password.random(3, :length => 10).size
170
+ end
171
+ should 'have the abilty to include strength' do
172
+ CipherBureau::Password.expects(:strength).twice.returns(10, 5)
173
+ CipherBureau::Password.any_instance.expects(:random).twice.returns('abcdefgh', 'jklmno')
174
+ assert_equal [['abcdefgh', 10], ['jklmno', 5]], CipherBureau::Password.random(2, :length => 10, :strength => true)
175
+ end
176
+ end
177
+ context 'hash option' do
178
+ should 'return hash' do
179
+ assert CipherBureau::Password.random(:length => 10, :strength => true).is_a?(Array)
180
+ CipherBureau::Password.stubs(:strength).returns(10)
181
+ CipherBureau::Password.any_instance.stubs(:random).returns('abcdefgh')
182
+ expected = {:password => 'abcdefgh', :strength => 10}
183
+ assert_equal expected, CipherBureau::Password.random(:length => 10, :strength => true, :as => :hash)
184
+ assert_equal [expected, expected], CipherBureau::Password.random(2, :length => 10, :strength => true, :as => :hash)
185
+ end
186
+ end
187
+ end
188
+ end