Skip to content

Commit 4285ca7

Browse files
committed
fix tasks bug and add unit tests
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 1b6e412 commit 4285ca7

4 files changed

Lines changed: 194 additions & 2 deletions

File tree

notification/tasks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from django_rq import job
1414

15+
from dje.models import get_unsecured_manager
1516
from notification.models import WebhookSubscription
1617

1718
logger = logging.getLogger("dje")
@@ -36,7 +37,7 @@ def deliver_webhook_task(
3637
if instance_app_label and instance_model_name and instance_pk:
3738
try:
3839
model_class = apps.get_model(instance_app_label, instance_model_name)
39-
instance = model_class.objects.get(pk=instance_pk)
40+
instance = get_unsecured_manager(model_class).get(pk=instance_pk)
4041
except Exception:
4142
logger.error(
4243
f"Instance {instance_app_label}.{instance_model_name} pk={instance_pk} not found."

notification/tests/test_tasks.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#
88

99
import json
10+
import uuid
1011
from unittest.mock import patch
1112

1213
from django.conf import settings
@@ -16,13 +17,54 @@
1617
from dje.models import Dataspace
1718
from dje.tests import create_superuser
1819
from notification.models import WebhookSubscription
20+
from notification.tasks import deliver_webhook_task
21+
from product_portfolio.tests import make_product
1922
from workflow.models import Priority
2023
from workflow.models import Question
2124
from workflow.models import Request
2225
from workflow.models import RequestComment
2326
from workflow.models import RequestTemplate
2427

2528

29+
class DeliverWebhookTaskTestCase(TestCase):
30+
def setUp(self):
31+
self.dataspace = Dataspace.objects.create(name="nexB")
32+
self.webhook = WebhookSubscription.objects.create(
33+
dataspace=self.dataspace,
34+
target_url="http://127.0.0.1:8000/",
35+
event="policy.violation_detected",
36+
)
37+
38+
@patch("notification.models.WebhookSubscription.deliver")
39+
def test_deliver_webhook_task_resolves_secured_model_instance(self, mock_deliver):
40+
product = make_product(self.dataspace)
41+
deliver_webhook_task(
42+
webhook_subscription_uuid=self.webhook.uuid,
43+
payload_override={"text": "test"},
44+
instance_app_label="product_portfolio",
45+
instance_model_name="product",
46+
instance_pk=product.pk,
47+
)
48+
mock_deliver.assert_called_once()
49+
delivered_instance = mock_deliver.call_args[0][0]
50+
self.assertEqual(product, delivered_instance)
51+
52+
def test_deliver_webhook_task_missing_subscription_logs_error(self):
53+
with self.assertLogs("dje", level="ERROR") as captured:
54+
deliver_webhook_task(webhook_subscription_uuid=uuid.uuid4())
55+
self.assertTrue(any("not found" in line for line in captured.output))
56+
57+
def test_deliver_webhook_task_missing_instance_logs_error(self):
58+
with self.assertLogs("dje", level="ERROR") as captured:
59+
deliver_webhook_task(
60+
webhook_subscription_uuid=self.webhook.uuid,
61+
instance_app_label="product_portfolio",
62+
instance_model_name="product",
63+
instance_pk=99999999,
64+
)
65+
self.assertTrue(any("not found" in line for line in captured.output))
66+
67+
2668
class NotificationTasksTestCase(TestCase):
2769
def setUp(self):
2870
self.nexb_dataspace = Dataspace.objects.create(name="nexB")

policy/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def evaluate_all_products_rules_task(include_locked=False, product_uuids=None):
7070
if product_uuids is not None:
7171
products = products.filter(uuid__in=product_uuids)
7272
elif not include_locked:
73-
products = products.exclude_locked()
73+
products = products.exclude(configuration_status__is_locked=True)
7474

7575
count = products.count()
7676
logger.info(f"Starting policy rule evaluation for {count} product(s).")

policy/tests/test_tasks.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
import uuid
10+
from unittest.mock import MagicMock
11+
from unittest.mock import patch
12+
13+
from django.test import TestCase
14+
15+
from dje.models import Dataspace
16+
from policy.tasks import evaluate_all_products_rules_task
17+
from policy.tasks import evaluate_product_rules_task
18+
from policy.tasks import fire_policy_webhooks
19+
from product_portfolio.tests import make_product
20+
from product_portfolio.tests import make_product_status
21+
22+
23+
class FirePolicyWebhooksTestCase(TestCase):
24+
def setUp(self):
25+
self.dataspace = Dataspace.objects.create(name="nexB")
26+
self.product = make_product(self.dataspace)
27+
28+
@patch("policy.tasks.fire_webhooks")
29+
def test_fire_policy_webhooks_dispatches_violation_detected(self, mock_fire):
30+
violation = MagicMock()
31+
violation.rule_label = "Usage Policy Error"
32+
violation.violation_count = 3
33+
fire_policy_webhooks(self.product, new_violations=[violation], resolved_count=0)
34+
mock_fire.assert_called_once()
35+
event_name, kwargs = mock_fire.call_args[0][0], mock_fire.call_args[1]
36+
self.assertEqual("policy.violation_detected", event_name)
37+
self.assertIn("Policy violations detected", kwargs["payload_override"]["text"])
38+
self.assertIn("Usage Policy Error", kwargs["payload_override"]["text"])
39+
40+
@patch("policy.tasks.fire_webhooks")
41+
def test_fire_policy_webhooks_dispatches_violation_resolved(self, mock_fire):
42+
fire_policy_webhooks(self.product, new_violations=[], resolved_count=2)
43+
mock_fire.assert_called_once()
44+
event_name, kwargs = mock_fire.call_args[0][0], mock_fire.call_args[1]
45+
self.assertEqual("policy.violation_resolved", event_name)
46+
self.assertIn("2 policy violation(s) resolved", kwargs["payload_override"]["text"])
47+
48+
@patch("policy.tasks.fire_webhooks")
49+
def test_fire_policy_webhooks_dispatches_both_events(self, mock_fire):
50+
violation = MagicMock()
51+
violation.rule_label = "License Coverage Gap"
52+
violation.violation_count = 1
53+
fire_policy_webhooks(self.product, new_violations=[violation], resolved_count=1)
54+
self.assertEqual(2, mock_fire.call_count)
55+
events_fired = [c[0][0] for c in mock_fire.call_args_list]
56+
self.assertIn("policy.violation_detected", events_fired)
57+
self.assertIn("policy.violation_resolved", events_fired)
58+
59+
@patch("policy.tasks.fire_webhooks")
60+
def test_fire_policy_webhooks_silent_when_no_changes(self, mock_fire):
61+
fire_policy_webhooks(self.product, new_violations=[], resolved_count=0)
62+
mock_fire.assert_not_called()
63+
64+
65+
class EvaluateProductRulesTaskTestCase(TestCase):
66+
def setUp(self):
67+
self.dataspace = Dataspace.objects.create(name="nexB")
68+
self.product = make_product(self.dataspace)
69+
70+
@patch("policy.tasks.fire_policy_webhooks")
71+
@patch("policy.tasks.evaluate_rules")
72+
def test_evaluate_product_rules_task_runs_evaluation(self, mock_evaluate, mock_fire):
73+
mock_evaluate.return_value = ([], 0)
74+
evaluate_product_rules_task(product_uuid=self.product.uuid)
75+
mock_evaluate.assert_called_once_with(self.product)
76+
mock_fire.assert_called_once_with(self.product, [], 0)
77+
78+
@patch("policy.tasks.fire_policy_webhooks")
79+
@patch("policy.tasks.evaluate_rules")
80+
def test_evaluate_product_rules_task_unknown_uuid_logs_error(self, mock_evaluate, mock_fire):
81+
with self.assertLogs("policy.tasks", level="ERROR") as captured:
82+
evaluate_product_rules_task(product_uuid=uuid.uuid4())
83+
mock_evaluate.assert_not_called()
84+
mock_fire.assert_not_called()
85+
self.assertTrue(any("not found" in line for line in captured.output))
86+
87+
88+
class EvaluateAllProductsRulesTaskTestCase(TestCase):
89+
def setUp(self):
90+
self.dataspace = Dataspace.objects.create(name="nexB")
91+
92+
@patch("policy.tasks.fire_policy_webhooks")
93+
@patch("policy.tasks.evaluate_rules")
94+
def test_evaluate_all_products_excludes_locked_by_default(self, mock_evaluate, mock_fire):
95+
# Regression: previously called .exclude_locked() on DataspacedQuerySet which lacks that
96+
# method. Now uses .exclude(configuration_status__is_locked=True) inline.
97+
mock_evaluate.return_value = ([], 0)
98+
active_product = make_product(self.dataspace)
99+
locked_status = make_product_status(self.dataspace, is_locked=True)
100+
locked_product = make_product(self.dataspace, configuration_status=locked_status)
101+
mock_evaluate.reset_mock()
102+
103+
evaluate_all_products_rules_task()
104+
105+
evaluated_products = [c[0][0] for c in mock_evaluate.call_args_list]
106+
self.assertIn(active_product, evaluated_products)
107+
self.assertNotIn(locked_product, evaluated_products)
108+
109+
@patch("policy.tasks.fire_policy_webhooks")
110+
@patch("policy.tasks.evaluate_rules")
111+
def test_evaluate_all_products_includes_locked_when_requested(self, mock_evaluate, mock_fire):
112+
mock_evaluate.return_value = ([], 0)
113+
locked_status = make_product_status(self.dataspace, is_locked=True)
114+
locked_product = make_product(self.dataspace, configuration_status=locked_status)
115+
mock_evaluate.reset_mock()
116+
117+
evaluate_all_products_rules_task(include_locked=True)
118+
119+
evaluated_products = [c[0][0] for c in mock_evaluate.call_args_list]
120+
self.assertIn(locked_product, evaluated_products)
121+
122+
@patch("policy.tasks.fire_policy_webhooks")
123+
@patch("policy.tasks.evaluate_rules")
124+
def test_evaluate_all_products_filters_by_uuids(self, mock_evaluate, mock_fire):
125+
mock_evaluate.return_value = ([], 0)
126+
product_a = make_product(self.dataspace)
127+
product_b = make_product(self.dataspace)
128+
mock_evaluate.reset_mock()
129+
130+
evaluate_all_products_rules_task(product_uuids=[product_a.uuid])
131+
132+
evaluated_products = [c[0][0] for c in mock_evaluate.call_args_list]
133+
self.assertIn(product_a, evaluated_products)
134+
self.assertNotIn(product_b, evaluated_products)
135+
136+
@patch("policy.tasks.fire_policy_webhooks")
137+
@patch("policy.tasks.evaluate_rules")
138+
def test_evaluate_all_products_uuid_filter_ignores_locked_exclusion(
139+
self, mock_evaluate, mock_fire
140+
):
141+
mock_evaluate.return_value = ([], 0)
142+
locked_status = make_product_status(self.dataspace, is_locked=True)
143+
locked_product = make_product(self.dataspace, configuration_status=locked_status)
144+
mock_evaluate.reset_mock()
145+
146+
evaluate_all_products_rules_task(product_uuids=[locked_product.uuid])
147+
148+
evaluated_products = [c[0][0] for c in mock_evaluate.call_args_list]
149+
self.assertIn(locked_product, evaluated_products)

0 commit comments

Comments
 (0)