From 2f9e1cee048c70ab18f2fe72dd12d95bfc40dd26 Mon Sep 17 00:00:00 2001 From: Petr Voronov Date: Fri, 4 Jul 2025 10:01:29 +0300 Subject: [PATCH] Fixes #19633. Added new test cases to handle cases where the field being compared (x) doesn't exist in the data dictionary, which would result in None when trying to access it, and you want these comparisons to safely return False rather than raising an error. For this moment it returns and brake process_event_rules(). TypeError: expected string or bytes-like object, got 'NoneType'. --- netbox/extras/tests/test_conditions.py | 77 +++++++++++++++++--------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py index dfe460f99..8d38802ca 100644 --- a/netbox/extras/tests/test_conditions.py +++ b/netbox/extras/tests/test_conditions.py @@ -62,66 +62,89 @@ class ConditionTestCase(TestCase): def test_eq(self): c = Condition('x', 1, 'eq') - self.assertTrue(c.eval({'x': 1})) - self.assertFalse(c.eval({'x': 2})) + self.assertTrue(c.eval({'x': 1})) # 1 == 1 → True + self.assertFalse(c.eval({'x': 2})) # 2 == 1 → False + self.assertFalse(c.eval({'x': None})) # None == 1 → False + self.assertFalse(c.eval({'z': 1})) # Missing 'x' → treated as None → False def test_eq_negated(self): c = Condition('x', 1, 'eq', negate=True) - self.assertFalse(c.eval({'x': 1})) - self.assertTrue(c.eval({'x': 2})) + self.assertFalse(c.eval({'x': 1})) # not (1 == 1) → False + self.assertTrue(c.eval({'x': 2})) # not (2 == 1) → True + self.assertTrue(c.eval({'x': None})) # not (None == 1) → True + self.assertTrue(c.eval({'z': 1})) # Missing 'x' → treated as None → True def test_gt(self): c = Condition('x', 1, 'gt') - self.assertTrue(c.eval({'x': 2})) - self.assertFalse(c.eval({'x': 1})) + self.assertTrue(c.eval({'x': 2})) # 2 > 1 → True + self.assertFalse(c.eval({'x': 1})) # 1 > 1 → False + self.assertFalse(c.eval({'x': None})) # None > 1 → False (safe handling) + self.assertFalse(c.eval({'z': 1})) # Missing 'x' → treated as None → False def test_gte(self): c = Condition('x', 1, 'gte') - self.assertTrue(c.eval({'x': 2})) - self.assertTrue(c.eval({'x': 1})) - self.assertFalse(c.eval({'x': 0})) + self.assertTrue(c.eval({'x': 2})) # 2 >= 1 → True + self.assertTrue(c.eval({'x': 1})) # 1 >= 1 → True + self.assertFalse(c.eval({'x': 0})) # 0 >= 1 → False + self.assertFalse(c.eval({'x': None})) # None >= 1 → False + self.assertFalse(c.eval({'z': 1})) # Missing 'x' → False def test_lt(self): c = Condition('x', 2, 'lt') - self.assertTrue(c.eval({'x': 1})) - self.assertFalse(c.eval({'x': 2})) + self.assertTrue(c.eval({'x': 1})) # 1 < 2 → True + self.assertFalse(c.eval({'x': 2})) # 2 < 2 → False + self.assertFalse(c.eval({'x': None})) # None < 2 → False + self.assertFalse(c.eval({'z': 1})) # Missing 'x' → False def test_lte(self): c = Condition('x', 2, 'lte') - self.assertTrue(c.eval({'x': 1})) - self.assertTrue(c.eval({'x': 2})) - self.assertFalse(c.eval({'x': 3})) + self.assertTrue(c.eval({'x': 1})) # 1 <= 2 → True + self.assertTrue(c.eval({'x': 2})) # 2 <= 2 → True + self.assertFalse(c.eval({'x': 3})) # 3 <= 2 → False + self.assertFalse(c.eval({'x': None})) # None <= 2 → False + self.assertFalse(c.eval({'z': 1})) # Missing 'x' → False def test_in(self): c = Condition('x', [1, 2, 3], 'in') - self.assertTrue(c.eval({'x': 1})) - self.assertFalse(c.eval({'x': 9})) + self.assertTrue(c.eval({'x': 1})) # 1 in [1,2,3] → True + self.assertFalse(c.eval({'x': 9})) # 9 in [1,2,3] → False + self.assertFalse(c.eval({'x': None})) # None in [1,2,3] → False + self.assertFalse(c.eval({'z': 1})) # Missing 'x' → False def test_in_negated(self): c = Condition('x', [1, 2, 3], 'in', negate=True) - self.assertFalse(c.eval({'x': 1})) - self.assertTrue(c.eval({'x': 9})) + self.assertFalse(c.eval({'x': 1})) # not (1 in [1,2,3]) → False + self.assertTrue(c.eval({'x': 9})) # not (9 in [1,2,3]) → True + self.assertTrue(c.eval({'x': None})) # not (None in [1,2,3]) → True + self.assertTrue(c.eval({'z': 1})) # Missing 'x' → True def test_contains(self): c = Condition('x', 1, 'contains') - self.assertTrue(c.eval({'x': [1, 2, 3]})) - self.assertFalse(c.eval({'x': [2, 3, 4]})) + self.assertTrue(c.eval({'x': [1, 2, 3]})) # 1 in [1,2,3] → True + self.assertFalse(c.eval({'x': [2, 3, 4]})) # 1 in [2,3,4] → False + self.assertFalse(c.eval({'x': None})) # 1 in None → False + self.assertFalse(c.eval({'z': [1, 2, 3]})) # Missing 'x' → False def test_contains_negated(self): c = Condition('x', 1, 'contains', negate=True) - self.assertFalse(c.eval({'x': [1, 2, 3]})) - self.assertTrue(c.eval({'x': [2, 3, 4]})) + self.assertFalse(c.eval({'x': [1, 2, 3]})) # not (1 in [1,2,3]) → False + self.assertTrue(c.eval({'x': [2, 3, 4]})) # not (1 in [2,3,4]) → True + self.assertTrue(c.eval({'x': None})) # not (1 in None) → True + self.assertTrue(c.eval({'z': [1, 2, 3]})) # Missing 'x' → True def test_regex(self): c = Condition('x', '[a-z]+', 'regex') - self.assertTrue(c.eval({'x': 'abc'})) - self.assertFalse(c.eval({'x': '123'})) + self.assertTrue(c.eval({'x': 'abc'})) # 'abc' matches regex → True + self.assertFalse(c.eval({'x': '123'})) # '123' doesn't match → False + self.assertFalse(c.eval({'x': None})) # None doesn't match → False + self.assertFalse(c.eval({'z': 'abc'})) # Missing 'x' → False def test_regex_negated(self): c = Condition('x', '[a-z]+', 'regex', negate=True) - self.assertFalse(c.eval({'x': 'abc'})) - self.assertTrue(c.eval({'x': '123'})) - + self.assertFalse(c.eval({'x': 'abc'})) # not (match) → False + self.assertTrue(c.eval({'x': '123'})) # not (no match) → True + self.assertTrue(c.eval({'x': None})) # not (None match) → True + self.assertTrue(c.eval({'z': 'abc'})) # Missing 'x' → True class ConditionSetTest(TestCase):