invisible_captcha 0.10.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InvisibleCaptcha
2
4
  module ControllerExt
3
5
  module ClassMethods
4
6
  def invisible_captcha(options = {})
5
- if respond_to?(:before_action)
6
- before_action(options) do
7
+ if options.key?(:prepend)
8
+ prepend_before_action(options) do
7
9
  detect_spam(options)
8
10
  end
9
11
  else
10
- before_filter(options) do
12
+ before_action(options) do
11
13
  detect_spam(options)
12
14
  end
13
15
  end
@@ -19,7 +21,7 @@ module InvisibleCaptcha
19
21
  def detect_spam(options = {})
20
22
  if timestamp_spam?(options)
21
23
  on_timestamp_spam(options)
22
- elsif honeypot_spam?(options)
24
+ elsif honeypot_spam?(options) || spinner_spam?
23
25
  on_spam(options)
24
26
  end
25
27
  end
@@ -28,11 +30,7 @@ module InvisibleCaptcha
28
30
  if action = options[:on_timestamp_spam]
29
31
  send(action)
30
32
  else
31
- if respond_to?(:redirect_back)
32
- redirect_back(fallback_location: root_path, flash: { error: InvisibleCaptcha.timestamp_error_message })
33
- else
34
- redirect_to :back, flash: { error: InvisibleCaptcha.timestamp_error_message }
35
- end
33
+ redirect_back(fallback_location: root_path, flash: { error: InvisibleCaptcha.timestamp_error_message })
36
34
  end
37
35
  end
38
36
 
@@ -53,11 +51,11 @@ module InvisibleCaptcha
53
51
 
54
52
  return false unless enabled
55
53
 
56
- timestamp = session[:invisible_captcha_timestamp]
54
+ timestamp = session.delete(:invisible_captcha_timestamp)
57
55
 
58
56
  # Consider as spam if timestamp not in session, cause that means the form was not fetched at all
59
57
  unless timestamp
60
- logger.warn("Potential spam detected for IP #{request.env['REMOTE_ADDR']}. Invisible Captcha timestamp not found in session.")
58
+ warn_spam("Invisible Captcha timestamp not found in session.")
61
59
  return true
62
60
  end
63
61
 
@@ -66,7 +64,16 @@ module InvisibleCaptcha
66
64
 
67
65
  # Consider as spam if form submitted too quickly
68
66
  if time_to_submit < threshold
69
- logger.warn("Potential spam detected for IP #{request.env['REMOTE_ADDR']}. Invisible Captcha timestamp threshold not reached (took #{time_to_submit.to_i}s).")
67
+ warn_spam("Invisible Captcha timestamp threshold not reached (took #{time_to_submit.to_i}s).")
68
+ return true
69
+ end
70
+
71
+ false
72
+ end
73
+
74
+ def spinner_spam?
75
+ if InvisibleCaptcha.spinner_enabled && params[:spinner] != session[:invisible_captcha_spinner]
76
+ warn_spam("Invisible Captcha spinner value mismatch")
70
77
  return true
71
78
  end
72
79
 
@@ -81,7 +88,8 @@ module InvisibleCaptcha
81
88
  # If honeypot is defined for this controller-action, search for:
82
89
  # - honeypot: params[:subtitle]
83
90
  # - honeypot with scope: params[:topic][:subtitle]
84
- if params[honeypot].present? || (params[scope] && params[scope][honeypot].present?)
91
+ if params[honeypot].present? || params.dig(scope, honeypot).present?
92
+ warn_spam("Invisible Captcha honeypot param '#{honeypot}' was present.")
85
93
  return true
86
94
  else
87
95
  # No honeypot spam detected, remove honeypot from params to avoid UnpermittedParameters exceptions
@@ -90,11 +98,29 @@ module InvisibleCaptcha
90
98
  end
91
99
  else
92
100
  InvisibleCaptcha.honeypots.each do |default_honeypot|
93
- return true if params[default_honeypot].present?
101
+ if params[default_honeypot].present?
102
+ warn_spam("Invisible Captcha honeypot param '#{default_honeypot}' was present.")
103
+ return true
104
+ end
94
105
  end
95
106
  end
96
107
 
97
108
  false
98
109
  end
110
+
111
+ def warn_spam(message)
112
+ logger.warn("Potential spam detected for IP #{request.remote_ip}. #{message}")
113
+
114
+ ActiveSupport::Notifications.instrument(
115
+ 'invisible_captcha.spam_detected',
116
+ message: message,
117
+ remote_ip: request.remote_ip,
118
+ user_agent: request.user_agent,
119
+ controller: params[:controller],
120
+ action: params[:action],
121
+ url: request.url,
122
+ params: request.filtered_parameters
123
+ )
124
+ end
99
125
  end
100
126
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InvisibleCaptcha
2
4
  module FormHelpers
3
5
  def invisible_captcha(honeypot, options = {})
4
6
  @template.invisible_captcha(honeypot, self.object_name, options)
5
7
  end
6
8
  end
7
- end
9
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InvisibleCaptcha
2
4
  class Railtie < Rails::Railtie
3
5
  initializer 'invisible_captcha.rails_integration' do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InvisibleCaptcha
2
- VERSION = "0.10.0"
4
+ VERSION = "2.0.0"
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module InvisibleCaptcha
2
4
  module ViewHelpers
3
5
  # Builds the honeypot html
@@ -8,9 +10,17 @@ module InvisibleCaptcha
8
10
  #
9
11
  # @return [String] the generated html
10
12
  def invisible_captcha(honeypot = nil, scope = nil, options = {})
11
- if InvisibleCaptcha.timestamp_enabled
13
+ @captcha_ocurrences = 0 unless defined?(@captcha_ocurrences)
14
+ @captcha_ocurrences += 1
15
+
16
+ if InvisibleCaptcha.timestamp_enabled || InvisibleCaptcha.spinner_enabled
12
17
  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
13
18
  end
19
+
20
+ if InvisibleCaptcha.spinner_enabled && @captcha_ocurrences == 1
21
+ session[:invisible_captcha_spinner] = InvisibleCaptcha.encode("#{session[:invisible_captcha_timestamp]}-#{current_request.remote_ip}")
22
+ end
23
+
14
24
  build_invisible_captcha(honeypot, scope, options)
15
25
  end
16
26
 
@@ -22,6 +32,10 @@ module InvisibleCaptcha
22
32
 
23
33
  private
24
34
 
35
+ def current_request
36
+ @request ||= request
37
+ end
38
+
25
39
  def build_invisible_captcha(honeypot = nil, scope = nil, options = {})
26
40
  if honeypot.is_a?(Hash)
27
41
  options = honeypot
@@ -41,7 +55,10 @@ module InvisibleCaptcha
41
55
  content_tag(:div, class: css_class) do
42
56
  concat styles unless InvisibleCaptcha.injectable_styles
43
57
  concat label_tag(build_label_name(honeypot, scope), label)
44
- concat text_field_tag(build_text_field_name(honeypot, scope), nil, options.merge(tabindex: -1))
58
+ concat text_field_tag(build_input_name(honeypot, scope), nil, default_honeypot_options.merge(options))
59
+ if InvisibleCaptcha.spinner_enabled
60
+ concat hidden_field_tag("spinner", session[:invisible_captcha_spinner])
61
+ end
45
62
  end
46
63
  end
47
64
 
@@ -54,7 +71,9 @@ module InvisibleCaptcha
54
71
 
55
72
  return if visible
56
73
 
57
- content_tag(:style, media: 'screen') do
74
+ nonce = content_security_policy_nonce if options[:nonce]
75
+
76
+ content_tag(:style, media: 'screen', nonce: nonce) do
58
77
  ".#{css_class} {#{InvisibleCaptcha.css_strategy}}"
59
78
  end
60
79
  end
@@ -67,12 +86,16 @@ module InvisibleCaptcha
67
86
  end
68
87
  end
69
88
 
70
- def build_text_field_name(honeypot, scope = nil)
89
+ def build_input_name(honeypot, scope = nil)
71
90
  if scope.present?
72
91
  "#{scope}[#{honeypot}]"
73
92
  else
74
93
  honeypot
75
94
  end
76
95
  end
96
+
97
+ def default_honeypot_options
98
+ { autocomplete: 'off', tabindex: -1 }
99
+ end
77
100
  end
78
101
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'invisible_captcha/version'
2
4
  require 'invisible_captcha/controller_ext'
3
5
  require 'invisible_captcha/view_helpers'
@@ -13,7 +15,9 @@ module InvisibleCaptcha
13
15
  :timestamp_threshold,
14
16
  :timestamp_enabled,
15
17
  :visual_honeypots,
16
- :injectable_styles
18
+ :injectable_styles,
19
+ :spinner_enabled,
20
+ :secret
17
21
 
18
22
  def init!
19
23
  # Default sentence for real users if text field was visible
@@ -31,9 +35,15 @@ module InvisibleCaptcha
31
35
  # Make honeypots visibles
32
36
  self.visual_honeypots = false
33
37
 
34
- # If enabled, you should call anywhere in of your layout the following helper, to inject the honeypot styles:
35
- # <%= invisible_captcha_styles %>
38
+ # If enabled, you should call anywhere in your layout the following helper, to inject the honeypot styles:
39
+ # <%= invisible_captcha_styles %>
36
40
  self.injectable_styles = false
41
+
42
+ # Spinner check enabled by default
43
+ self.spinner_enabled = true
44
+
45
+ # A secret key to encode some internal values
46
+ self.secret = ENV['INVISIBLE_CAPTCHA_SECRET'] || SecureRandom.hex(64)
37
47
  end
38
48
 
39
49
  def sentence_for_humans
@@ -68,6 +78,10 @@ module InvisibleCaptcha
68
78
  ].sample
69
79
  end
70
80
 
81
+ def encode(value)
82
+ Digest::MD5.hexdigest("#{self.secret}-#{value}")
83
+ end
84
+
71
85
  private
72
86
 
73
87
  def call_lambda_or_return(obj)
@@ -1,41 +1,27 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe InvisibleCaptcha::ControllerExt, type: :controller do
3
+ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
4
4
  render_views
5
5
 
6
- def switchable_post(action, params = {})
7
- if ::Rails::VERSION::STRING > '5'
8
- post action, params: params
9
- else
10
- post action, params
11
- end
12
- end
13
-
14
- def switchable_put(action, params = {})
15
- if ::Rails::VERSION::STRING > '5'
16
- put action, params: params
17
- else
18
- put action, params
19
- end
20
- end
21
-
22
6
  before(:each) do
23
7
  @controller = TopicsController.new
24
8
  request.env['HTTP_REFERER'] = 'http://test.host/topics'
9
+
25
10
  InvisibleCaptcha.init!
26
11
  InvisibleCaptcha.timestamp_threshold = 1
12
+ InvisibleCaptcha.spinner_enabled = false
27
13
  end
28
14
 
29
15
  context 'without invisible_captcha_timestamp in session' do
30
16
  it 'fails like if it was submitted too fast' do
31
- switchable_post :create, topic: { title: 'foo' }
17
+ post :create, params: { topic: { title: 'foo' } }
32
18
 
33
19
  expect(response).to redirect_to 'http://test.host/topics'
34
20
  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
35
21
  end
36
22
 
37
23
  it 'passes if disabled at action level' do
38
- switchable_post :copy, topic: { title: 'foo' }
24
+ post :copy, params: { topic: { title: 'foo' } }
39
25
 
40
26
  expect(flash[:error]).not_to be_present
41
27
  expect(response.body).to be_present
@@ -43,7 +29,8 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
43
29
 
44
30
  it 'passes if disabled at app level' do
45
31
  InvisibleCaptcha.timestamp_enabled = false
46
- switchable_post :create, topic: { title: 'foo' }
32
+
33
+ post :create, params: { topic: { title: 'foo' } }
47
34
 
48
35
  expect(flash[:error]).not_to be_present
49
36
  expect(response.body).to be_present
@@ -56,32 +43,57 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
56
43
  end
57
44
 
58
45
  it 'fails if submission before timestamp_threshold' do
59
- switchable_post :create, topic: { title: 'foo' }
46
+ post :create, params: { topic: { title: 'foo' } }
60
47
 
61
48
  expect(response).to redirect_to 'http://test.host/topics'
62
49
  expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message)
50
+
51
+ # Make sure session is cleared
52
+ expect(session[:invisible_captcha_timestamp]).to be_nil
63
53
  end
64
54
 
65
- it 'allow a custom on_timestamp_spam callback' do
66
- switchable_put :update, id: 1, topic: { title: 'bar' }
55
+ it 'allows a custom on_timestamp_spam callback' do
56
+ put :update, params: { id: 1, topic: { title: 'bar' } }
67
57
 
68
58
  expect(response.status).to eq(204)
69
59
  end
70
60
 
61
+ it 'allows a new timestamp to be set in the on_timestamp_spam callback' do
62
+ @controller.singleton_class.class_eval do
63
+ def custom_timestamp_callback
64
+ session[:invisible_captcha_timestamp] = 2.seconds.from_now(Time.zone.now).iso8601
65
+ head(204)
66
+ end
67
+ end
68
+
69
+ expect { put :update, params: { id: 1, topic: { title: 'bar' } } }
70
+ .to change { session[:invisible_captcha_timestamp] }
71
+ .to be_present
72
+ end
73
+
71
74
  context 'successful submissions' do
72
75
  it 'passes if submission on or after timestamp_threshold' do
73
76
  sleep InvisibleCaptcha.timestamp_threshold
74
77
 
75
- switchable_post :create, topic: { title: 'foo' }
78
+ post :create, params: {
79
+ topic: {
80
+ title: 'foobar',
81
+ author: 'author',
82
+ body: 'body that passes validation'
83
+ }
84
+ }
76
85
 
77
86
  expect(flash[:error]).not_to be_present
78
87
  expect(response.body).to be_present
88
+
89
+ # Make sure session is cleared
90
+ expect(session[:invisible_captcha_timestamp]).to be_nil
79
91
  end
80
92
 
81
93
  it 'allow to set a custom timestamp_threshold per action' do
82
94
  sleep 2 # custom threshold
83
95
 
84
- switchable_post :publish, id: 1
96
+ post :publish, params: { id: 1 }
85
97
 
86
98
  expect(flash[:error]).not_to be_present
87
99
  expect(response.body).to be_present
@@ -92,33 +104,90 @@ describe InvisibleCaptcha::ControllerExt, type: :controller do
92
104
  context 'honeypot attribute' do
93
105
  before(:each) do
94
106
  session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
107
+
95
108
  # Wait for valid submission
96
109
  sleep InvisibleCaptcha.timestamp_threshold
97
110
  end
98
111
 
99
112
  it 'fails with spam' do
100
- switchable_post :create, topic: { subtitle: 'foo' }
113
+ post :create, params: { topic: { subtitle: 'foo' } }
101
114
 
102
115
  expect(response.body).to be_blank
103
116
  end
104
117
 
105
118
  it 'passes with no spam' do
106
- switchable_post :create, topic: { title: 'foo' }
119
+ post :create, params: { topic: { title: 'foo' } }
107
120
 
108
121
  expect(response.body).to be_present
109
122
  end
110
123
 
111
124
  it 'allow a custom on_spam callback' do
112
- switchable_put :update, id: 1, topic: { subtitle: 'foo' }
125
+ put :update, params: { id: 1, topic: { subtitle: 'foo' } }
113
126
 
114
127
  expect(response.body).to redirect_to(new_topic_path)
115
128
  end
116
129
 
117
130
  it 'honeypot is removed from params if you use a custom honeypot' do
118
- switchable_post :create, topic: { title: 'foo', subtitle: '' }
131
+ post :create, params: { topic: { title: 'foo', subtitle: '' } }
119
132
 
120
133
  expect(flash[:error]).not_to be_present
121
134
  expect(@controller.params[:topic].key?(:subtitle)).to eq(false)
122
135
  end
136
+
137
+ describe 'ActiveSupport::Notifications' do
138
+ let(:dummy_handler) { double(handle_event: nil) }
139
+
140
+ let!(:subscriber) do
141
+ subscriber = ActiveSupport::Notifications.subscribe('invisible_captcha.spam_detected') do |*args, data|
142
+ dummy_handler.handle_event(data)
143
+ end
144
+
145
+ subscriber
146
+ end
147
+
148
+ after { ActiveSupport::Notifications.unsubscribe(subscriber) }
149
+
150
+ it 'dispatches an `invisible_captcha.spam_detected` event' do
151
+ expect(dummy_handler).to receive(:handle_event).once.with(
152
+ message: "Invisible Captcha honeypot param 'subtitle' was present.",
153
+ remote_ip: '0.0.0.0',
154
+ user_agent: 'Rails Testing',
155
+ controller: 'topics',
156
+ action: 'create',
157
+ url: 'http://test.host/topics',
158
+ params: {
159
+ topic: { subtitle: "foo"},
160
+ controller: 'topics',
161
+ action: 'create'
162
+ }
163
+ )
164
+
165
+ post :create, params: { topic: { subtitle: 'foo' } }
166
+ end
167
+ end
168
+ end
169
+
170
+ context 'spinner attribute' do
171
+ before(:each) do
172
+ InvisibleCaptcha.spinner_enabled = true
173
+ InvisibleCaptcha.secret = 'secret'
174
+ session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
175
+ session[:invisible_captcha_spinner] = '32ab649161f9f6faeeb323746de1a25d'
176
+
177
+ # Wait for valid submission
178
+ sleep InvisibleCaptcha.timestamp_threshold
179
+ end
180
+
181
+ it 'fails with no spam, but mismatch of spinner' do
182
+ post :create, params: { topic: { title: 'foo' }, spinner: 'mismatch' }
183
+
184
+ expect(response.body).to be_blank
185
+ end
186
+
187
+ it 'passes with no spam and spinner match' do
188
+ post :create, params: { topic: { title: 'foo' }, spinner: '32ab649161f9f6faeeb323746de1a25d' }
189
+
190
+ expect(response.body).to be_present
191
+ end
123
192
  end
124
193
  end
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
@@ -14,13 +14,8 @@ Dummy::Application.configure do
14
14
 
15
15
  # Disable serving static files from the `/public` folder by default since
16
16
  # Apache or NGINX already handles this.
17
- if Rails.version >= "5.0.0"
18
- config.public_file_server.enabled = true
19
- config.public_file_server.headers = {'Cache-Control' => 'public, max-age=3600'}
20
- else
21
- config.serve_static_files = true
22
- config.static_cache_control = "public, max-age=3600"
23
- end
17
+ config.public_file_server.enabled = true
18
+ config.public_file_server.headers = {'Cache-Control' => 'public, max-age=3600'}
24
19
 
25
20
  # Show full error reports and disable caching.
26
21
  config.consider_all_requests_local = true
@@ -39,4 +34,4 @@ Dummy::Application.configure do
39
34
 
40
35
  # Print deprecation notices to the stderr.
41
36
  config.active_support.deprecation = :stderr
42
- end
37
+ end
@@ -1,6 +1,6 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe InvisibleCaptcha do
3
+ RSpec.describe InvisibleCaptcha do
4
4
  it 'initialize with defaults' do
5
5
  InvisibleCaptcha.init!
6
6
 
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ENV['RAILS_ENV'] = 'test'
4
+
2
5
  require File.expand_path("../dummy/config/environment.rb", __FILE__)
3
6
  require 'rspec/rails'
4
7
  require 'invisible_captcha'
5
8
 
6
9
  RSpec.configure do |config|
7
- config.order = 'random'
10
+ config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
11
+ config.disable_monkey_patching!
12
+ config.order = :random
8
13
  config.expect_with :rspec
9
14
  config.mock_with :rspec do |mocks|
10
15
  mocks.verify_partial_doubles = true
@@ -1,9 +1,9 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
- describe InvisibleCaptcha::ViewHelpers, type: :helper do
3
+ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
4
4
  before(:each) do
5
- allow(Time.zone).to receive(:now).and_return(Time.zone.parse('Feb 19 1986'))
6
5
  allow(InvisibleCaptcha).to receive(:css_strategy).and_return("display:none;")
6
+ allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
7
7
 
8
8
  # to test content_for and provide
9
9
  @view_flow = ActionView::OutputFlow.new
@@ -28,10 +28,14 @@ describe InvisibleCaptcha::ViewHelpers, type: :helper do
28
28
  expect(invisible_captcha(:subtitle, :topic, { class: 'foo_class' })).to match(/class="foo_class"/)
29
29
  end
30
30
 
31
+ it 'with CSP nonce' do
32
+ expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
33
+ end
34
+
31
35
  it 'generated html + styles' do
32
36
  InvisibleCaptcha.honeypots = [:foo_id]
33
37
  output = invisible_captcha.gsub("\"", "'")
34
- regexp = %r{<div class='foo_id_\w*'><style.*>.foo_id_\w* {display:none;}</style><label.*>#{InvisibleCaptcha.sentence_for_humans}.*<input.*name='foo_id'.*tabindex='-1'.*</div>}
38
+ regexp = %r{<div class='foo_id_\w*'><style.*>.foo_id_\w* {display:none;}</style><label.*>#{InvisibleCaptcha.sentence_for_humans}</label><input (?=.*name='foo_id'.*)(?=.*autocomplete='off'.*)(?=.*tabindex='-1'.*).*/></div>}
35
39
 
36
40
  expect(output).to match(regexp)
37
41
  end
@@ -54,6 +58,18 @@ describe InvisibleCaptcha::ViewHelpers, type: :helper do
54
58
  end
55
59
  end
56
60
 
61
+ context "should have spinner field" do
62
+ it 'that exists by default, spinner_enabled is true' do
63
+ InvisibleCaptcha.spinner_enabled = true
64
+ expect(invisible_captcha).to match(/spinner/)
65
+ end
66
+
67
+ it 'that does not exist if spinner_enabled is false' do
68
+ InvisibleCaptcha.spinner_enabled = false
69
+ expect(invisible_captcha).not_to match(/spinner/)
70
+ end
71
+ end
72
+
57
73
  it 'should set spam timestamp' do
58
74
  invisible_captcha
59
75
  expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601)
@@ -62,14 +78,14 @@ describe InvisibleCaptcha::ViewHelpers, type: :helper do
62
78
  context 'injectable_styles option' do
63
79
  it 'by default, render styles along with the honeypot' do
64
80
  expect(invisible_captcha).to match(/display:none/)
65
- expect(helper.content_for(:invisible_captcha_styles)).to be_blank
81
+ expect(@view_flow.content[:invisible_captcha_styles]).to be_blank
66
82
  end
67
83
 
68
84
  it 'if injectable_styles is set, do not append styles inline' do
69
85
  InvisibleCaptcha.injectable_styles = true
70
86
 
71
87
  expect(invisible_captcha).not_to match(/display:none;/)
72
- expect(helper.content_for(:invisible_captcha_styles)).to match(/display:none;/)
88
+ expect(@view_flow.content[:invisible_captcha_styles]).to match(/display:none;/)
73
89
  end
74
90
  end
75
91
  end