input_sanitizer 0.1.10 → 0.4.1
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 +7 -0
- data/.github/workflows/ci.yaml +26 -0
- data/.github/workflows/gempush.yml +28 -0
- data/.gitignore +2 -1
- data/CHANGELOG +99 -0
- data/LICENSE +201 -22
- data/README.md +24 -4
- data/input_sanitizer.gemspec +10 -4
- data/lib/input_sanitizer.rb +5 -2
- data/lib/input_sanitizer/errors.rb +142 -0
- data/lib/input_sanitizer/extended_converters.rb +5 -42
- data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
- data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
- data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
- data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
- data/lib/input_sanitizer/restricted_hash.rb +49 -8
- data/lib/input_sanitizer/v1.rb +22 -0
- data/lib/input_sanitizer/v1/clean_field.rb +38 -0
- data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +20 -13
- data/lib/input_sanitizer/v1/sanitizer.rb +166 -0
- data/lib/input_sanitizer/v2.rb +13 -0
- data/lib/input_sanitizer/v2/clean_field.rb +36 -0
- data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
- data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
- data/lib/input_sanitizer/v2/error_collection.rb +49 -0
- data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
- data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
- data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
- data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
- data/lib/input_sanitizer/v2/types.rb +227 -0
- data/lib/input_sanitizer/version.rb +1 -1
- data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
- data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
- data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
- data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
- data/spec/restricted_hash_spec.rb +37 -7
- data/spec/sanitizer_spec.rb +129 -26
- data/spec/spec_helper.rb +17 -2
- data/spec/v1/default_converters_spec.rb +141 -0
- data/spec/v2/converters_spec.rb +174 -0
- data/spec/v2/payload_sanitizer_spec.rb +534 -0
- data/spec/v2/payload_transform_spec.rb +98 -0
- data/spec/v2/query_sanitizer_spec.rb +300 -0
- data/v2.md +52 -0
- metadata +105 -40
- data/.travis.yml +0 -12
- data/lib/input_sanitizer/sanitizer.rb +0 -140
- data/spec/default_converters_spec.rb +0 -101
- data/spec/extended_converters_spec.rb +0 -62
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
|
3
|
+
class InputSanitizer::V2::PayloadTransform
|
4
|
+
attr_reader :original_payload, :context
|
5
|
+
|
6
|
+
def self.call(original_payload, context = {})
|
7
|
+
new(original_payload, context).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(original_payload, context = {})
|
11
|
+
fail "#{self.class} is missing #transform method" unless respond_to?(:transform)
|
12
|
+
@original_payload, @context = original_payload, context
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
transform
|
17
|
+
payload
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def rename(from, to)
|
22
|
+
if has?(from)
|
23
|
+
data = payload.delete(from)
|
24
|
+
payload[to] = data
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def merge_in(field, options = {})
|
29
|
+
if source = payload.delete(field)
|
30
|
+
source = options[:using].call(source) if options[:using]
|
31
|
+
payload.merge!(source)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def has?(key)
|
36
|
+
payload.has_key?(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def payload
|
40
|
+
@payload ||= original_payload.with_indifferent_access
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class InputSanitizer::V2::QuerySanitizer < InputSanitizer::V2::PayloadSanitizer
|
2
|
+
def self.converters
|
3
|
+
{
|
4
|
+
:integer => InputSanitizer::V2::Types::CoercingIntegerCheck.new,
|
5
|
+
:float => InputSanitizer::V2::Types::CoercingFloatCheck.new,
|
6
|
+
:string => InputSanitizer::V2::Types::StringCheck.new,
|
7
|
+
:boolean => InputSanitizer::V2::Types::CoercingBooleanCheck.new,
|
8
|
+
:datetime => InputSanitizer::V2::Types::DatetimeCheck.new,
|
9
|
+
:date => InputSanitizer::V2::Types::DatetimeCheck.new(:check_date => true),
|
10
|
+
:url => InputSanitizer::V2::Types::URLCheck.new,
|
11
|
+
}
|
12
|
+
end
|
13
|
+
initialize_types_dsl
|
14
|
+
|
15
|
+
def self.sort_by(allowed_values, options = {})
|
16
|
+
set_keys_to_converter([:sort_by, { :allow => allowed_values }.merge(options)], InputSanitizer::V2::Types::SortByCheck.new)
|
17
|
+
end
|
18
|
+
|
19
|
+
# allow underscore cache buster by default
|
20
|
+
string :_
|
21
|
+
|
22
|
+
private
|
23
|
+
def perform_clean
|
24
|
+
super
|
25
|
+
@errors.each do |error|
|
26
|
+
error.field = error.field[1..-1] if error.field.start_with?('/')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def sanitizer_type
|
31
|
+
:query
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
|
3
|
+
module InputSanitizer::V2::Types
|
4
|
+
class IntegerCheck
|
5
|
+
def call(value, options = {})
|
6
|
+
if value == nil && (options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true)
|
7
|
+
raise InputSanitizer::BlankValueError
|
8
|
+
elsif value == nil
|
9
|
+
value
|
10
|
+
else
|
11
|
+
Integer(value).tap do |integer|
|
12
|
+
raise InputSanitizer::TypeMismatchError.new(value, :integer) unless integer == value
|
13
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && integer < options[:minimum]
|
14
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && integer > options[:maximum]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
rescue ArgumentError, TypeError
|
18
|
+
raise InputSanitizer::TypeMismatchError.new(value, :integer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class CoercingIntegerCheck
|
23
|
+
def call(value, options = {})
|
24
|
+
if value == nil || value == 'null'
|
25
|
+
if options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true
|
26
|
+
raise InputSanitizer::BlankValueError
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
else
|
31
|
+
Integer(value).tap do |integer|
|
32
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && integer < options[:minimum]
|
33
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && integer > options[:maximum]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
rescue ArgumentError
|
37
|
+
raise InputSanitizer::TypeMismatchError.new(value, :integer)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class FloatCheck
|
42
|
+
def call(value, options = {})
|
43
|
+
if value == nil && (options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true)
|
44
|
+
raise InputSanitizer::BlankValueError
|
45
|
+
elsif value == nil
|
46
|
+
value
|
47
|
+
else
|
48
|
+
Float(value).tap do |float|
|
49
|
+
raise InputSanitizer::TypeMismatchError.new(value, :float) unless float == value
|
50
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && float < options[:minimum]
|
51
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && float > options[:maximum]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue ArgumentError, TypeError
|
55
|
+
raise InputSanitizer::TypeMismatchError.new(value, :float)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class CoercingFloatCheck
|
60
|
+
def call(value, options = {})
|
61
|
+
if value == nil || value == 'null'
|
62
|
+
if options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true
|
63
|
+
raise InputSanitizer::BlankValueError
|
64
|
+
else
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
else
|
68
|
+
Float(value).tap do |float|
|
69
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && float < options[:minimum]
|
70
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && float > options[:maximum]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue ArgumentError
|
74
|
+
raise InputSanitizer::TypeMismatchError.new(value, :float)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class StringCheck
|
79
|
+
def call(value, options = {})
|
80
|
+
if options[:allow] && !options[:allow].include?(value)
|
81
|
+
raise InputSanitizer::ValueNotAllowedError.new(value)
|
82
|
+
elsif value.blank? && (options[:allow_blank] == false || options[:required] == true)
|
83
|
+
raise InputSanitizer::BlankValueError
|
84
|
+
elsif options[:regexp] && options[:regexp].match(value).nil?
|
85
|
+
raise InputSanitizer::RegexpMismatchError.new
|
86
|
+
elsif value == nil && options[:allow_nil] == false
|
87
|
+
raise InputSanitizer::BlankValueError
|
88
|
+
elsif value.blank?
|
89
|
+
value
|
90
|
+
else
|
91
|
+
value.to_s.tap do |string|
|
92
|
+
raise InputSanitizer::TypeMismatchError.new(value, :string) unless string == value
|
93
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && string.length < options[:minimum]
|
94
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && string.length > options[:maximum]
|
95
|
+
end
|
96
|
+
|
97
|
+
if options[:strip_4byte_chars] && !options[:already_stripped]
|
98
|
+
value_without_4byte_chars = strip_4byte_chars(value)
|
99
|
+
updated_options = options.merge(:already_stripped => true) # to prevent infinite loop
|
100
|
+
call(value_without_4byte_chars, updated_options) # run checks once again to ensure string is still valid after stripping 4-byte chars
|
101
|
+
else
|
102
|
+
value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def strip_4byte_chars(string)
|
110
|
+
string.each_char.with_object(String.new) { |char, output| output << char if char.bytesize < 4 }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class BooleanCheck
|
115
|
+
def call(value, options = {})
|
116
|
+
if value == nil
|
117
|
+
raise InputSanitizer::BlankValueError
|
118
|
+
elsif [true, false].include?(value)
|
119
|
+
value
|
120
|
+
else
|
121
|
+
raise InputSanitizer::TypeMismatchError.new(value, :boolean)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class CoercingBooleanCheck
|
127
|
+
def call(value, options = {})
|
128
|
+
if [true, 'true'].include?(value)
|
129
|
+
true
|
130
|
+
elsif [false, 'false'].include?(value)
|
131
|
+
false
|
132
|
+
else
|
133
|
+
raise InputSanitizer::TypeMismatchError.new(value, :boolean)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class DatetimeCheck
|
139
|
+
def initialize(options = {})
|
140
|
+
@check_date = options && options[:check_date]
|
141
|
+
@klass = @check_date ? Date : DateTime
|
142
|
+
end
|
143
|
+
|
144
|
+
def call(value, options = {})
|
145
|
+
raise InputSanitizer::TypeMismatchError.new(value, @check_date ? :date : :datetime) unless value == nil || value.is_a?(String)
|
146
|
+
|
147
|
+
if value.blank? && (options[:allow_blank] == false || options[:required] == true)
|
148
|
+
raise InputSanitizer::BlankValueError
|
149
|
+
elsif value == nil && options[:allow_nil] == false
|
150
|
+
raise InputSanitizer::BlankValueError
|
151
|
+
elsif value.blank?
|
152
|
+
value
|
153
|
+
else
|
154
|
+
@klass.parse(value).tap do |datetime|
|
155
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && datetime < options[:minimum]
|
156
|
+
raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && datetime > options[:maximum]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
rescue ArgumentError, TypeError
|
160
|
+
raise InputSanitizer::TypeMismatchError.new(value, @check_date ? :date : :datetime)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class URLCheck
|
165
|
+
def call(value, options = {})
|
166
|
+
if value.blank? && (options[:allow_blank] == false || options[:required] == true)
|
167
|
+
raise InputSanitizer::BlankValueError
|
168
|
+
elsif value == nil && options[:allow_nil] == false
|
169
|
+
raise InputSanitizer::BlankValueError
|
170
|
+
elsif value.blank?
|
171
|
+
value
|
172
|
+
else
|
173
|
+
unless /\A#{URI.regexp(%w(http https)).to_s}\z/.match(value)
|
174
|
+
raise InputSanitizer::TypeMismatchError.new(value, :url)
|
175
|
+
end
|
176
|
+
value
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class SortByCheck
|
182
|
+
def call(value, options = {})
|
183
|
+
check_options!(options)
|
184
|
+
|
185
|
+
key, direction = split(value)
|
186
|
+
direction = 'asc' if direction.blank?
|
187
|
+
|
188
|
+
# special case when fallback takes care of separator sanitization e.g. custom fields
|
189
|
+
if options[:fallback] && !allowed_directions.include?(direction)
|
190
|
+
direction = 'asc'
|
191
|
+
key = value
|
192
|
+
end
|
193
|
+
|
194
|
+
unless valid?(key, direction, options)
|
195
|
+
raise InputSanitizer::ValueNotAllowedError.new(value)
|
196
|
+
end
|
197
|
+
|
198
|
+
[key, direction]
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
def valid?(key, direction, options)
|
203
|
+
allowed_keys = options[:allow]
|
204
|
+
fallback = options[:fallback]
|
205
|
+
|
206
|
+
allowed_directions.include?(direction) &&
|
207
|
+
((allowed_keys && allowed_keys.include?(key)) ||
|
208
|
+
(fallback && fallback.call(key, direction, options)))
|
209
|
+
end
|
210
|
+
|
211
|
+
def split(value)
|
212
|
+
head, _, tail = value.to_s.rpartition(':')
|
213
|
+
head.empty? ? [tail, head] : [head, tail]
|
214
|
+
end
|
215
|
+
|
216
|
+
def check_options!(options)
|
217
|
+
fallback = options[:fallback]
|
218
|
+
if fallback && !fallback.respond_to?(:call)
|
219
|
+
raise ArgumentError, ":fallback option must respond to method :call (proc, lambda etc)"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def allowed_directions
|
224
|
+
['asc', 'desc']
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'input_sanitizer/extended_converters/comma_joined_integers_converter'
|
3
|
+
|
4
|
+
describe InputSanitizer::CommaJoinedIntegersConverter do
|
5
|
+
let(:converter) { described_class.new }
|
6
|
+
|
7
|
+
it "parses to array of ids" do
|
8
|
+
converter.call("1,2,3,5").should eq([1, 2, 3, 5])
|
9
|
+
end
|
10
|
+
|
11
|
+
it "converts to array if given an integer" do
|
12
|
+
converter.call(7).should eq([7])
|
13
|
+
end
|
14
|
+
|
15
|
+
it "raises on invalid character" do
|
16
|
+
lambda { converter.call(":") }.should raise_error(InputSanitizer::ConversionError)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'input_sanitizer/extended_converters/comma_joined_strings_converter'
|
3
|
+
|
4
|
+
describe InputSanitizer::CommaJoinedStringsConverter do
|
5
|
+
let(:converter) { described_class.new }
|
6
|
+
|
7
|
+
it "parses to array of ids" do
|
8
|
+
converter.call("input,Sanitizer,ROCKS").should eq(["input", "Sanitizer", "ROCKS"])
|
9
|
+
end
|
10
|
+
|
11
|
+
it "allows underscores" do
|
12
|
+
converter.call("input_sanitizer,rocks").should eq(["input_sanitizer", "rocks"])
|
13
|
+
end
|
14
|
+
|
15
|
+
it "raises on invalid character" do
|
16
|
+
lambda { converter.call(":") }.should raise_error(InputSanitizer::ConversionError)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'input_sanitizer/extended_converters/positive_integer_converter'
|
3
|
+
|
4
|
+
describe InputSanitizer::PositiveIntegerConverter do
|
5
|
+
let(:converter) { described_class.new }
|
6
|
+
|
7
|
+
it "casts string to integer" do
|
8
|
+
converter.call("3").should == 3
|
9
|
+
end
|
10
|
+
|
11
|
+
it "raises error if integer less than zero" do
|
12
|
+
lambda { converter.call("-3") }.should raise_error(InputSanitizer::ConversionError)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "raises error if integer equals zero" do
|
16
|
+
lambda { converter.call("0") }.should raise_error(InputSanitizer::ConversionError)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'input_sanitizer/extended_converters/specific_values_converter'
|
3
|
+
|
4
|
+
describe InputSanitizer::SpecificValuesConverter do
|
5
|
+
let(:converter) { described_class.new(values) }
|
6
|
+
let(:values) { [:a, :b] }
|
7
|
+
|
8
|
+
it "converts valid value to symbol" do
|
9
|
+
converter.call("b").should eq(:b)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "raises on invalid value" do
|
13
|
+
lambda { converter.call("c") }.should raise_error(InputSanitizer::ConversionError)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "raises on nil value" do
|
17
|
+
lambda { converter.call(nil) }.should raise_error(InputSanitizer::ConversionError)
|
18
|
+
end
|
19
|
+
|
20
|
+
context "when specific values are strings" do
|
21
|
+
let(:values) { ["a", "b"] }
|
22
|
+
|
23
|
+
it "keeps given value as string" do
|
24
|
+
converter.call("a").should eq("a")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,19 +1,49 @@
|
|
1
|
-
require
|
1
|
+
require 'spec_helper'
|
2
2
|
|
3
3
|
describe InputSanitizer::RestrictedHash do
|
4
|
-
let(:hash) { InputSanitizer::RestrictedHash.new([:a
|
5
|
-
subject { hash }
|
4
|
+
let(:hash) { InputSanitizer::RestrictedHash.new([:a]) }
|
6
5
|
|
7
|
-
it
|
8
|
-
lambda{hash[:
|
6
|
+
it 'does not allow bad keys' do
|
7
|
+
lambda{ hash[:b] }.should raise_error(InputSanitizer::KeyNotAllowedError)
|
9
8
|
end
|
10
9
|
|
11
|
-
it
|
10
|
+
it 'does allow correct keys' do
|
12
11
|
hash[:a].should be_nil
|
13
12
|
end
|
14
13
|
|
15
|
-
it
|
14
|
+
it 'returns value for correct key' do
|
16
15
|
hash[:a] = 'stuff'
|
17
16
|
hash[:a].should == 'stuff'
|
18
17
|
end
|
18
|
+
|
19
|
+
it 'allows to set value for a new key' do
|
20
|
+
hash[:b] = 'other stuff'
|
21
|
+
hash[:b].should == 'other stuff'
|
22
|
+
hash.key_allowed?(:b).should be_truthy
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'adds new allowed keys after `transform_keys` is done' do
|
26
|
+
hash[:a] = 'stuff'
|
27
|
+
hash.transform_keys! { |key| key.to_s }
|
28
|
+
hash['a'].should == 'stuff'
|
29
|
+
hash.key_allowed?('a').should be_truthy
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'removes previous allowed keys after `transform_keys` is done' do
|
33
|
+
hash[:a] = 'stuff'
|
34
|
+
hash.transform_keys! { |key| key.to_s }
|
35
|
+
lambda{ hash[:a] }.should raise_error(InputSanitizer::KeyNotAllowedError)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'updates allowed keys on merge!' do
|
39
|
+
merge_hash = { merged: '1' }
|
40
|
+
hash.merge!(merge_hash)
|
41
|
+
hash.key_allowed?(:merged).should be_truthy
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'updates allowed keys on merge' do
|
45
|
+
merge_hash = { merged: '1' }
|
46
|
+
new_hash = hash.merge(some_key: merge_hash)
|
47
|
+
new_hash.key_allowed?(:some_key).should be_truthy
|
48
|
+
end
|
19
49
|
end
|