spamtrap 0.3.4 → 0.4.2
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/lib/generators/spamtrap/install/install_generator.rb +15 -0
- data/lib/generators/spamtrap/install/templates/spamtrap.rb +75 -0
- data/lib/spamtrap/controller.rb +237 -43
- data/lib/spamtrap/crypto.rb +68 -19
- data/lib/spamtrap/helper.rb +251 -64
- data/lib/spamtrap/railtie.rb +13 -0
- data/lib/spamtrap/test_helper.rb +103 -0
- data/lib/spamtrap/version.rb +1 -1
- data/lib/spamtrap.rb +166 -1
- metadata +100 -14
- data/test/controller_test.rb +0 -473
- data/test/form_builder_mutation_test.rb +0 -142
- data/test/test_helper.rb +0 -33
data/test/controller_test.rb
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
require File.join(File.dirname(__FILE__), 'test_helper')
|
|
2
|
-
|
|
3
|
-
# Test controllers — render a body so we can distinguish a real response
|
|
4
|
-
# from a spamtrap-blocked one (which uses head 200 and has an empty body).
|
|
5
|
-
|
|
6
|
-
class HoneypotController < ActionController::Base
|
|
7
|
-
spamtrap :trap_field, only: :create
|
|
8
|
-
|
|
9
|
-
def create
|
|
10
|
-
render plain: 'success'
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class NonceController < ActionController::Base
|
|
15
|
-
spamtrap :trap_field, nonce: true, only: :create
|
|
16
|
-
|
|
17
|
-
def create
|
|
18
|
-
render plain: 'success'
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
class NonceTimeoutController < ActionController::Base
|
|
23
|
-
spamtrap :trap_field, nonce: true, nonce_timeout: 60, only: :create
|
|
24
|
-
|
|
25
|
-
def create
|
|
26
|
-
render plain: 'success'
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class MutationController < ActionController::Base
|
|
31
|
-
spamtrap :trap_field, mutate: true, only: :create
|
|
32
|
-
|
|
33
|
-
def create
|
|
34
|
-
# Render the remapped comment param keys so tests can assert on them
|
|
35
|
-
render plain: params[:comment].to_unsafe_h.keys.sort.join(',')
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
class NestedMutationController < ActionController::Base
|
|
40
|
-
spamtrap :trap_field, mutate: true, only: :create
|
|
41
|
-
|
|
42
|
-
def create
|
|
43
|
-
# Render top-level and nested address keys to verify salt propagation
|
|
44
|
-
comment_keys = params[:comment].to_unsafe_h.except('address').keys.sort
|
|
45
|
-
address_keys = params.dig(:comment, :address).to_unsafe_h.keys.sort rescue []
|
|
46
|
-
render plain: "#{comment_keys.join(',')};#{address_keys.join(',')}"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
module NonceTestHelper
|
|
51
|
-
REMOTE_IP = '0.0.0.0'
|
|
52
|
-
|
|
53
|
-
def generate_nonce(timestamp, ip = REMOTE_IP)
|
|
54
|
-
secret = Rails.application.secret_key_base
|
|
55
|
-
OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp}:#{ip}")
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
module MutationTestHelper
|
|
60
|
-
include Spamtrap::Crypto
|
|
61
|
-
|
|
62
|
-
MUTATION_SALT = SecureRandom.bytes(Spamtrap::Crypto::NONCE_LEN)
|
|
63
|
-
MUTATION_SALT_HEX = MUTATION_SALT.unpack1('H*')
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
class HoneypotControllerTest < ActionController::TestCase
|
|
67
|
-
tests HoneypotController
|
|
68
|
-
|
|
69
|
-
def test_empty_honeypot_allows_request
|
|
70
|
-
post :create, params: { trap_field: '' }
|
|
71
|
-
assert_response :ok
|
|
72
|
-
assert_equal 'success', response.body
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def test_filled_honeypot_silently_discards
|
|
76
|
-
post :create, params: { trap_field: 'buy cheap meds' }
|
|
77
|
-
assert_response :ok
|
|
78
|
-
assert_empty response.body
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
class NonceControllerTest < ActionController::TestCase
|
|
83
|
-
include NonceTestHelper
|
|
84
|
-
tests NonceController
|
|
85
|
-
|
|
86
|
-
def test_valid_nonce_allows_request
|
|
87
|
-
timestamp = Time.now.to_i
|
|
88
|
-
post :create, params: {
|
|
89
|
-
trap_field: '',
|
|
90
|
-
spamtrap_timestamp: timestamp,
|
|
91
|
-
spamtrap_nonce: generate_nonce(timestamp)
|
|
92
|
-
}
|
|
93
|
-
assert_response :ok
|
|
94
|
-
assert_equal 'success', response.body
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def test_tampered_nonce_is_rejected
|
|
98
|
-
timestamp = Time.now.to_i
|
|
99
|
-
post :create, params: {
|
|
100
|
-
trap_field: '',
|
|
101
|
-
spamtrap_timestamp: timestamp,
|
|
102
|
-
spamtrap_nonce: 'deadbeef'
|
|
103
|
-
}
|
|
104
|
-
assert_response :ok
|
|
105
|
-
assert_empty response.body
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def test_expired_nonce_is_rejected
|
|
109
|
-
timestamp = Time.now.to_i - 7200
|
|
110
|
-
post :create, params: {
|
|
111
|
-
trap_field: '',
|
|
112
|
-
spamtrap_timestamp: timestamp,
|
|
113
|
-
spamtrap_nonce: generate_nonce(timestamp)
|
|
114
|
-
}
|
|
115
|
-
assert_response :ok
|
|
116
|
-
assert_empty response.body
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def test_missing_nonce_fields_are_rejected
|
|
120
|
-
post :create, params: { trap_field: '' }
|
|
121
|
-
assert_response :ok
|
|
122
|
-
assert_empty response.body
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def test_filled_honeypot_takes_priority_over_valid_nonce
|
|
126
|
-
timestamp = Time.now.to_i
|
|
127
|
-
post :create, params: {
|
|
128
|
-
trap_field: 'spam',
|
|
129
|
-
spamtrap_timestamp: timestamp,
|
|
130
|
-
spamtrap_nonce: generate_nonce(timestamp)
|
|
131
|
-
}
|
|
132
|
-
assert_response :ok
|
|
133
|
-
assert_empty response.body
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
class NonceTimeoutControllerTest < ActionController::TestCase
|
|
138
|
-
include NonceTestHelper
|
|
139
|
-
tests NonceTimeoutController
|
|
140
|
-
|
|
141
|
-
def test_nonce_within_custom_timeout_passes
|
|
142
|
-
timestamp = Time.now.to_i - 30
|
|
143
|
-
post :create, params: {
|
|
144
|
-
trap_field: '',
|
|
145
|
-
spamtrap_timestamp: timestamp,
|
|
146
|
-
spamtrap_nonce: generate_nonce(timestamp)
|
|
147
|
-
}
|
|
148
|
-
assert_response :ok
|
|
149
|
-
assert_equal 'success', response.body
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def test_nonce_beyond_custom_timeout_is_rejected
|
|
153
|
-
timestamp = Time.now.to_i - 120
|
|
154
|
-
post :create, params: {
|
|
155
|
-
trap_field: '',
|
|
156
|
-
spamtrap_timestamp: timestamp,
|
|
157
|
-
spamtrap_nonce: generate_nonce(timestamp)
|
|
158
|
-
}
|
|
159
|
-
assert_response :ok
|
|
160
|
-
assert_empty response.body
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
class MutationControllerTest < ActionController::TestCase
|
|
165
|
-
include MutationTestHelper
|
|
166
|
-
tests MutationController
|
|
167
|
-
|
|
168
|
-
def test_encrypted_field_names_are_remapped_to_real_names
|
|
169
|
-
body_token = spamtrap_encrypt_field('body', MUTATION_SALT)
|
|
170
|
-
email_token = spamtrap_encrypt_field('email', MUTATION_SALT)
|
|
171
|
-
|
|
172
|
-
post :create, params: {
|
|
173
|
-
trap_field: '',
|
|
174
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
175
|
-
comment: { body_token => 'Hello world', email_token => 'test@example.com' }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
assert_response :ok
|
|
179
|
-
assert_equal 'body,email', response.body
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def test_unencrypted_field_names_pass_through_unchanged
|
|
183
|
-
post :create, params: {
|
|
184
|
-
trap_field: '',
|
|
185
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
186
|
-
comment: { body: 'Hello', email: 'test@example.com' }
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
assert_response :ok
|
|
190
|
-
assert_equal 'body,email', response.body
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def test_no_salt_leaves_encrypted_params_unmapped
|
|
194
|
-
body_token = spamtrap_encrypt_field('body', MUTATION_SALT)
|
|
195
|
-
|
|
196
|
-
post :create, params: {
|
|
197
|
-
trap_field: '',
|
|
198
|
-
comment: { body_token => 'Hello' }
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
assert_response :ok
|
|
202
|
-
refute_equal 'body', response.body
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def test_wrong_salt_fails_to_decrypt
|
|
206
|
-
other_salt = SecureRandom.bytes(Spamtrap::Crypto::NONCE_LEN)
|
|
207
|
-
body_token = spamtrap_encrypt_field('body', other_salt)
|
|
208
|
-
|
|
209
|
-
post :create, params: {
|
|
210
|
-
trap_field: '',
|
|
211
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
212
|
-
comment: { body_token => 'Hello' }
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
assert_response :ok
|
|
216
|
-
refute_equal 'body', response.body
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
class NestedMutationControllerTest < ActionController::TestCase
|
|
221
|
-
include MutationTestHelper
|
|
222
|
-
tests NestedMutationController
|
|
223
|
-
|
|
224
|
-
def test_salt_propagates_to_fields_for_child_builder
|
|
225
|
-
body_token = spamtrap_encrypt_field('body', MUTATION_SALT)
|
|
226
|
-
street_token = spamtrap_encrypt_field('street', MUTATION_SALT)
|
|
227
|
-
city_token = spamtrap_encrypt_field('city', MUTATION_SALT)
|
|
228
|
-
|
|
229
|
-
post :create, params: {
|
|
230
|
-
trap_field: '',
|
|
231
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
232
|
-
comment: {
|
|
233
|
-
body_token => 'Hello',
|
|
234
|
-
address: {
|
|
235
|
-
street_token => '123 Main St',
|
|
236
|
-
city_token => 'Springfield'
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
assert_response :ok
|
|
242
|
-
assert_equal 'body;city,street', response.body
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Controller that relies entirely on global defaults (no per-call options).
|
|
247
|
-
class GlobalDefaultsController < ActionController::Base
|
|
248
|
-
spamtrap :trap_field, only: :create
|
|
249
|
-
|
|
250
|
-
def create
|
|
251
|
-
render plain: params[:comment].to_unsafe_h.keys.sort.join(',')
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Controller that explicitly overrides global defaults with false.
|
|
256
|
-
class GlobalOverrideController < ActionController::Base
|
|
257
|
-
spamtrap :trap_field, nonce: false, mutate: false, only: :create
|
|
258
|
-
|
|
259
|
-
def create
|
|
260
|
-
render plain: params[:comment].to_unsafe_h.keys.sort.join(',')
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
class GlobalDefaultsControllerTest < ActionController::TestCase
|
|
265
|
-
include MutationTestHelper
|
|
266
|
-
include NonceTestHelper
|
|
267
|
-
tests GlobalDefaultsController
|
|
268
|
-
|
|
269
|
-
setup do
|
|
270
|
-
Spamtrap.nonce = true
|
|
271
|
-
Spamtrap.mutate = true
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
teardown do
|
|
275
|
-
Spamtrap.nonce = false
|
|
276
|
-
Spamtrap.mutate = false
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def test_global_nonce_default_rejects_missing_nonce
|
|
280
|
-
post :create, params: { trap_field: '', comment: { body: 'Hello' } }
|
|
281
|
-
assert_response :ok
|
|
282
|
-
assert_empty response.body
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def test_global_nonce_default_accepts_valid_nonce
|
|
286
|
-
timestamp = Time.now.to_i
|
|
287
|
-
post :create, params: {
|
|
288
|
-
trap_field: '',
|
|
289
|
-
spamtrap_timestamp: timestamp,
|
|
290
|
-
spamtrap_nonce: generate_nonce(timestamp),
|
|
291
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
292
|
-
comment: { spamtrap_encrypt_field('body', MUTATION_SALT) => 'Hello' }
|
|
293
|
-
}
|
|
294
|
-
assert_response :ok
|
|
295
|
-
assert_equal 'body', response.body
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def test_global_mutate_default_remaps_encrypted_fields
|
|
299
|
-
body_token = spamtrap_encrypt_field('body', MUTATION_SALT)
|
|
300
|
-
timestamp = Time.now.to_i
|
|
301
|
-
post :create, params: {
|
|
302
|
-
trap_field: '',
|
|
303
|
-
spamtrap_timestamp: timestamp,
|
|
304
|
-
spamtrap_nonce: generate_nonce(timestamp),
|
|
305
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
306
|
-
comment: { body_token => 'Hello' }
|
|
307
|
-
}
|
|
308
|
-
assert_response :ok
|
|
309
|
-
assert_equal 'body', response.body
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
class GlobalOverrideControllerTest < ActionController::TestCase
|
|
314
|
-
include MutationTestHelper
|
|
315
|
-
tests GlobalOverrideController
|
|
316
|
-
|
|
317
|
-
setup do
|
|
318
|
-
Spamtrap.nonce = true
|
|
319
|
-
Spamtrap.mutate = true
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
teardown do
|
|
323
|
-
Spamtrap.nonce = false
|
|
324
|
-
Spamtrap.mutate = false
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
def test_per_call_false_overrides_global_nonce
|
|
328
|
-
# No nonce params — would fail if global nonce: true were in effect
|
|
329
|
-
post :create, params: {
|
|
330
|
-
trap_field: '',
|
|
331
|
-
comment: { body: 'Hello' }
|
|
332
|
-
}
|
|
333
|
-
assert_response :ok
|
|
334
|
-
assert_equal 'body', response.body
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def test_per_call_false_overrides_global_mutate
|
|
338
|
-
# Plain field name — would be remapped if global mutate: true were in effect
|
|
339
|
-
post :create, params: {
|
|
340
|
-
trap_field: '',
|
|
341
|
-
spamtrap_mutation_salt: MUTATION_SALT_HEX,
|
|
342
|
-
comment: { body: 'Hello' }
|
|
343
|
-
}
|
|
344
|
-
assert_response :ok
|
|
345
|
-
assert_equal 'body', response.body
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
# Controllers for on_trap callback tests.
|
|
350
|
-
class OnTrapGlobalCallbackController < ActionController::Base
|
|
351
|
-
spamtrap :trap_field, only: :create
|
|
352
|
-
|
|
353
|
-
def create
|
|
354
|
-
render plain: 'success'
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
class OnTrapGlobalNonceCallbackController < ActionController::Base
|
|
359
|
-
spamtrap :trap_field, nonce: true, only: :create
|
|
360
|
-
|
|
361
|
-
def create
|
|
362
|
-
render plain: 'success'
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
class OnTrapPerDeclarationController < ActionController::Base
|
|
367
|
-
spamtrap :trap_field, only: :create,
|
|
368
|
-
on_trap: ->(reason:, request:) { Thread.current[:per_decl_calls] << { reason: reason, ip: request.remote_ip } }
|
|
369
|
-
|
|
370
|
-
def create
|
|
371
|
-
render plain: 'success'
|
|
372
|
-
end
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
class OnTrapCallbackTest < ActionController::TestCase
|
|
376
|
-
tests OnTrapGlobalCallbackController
|
|
377
|
-
|
|
378
|
-
setup do
|
|
379
|
-
@calls = []
|
|
380
|
-
Spamtrap.on_trap = ->(reason:, request:) { @calls << { reason: reason, ip: request.remote_ip } }
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
teardown do
|
|
384
|
-
Spamtrap.on_trap = nil
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
def test_global_callback_invoked_on_honeypot_trap
|
|
388
|
-
post :create, params: { trap_field: 'spam' }
|
|
389
|
-
assert_response :ok
|
|
390
|
-
assert_empty response.body
|
|
391
|
-
assert_equal 1, @calls.size
|
|
392
|
-
assert_equal :honeypot, @calls.first[:reason]
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def test_global_callback_not_invoked_on_legitimate_request
|
|
396
|
-
post :create, params: { trap_field: '' }
|
|
397
|
-
assert_response :ok
|
|
398
|
-
assert_equal 'success', response.body
|
|
399
|
-
assert_empty @calls
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
def test_no_callback_when_on_trap_is_nil
|
|
403
|
-
Spamtrap.on_trap = nil
|
|
404
|
-
post :create, params: { trap_field: 'spam' }
|
|
405
|
-
assert_response :ok
|
|
406
|
-
assert_empty response.body
|
|
407
|
-
# no error raised — test simply passes
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
class OnTrapNonceCallbackTest < ActionController::TestCase
|
|
412
|
-
include NonceTestHelper
|
|
413
|
-
tests OnTrapGlobalNonceCallbackController
|
|
414
|
-
|
|
415
|
-
setup do
|
|
416
|
-
@calls = []
|
|
417
|
-
Spamtrap.on_trap = ->(reason:, request:) { @calls << { reason: reason, ip: request.remote_ip } }
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
teardown do
|
|
421
|
-
Spamtrap.on_trap = nil
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
def test_global_callback_invoked_on_nonce_trap
|
|
425
|
-
post :create, params: { trap_field: '' }
|
|
426
|
-
assert_response :ok
|
|
427
|
-
assert_empty response.body
|
|
428
|
-
assert_equal 1, @calls.size
|
|
429
|
-
assert_equal :nonce, @calls.first[:reason]
|
|
430
|
-
end
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
class OnTrapPerDeclarationCallbackTest < ActionController::TestCase
|
|
434
|
-
tests OnTrapPerDeclarationController
|
|
435
|
-
|
|
436
|
-
setup do
|
|
437
|
-
Thread.current[:per_decl_calls] = []
|
|
438
|
-
@global_calls = []
|
|
439
|
-
Spamtrap.on_trap = ->(reason:, request:) { @global_calls << reason }
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
teardown do
|
|
443
|
-
Spamtrap.on_trap = nil
|
|
444
|
-
Thread.current[:per_decl_calls] = nil
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
def test_per_declaration_callback_takes_precedence_over_global
|
|
448
|
-
post :create, params: { trap_field: 'spam' }
|
|
449
|
-
assert_response :ok
|
|
450
|
-
assert_empty response.body
|
|
451
|
-
assert_equal 1, Thread.current[:per_decl_calls].size
|
|
452
|
-
assert_equal :honeypot, Thread.current[:per_decl_calls].first[:reason]
|
|
453
|
-
assert_empty @global_calls
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
class OnTrapCallbackErrorResilienceTest < ActionController::TestCase
|
|
458
|
-
tests OnTrapGlobalCallbackController
|
|
459
|
-
|
|
460
|
-
setup do
|
|
461
|
-
Spamtrap.on_trap = ->(**) { raise 'callback exploded' }
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
teardown do
|
|
465
|
-
Spamtrap.on_trap = nil
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
def test_broken_callback_does_not_prevent_head_200
|
|
469
|
-
post :create, params: { trap_field: 'spam' }
|
|
470
|
-
assert_response :ok
|
|
471
|
-
assert_empty response.body
|
|
472
|
-
end
|
|
473
|
-
end
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require File.join(File.dirname(__FILE__), 'test_helper')
|
|
4
|
-
require 'action_view'
|
|
5
|
-
require 'action_view/test_case'
|
|
6
|
-
|
|
7
|
-
# A simple model-like struct to back the form builder, mimicking an AR model.
|
|
8
|
-
Message = Struct.new(:name, :email, :body, :active, :country)
|
|
9
|
-
|
|
10
|
-
class FormBuilderMutationTest < ActionView::TestCase
|
|
11
|
-
include Spamtrap::Crypto
|
|
12
|
-
|
|
13
|
-
# Build a minimal form builder instance backed by a Message object.
|
|
14
|
-
def build_form_builder(object)
|
|
15
|
-
ActionView::Helpers::FormBuilder.new(:message, object, self, {})
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# --- text-like inputs ---
|
|
19
|
-
|
|
20
|
-
def test_text_field_does_not_raise_when_mutate_is_true
|
|
21
|
-
msg = Message.new('Alice', 'alice@example.com', 'Hello')
|
|
22
|
-
f = build_form_builder(msg)
|
|
23
|
-
f.spamtrap(:trap, mutate: true)
|
|
24
|
-
|
|
25
|
-
assert_nothing_raised { f.text_field(:name) }
|
|
26
|
-
assert_nothing_raised { f.email_field(:email) }
|
|
27
|
-
assert_nothing_raised { f.text_area(:body) }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def test_model_value_is_preserved_in_encrypted_field
|
|
31
|
-
msg = Message.new('Alice', 'alice@example.com', 'Hello')
|
|
32
|
-
f = build_form_builder(msg)
|
|
33
|
-
f.spamtrap(:trap, mutate: true)
|
|
34
|
-
|
|
35
|
-
assert_includes f.text_field(:name), 'Alice'
|
|
36
|
-
assert_includes f.email_field(:email), 'alice@example.com'
|
|
37
|
-
assert_includes f.text_area(:body), 'Hello'
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def test_encrypted_field_name_is_used_in_html
|
|
41
|
-
msg = Message.new('Alice', 'alice@example.com', 'Hello')
|
|
42
|
-
f = build_form_builder(msg)
|
|
43
|
-
f.spamtrap(:trap, mutate: true)
|
|
44
|
-
|
|
45
|
-
html = f.text_field(:name)
|
|
46
|
-
|
|
47
|
-
# The original unencrypted name must NOT appear as the input name attribute
|
|
48
|
-
refute_match(/name="message\[name\]"/, html)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def test_nil_model_value_does_not_raise
|
|
52
|
-
msg = Message.new(nil, nil, nil)
|
|
53
|
-
f = build_form_builder(msg)
|
|
54
|
-
f.spamtrap(:trap, mutate: true)
|
|
55
|
-
|
|
56
|
-
assert_nothing_raised { f.text_field(:name) }
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def test_virtual_field_without_model_method_does_not_raise
|
|
60
|
-
msg = Message.new('Alice', 'alice@example.com', 'Hello')
|
|
61
|
-
f = build_form_builder(msg)
|
|
62
|
-
f.spamtrap(:trap, mutate: true)
|
|
63
|
-
|
|
64
|
-
# :nonexistent is not a method on Message; should render with empty value, not crash
|
|
65
|
-
assert_nothing_raised { f.text_field(:nonexistent) }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def test_explicit_value_option_is_preserved
|
|
69
|
-
msg = Message.new('Alice', 'alice@example.com', 'Hello')
|
|
70
|
-
f = build_form_builder(msg)
|
|
71
|
-
f.spamtrap(:trap, mutate: true)
|
|
72
|
-
|
|
73
|
-
html = f.text_field(:name, value: 'Overridden')
|
|
74
|
-
|
|
75
|
-
assert_includes html, 'Overridden'
|
|
76
|
-
refute_includes html, 'Alice'
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# --- no mutation ---
|
|
80
|
-
|
|
81
|
-
def test_no_salt_leaves_field_names_unencrypted
|
|
82
|
-
msg = Message.new('Alice', 'alice@example.com', 'Hello')
|
|
83
|
-
f = build_form_builder(msg)
|
|
84
|
-
|
|
85
|
-
html = f.text_field(:name)
|
|
86
|
-
|
|
87
|
-
assert_match(/name="message\[name\]"/, html)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# --- check_box ---
|
|
91
|
-
|
|
92
|
-
def test_check_box_does_not_raise_when_mutate_is_true
|
|
93
|
-
msg = Message.new(nil, nil, nil, true)
|
|
94
|
-
f = build_form_builder(msg)
|
|
95
|
-
f.spamtrap(:trap, mutate: true)
|
|
96
|
-
|
|
97
|
-
assert_nothing_raised { f.check_box(:active) }
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def test_check_box_checked_when_model_value_is_true
|
|
101
|
-
msg = Message.new(nil, nil, nil, true)
|
|
102
|
-
f = build_form_builder(msg)
|
|
103
|
-
f.spamtrap(:trap, mutate: true)
|
|
104
|
-
|
|
105
|
-
assert_includes f.check_box(:active), 'checked'
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def test_check_box_unchecked_when_model_value_is_false
|
|
109
|
-
msg = Message.new(nil, nil, nil, false)
|
|
110
|
-
f = build_form_builder(msg)
|
|
111
|
-
f.spamtrap(:trap, mutate: true)
|
|
112
|
-
|
|
113
|
-
refute_includes f.check_box(:active), 'checked'
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# --- select ---
|
|
117
|
-
|
|
118
|
-
def test_select_does_not_raise_when_mutate_is_true
|
|
119
|
-
msg = Message.new(nil, nil, nil, nil, 'US')
|
|
120
|
-
f = build_form_builder(msg)
|
|
121
|
-
f.spamtrap(:trap, mutate: true)
|
|
122
|
-
|
|
123
|
-
assert_nothing_raised { f.select(:country, %w[US CA GB]) }
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def test_select_preselects_model_value
|
|
127
|
-
msg = Message.new(nil, nil, nil, nil, 'CA')
|
|
128
|
-
f = build_form_builder(msg)
|
|
129
|
-
f.spamtrap(:trap, mutate: true)
|
|
130
|
-
|
|
131
|
-
html = f.select(:country, %w[US CA GB])
|
|
132
|
-
|
|
133
|
-
assert_match(/selected.*CA|CA.*selected/, html)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
private
|
|
137
|
-
|
|
138
|
-
# ActionView::TestCase helpers expect a #request method; provide a stub.
|
|
139
|
-
def request
|
|
140
|
-
@request ||= ActionDispatch::TestRequest.create
|
|
141
|
-
end
|
|
142
|
-
end
|
data/test/test_helper.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
require 'rubygems'
|
|
2
|
-
require 'rails'
|
|
3
|
-
require 'action_controller'
|
|
4
|
-
require 'action_controller/test_case'
|
|
5
|
-
require 'minitest/autorun'
|
|
6
|
-
require 'spamtrap'
|
|
7
|
-
|
|
8
|
-
class SpamtrapTestApp < Rails::Application
|
|
9
|
-
config.secret_key_base = 'a' * 64
|
|
10
|
-
config.eager_load = false
|
|
11
|
-
config.logger = Logger.new(nil)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
Rails.application.initialize!
|
|
15
|
-
|
|
16
|
-
Rails.application.routes.draw do
|
|
17
|
-
post 'honeypot/create', to: 'honeypot#create'
|
|
18
|
-
post 'nonce/create', to: 'nonce#create'
|
|
19
|
-
post 'nonce_timeout/create', to: 'nonce_timeout#create'
|
|
20
|
-
post 'mutation/create', to: 'mutation#create'
|
|
21
|
-
post 'nested_mutation/create', to: 'nested_mutation#create'
|
|
22
|
-
post 'global_defaults/create', to: 'global_defaults#create'
|
|
23
|
-
post 'global_override/create', to: 'global_override#create'
|
|
24
|
-
post 'on_trap_global_callback/create', to: 'on_trap_global_callback#create'
|
|
25
|
-
post 'on_trap_global_nonce_callback/create', to: 'on_trap_global_nonce_callback#create'
|
|
26
|
-
post 'on_trap_per_declaration/create', to: 'on_trap_per_declaration#create'
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
class ActionController::TestCase
|
|
30
|
-
setup do
|
|
31
|
-
@routes = Rails.application.routes
|
|
32
|
-
end
|
|
33
|
-
end
|