invisible_captcha 1.1.0 → 2.1.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 +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)
|