invisible_captcha 1.1.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +35 -0
- data/Appraisals +9 -18
- data/CHANGELOG.md +13 -0
- data/LICENSE +1 -1
- data/README.md +25 -15
- data/Rakefile +1 -6
- data/gemfiles/{rails_4.2.gemfile → rails_6.1.gemfile} +1 -1
- data/gemfiles/{rails_5.0.gemfile → rails_7.0.gemfile} +1 -1
- data/invisible_captcha.gemspec +7 -4
- data/lib/invisible_captcha/controller_ext.rb +24 -17
- data/lib/invisible_captcha/form_helpers.rb +1 -1
- data/lib/invisible_captcha/version.rb +1 -1
- data/lib/invisible_captcha/view_helpers.rb +17 -6
- data/lib/invisible_captcha.rb +15 -3
- data/spec/controllers_spec.rb +103 -51
- data/spec/dummy/app/controllers/topics_controller.rb +12 -0
- data/spec/dummy/app/views/layouts/application.html.erb +1 -2
- data/spec/dummy/config/application.rb +0 -2
- data/spec/dummy/config/environments/development.rb +1 -4
- data/spec/dummy/config/environments/test.rb +3 -8
- data/spec/dummy/config/routes.rb +2 -0
- data/spec/dummy/{app/assets/stylesheets/application.css → public/styles.css} +9 -4
- data/spec/spec_helper.rb +8 -17
- data/spec/view_helpers_spec.rb +15 -9
- metadata +61 -30
- data/.travis.yml +0 -27
- data/gemfiles/rails_5.1.gemfile +0 -7
- data/spec/dummy/app/assets/config/manifest.js +0 -2
- data/spec/dummy/app/assets/javascripts/application.js +0 -1
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/config/environments/production.rb +0 -86
- data/spec/dummy/lib/assets/.gitkeep +0 -0
data/spec/controllers_spec.rb
CHANGED
@@ -3,39 +3,25 @@
|
|
3
3
|
RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
4
4
|
render_views
|
5
5
|
|
6
|
-
def switchable_post(action, params = {})
|
7
|
-
if Rails.version > '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 > '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 @@ RSpec.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,7 +43,7 @@ RSpec.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)
|
@@ -66,7 +53,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
66
53
|
end
|
67
54
|
|
68
55
|
it 'allows a custom on_timestamp_spam callback' do
|
69
|
-
|
56
|
+
put :update, params: { id: 1, topic: { title: 'bar' } }
|
70
57
|
|
71
58
|
expect(response.status).to eq(204)
|
72
59
|
end
|
@@ -79,7 +66,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
79
66
|
end
|
80
67
|
end
|
81
68
|
|
82
|
-
expect {
|
69
|
+
expect { put :update, params: { id: 1, topic: { title: 'bar' } } }
|
83
70
|
.to change { session[:invisible_captcha_timestamp] }
|
84
71
|
.to be_present
|
85
72
|
end
|
@@ -88,10 +75,12 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
88
75
|
it 'passes if submission on or after timestamp_threshold' do
|
89
76
|
sleep InvisibleCaptcha.timestamp_threshold
|
90
77
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
78
|
+
post :create, params: {
|
79
|
+
topic: {
|
80
|
+
title: 'foobar',
|
81
|
+
author: 'author',
|
82
|
+
body: 'body that passes validation'
|
83
|
+
}
|
95
84
|
}
|
96
85
|
|
97
86
|
expect(flash[:error]).not_to be_present
|
@@ -104,7 +93,7 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
104
93
|
it 'allow to set a custom timestamp_threshold per action' do
|
105
94
|
sleep 2 # custom threshold
|
106
95
|
|
107
|
-
|
96
|
+
post :publish, params: { id: 1 }
|
108
97
|
|
109
98
|
expect(flash[:error]).not_to be_present
|
110
99
|
expect(response.body).to be_present
|
@@ -115,30 +104,75 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
115
104
|
context 'honeypot attribute' do
|
116
105
|
before(:each) do
|
117
106
|
session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
|
107
|
+
|
118
108
|
# Wait for valid submission
|
119
109
|
sleep InvisibleCaptcha.timestamp_threshold
|
120
110
|
end
|
121
111
|
|
122
112
|
it 'fails with spam' do
|
123
|
-
|
113
|
+
post :create, params: { topic: { subtitle: 'foo' } }
|
124
114
|
|
125
115
|
expect(response.body).to be_blank
|
126
116
|
end
|
127
117
|
|
128
118
|
it 'passes with no spam' do
|
129
|
-
|
119
|
+
post :create, params: { topic: { title: 'foo' } }
|
130
120
|
|
131
121
|
expect(response.body).to be_present
|
132
122
|
end
|
133
123
|
|
124
|
+
context 'with random honeypot' do
|
125
|
+
context 'auto-scoped' do
|
126
|
+
it 'passes with no spam' do
|
127
|
+
post :categorize, params: { topic: { title: 'foo' } }
|
128
|
+
|
129
|
+
expect(response.body).to be_present
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'fails with spam' do
|
133
|
+
post :categorize, params: { topic: { "#{InvisibleCaptcha.honeypots.sample}": 'foo' } }
|
134
|
+
|
135
|
+
expect(response.body).to be_blank
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context 'with no scope' do
|
140
|
+
it 'passes with no spam' do
|
141
|
+
post :categorize
|
142
|
+
|
143
|
+
expect(response.body).to be_present
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'fails with spam' do
|
147
|
+
post :categorize, params: { "#{InvisibleCaptcha.honeypots.sample}": 'foo' }
|
148
|
+
|
149
|
+
expect(response.body).to be_blank
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'with scope' do
|
154
|
+
it 'fails with spam' do
|
155
|
+
post :rename, params: { topic: { "#{InvisibleCaptcha.honeypots.sample}": 'foo' } }
|
156
|
+
|
157
|
+
expect(response.body).to be_blank
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'passes with no spam' do
|
161
|
+
post :rename, params: { topic: { title: 'foo' } }
|
162
|
+
|
163
|
+
expect(response.body).to be_blank
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
134
168
|
it 'allow a custom on_spam callback' do
|
135
|
-
|
169
|
+
put :update, params: { id: 1, topic: { subtitle: 'foo' } }
|
136
170
|
|
137
171
|
expect(response.body).to redirect_to(new_topic_path)
|
138
172
|
end
|
139
173
|
|
140
174
|
it 'honeypot is removed from params if you use a custom honeypot' do
|
141
|
-
|
175
|
+
post :create, params: { topic: { title: 'foo', subtitle: '' } }
|
142
176
|
|
143
177
|
expect(flash[:error]).not_to be_present
|
144
178
|
expect(@controller.params[:topic].key?(:subtitle)).to eq(false)
|
@@ -155,31 +189,49 @@ RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do
|
|
155
189
|
subscriber
|
156
190
|
end
|
157
191
|
|
158
|
-
after
|
192
|
+
after { ActiveSupport::Notifications.unsubscribe(subscriber) }
|
159
193
|
|
160
194
|
it 'dispatches an `invisible_captcha.spam_detected` event' do
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
195
|
+
expect(dummy_handler).to receive(:handle_event).once.with({
|
196
|
+
message: "[Invisible Captcha] Potential spam detected for IP 0.0.0.0. Honeypot param 'subtitle' was present.",
|
197
|
+
remote_ip: '0.0.0.0',
|
198
|
+
user_agent: 'Rails Testing',
|
199
|
+
controller: 'topics',
|
200
|
+
action: 'create',
|
201
|
+
url: 'http://test.host/topics',
|
202
|
+
params: {
|
203
|
+
topic: { subtitle: "foo"},
|
168
204
|
controller: 'topics',
|
169
|
-
action: 'create'
|
170
|
-
|
171
|
-
|
172
|
-
topic: { subtitle: "foo"},
|
173
|
-
controller: 'topics',
|
174
|
-
action: 'create'
|
175
|
-
}
|
176
|
-
)
|
177
|
-
else
|
178
|
-
expect(dummy_handler).to receive(:handle_event).once
|
179
|
-
end
|
205
|
+
action: 'create'
|
206
|
+
}
|
207
|
+
})
|
180
208
|
|
181
|
-
|
209
|
+
post :create, params: { topic: { subtitle: 'foo' } }
|
182
210
|
end
|
183
211
|
end
|
184
212
|
end
|
213
|
+
|
214
|
+
context 'spinner attribute' do
|
215
|
+
before(:each) do
|
216
|
+
InvisibleCaptcha.spinner_enabled = true
|
217
|
+
InvisibleCaptcha.secret = 'secret'
|
218
|
+
session[:invisible_captcha_timestamp] = Time.zone.now.iso8601
|
219
|
+
session[:invisible_captcha_spinner] = '32ab649161f9f6faeeb323746de1a25d'
|
220
|
+
|
221
|
+
# Wait for valid submission
|
222
|
+
sleep InvisibleCaptcha.timestamp_threshold
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'fails with no spam, but mismatch of spinner' do
|
226
|
+
post :create, params: { topic: { title: 'foo' }, spinner: 'mismatch' }
|
227
|
+
|
228
|
+
expect(response.body).to be_blank
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'passes with no spam and spinner match' do
|
232
|
+
post :create, params: { topic: { title: 'foo' }, spinner: '32ab649161f9f6faeeb323746de1a25d' }
|
233
|
+
|
234
|
+
expect(response.body).to be_present
|
235
|
+
end
|
236
|
+
end
|
185
237
|
end
|
@@ -9,6 +9,10 @@ class TopicsController < ApplicationController
|
|
9
9
|
|
10
10
|
invisible_captcha honeypot: :subtitle, only: :copy, timestamp_enabled: false
|
11
11
|
|
12
|
+
invisible_captcha scope: :topic, only: :rename
|
13
|
+
|
14
|
+
invisible_captcha only: :categorize
|
15
|
+
|
12
16
|
def index
|
13
17
|
redirect_to new_topic_path
|
14
18
|
end
|
@@ -28,6 +32,14 @@ class TopicsController < ApplicationController
|
|
28
32
|
end
|
29
33
|
|
30
34
|
def update
|
35
|
+
redirect_to new_topic_path
|
36
|
+
end
|
37
|
+
|
38
|
+
def rename
|
39
|
+
end
|
40
|
+
|
41
|
+
def categorize
|
42
|
+
redirect_to new_topic_path
|
31
43
|
end
|
32
44
|
|
33
45
|
def publish
|
@@ -2,9 +2,7 @@ require File.expand_path('../boot', __FILE__)
|
|
2
2
|
|
3
3
|
require 'action_controller/railtie'
|
4
4
|
require 'action_view/railtie'
|
5
|
-
require 'action_mailer/railtie'
|
6
5
|
require 'active_model/railtie'
|
7
|
-
require 'sprockets/railtie'
|
8
6
|
|
9
7
|
# Require the gems listed in Gemfile, including any gems
|
10
8
|
# you've limited to :test, :development, or :production.
|
@@ -14,7 +14,7 @@ Dummy::Application.configure do
|
|
14
14
|
config.action_controller.perform_caching = false
|
15
15
|
|
16
16
|
# Don't care if the mailer can't send.
|
17
|
-
config.action_mailer.raise_delivery_errors = false
|
17
|
+
# config.action_mailer.raise_delivery_errors = false
|
18
18
|
|
19
19
|
# Print deprecation notices to the Rails logger.
|
20
20
|
config.active_support.deprecation = :log
|
@@ -31,9 +31,6 @@ Dummy::Application.configure do
|
|
31
31
|
# yet still be able to expire them through the digest params.
|
32
32
|
# config.assets.digest = true
|
33
33
|
|
34
|
-
# quiet assets
|
35
|
-
config.assets.quiet = true
|
36
|
-
|
37
34
|
# Adds additional error checking when serving assets at runtime.
|
38
35
|
# Checks for improperly declared sprockets dependencies.
|
39
36
|
# Raises helpful error messages.
|
@@ -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
|
@@ -35,7 +30,7 @@ Dummy::Application.configure do
|
|
35
30
|
# Tell Action Mailer not to deliver emails to the real world.
|
36
31
|
# The :test delivery method accumulates sent emails in the
|
37
32
|
# ActionMailer::Base.deliveries array.
|
38
|
-
config.action_mailer.delivery_method = :test
|
33
|
+
# config.action_mailer.delivery_method = :test
|
39
34
|
|
40
35
|
# Print deprecation notices to the stderr.
|
41
36
|
config.active_support.deprecation = :stderr
|
data/spec/dummy/config/routes.rb
CHANGED
@@ -8,8 +8,11 @@ h1 {
|
|
8
8
|
border-bottom: 3px solid;
|
9
9
|
}
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
a {
|
12
|
+
color: #000;
|
13
|
+
}
|
14
|
+
|
15
|
+
input, textarea {
|
13
16
|
border: 0;
|
14
17
|
margin-bottom: 1.5em;
|
15
18
|
}
|
@@ -19,7 +22,9 @@ input {
|
|
19
22
|
}
|
20
23
|
|
21
24
|
button {
|
22
|
-
|
25
|
+
color: #fff;
|
26
|
+
background-color: #000;
|
27
|
+
border: none;
|
23
28
|
border-radius: 0.25em;
|
24
29
|
height: 3em;
|
25
30
|
width: 10em;
|
@@ -28,4 +33,4 @@ button {
|
|
28
33
|
|
29
34
|
.errors {
|
30
35
|
color: darkred;
|
31
|
-
}
|
36
|
+
}
|
data/spec/spec_helper.rb
CHANGED
@@ -2,14 +2,19 @@
|
|
2
2
|
|
3
3
|
ENV['RAILS_ENV'] = 'test'
|
4
4
|
|
5
|
+
require 'simplecov'
|
6
|
+
if ENV['CI']
|
7
|
+
require 'simplecov-cobertura'
|
8
|
+
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
9
|
+
end
|
10
|
+
SimpleCov.start
|
11
|
+
|
5
12
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
6
13
|
require 'rspec/rails'
|
7
14
|
require 'invisible_captcha'
|
8
15
|
|
9
16
|
RSpec.configure do |config|
|
10
|
-
|
11
|
-
config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
|
12
|
-
end
|
17
|
+
config.include ActionDispatch::ContentSecurityPolicy::Request, type: :helper
|
13
18
|
config.disable_monkey_patching!
|
14
19
|
config.order = :random
|
15
20
|
config.expect_with :rspec
|
@@ -17,17 +22,3 @@ RSpec.configure do |config|
|
|
17
22
|
mocks.verify_partial_doubles = true
|
18
23
|
end
|
19
24
|
end
|
20
|
-
|
21
|
-
# Rails 4.2 call `initialize` inside `recycle!`. However Ruby 2.6 doesn't allow calling `initialize` twice.
|
22
|
-
# More info: https://github.com/rails/rails/issues/34790
|
23
|
-
if RUBY_VERSION >= "2.6.0" && Rails.version < "5"
|
24
|
-
module ActionController
|
25
|
-
class TestResponse < ActionDispatch::TestResponse
|
26
|
-
def recycle!
|
27
|
-
@mon_mutex_owner_object_id = nil
|
28
|
-
@mon_mutex = nil
|
29
|
-
initialize
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
data/spec/view_helpers_spec.rb
CHANGED
@@ -2,12 +2,8 @@
|
|
2
2
|
|
3
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;")
|
7
|
-
|
8
|
-
if Rails.version >= '5.2'
|
9
|
-
allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
|
10
|
-
end
|
6
|
+
allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123')
|
11
7
|
|
12
8
|
# to test content_for and provide
|
13
9
|
@view_flow = ActionView::OutputFlow.new
|
@@ -32,10 +28,8 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
|
32
28
|
expect(invisible_captcha(:subtitle, :topic, { class: 'foo_class' })).to match(/class="foo_class"/)
|
33
29
|
end
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
|
38
|
-
end
|
31
|
+
it 'with CSP nonce' do
|
32
|
+
expect(invisible_captcha(:subtitle, :topic, { nonce: true })).to match(/nonce="123"/)
|
39
33
|
end
|
40
34
|
|
41
35
|
it 'generated html + styles' do
|
@@ -64,6 +58,18 @@ RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do
|
|
64
58
|
end
|
65
59
|
end
|
66
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
|
+
|
67
73
|
it 'should set spam timestamp' do
|
68
74
|
invisible_captcha
|
69
75
|
expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601)
|