Skip to content

Commit 2c35eb8

Browse files
oakbanialiabbasrizvi
authored andcommitted
feat(attribute_value): Don't target NAN, INF, -INF and > 2^53 (#151)
1 parent 3121437 commit 2c35eb8

File tree

4 files changed

+184
-8
lines changed

4 files changed

+184
-8
lines changed

optimizely/helpers/validator.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,20 +187,25 @@ def is_attribute_valid(attribute_key, attribute_value):
187187
if not isinstance(attribute_key, string_types):
188188
return False
189189

190-
if isinstance(attribute_value, string_types) or type(attribute_value) in (int, float, bool):
190+
if isinstance(attribute_value, (string_types, bool)):
191191
return True
192192

193+
if isinstance(attribute_value, (numbers.Integral, float)):
194+
return is_finite_number(attribute_value)
195+
193196
return False
194197

195198

196199
def is_finite_number(value):
197-
""" Method to validate if the given value is a number and not one of NAN, INF, -INF.
200+
""" Validates if the given value is a number, enforces
201+
absolute limit of 2^53 and restricts NAN, INF, -INF.
198202
199203
Args:
200204
value: Value to be validated.
201205
202206
Returns:
203-
Boolean: True if value is a number and not NAN, INF or -INF else False.
207+
Boolean: True if value is a number and not NAN, INF, -INF or
208+
greater than absolute limit of 2^53 else False.
204209
"""
205210
if not isinstance(value, (numbers.Integral, float)):
206211
# numbers.Integral instead of int to accomodate long integer in python 2
@@ -214,6 +219,9 @@ def is_finite_number(value):
214219
if math.isnan(value) or math.isinf(value):
215220
return False
216221

222+
if abs(value) > (2**53):
223+
return False
224+
217225
return True
218226

219227

tests/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@
1313

1414
import json
1515
import unittest
16+
from six import PY3
1617

1718
from optimizely import optimizely
1819

20+
if PY3:
21+
def long(a):
22+
raise NotImplementedError('Tests should only call `long` if running in PY2')
23+
1924

2025
class BaseTest(unittest.TestCase):
2126

tests/helpers_tests/test_condition.py

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@
1212
# limitations under the License.
1313

1414
import mock
15-
from six import PY2, PY3
15+
from six import PY2
1616

1717
from optimizely.helpers import condition as condition_helper
1818

1919
from tests import base
2020

21-
if PY3:
22-
def long(a):
23-
raise NotImplementedError('Tests should only call `long` if running in PY2')
24-
2521
browserConditionSafari = ['browser_type', 'safari', 'custom_attribute', 'exact']
2622
booleanCondition = ['is_firefox', True, 'custom_attribute', 'exact']
2723
integerCondition = ['num_users', 10, 'custom_attribute', 'exact']
@@ -286,6 +282,36 @@ def test_exact_float__returns_null__when_no_user_provided_value(self):
286282

287283
self.assertIsNone(evaluator.evaluate(0))
288284

285+
def test_exact__given_number_values__calls_is_finite_number(self):
286+
""" Test that CustomAttributeConditionEvaluator.evaluate returns True
287+
if is_finite_number returns True. Returns None if is_finite_number returns False. """
288+
289+
evaluator = condition_helper.CustomAttributeConditionEvaluator(
290+
exact_int_condition_list, {'lasers_count': 9000}
291+
)
292+
293+
# assert that isFiniteNumber only needs to reject condition value to stop evaluation.
294+
with mock.patch('optimizely.helpers.validator.is_finite_number',
295+
side_effect=[False, True]) as mock_is_finite:
296+
self.assertIsNone(evaluator.evaluate(0))
297+
298+
mock_is_finite.assert_called_once_with(9000)
299+
300+
# assert that isFiniteNumber evaluates user value only if it has accepted condition value.
301+
with mock.patch('optimizely.helpers.validator.is_finite_number',
302+
side_effect=[True, False]) as mock_is_finite:
303+
self.assertIsNone(evaluator.evaluate(0))
304+
305+
mock_is_finite.assert_has_calls([mock.call(9000), mock.call(9000)])
306+
307+
# assert CustomAttributeConditionEvaluator.evaluate returns True only when isFiniteNumber returns
308+
# True both for condition and user values.
309+
with mock.patch('optimizely.helpers.validator.is_finite_number',
310+
side_effect=[True, True]) as mock_is_finite:
311+
self.assertTrue(evaluator.evaluate(0))
312+
313+
mock_is_finite.assert_has_calls([mock.call(9000), mock.call(9000)])
314+
289315
def test_exact_bool__returns_true__when_user_provided_value_is_equal_to_condition_value(self):
290316

291317
evaluator = condition_helper.CustomAttributeConditionEvaluator(
@@ -594,6 +620,84 @@ def test_less_than_float__returns_null__when_no_user_provided_value(self):
594620

595621
self.assertIsNone(evaluator.evaluate(0))
596622

623+
def test_greater_than__calls_is_finite_number(self):
624+
""" Test that CustomAttributeConditionEvaluator.evaluate returns True
625+
if is_finite_number returns True. Returns None if is_finite_number returns False. """
626+
627+
evaluator = condition_helper.CustomAttributeConditionEvaluator(
628+
gt_int_condition_list, {'meters_travelled': 48.1}
629+
)
630+
631+
def is_finite_number__rejecting_condition_value(value):
632+
if value == 48:
633+
return False
634+
return True
635+
636+
with mock.patch('optimizely.helpers.validator.is_finite_number',
637+
side_effect=is_finite_number__rejecting_condition_value) as mock_is_finite:
638+
self.assertIsNone(evaluator.evaluate(0))
639+
640+
# assert that isFiniteNumber only needs to reject condition value to stop evaluation.
641+
mock_is_finite.assert_called_once_with(48)
642+
643+
def is_finite_number__rejecting_user_attribute_value(value):
644+
if value == 48.1:
645+
return False
646+
return True
647+
648+
with mock.patch('optimizely.helpers.validator.is_finite_number',
649+
side_effect=is_finite_number__rejecting_user_attribute_value) as mock_is_finite:
650+
self.assertIsNone(evaluator.evaluate(0))
651+
652+
# assert that isFiniteNumber evaluates user value only if it has accepted condition value.
653+
mock_is_finite.assert_has_calls([mock.call(48), mock.call(48.1)])
654+
655+
def is_finite_number__accepting_both_values(value):
656+
return True
657+
658+
with mock.patch('optimizely.helpers.validator.is_finite_number',
659+
side_effect=is_finite_number__accepting_both_values):
660+
self.assertTrue(evaluator.evaluate(0))
661+
662+
def test_less_than__calls_is_finite_number(self):
663+
""" Test that CustomAttributeConditionEvaluator.evaluate returns True
664+
if is_finite_number returns True. Returns None if is_finite_number returns False. """
665+
666+
evaluator = condition_helper.CustomAttributeConditionEvaluator(
667+
lt_int_condition_list, {'meters_travelled': 47}
668+
)
669+
670+
def is_finite_number__rejecting_condition_value(value):
671+
if value == 48:
672+
return False
673+
return True
674+
675+
with mock.patch('optimizely.helpers.validator.is_finite_number',
676+
side_effect=is_finite_number__rejecting_condition_value) as mock_is_finite:
677+
self.assertIsNone(evaluator.evaluate(0))
678+
679+
# assert that isFiniteNumber only needs to reject condition value to stop evaluation.
680+
mock_is_finite.assert_called_once_with(48)
681+
682+
def is_finite_number__rejecting_user_attribute_value(value):
683+
if value == 47:
684+
return False
685+
return True
686+
687+
with mock.patch('optimizely.helpers.validator.is_finite_number',
688+
side_effect=is_finite_number__rejecting_user_attribute_value) as mock_is_finite:
689+
self.assertIsNone(evaluator.evaluate(0))
690+
691+
# assert that isFiniteNumber evaluates user value only if it has accepted condition value.
692+
mock_is_finite.assert_has_calls([mock.call(48), mock.call(47)])
693+
694+
def is_finite_number__accepting_both_values(value):
695+
return True
696+
697+
with mock.patch('optimizely.helpers.validator.is_finite_number',
698+
side_effect=is_finite_number__accepting_both_values):
699+
self.assertTrue(evaluator.evaluate(0))
700+
597701

598702
class ConditionDecoderTests(base.BaseTest):
599703

tests/helpers_tests/test_validator.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# limitations under the License.
1313

1414
import json
15+
import mock
16+
17+
from six import PY2
1518

1619
from optimizely import error_handler
1720
from optimizely import event_dispatcher
@@ -168,6 +171,62 @@ def test_is_attribute_valid(self):
168171
self.assertTrue(validator.is_attribute_valid('test_attribute', ""))
169172
self.assertTrue(validator.is_attribute_valid('test_attribute', 'test_value'))
170173

174+
# test if attribute value is a number, it calls is_finite_number and returns it's result
175+
with mock.patch('optimizely.helpers.validator.is_finite_number',
176+
return_value=True) as mock_is_finite:
177+
self.assertTrue(validator.is_attribute_valid('test_attribute', 5))
178+
179+
mock_is_finite.assert_called_once_with(5)
180+
181+
with mock.patch('optimizely.helpers.validator.is_finite_number',
182+
return_value=False) as mock_is_finite:
183+
self.assertFalse(validator.is_attribute_valid('test_attribute', 5.5))
184+
185+
mock_is_finite.assert_called_once_with(5.5)
186+
187+
if PY2:
188+
with mock.patch('optimizely.helpers.validator.is_finite_number',
189+
return_value=None) as mock_is_finite:
190+
self.assertIsNone(validator.is_attribute_valid('test_attribute', long(5)))
191+
192+
mock_is_finite.assert_called_once_with(long(5))
193+
194+
def test_is_finite_number(self):
195+
""" Test that it returns true if value is a number and not NAN, INF, -INF or greater than 2^53.
196+
Otherwise False.
197+
"""
198+
# test non number values
199+
self.assertFalse(validator.is_finite_number('HelloWorld'))
200+
self.assertFalse(validator.is_finite_number(True))
201+
self.assertFalse(validator.is_finite_number(False))
202+
self.assertFalse(validator.is_finite_number(None))
203+
self.assertFalse(validator.is_finite_number({}))
204+
self.assertFalse(validator.is_finite_number([]))
205+
self.assertFalse(validator.is_finite_number(()))
206+
207+
# test invalid numbers
208+
self.assertFalse(validator.is_finite_number(float('inf')))
209+
self.assertFalse(validator.is_finite_number(float('-inf')))
210+
self.assertFalse(validator.is_finite_number(float('nan')))
211+
self.assertFalse(validator.is_finite_number(int(2**53) + 1))
212+
self.assertFalse(validator.is_finite_number(-int(2**53) - 1))
213+
self.assertFalse(validator.is_finite_number(float(2**53) + 2.0))
214+
self.assertFalse(validator.is_finite_number(-float(2**53) - 2.0))
215+
if PY2:
216+
self.assertFalse(validator.is_finite_number(long(2**53) + 1))
217+
self.assertFalse(validator.is_finite_number(-long(2**53) - 1))
218+
219+
# test valid numbers
220+
self.assertTrue(validator.is_finite_number(0))
221+
self.assertTrue(validator.is_finite_number(5))
222+
self.assertTrue(validator.is_finite_number(5.5))
223+
# float(2**53) + 1.0 evaluates to float(2**53)
224+
self.assertTrue(validator.is_finite_number(float(2**53) + 1.0))
225+
self.assertTrue(validator.is_finite_number(-float(2**53) - 1.0))
226+
self.assertTrue(validator.is_finite_number(int(2**53)))
227+
if PY2:
228+
self.assertTrue(validator.is_finite_number(long(2**53)))
229+
171230

172231
class DatafileValidationTests(base.BaseTest):
173232

0 commit comments

Comments
 (0)