Skip to content

Commit b8017ae

Browse files
committed
add unit tests
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent f4709ef commit b8017ae

10 files changed

Lines changed: 575 additions & 3 deletions

File tree

dje/tests/test_admin.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
from django.test import override_settings
1414
from django.urls import reverse
1515

16+
from dje.admin import PolicyRulesConfigurationForm
1617
from dje.copier import copy_object
1718
from dje.filters import DataspaceFilter
1819
from dje.filters import MissingInFilter
1920
from dje.models import Dataspace
21+
from dje.models import DataspaceConfiguration
2022
from dje.models import History
2123
from dje.search import advanced_search
2224
from dje.tests import add_perm
@@ -25,6 +27,7 @@
2527
from dje.tests import create_superuser
2628
from dje.tests import create_user
2729
from organization.models import Owner
30+
from policy.rules import RULE_REGISTRY
2831

2932

3033
class DataspacedModelAdminTestCase(TestCase):
@@ -613,3 +616,78 @@ def test_admin_group_permission_export_csv(self):
613616
'attachment; filename="dejacode_group_permission.csv"', response["Content-Disposition"]
614617
)
615618
self.assertEqual(b",change_license\r\nchange license,X\r\n", response.content)
619+
620+
621+
class PolicyRulesConfigurationFormTestCase(TestCase):
622+
def setUp(self):
623+
self.dataspace = Dataspace.objects.create(name="nexB")
624+
self.config = DataspaceConfiguration.objects.create(dataspace=self.dataspace)
625+
self.Form = PolicyRulesConfigurationForm
626+
627+
def _bound_form(self, extra_data=None):
628+
data = {}
629+
for rule_type in RULE_REGISTRY:
630+
data[f"rule_{rule_type}_enabled"] = False
631+
data[f"rule_{rule_type}_threshold"] = ""
632+
if extra_data:
633+
data.update(extra_data)
634+
return self.Form(data=data, instance=self.config)
635+
636+
def test_enabled_rule_included_in_config(self):
637+
form = self._bound_form({"rule_usage_policy_error_enabled": True})
638+
self.assertTrue(form.is_valid(), form.errors)
639+
result = form.build_policy_rules_config()
640+
self.assertIn("usage_policy_error", result)
641+
self.assertTrue(result["usage_policy_error"]["is_active"])
642+
643+
def test_disabled_rule_not_in_config(self):
644+
form = self._bound_form()
645+
self.assertTrue(form.is_valid(), form.errors)
646+
result = form.build_policy_rules_config()
647+
self.assertEqual({}, result)
648+
649+
def test_threshold_saved_when_set(self):
650+
form = self._bound_form(
651+
{
652+
"rule_usage_policy_error_enabled": True,
653+
"rule_usage_policy_error_threshold": "3",
654+
}
655+
)
656+
self.assertTrue(form.is_valid(), form.errors)
657+
result = form.build_policy_rules_config()
658+
self.assertEqual(3, result["usage_policy_error"]["threshold"])
659+
660+
def test_initial_values_loaded_from_existing_config(self):
661+
self.config.policy_rules_config = {
662+
"usage_policy_error": {"is_active": True, "threshold": 7}
663+
}
664+
self.config.save()
665+
form = self.Form(instance=self.config)
666+
self.assertTrue(form.fields["rule_usage_policy_error_enabled"].initial)
667+
self.assertEqual(7, form.fields["rule_usage_policy_error_threshold"].initial)
668+
self.assertFalse(form.fields["rule_license_coverage_gap_enabled"].initial)
669+
670+
def test_save_persists_policy_rules_config_to_db(self):
671+
form = self._bound_form({"rule_usage_policy_error_enabled": True})
672+
self.assertTrue(form.is_valid(), form.errors)
673+
form.save()
674+
self.config.refresh_from_db()
675+
self.assertTrue(self.config.policy_rules_config["usage_policy_error"]["is_active"])
676+
677+
def test_inline_shows_rule_fieldsets_on_existing_dataspace(self):
678+
self.super_user = create_superuser("super_user", self.dataspace)
679+
self.client.login(username="super_user", password="secret")
680+
url = reverse("admin:dje_dataspace_change", args=[self.dataspace.pk])
681+
response = self.client.get(url)
682+
self.assertEqual(200, response.status_code)
683+
self.assertContains(response, "Policy Rules Configuration")
684+
for rule_type in RULE_REGISTRY:
685+
self.assertContains(response, f"rule_{rule_type}_enabled")
686+
687+
def test_inline_not_shown_on_dataspace_add(self):
688+
self.super_user = create_superuser("super_user", self.dataspace)
689+
self.client.login(username="super_user", password="secret")
690+
url = reverse("admin:dje_dataspace_add")
691+
response = self.client.get(url)
692+
self.assertEqual(200, response.status_code)
693+
self.assertNotContains(response, "rule_usage_policy_error_enabled")

policy/tests/test_engine.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from django.test import TestCase
1212

1313
from dje.models import Dataspace
14+
from dje.models import DataspaceConfiguration
1415
from policy.engine import evaluate_rule
16+
from policy.engine import evaluate_rules
17+
from policy.engine import get_effective_config
1518
from product_portfolio.models import ProductPolicyViolation
1619
from product_portfolio.tests import make_product
1720

@@ -71,3 +74,69 @@ def test_evaluate_rule_updates_count_on_existing_active_violation(self):
7174
self.assertFalse(created)
7275
self.assertEqual(7, violation.violation_count)
7376
self.assertEqual(1, ProductPolicyViolation.objects.count())
77+
78+
79+
class GetEffectiveConfigTestCase(TestCase):
80+
def setUp(self):
81+
self.dataspace = Dataspace.objects.create(name="nexB")
82+
self.product = make_product(self.dataspace)
83+
84+
def test_returns_defaults_when_no_dataspace_configuration(self):
85+
config = get_effective_config("usage_policy_error", self.dataspace)
86+
self.assertFalse(config["is_active"])
87+
self.assertEqual(0, config["threshold"])
88+
self.assertEqual({}, config["parameters"])
89+
90+
def test_reads_dataspace_override(self):
91+
DataspaceConfiguration.objects.create(
92+
dataspace=self.dataspace,
93+
policy_rules_config={"usage_policy_error": {"is_active": True, "threshold": 5}},
94+
)
95+
self.dataspace.refresh_from_db()
96+
config = get_effective_config("usage_policy_error", self.dataspace)
97+
self.assertTrue(config["is_active"])
98+
self.assertEqual(5, config["threshold"])
99+
100+
def test_falls_back_to_code_defaults_for_partial_config(self):
101+
DataspaceConfiguration.objects.create(
102+
dataspace=self.dataspace,
103+
policy_rules_config={"usage_policy_error": {"is_active": True}},
104+
)
105+
self.dataspace.refresh_from_db()
106+
config = get_effective_config("usage_policy_error", self.dataspace)
107+
self.assertTrue(config["is_active"])
108+
self.assertEqual(0, config["threshold"])
109+
110+
111+
class EvaluateRulesTestCase(TestCase):
112+
def setUp(self):
113+
self.dataspace = Dataspace.objects.create(name="nexB")
114+
DataspaceConfiguration.objects.create(
115+
dataspace=self.dataspace,
116+
policy_rules_config={"usage_policy_error": {"is_active": True}},
117+
)
118+
self.product = make_product(self.dataspace)
119+
120+
@patch("policy.engine.fire_policy_webhooks")
121+
@patch("policy.rules.UsagePolicyErrorRule.count_violations", return_value=3)
122+
def test_evaluate_rules_creates_violation_for_active_rule(self, _mock_count, mock_fire):
123+
new_violations, resolved_count = evaluate_rules(self.product)
124+
self.assertEqual(1, len(new_violations))
125+
self.assertEqual(0, resolved_count)
126+
self.assertEqual(1, ProductPolicyViolation.objects.filter(resolved=False).count())
127+
mock_fire.assert_called_once()
128+
129+
@patch("policy.engine.fire_policy_webhooks")
130+
def test_evaluate_rules_resolves_violation_when_rule_inactive(self, _mock_fire):
131+
ProductPolicyViolation.objects.create(
132+
product=self.product,
133+
dataspace=self.dataspace,
134+
rule_type="license_coverage_gap",
135+
violation_count=2,
136+
)
137+
new_violations, resolved_count = evaluate_rules(self.product)
138+
self.assertEqual(0, len(new_violations))
139+
self.assertGreater(resolved_count, 0)
140+
self.assertTrue(
141+
ProductPolicyViolation.objects.get(rule_type="license_coverage_gap").resolved
142+
)

policy/tests/test_signals.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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 component_catalog.tests import make_package
14+
from dje.models import Dataspace
15+
from dje.models import DataspaceConfiguration
16+
from product_portfolio.tests import make_product
17+
from product_portfolio.tests import make_product_package
18+
19+
20+
class ProductSaveSignalTestCase(TestCase):
21+
def setUp(self):
22+
self.dataspace = Dataspace.objects.create(name="nexB")
23+
24+
@patch("policy.signals.evaluate_product_rules_task.delay")
25+
def test_product_save_queues_task(self, mock_delay):
26+
product = make_product(self.dataspace)
27+
mock_delay.assert_called_with(product_uuid=product.uuid)
28+
29+
@patch("policy.signals.evaluate_product_rules_task.delay")
30+
def test_product_update_queues_task(self, mock_delay):
31+
product = make_product(self.dataspace)
32+
mock_delay.reset_mock()
33+
product.save()
34+
mock_delay.assert_called_once_with(product_uuid=product.uuid)
35+
36+
37+
class ProductPackageSignalTestCase(TestCase):
38+
def setUp(self):
39+
self.dataspace = Dataspace.objects.create(name="nexB")
40+
self.product = make_product(self.dataspace)
41+
42+
@patch("policy.signals.evaluate_product_rules_task.delay")
43+
def test_productpackage_save_queues_task(self, mock_delay):
44+
make_product_package(self.product)
45+
mock_delay.assert_called_with(product_uuid=self.product.uuid)
46+
47+
@patch("policy.signals.evaluate_product_rules_task.delay")
48+
def test_productpackage_delete_queues_task(self, mock_delay):
49+
pp = make_product_package(self.product)
50+
mock_delay.reset_mock()
51+
pp.delete()
52+
mock_delay.assert_called_once_with(product_uuid=self.product.uuid)
53+
54+
55+
class PackageSaveSignalTestCase(TestCase):
56+
def setUp(self):
57+
self.dataspace = Dataspace.objects.create(name="nexB")
58+
self.product = make_product(self.dataspace)
59+
60+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
61+
def test_package_create_does_not_queue_task(self, mock_delay):
62+
make_package(self.dataspace)
63+
mock_delay.assert_not_called()
64+
65+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
66+
def test_package_update_with_products_queues_task(self, mock_delay):
67+
package = make_package(self.dataspace)
68+
make_product_package(self.product, package=package)
69+
mock_delay.reset_mock()
70+
package.save()
71+
mock_delay.assert_called_once()
72+
called_uuids = mock_delay.call_args[1]["product_uuids"]
73+
self.assertIn(self.product.uuid, called_uuids)
74+
75+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
76+
def test_package_update_without_products_does_not_queue_task(self, mock_delay):
77+
package = make_package(self.dataspace)
78+
mock_delay.reset_mock()
79+
package.save()
80+
mock_delay.assert_not_called()
81+
82+
83+
class DataspaceConfigurationSignalTestCase(TestCase):
84+
def setUp(self):
85+
self.dataspace = Dataspace.objects.create(name="nexB")
86+
self.config = DataspaceConfiguration.objects.create(dataspace=self.dataspace)
87+
self.product = make_product(self.dataspace)
88+
89+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
90+
def test_full_save_queues_task(self, mock_delay):
91+
self.config.save()
92+
mock_delay.assert_called_once()
93+
94+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
95+
def test_policy_rules_config_update_fields_queues_task(self, mock_delay):
96+
self.config.policy_rules_config = {"usage_policy_error": {"is_active": True}}
97+
self.config.save(update_fields=["policy_rules_config"])
98+
mock_delay.assert_called_once()
99+
called_uuids = mock_delay.call_args[1]["product_uuids"]
100+
self.assertIn(self.product.uuid, called_uuids)
101+
102+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
103+
def test_unrelated_update_fields_does_not_queue_task(self, mock_delay):
104+
self.config.save(update_fields=["scancodeio_url"])
105+
mock_delay.assert_not_called()
106+
107+
@patch("policy.signals.evaluate_all_products_rules_task.delay")
108+
def test_no_products_in_dataspace_does_not_queue_task(self, mock_delay):
109+
empty_dataspace = Dataspace.objects.create(name="Empty")
110+
empty_config = DataspaceConfiguration.objects.create(dataspace=empty_dataspace)
111+
mock_delay.reset_mock()
112+
empty_config.save(update_fields=["policy_rules_config"])
113+
mock_delay.assert_not_called()

product_portfolio/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ class Product(
432432
# WARNING: Bypass the security system implemented in ProductSecuredManager.
433433
# This is to be used only in a few cases where the User scoping is not appropriated.
434434
# For example: `self.dataspace.product_set(manager='unsecured_objects').count()`
435-
unsecured_objects = DataspacedManager()
435+
unsecured_objects = DataspacedManager.from_queryset(ProductQuerySet)()
436436

437437
class Meta(BaseProductMixin.Meta):
438438
permissions = (("view_product", "Can view product"),)

product_portfolio/tests/test_admin.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
# See https://aboutcode.org for more information about AboutCode FOSS projects.
77
#
88

9+
from unittest.mock import patch
10+
911
from django.core.exceptions import NON_FIELD_ERRORS
1012
from django.test import TestCase
1113
from django.urls import NoReverseMatch
@@ -101,7 +103,11 @@ def test_product_admin_form_clean_license_expression_in_alternate_dataspace(self
101103
def test_product_security_admin_changelist_available_actions(self):
102104
self.client.login(username=self.user.username, password="secret")
103105
response = self.client.get(self.product_changelist_url)
104-
expected = [("", "---------"), ("mass_update", "Mass update")]
106+
expected = [
107+
("", "---------"),
108+
("evaluate_policy_rules", "Evaluate policy rules"),
109+
("mass_update", "Mass update"),
110+
]
105111
self.assertEqual(expected, response.context_data["action_form"].fields["action"].choices)
106112

107113
with self.assertRaises(NoReverseMatch):
@@ -539,3 +545,26 @@ def test_product_dependencies_add_view(self):
539545
dependency = self.product1.dependencies.get()
540546
self.assertEqual(self.package1, dependency.for_package)
541547
self.assertEqual(package2, dependency.resolved_to_package)
548+
549+
550+
class EvaluatePolicyRulesActionTestCase(TestCase):
551+
def setUp(self):
552+
self.dataspace = Dataspace.objects.create(name="nexB")
553+
self.super_user = create_superuser("nexb_user", self.dataspace)
554+
self.product1 = Product.objects.create(name="Product1", dataspace=self.dataspace)
555+
self.product2 = Product.objects.create(name="Product2", dataspace=self.dataspace)
556+
557+
@patch("product_portfolio.admin.evaluate_product_rules_task.delay")
558+
def test_evaluate_policy_rules_action_queues_task_for_selected_products(self, mock_delay):
559+
self.client.login(username="nexb_user", password="secret")
560+
url = reverse("admin:product_portfolio_product_changelist")
561+
data = {
562+
"action": "evaluate_policy_rules",
563+
"_selected_action": [self.product1.pk, self.product2.pk],
564+
}
565+
response = self.client.post(url, data, follow=True)
566+
self.assertEqual(200, response.status_code)
567+
self.assertEqual(2, mock_delay.call_count)
568+
called_uuids = {call[1]["product_uuid"] for call in mock_delay.call_args_list}
569+
self.assertIn(self.product1.uuid, called_uuids)
570+
self.assertIn(self.product2.uuid, called_uuids)

product_portfolio/tests/test_admin_guardian.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ def test_product_guardian_admin_security_attributes(self):
5858

5959
# actions = []
6060
# actions_to_remove = ['copy_to', 'compare_with', 'delete_selected']
61-
expected = [("", "---------"), ("mass_update", "Mass update")]
61+
expected = [
62+
("", "---------"),
63+
("evaluate_policy_rules", "Evaluate policy rules"),
64+
("mass_update", "Mass update"),
65+
]
6266
self.assertEqual(expected, response.context_data["action_form"].fields["action"].choices)
6367

6468
# activity_log = False

0 commit comments

Comments
 (0)