mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
316 lines
10 KiB
Python
316 lines
10 KiB
Python
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):
|
|
|
|
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}))
|
|
self.assertFalse(c.eval({'x': 2}))
|
|
|
|
def test_eq_negated(self):
|
|
c = Condition('x', 1, 'eq', negate=True)
|
|
self.assertFalse(c.eval({'x': 1}))
|
|
self.assertTrue(c.eval({'x': 2}))
|
|
|
|
def test_gt(self):
|
|
c = Condition('x', 1, 'gt')
|
|
self.assertTrue(c.eval({'x': 2}))
|
|
self.assertFalse(c.eval({'x': 1}))
|
|
|
|
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}))
|
|
|
|
def test_lt(self):
|
|
c = Condition('x', 2, 'lt')
|
|
self.assertTrue(c.eval({'x': 1}))
|
|
self.assertFalse(c.eval({'x': 2}))
|
|
|
|
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}))
|
|
|
|
def test_in(self):
|
|
c = Condition('x', [1, 2, 3], 'in')
|
|
self.assertTrue(c.eval({'x': 1}))
|
|
self.assertFalse(c.eval({'x': 9}))
|
|
|
|
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}))
|
|
|
|
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]}))
|
|
|
|
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]}))
|
|
|
|
def test_regex(self):
|
|
c = Condition('x', '[a-z]+', 'regex')
|
|
self.assertTrue(c.eval({'x': 'abc'}))
|
|
self.assertFalse(c.eval({'x': '123'}))
|
|
|
|
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'}))
|
|
|
|
|
|
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',
|
|
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())
|