Skip to content

Commit cad0c61

Browse files
committed
fix and add unit tests
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent a27f71f commit cad0c61

11 files changed

Lines changed: 623 additions & 365 deletions

File tree

product_portfolio/tests/test_admin.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,39 @@ def test_product_admin_changeform_save_as_includes_relationships(self):
457457
self.assertEqual(1, new_product.productpackages.count())
458458
self.assertEqual(1, new_product.product_triage_rulesets.count())
459459

460+
@patch("product_portfolio.admin.reevaluate_products")
461+
def test_product_admin_changeform_save_as_evaluates_the_new_product_once(self, mock_reevaluate):
462+
# Cloning must evaluate the new product exactly once, not once per cloned
463+
# ProductPackage relationship.
464+
package2 = make_package(self.dataspace)
465+
ProductPackage.objects.create(
466+
product=self.product1, package=self.package1, dataspace=self.dataspace
467+
)
468+
ProductPackage.objects.create(
469+
product=self.product1, package=package2, dataspace=self.dataspace
470+
)
471+
self.client.login(username=self.user.username, password="secret")
472+
473+
url = self.product1.get_admin_url()
474+
data = {
475+
"_saveasnew": "Save as new",
476+
"name": self.product1.name,
477+
"version": "new version 2",
478+
"productcomponents-INITIAL_FORMS": 0,
479+
"productcomponents-TOTAL_FORMS": 0,
480+
"productpackages-INITIAL_FORMS": 0,
481+
"productpackages-TOTAL_FORMS": 0,
482+
}
483+
484+
response = self.client.post(url, data)
485+
486+
self.assertEqual(response.status_code, 302)
487+
new_product = Product.unsecured_objects.get(
488+
name=self.product1.name, version="new version 2"
489+
)
490+
self.assertEqual(2, new_product.productpackages.count())
491+
mock_reevaluate.assert_called_once_with([new_product])
492+
460493
def test_codebaseresource_admin_changeform_product_prefill_on_save_addanother(self):
461494
self.client.login(username=self.user.username, password="secret")
462495
url = reverse("admin:product_portfolio_codebaseresource_add")

product_portfolio/tests/test_importers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,28 @@ def test_productpackage_import_save_all(self):
378378
self.assertEqual(False, productpackage.is_deployed)
379379
self.assertEqual(True, productpackage.is_modified)
380380

381+
@mock.patch("product_portfolio.importers.reevaluate_product_triage_rulesets_task")
382+
@mock.patch("product_portfolio.importers.evaluate_product_rules_task")
383+
def test_productpackage_import_evaluates_each_product_once_for_multiple_rows(
384+
self, mock_policy_task, mock_triage_task
385+
):
386+
formset_data = self.package_formset_data.copy()
387+
package1 = Package.objects.create(filename="p1.zip", dataspace=self.dataspace)
388+
package2 = Package.objects.create(filename="p2.zip", dataspace=self.dataspace)
389+
390+
formset_data["form-TOTAL_FORMS"] = "2"
391+
formset_data["form-0-package"] = package1.filename
392+
formset_data["form-1-product"] = formset_data["form-0-product"]
393+
formset_data["form-1-package"] = package2.filename
394+
395+
importer = ProductPackageImporter(self.super_user, formset_data=formset_data)
396+
self.assertTrue(importer.formset.is_valid())
397+
importer.save_all()
398+
399+
self.assertEqual(2, len(importer.results["added"]))
400+
mock_policy_task.delay.assert_called_once_with(product_uuid=self.p1.uuid)
401+
mock_triage_task.delay.assert_called_once_with(product_uuid=self.p1.uuid)
402+
381403

382404
class CodebaseResourceImporterTestCase(TestCase):
383405
def setUp(self):

product_portfolio/tests/test_views.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4416,8 +4416,8 @@ def test_post_without_change_perm_returns_404(self):
44164416
response = self.client.post(url)
44174417
self.assertEqual(404, response.status_code)
44184418

4419-
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
4420-
def test_post_assigns_the_submitted_rulesets(self, mock_evaluate):
4419+
@patch("product_portfolio.views.reevaluate_product_rulesets")
4420+
def test_post_assigns_the_submitted_rulesets(self, mock_reevaluate):
44214421
self.client.login(username="nexb_user", password="secret")
44224422
url = self.product1.get_manage_triage_rulesets_url()
44234423
response = self.client.post(url, {"ruleset_uuids": [str(self.ruleset.uuid)]})
@@ -4427,14 +4427,14 @@ def test_post_assigns_the_submitted_rulesets(self, mock_evaluate):
44274427
product=self.product1, ruleset=self.ruleset
44284428
).exists()
44294429
)
4430-
mock_evaluate.assert_called_once_with(ruleset=self.ruleset, product=self.product1)
4430+
mock_reevaluate.assert_called_once_with(self.product1)
44314431

4432-
@patch("vulnerabilities.triage.signals.evaluate_ruleset")
4433-
def test_post_unassigns_the_deselected_rulesets(self, mock_evaluate):
4432+
@patch("product_portfolio.views.delete_triage_records_for_assignment")
4433+
@patch("product_portfolio.views.reevaluate_product_rulesets")
4434+
def test_post_unassigns_the_deselected_rulesets(self, mock_reevaluate, mock_delete):
44344435
ProductTriageRuleset.objects.create(
44354436
product=self.product1, ruleset=self.ruleset, dataspace=self.dataspace
44364437
)
4437-
mock_evaluate.reset_mock()
44384438
self.client.login(username="nexb_user", password="secret")
44394439
url = self.product1.get_manage_triage_rulesets_url()
44404440
response = self.client.post(url, {"ruleset_uuids": []})
@@ -4444,7 +4444,8 @@ def test_post_unassigns_the_deselected_rulesets(self, mock_evaluate):
44444444
product=self.product1, ruleset=self.ruleset
44454445
).exists()
44464446
)
4447-
mock_evaluate.assert_not_called()
4447+
mock_delete.assert_called_once_with(ruleset=self.ruleset, product=self.product1)
4448+
mock_reevaluate.assert_called_once_with(self.product1)
44484449

44494450

44504451
class TabCompliancePolicyContextTestCase(TestCase):

vulnerabilities/triage/engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def apply_preset_for_vulnerabilities(preset, product, vulnerability_ids):
9696
existing = existing_analyses.get((product_package_id, vulnerability_id))
9797

9898
if existing is not None and existing.applied_by_preset_id is None:
99-
continue # Human-owned analysis -- never overwrite
99+
continue # User-owned analysis, never overwrite
100100

101101
if existing is None:
102102
product_package = product_packages_by_id[product_package_id]

vulnerabilities/triage/tasks.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ def reevaluate_product_triage_rulesets_task(product_uuid):
2929
product = get_unsecured_manager(Product).get(uuid=product_uuid)
3030
except Product.DoesNotExist:
3131
logger.error(
32-
f"reevaluate_product_triage_rulesets_task: product {product_uuid} not found,"
33-
" skipping."
32+
f"reevaluate_product_triage_rulesets_task: product {product_uuid} not found, skipping."
3433
)
3534
return
3635

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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 django.contrib.contenttypes.models import ContentType
10+
from django.test import TestCase
11+
from django.urls import reverse
12+
13+
from component_catalog.tests import make_package
14+
from dje.models import Dataspace
15+
from dje.tests import create_superuser
16+
from dje.tests import create_user
17+
from product_portfolio.models import Product
18+
from product_portfolio.tests import make_product
19+
from product_portfolio.tests import make_product_package
20+
from vulnerabilities.tests import make_vulnerability
21+
from vulnerabilities.triage.engine import evaluate_ruleset
22+
from vulnerabilities.triage.models import TriageAction
23+
from vulnerabilities.triage.models import TriageRecord
24+
from vulnerabilities.triage.tests import make_product_triage_ruleset
25+
from vulnerabilities.triage.tests import make_triage_ruleset
26+
from workflow.models import RequestTemplate
27+
28+
RULE_CONFIG_DATA = {
29+
"rule_risk_score_enabled": "on",
30+
"rule_risk_score_min_risk_score": "8.0",
31+
}
32+
33+
34+
class TriageRulesetAdminSaveModelTestCase(TestCase):
35+
def setUp(self):
36+
self.dataspace = Dataspace.objects.create(name="nexB")
37+
self.super_user = create_superuser("nexb_user", self.dataspace)
38+
self.client.login(username="nexb_user", password="secret")
39+
self.product = make_product(self.dataspace)
40+
self.package = make_package(self.dataspace)
41+
make_product_package(self.product, package=self.package)
42+
43+
def test_creating_a_ruleset_does_not_evaluate_any_product(self):
44+
make_vulnerability(self.dataspace, affecting=self.package, risk_score=9.0)
45+
url = reverse("admin:vulnerabilities_triage_triageruleset_add")
46+
data = {
47+
"name": "My Ruleset",
48+
"precedence": 100,
49+
"enabled": "on",
50+
**RULE_CONFIG_DATA,
51+
}
52+
53+
response = self.client.post(url, data)
54+
55+
self.assertEqual(302, response.status_code)
56+
self.assertFalse(TriageRecord.objects.exists())
57+
58+
def test_updating_an_enabled_assigned_ruleset_reevaluates_its_products(self):
59+
vulnerability = make_vulnerability(self.dataspace, affecting=self.package, risk_score=9.0)
60+
ruleset = make_triage_ruleset(
61+
self.dataspace,
62+
recommended_action=TriageAction.UPGRADE,
63+
enabled=True,
64+
)
65+
make_product_triage_ruleset(self.product, ruleset=ruleset)
66+
self.assertFalse(TriageRecord.objects.exists())
67+
68+
url = ruleset.get_admin_url()
69+
data = {
70+
"name": ruleset.name,
71+
"precedence": ruleset.precedence,
72+
"recommended_action": TriageAction.UPGRADE,
73+
"enabled": "on",
74+
**RULE_CONFIG_DATA,
75+
}
76+
77+
response = self.client.post(url, data)
78+
79+
self.assertEqual(302, response.status_code)
80+
record = TriageRecord.objects.get()
81+
self.assertEqual(vulnerability, record.vulnerability)
82+
83+
def test_disabling_a_ruleset_deletes_its_triage_records(self):
84+
make_vulnerability(self.dataspace, affecting=self.package, risk_score=9.0)
85+
ruleset = make_triage_ruleset(
86+
self.dataspace,
87+
recommended_action=TriageAction.UPGRADE,
88+
enabled=True,
89+
rules_config={"risk_score": {"is_active": True, "min_risk_score": 8.0}},
90+
)
91+
make_product_triage_ruleset(self.product, ruleset=ruleset)
92+
evaluate_ruleset(ruleset, self.product)
93+
self.assertTrue(TriageRecord.objects.exists())
94+
95+
url = ruleset.get_admin_url()
96+
data = {
97+
"name": ruleset.name,
98+
"precedence": ruleset.precedence,
99+
"recommended_action": TriageAction.UPGRADE,
100+
# "enabled" omitted: disables the ruleset.
101+
**RULE_CONFIG_DATA,
102+
}
103+
104+
response = self.client.post(url, data)
105+
106+
self.assertEqual(302, response.status_code)
107+
self.assertFalse(TriageRecord.objects.exists())
108+
109+
def test_disabling_a_ruleset_keeps_records_that_have_an_open_request(self):
110+
vulnerability = make_vulnerability(self.dataspace, affecting=self.package, risk_score=9.0)
111+
requester = create_user("requester", self.dataspace)
112+
request_template = RequestTemplate.objects.create(
113+
name="Template",
114+
description="Header",
115+
dataspace=self.dataspace,
116+
content_type=ContentType.objects.get_for_model(Product),
117+
created_by=requester,
118+
)
119+
ruleset = make_triage_ruleset(
120+
self.dataspace,
121+
recommended_action=TriageAction.UPGRADE,
122+
enabled=True,
123+
request_template=request_template,
124+
rules_config={"risk_score": {"is_active": True, "min_risk_score": 8.0}},
125+
)
126+
make_product_triage_ruleset(self.product, ruleset=ruleset)
127+
evaluate_ruleset(ruleset, self.product)
128+
record = TriageRecord.objects.get()
129+
self.assertIsNotNone(record.request)
130+
131+
url = ruleset.get_admin_url()
132+
data = {
133+
"name": ruleset.name,
134+
"precedence": ruleset.precedence,
135+
"recommended_action": TriageAction.UPGRADE,
136+
# "enabled" omitted: disables the ruleset.
137+
**RULE_CONFIG_DATA,
138+
}
139+
140+
response = self.client.post(url, data)
141+
142+
self.assertEqual(302, response.status_code)
143+
record.refresh_from_db()
144+
self.assertEqual(vulnerability, record.vulnerability)
145+
146+
def test_disabling_then_reenabling_a_ruleset_reuses_the_existing_request(self):
147+
# Regression: disabling then re-enabling a ruleset used to reopen a new Request
148+
# instead of reconnecting to the one already tracking this vulnerability.
149+
make_vulnerability(self.dataspace, affecting=self.package, risk_score=9.0)
150+
requester = create_user("requester", self.dataspace)
151+
request_template = RequestTemplate.objects.create(
152+
name="Template",
153+
description="Header",
154+
dataspace=self.dataspace,
155+
content_type=ContentType.objects.get_for_model(Product),
156+
created_by=requester,
157+
)
158+
ruleset = make_triage_ruleset(
159+
self.dataspace,
160+
recommended_action=TriageAction.UPGRADE,
161+
enabled=True,
162+
request_template=request_template,
163+
rules_config={"risk_score": {"is_active": True, "min_risk_score": 8.0}},
164+
)
165+
make_product_triage_ruleset(self.product, ruleset=ruleset)
166+
evaluate_ruleset(ruleset, self.product)
167+
original_request = TriageRecord.objects.get().request
168+
self.assertIsNotNone(original_request)
169+
170+
url = ruleset.get_admin_url()
171+
base_data = {
172+
"name": ruleset.name,
173+
"precedence": ruleset.precedence,
174+
"recommended_action": TriageAction.UPGRADE,
175+
**RULE_CONFIG_DATA,
176+
}
177+
self.client.post(url, base_data) # Disable.
178+
self.client.post(url, {**base_data, "enabled": "on"}) # Re-enable.
179+
180+
self.assertEqual(1, ruleset.triage_records.count())
181+
self.assertEqual(original_request, TriageRecord.objects.get().request)

0 commit comments

Comments
 (0)