|
| 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