Skip to content

Commit 5673655

Browse files
committed
fix bug in rule evaluation when previously raised then closed
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 4e1762c commit 5673655

3 files changed

Lines changed: 84 additions & 8 deletions

File tree

policy/engine.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,23 @@ def evaluate_rule(rule_type, product, threshold, parameters):
4141
handler = RULE_REGISTRY[rule_type]
4242
violation_count = handler.count_violations(product, threshold, parameters)
4343

44-
lookup = {"rule_type": rule_type, "product": product, "resolved": False}
44+
lookup = {"rule_type": rule_type, "product": product}
4545

4646
if violation_count > 0:
47-
violation, created = ProductPolicyViolation.objects.get_or_create(
47+
# update_or_create on the unique (rule_type, product) pair so that a previously
48+
# resolved violation can be re-activated without hitting the unique constraint.
49+
violation, created = ProductPolicyViolation.objects.update_or_create(
4850
**lookup,
49-
defaults={"dataspace": product.dataspace, "violation_count": violation_count},
51+
defaults={
52+
"dataspace": product.dataspace,
53+
"violation_count": violation_count,
54+
"resolved": False,
55+
"resolved_date": None,
56+
},
5057
)
51-
if not created:
52-
violation.violation_count = violation_count
53-
violation.save()
5458
return violation, created, 0
5559

56-
resolved_count = ProductPolicyViolation.objects.filter(**lookup).update(
60+
resolved_count = ProductPolicyViolation.objects.filter(**lookup, resolved=False).update(
5761
resolved=True,
5862
resolved_date=timezone.now(),
5963
)

policy/tests/test_engine.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# DejaCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: AGPL-3.0-only
5+
# See https://github.com/aboutcode-org/dejacode for support or download.
6+
# See https://aboutcode.org for more information about AboutCode FOSS projects.
7+
#
8+
9+
from unittest.mock import patch
10+
11+
from django.test import TestCase
12+
13+
from dje.models import Dataspace
14+
from policy.engine import evaluate_rule
15+
from product_portfolio.models import ProductPolicyViolation
16+
from product_portfolio.tests import make_product
17+
18+
RULE_TYPE = "usage_policy_error"
19+
20+
21+
class EvaluateRuleTestCase(TestCase):
22+
def setUp(self):
23+
self.dataspace = Dataspace.objects.create(name="nexB")
24+
self.product = make_product(self.dataspace)
25+
26+
def _count_violations(self, count):
27+
"""Patch count_violations for RULE_TYPE to return a fixed count."""
28+
return patch(
29+
"policy.rules.UsagePolicyErrorRule.count_violations",
30+
return_value=count,
31+
)
32+
33+
def test_evaluate_rule_creates_violation_on_first_trigger(self):
34+
with self._count_violations(3):
35+
violation, created, resolved_count = evaluate_rule(RULE_TYPE, self.product, 0, {})
36+
self.assertTrue(created)
37+
self.assertEqual(0, resolved_count)
38+
self.assertEqual(3, violation.violation_count)
39+
self.assertFalse(violation.resolved)
40+
self.assertEqual(1, ProductPolicyViolation.objects.count())
41+
42+
def test_evaluate_rule_resolves_violation_when_count_drops_to_zero(self):
43+
with self._count_violations(3):
44+
evaluate_rule(RULE_TYPE, self.product, 0, {})
45+
with self._count_violations(0):
46+
violation, created, resolved_count = evaluate_rule(RULE_TYPE, self.product, 0, {})
47+
self.assertIsNone(violation)
48+
self.assertFalse(created)
49+
self.assertEqual(1, resolved_count)
50+
db_violation = ProductPolicyViolation.objects.get()
51+
self.assertTrue(db_violation.resolved)
52+
self.assertIsNotNone(db_violation.resolved_date)
53+
54+
def test_evaluate_rule_retrigger_after_resolution_does_not_raise(self):
55+
with self._count_violations(3):
56+
evaluate_rule(RULE_TYPE, self.product, 0, {})
57+
with self._count_violations(0):
58+
evaluate_rule(RULE_TYPE, self.product, 0, {})
59+
with self._count_violations(5):
60+
violation, created, resolved_count = evaluate_rule(RULE_TYPE, self.product, 0, {})
61+
self.assertFalse(violation.resolved)
62+
self.assertIsNone(violation.resolved_date)
63+
self.assertEqual(5, violation.violation_count)
64+
self.assertEqual(1, ProductPolicyViolation.objects.count())
65+
66+
def test_evaluate_rule_updates_count_on_existing_active_violation(self):
67+
with self._count_violations(2):
68+
evaluate_rule(RULE_TYPE, self.product, 0, {})
69+
with self._count_violations(7):
70+
violation, created, resolved_count = evaluate_rule(RULE_TYPE, self.product, 0, {})
71+
self.assertFalse(created)
72+
self.assertEqual(7, violation.violation_count)
73+
self.assertEqual(1, ProductPolicyViolation.objects.count())

product_portfolio/tests/test_api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,6 @@ def test_api_product_endpoint_manage_permissions_action(self):
717717
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
718718
self.assertIn("errors", response.data)
719719

720-
721720
def test_api_product_endpoint_policy_violations_action(self):
722721
url = reverse("api_v2:product-policy-violations", args=[self.product1.uuid])
723722

0 commit comments

Comments
 (0)