invisible_captcha 0.10.0 → 2.0.0
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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -23
- data/Appraisals +6 -14
- data/CHANGELOG.md +151 -0
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +178 -40
- data/Rakefile +2 -0
- data/gemfiles/{rails_3.2.gemfile → rails_5.2.gemfile} +2 -2
- data/gemfiles/{rails_4.1.gemfile → rails_6.0.gemfile} +2 -2
- data/gemfiles/{rails_5.1.gemfile → rails_6.1.gemfile} +2 -2
- data/invisible_captcha.gemspec +4 -7
- data/lib/invisible_captcha/controller_ext.rb +40 -14
- data/lib/invisible_captcha/form_helpers.rb +3 -1
- data/lib/invisible_captcha/railtie.rb +2 -0
- data/lib/invisible_captcha/version.rb +3 -1
- data/lib/invisible_captcha/view_helpers.rb +27 -4
- data/lib/invisible_captcha.rb +17 -3
- data/spec/controllers_spec.rb +99 -30
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/config/environments/test.rb +3 -8
- data/spec/invisible_captcha_spec.rb +2 -2
- data/spec/spec_helper.rb +6 -1
- data/spec/view_helpers_spec.rb +22 -6
- metadata +15 -42
- data/gemfiles/rails_4.2.gemfile +0 -7
- data/gemfiles/rails_5.0.gemfile +0 -7
@@ -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
|
6
|
-
|
7
|
+
if options.key?(:prepend)
|
8
|
+
prepend_before_action(options) do
|
7
9
|
detect_spam(options)
|
8
10
|
end
|
9
11
|
else
|
10
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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? || (
|
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
|
-
|
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,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
|
-
|
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(
|
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
|
-
|
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
|
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
|
data/lib/invisible_captcha.rb
CHANGED
@@ -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
|
35
|
-
#
|
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)
|
data/spec/controllers_spec.rb
CHANGED
@@ -1,41 +1,27 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 '
|
66
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
18
|
-
|
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
|
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.
|
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
|
data/spec/view_helpers_spec.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
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}
|
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(
|
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(
|
88
|
+
expect(@view_flow.content[:invisible_captcha_styles]).to match(/display:none;/)
|
73
89
|
end
|
74
90
|
end
|
75
91
|
end
|