Skip to content

Commit

Permalink
Refine the code and add unit tests #95
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez committed Aug 27, 2024
1 parent 92a465b commit 6ee1976
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 75 deletions.
10 changes: 5 additions & 5 deletions component_catalog/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ class VulnerabilityFilterSet(DataspacedFilterSet):
sort = DefaultOrderingFilter(
label=_("Sort"),
fields=[
"highest_score",
"lowest_score",
"max_score",
"min_score",
"vulnerability_id",
"affected_products_count",
"affected_packages_count",
Expand All @@ -316,7 +316,7 @@ class VulnerabilityFilterSet(DataspacedFilterSet):
],
widget=SortDropDownWidget,
)
highest_score = django_filters.ChoiceFilter(
max_score = django_filters.ChoiceFilter(
choices=SCORE_CHOICES,
method="filter_by_score_range",
label="Score Range",
Expand All @@ -331,10 +331,10 @@ class Meta:

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["highest_score"].extra["widget"] = DropDownRightWidget()
self.filters["max_score"].extra["widget"] = DropDownRightWidget()

def filter_by_score_range(self, queryset, name, value):
if value in vulnerability_score_ranges:
low, high = vulnerability_score_ranges[value]
return queryset.filter(highest_score__gte=low, highest_score__lte=high)
return queryset.filter(max_score__gte=low, max_score__lte=high)
return queryset
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.6 on 2024-08-27 13:04
# Generated by Django 5.0.6 on 2024-08-27 13:15

from django.db import migrations, models

Expand All @@ -17,12 +17,12 @@ class Migration(migrations.Migration):
),
migrations.AddField(
model_name='vulnerability',
name='highest_score',
field=models.FloatField(blank=True, help_text='The highest score of the range.', null=True),
name='max_score',
field=models.FloatField(blank=True, help_text='The maximum score of the range.', null=True),
),
migrations.AddField(
model_name='vulnerability',
name='lowest_score',
field=models.FloatField(blank=True, help_text='The lowest score of the range.', null=True),
name='min_score',
field=models.FloatField(blank=True, help_text='The minimum score of the range.', null=True),
),
]
33 changes: 16 additions & 17 deletions component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2626,15 +2626,15 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel):
output_field=models.IntegerField(),
db_persist=True,
)
highest_score = models.FloatField(
min_score = models.FloatField(
null=True,
blank=True,
help_text=_("The highest score of the range."),
help_text=_("The minimum score of the range."),
)
lowest_score = models.FloatField(
max_score = models.FloatField(
null=True,
blank=True,
help_text=_("The lowest score of the range."),
help_text=_("The maximum score of the range."),
)

objects = DataspacedManager.from_queryset(VulnerabilityQuerySet)()
Expand Down Expand Up @@ -2678,8 +2678,8 @@ def add_affected_components(self, components):
@staticmethod
def range_to_values(self, range_str):
try:
min_str, max_str = range_str.split("-")
return float(min_str.strip()), float(max_str.strip())
min_score, max_score = range_str.split("-")
return float(min_score.strip()), float(max_score.strip())
except Exception:
return

Expand All @@ -2688,16 +2688,15 @@ def create_from_data(cls, dataspace, data, validate=False, affecting=None):
# severity_range_score = data.get("severity_range_score")
# if severity_range_score:
# min_score, max_score = self.range_to_values(severity_range_score)
# data["lowest_score"] = min_score
# data["highest_score"] = max_score
# data["min_score"] = min_score
# data["max_score"] = max_score

severities = [
score for reference in data.get("references") for score in reference.get("scores", [])
]
scores = cls.get_severity_scores(severities)
if scores:
data["lowest_score"] = min(scores)
data["highest_score"] = max(scores)
if scores := cls.get_severity_scores(severities):
data["min_score"] = min(scores)
data["max_score"] = max(scores)

instance = super().create_from_data(user=dataspace, data=data, validate=False)

Expand All @@ -2708,11 +2707,11 @@ def create_from_data(cls, dataspace, data, validate=False, affecting=None):

@property
def severity_score_range(self):
if not (self.lowest_score and self.highest_score):
if not (self.min_score and self.max_score):
return ""
if self.lowest_score == self.highest_score:
return str(self.highest_score)
return f"{self.lowest_score} - {self.highest_score}"
if self.min_score == self.max_score:
return str(self.max_score)
return f"{self.min_score} - {self.max_score}"

def get_severities(self):
return [score for reference in self.references for score in reference.get("scores", [])]
Expand Down Expand Up @@ -2751,7 +2750,7 @@ def get_severity_scores(severities):

return consolidated_scores

def get_highest_score(self):
def get_max_score(self):
severities = self.get_severities()
scores = self.get_severity_scores(severities)
if scores:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,15 @@
{% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
</td>
<td>
{% if vulnerability.lowest_score %}
{{ vulnerability.lowest_score }} -
{% if vulnerability.min_score %}
{{ vulnerability.min_score }} -
{% endif %}
{% if vulnerability.highest_score %}
{% if vulnerability.max_score %}
<strong>
{{ vulnerability.highest_score }}
{{ vulnerability.max_score }}
</strong>
{% endif %}
</td>
<!-- <td>-->
<!-- {% if vulnerability.priority %}-->
<!-- {{ vulnerability.priority }}-->
<!-- {% endif %}-->
<!-- </td>-->
<td>
{% if vulnerability.summary %}
{% if vulnerability.summary|length > 120 %}
Expand Down
10 changes: 2 additions & 8 deletions component_catalog/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,10 @@
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

import random
import string

from component_catalog.models import Component
from component_catalog.models import Package
from component_catalog.models import Vulnerability


def make_string(length):
return "".join(random.choices(string.ascii_letters, k=length))
from dje.tests import make_string


def make_package(dataspace, package_url=None, is_vulnerable=False, **data):
Expand Down Expand Up @@ -53,7 +47,7 @@ def make_component(dataspace, is_vulnerable=False, **data):
def make_vulnerability(dataspace, affecting=None, **data):
"""Create a vulnerability for test purposes."""
if "vulnerability_id" not in data:
data["vulnerability_id"] = f"VCID-0000-{random.randint(1, 9999):04}"
data["vulnerability_id"] = f"VCID-0000-{make_string(4)}"

vulnerability = Vulnerability.objects.create(
dataspace=dataspace,
Expand Down
47 changes: 46 additions & 1 deletion component_catalog/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from component_catalog.filters import ComponentFilterSet
from component_catalog.filters import PackageFilterSet
from component_catalog.filters import VulnerabilityFilterSet
from component_catalog.models import Component
from component_catalog.models import ComponentKeyword
from component_catalog.models import ComponentType
Expand Down Expand Up @@ -284,7 +285,7 @@ def test_component_filterset_sort_keeps_default_ordering_from_model(self):
)


class PackageFilterSearchTestCase(TestCase):
class PackageFilterSetTestCase(TestCase):
def sorted_results(self, qs):
return sorted([str(package) for package in qs])

Expand Down Expand Up @@ -409,3 +410,47 @@ def test_package_filterset_affected_by_filter(self):
data = {"affected_by": vulnerability1.vulnerability_id}
filterset = PackageFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [package1])


class VulnerabilityFilterSetTestCase(TestCase):
def setUp(self):
self.dataspace = Dataspace.objects.create(name="Reference")
self.vulnerability1 = make_vulnerability(self.dataspace, max_score=10.0)
self.vulnerability2 = make_vulnerability(
self.dataspace, max_score=5.5, aliases=["ALIAS-V2"]
)
self.vulnerability3 = make_vulnerability(self.dataspace, max_score=2.0)

def test_vulnerability_filterset_search(self):
data = {"q": self.vulnerability1.vulnerability_id}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [self.vulnerability1])

data = {"q": "ALIAS-V2"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [self.vulnerability2])

def test_vulnerability_filterset_sort(self):
data = {"sort": "max_score"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
expected = [self.vulnerability3, self.vulnerability2, self.vulnerability1]
self.assertQuerySetEqual(filterset.qs, expected)

data = {"sort": "-max_score"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
expected = [self.vulnerability1, self.vulnerability2, self.vulnerability3]
self.assertQuerySetEqual(filterset.qs, expected)

def test_vulnerability_filterset_max_score(self):
data = {"max_score": "critical"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [self.vulnerability1])
data = {"max_score": "high"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [])
data = {"max_score": "medium"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [self.vulnerability2])
data = {"max_score": "low"}
filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
self.assertQuerySetEqual(filterset.qs, [self.vulnerability3])
28 changes: 18 additions & 10 deletions component_catalog/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@
from license_library.models import LicenseChoice
from license_library.models import LicenseTag
from organization.models import Owner
from product_portfolio.models import Product
from product_portfolio.models import ProductComponent
from product_portfolio.models import ProductPackage
from product_portfolio.tests import make_product


class ComponentCatalogModelsTestCase(TestCase):
Expand Down Expand Up @@ -2490,21 +2488,16 @@ def test_package_create_save_set_usage_policy_from_license(self):
self.assertEqual(package_policy, package5.usage_policy)

def test_component_model_where_used_property(self):
product1 = Product.objects.create(name="P1", dataspace=self.dataspace)
ProductComponent.objects.create(
product=product1, component=self.component1, dataspace=self.dataspace
)

make_product(self.dataspace, inventory=[self.component1])
basic_user = create_user("basic_user", self.dataspace)
self.assertEqual("Product 0\n", self.component1.where_used(user=basic_user))

self.assertTrue(self.user.is_superuser)
self.assertEqual("Product 1\n", self.component1.where_used(user=self.user))

def test_package_model_where_used_property(self):
product1 = Product.objects.create(name="P1", dataspace=self.dataspace)
package1 = Package.objects.create(filename="package", dataspace=self.dataspace)
ProductPackage.objects.create(product=product1, package=package1, dataspace=self.dataspace)
make_product(self.dataspace, inventory=[package1])

basic_user = create_user("basic_user", self.dataspace)
self.assertEqual("Product 0\nComponent 0\n", package1.where_used(user=basic_user))
Expand Down Expand Up @@ -2655,3 +2648,18 @@ def test_vulnerability_model_create_from_data(self):
self.assertEqual(vulnerability_data["aliases"], vulnerability1.aliases)
self.assertEqual(vulnerability_data["references"], vulnerability1.references)
self.assertQuerySetEqual(vulnerability1.affected_packages.all(), [package1])

def test_vulnerability_model_queryset_count_methods(self):
package1 = make_package(self.dataspace)
package2 = make_package(self.dataspace)
vulnerablity1 = make_vulnerability(dataspace=self.dataspace)
vulnerablity1.add_affected([package1, package2])
make_product(self.dataspace, inventory=[package1, package2])

qs = (
Vulnerability.objects.scope(self.dataspace)
.with_affected_products_count()
.with_affected_packages_count()
)
self.assertEqual(2, qs[0].affected_packages_count)
self.assertEqual(1, qs[0].affected_products_count)
46 changes: 46 additions & 0 deletions component_catalog/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
from component_catalog.models import ComponentType
from component_catalog.models import Package
from component_catalog.models import Subcomponent
from component_catalog.models import Vulnerability
from component_catalog.tests import make_component
from component_catalog.tests import make_package
from component_catalog.tests import make_vulnerability
from component_catalog.views import ComponentAddView
from component_catalog.views import ComponentListView
Expand All @@ -48,6 +51,7 @@
from dejacode_toolkit.vulnerablecode import get_plain_purls
from dje.copier import copy_object
from dje.models import Dataspace
from dje.models import DataspaceConfiguration
from dje.models import ExternalReference
from dje.models import ExternalSource
from dje.models import History
Expand Down Expand Up @@ -4811,3 +4815,45 @@ def test_anonymous_user_cannot_access_reference_data(self):
self.assertEqual(404, self.client.get(self.d1c2.get_absolute_url()).status_code)
self.assertEqual(200, self.client.get(self.dmc1.get_absolute_url()).status_code)
self.assertEqual(200, self.client.get(self.dmc2.get_absolute_url()).status_code)


class VulnerabilityViewsTestCase(TestCase):
def setUp(self):
self.dataspace = Dataspace.objects.create(
name="Dataspace",
enable_vulnerablecodedb_access=True,
)
DataspaceConfiguration.objects.create(
dataspace=self.dataspace,
vulnerablecode_url="vulnerablecode_url/",
)
self.super_user = create_superuser("super_user", self.dataspace)

self.component1 = make_component(self.dataspace)
self.component2 = make_component(self.dataspace)
self.package1 = make_package(self.dataspace)
self.package2 = make_package(self.dataspace)
self.vulnerability_p1 = make_vulnerability(self.dataspace, affecting=self.component1)
self.vulnerability_c1 = make_vulnerability(self.dataspace, affecting=self.package1)
self.vulnerability1 = make_vulnerability(self.dataspace)

def test_vulnerability_list_view_num_queries(self):
self.client.login(username=self.super_user.username, password="secret")
with self.assertNumQueries(8):
response = self.client.get(reverse("component_catalog:vulnerability_list"))

vulnerability_count = Vulnerability.objects.count()
expected = f'<a class="nav-link disabled">{vulnerability_count} results</a>'
self.assertContains(response, expected, html=True)

def test_vulnerability_list_view_vulnerability_id_link(self):
self.client.login(username=self.super_user.username, password="secret")
response = self.client.get(reverse("component_catalog:vulnerability_list"))
expected = f"""
<a href="vulnerablecode_url/vulnerabilities/{self.vulnerability1.vulnerability_id}"
target="_blank">
{self.vulnerability1.vulnerability_id}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
"""
self.assertContains(response, expected, html=True)
Loading

0 comments on commit 6ee1976

Please sign in to comment.