from django.contrib.contenttypes.models import ContentType from django.test import TestCase from core.events import * from dcim.choices import SiteStatusChoices from dcim.models import Site from extras.conditions import Condition, ConditionSet from extras.events import serialize_for_event from extras.forms import EventRuleForm from extras.models import EventRule, Webhook class ConditionTestCase(TestCase): def test_dotted_path_access(self): c = Condition('a.b.c', 1, 'eq') self.assertTrue(c.eval({'a': {'b': {'c': 1}}})) self.assertFalse(c.eval({'a': {'b': {'c': 2}}})) self.assertFalse(c.eval({'a': {'b': {'x': 1}}})) def test_undefined_attr(self): c = Condition('x', 1, 'eq') self.assertFalse(c.eval({})) self.assertTrue(c.eval({'x': 1})) # # Validation tests # def test_invalid_op(self): with self.assertRaises(ValueError): # 'blah' is not a valid operator Condition('x', 1, 'blah') def test_invalid_type(self): with self.assertRaises(ValueError): # dict type is unsupported Condition('x', 1, dict()) def test_invalid_op_type(self): with self.assertRaises(ValueError): # 'gt' supports only numeric values Condition('x', 'foo', 'gt') # # Nested attrs tests # def test_nested(self): c = Condition('x.y.z', 1) self.assertTrue(c.eval({'x': {'y': {'z': 1}}})) self.assertFalse(c.eval({'x': {'y': {'z': 2}}})) self.assertFalse(c.eval({'a': {'b': {'c': 1}}})) # # Operator tests # def test_default_operator(self): c = Condition('x', 1) self.assertEqual(c.eval_func, c.eval_eq) def test_eq(self): c = Condition('x', 1, 'eq') 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})) # 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})) # 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})) # 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})) # 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})) # 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})) # 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})) # 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]})) # 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]})) # 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'})) # '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'})) # 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): def test_empty(self): with self.assertRaises(ValueError): ConditionSet({}) def test_invalid_logic(self): with self.assertRaises(ValueError): ConditionSet({'foo': []}) def test_null_value(self): cs = ConditionSet({ 'and': [ {'attr': 'a', 'value': None, 'op': 'eq', 'negate': True}, ] }) self.assertFalse(cs.eval({'a': None})) self.assertTrue(cs.eval({'a': "string"})) self.assertTrue(cs.eval({'a': {"key": "value"}})) def test_and_single_depth(self): cs = ConditionSet({ 'and': [ {'attr': 'a', 'value': 1, 'op': 'eq'}, {'attr': 'b', 'value': 1, 'op': 'eq', 'negate': True}, ] }) self.assertTrue(cs.eval({'a': 1, 'b': 2})) self.assertFalse(cs.eval({'a': 1, 'b': 1})) def test_or_single_depth(self): cs = ConditionSet({ 'or': [ {'attr': 'a', 'value': 1, 'op': 'eq'}, {'attr': 'b', 'value': 1, 'op': 'eq'}, ] }) self.assertTrue(cs.eval({'a': 1, 'b': 2})) self.assertTrue(cs.eval({'a': 2, 'b': 1})) self.assertFalse(cs.eval({'a': 2, 'b': 2})) def test_and_multi_depth(self): cs = ConditionSet({ 'and': [ {'attr': 'a', 'value': 1, 'op': 'eq'}, {'and': [ {'attr': 'b', 'value': 2, 'op': 'eq'}, {'attr': 'c', 'value': 3, 'op': 'eq'}, ]} ] }) self.assertTrue(cs.eval({'a': 1, 'b': 2, 'c': 3})) self.assertFalse(cs.eval({'a': 9, 'b': 2, 'c': 3})) self.assertFalse(cs.eval({'a': 1, 'b': 9, 'c': 3})) self.assertFalse(cs.eval({'a': 1, 'b': 2, 'c': 9})) def test_or_multi_depth(self): cs = ConditionSet({ 'or': [ {'attr': 'a', 'value': 1, 'op': 'eq'}, {'or': [ {'attr': 'b', 'value': 2, 'op': 'eq'}, {'attr': 'c', 'value': 3, 'op': 'eq'}, ]} ] }) self.assertTrue(cs.eval({'a': 1, 'b': 9, 'c': 9})) self.assertTrue(cs.eval({'a': 9, 'b': 2, 'c': 9})) self.assertTrue(cs.eval({'a': 9, 'b': 9, 'c': 3})) self.assertFalse(cs.eval({'a': 9, 'b': 9, 'c': 9})) def test_mixed_and(self): cs = ConditionSet({ 'and': [ {'attr': 'a', 'value': 1, 'op': 'eq'}, {'or': [ {'attr': 'b', 'value': 2, 'op': 'eq'}, {'attr': 'c', 'value': 3, 'op': 'eq'}, ]} ] }) self.assertTrue(cs.eval({'a': 1, 'b': 2, 'c': 9})) self.assertTrue(cs.eval({'a': 1, 'b': 9, 'c': 3})) self.assertFalse(cs.eval({'a': 1, 'b': 9, 'c': 9})) self.assertFalse(cs.eval({'a': 9, 'b': 2, 'c': 3})) def test_mixed_or(self): cs = ConditionSet({ 'or': [ {'attr': 'a', 'value': 1, 'op': 'eq'}, {'and': [ {'attr': 'b', 'value': 2, 'op': 'eq'}, {'attr': 'c', 'value': 3, 'op': 'eq'}, ]} ] }) self.assertTrue(cs.eval({'a': 1, 'b': 9, 'c': 9})) self.assertTrue(cs.eval({'a': 9, 'b': 2, 'c': 3})) self.assertTrue(cs.eval({'a': 1, 'b': 2, 'c': 9})) self.assertFalse(cs.eval({'a': 9, 'b': 2, 'c': 9})) self.assertFalse(cs.eval({'a': 9, 'b': 9, 'c': 3})) def test_event_rule_conditions_without_logic_operator(self): """ Test evaluation of EventRule conditions without logic operator. """ event_rule = EventRule( name='Event Rule 1', event_types=[OBJECT_CREATED, OBJECT_UPDATED], conditions={ 'attr': 'status.value', 'value': 'active', } ) # Create a Site to evaluate - Status = active site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE) data = serialize_for_event(site) # Evaluate the conditions (status='active') self.assertTrue(event_rule.eval_conditions(data)) def test_event_rule_conditions_with_logical_operation(self): """ Test evaluation of EventRule conditions without logic operator, but with logical operation (in). """ event_rule = EventRule( name='Event Rule 1', event_types=[OBJECT_CREATED, OBJECT_UPDATED], conditions={ "attr": "status.value", "value": ["planned", "staging"], "op": "in", } ) # Create a Site to evaluate - Status = active site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE) data = serialize_for_event(site) # Evaluate the conditions (status in ['planned, 'staging']) self.assertFalse(event_rule.eval_conditions(data)) def test_event_rule_conditions_with_logical_operation_and_negate(self): """ Test evaluation of EventRule with logical operation (in) and negate. """ event_rule = EventRule( name='Event Rule 1', event_types=[OBJECT_CREATED, OBJECT_UPDATED], conditions={ "attr": "status.value", "value": ["planned", "staging"], "op": "in", "negate": True, } ) # Create a Site to evaluate - Status = active site = Site.objects.create(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE) data = serialize_for_event(site) # Evaluate the conditions (status NOT in ['planned, 'staging']) self.assertTrue(event_rule.eval_conditions(data)) def test_event_rule_conditions_with_incorrect_key_must_return_false(self): """ Test Event Rule with incorrect condition (key "foo" is wrong). Must return false. """ ct = ContentType.objects.get(app_label='extras', model='webhook') site_ct = ContentType.objects.get_for_model(Site) webhook = Webhook.objects.create(name='Webhook 100', payload_url='http://example.com/?1', http_method='POST') form = EventRuleForm({ "name": "Event Rule 1", "event_types": [OBJECT_CREATED, OBJECT_UPDATED], "action_object_type": ct.pk, "action_type": "webhook", "action_choice": webhook.pk, "content_types": [site_ct.pk], "conditions": { "foo": "status.value", "value": "active" } }) self.assertFalse(form.is_valid())