From 5788b6cb289cdc6f13da1e348f2af8061a78c08a Mon Sep 17 00:00:00 2001 From: Julio Oliveira at Encora <149191228+Julio-Oliveira-Encora@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:45:19 -0300 Subject: [PATCH] Fixes #14829 Simple condition (without and/or) does not work in event rule (#14870) --- netbox/extras/conditions.py | 30 ++++---- netbox/extras/tests/test_conditions.py | 96 ++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/netbox/extras/conditions.py b/netbox/extras/conditions.py index 39005b752..5680be444 100644 --- a/netbox/extras/conditions.py +++ b/netbox/extras/conditions.py @@ -135,23 +135,23 @@ class ConditionSet: def __init__(self, ruleset): if type(ruleset) is not dict: raise ValueError(_("Ruleset must be a dictionary, not {ruleset}.").format(ruleset=type(ruleset))) - if len(ruleset) != 1: - raise ValueError(_("Ruleset must have exactly one logical operator (found {ruleset})").format( - ruleset=len(ruleset))) - # Determine the logic type - logic = list(ruleset.keys())[0] - if type(logic) is not str or logic.lower() not in (AND, OR): - raise ValueError(_("Invalid logic type: {logic} (must be '{op_and}' or '{op_or}')").format( - logic=logic, op_and=AND, op_or=OR - )) - self.logic = logic.lower() + if len(ruleset) == 1: + self.logic = (list(ruleset.keys())[0]).lower() + if self.logic not in (AND, OR): + raise ValueError(_("Invalid logic type: must be 'AND' or 'OR'. Please check documentation.")) - # Compile the set of Conditions - self.conditions = [ - ConditionSet(rule) if is_ruleset(rule) else Condition(**rule) - for rule in ruleset[self.logic] - ] + # Compile the set of Conditions + self.conditions = [ + ConditionSet(rule) if is_ruleset(rule) else Condition(**rule) + for rule in ruleset[self.logic] + ] + else: + try: + self.logic = None + self.conditions = [Condition(**ruleset)] + except TypeError: + raise ValueError(_("Incorrect key(s) informed. Please check documentation.")) def eval(self, data): """ diff --git a/netbox/extras/tests/test_conditions.py b/netbox/extras/tests/test_conditions.py index e7275482a..dd528b918 100644 --- a/netbox/extras/tests/test_conditions.py +++ b/netbox/extras/tests/test_conditions.py @@ -1,6 +1,12 @@ +from django.contrib.contenttypes.models import ContentType from django.test import TestCase +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): @@ -217,3 +223,93 @@ class ConditionSetTest(TestCase): 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', + type_create=True, + type_update=True, + 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', + type_create=True, + type_update=True, + 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', + type_create=True, + type_update=True, + 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", + "type_create": True, + "type_update": True, + "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())