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.
- 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
|