|
| 1 | +""" |
| 2 | +Regression test for: Product Metrics shows 0 Closed Findings while the findings |
| 3 | +list displays them correctly. |
| 4 | +
|
| 5 | +Root cause: finding_queries() in dojo/product/views.py used end_date (derived from |
| 6 | +the latest finding discovery date, built as midnight 00:00:00) as the upper bound |
| 7 | +for mitigated__range. This made no semantic sense - a finding can be closed at any |
| 8 | +time after discovery, so using the discovery date as the cutoff for mitigated is |
| 9 | +incorrect. |
| 10 | +
|
| 11 | +Fix: replace end_date with timezone.now() as the upper bound so any finding closed |
| 12 | +up to the current moment is counted. |
| 13 | +""" |
| 14 | + |
| 15 | +import zoneinfo |
| 16 | +from datetime import date, datetime |
| 17 | + |
| 18 | +from django.test import RequestFactory |
| 19 | + |
| 20 | +from dojo.models import Engagement, Finding, Product, Product_Type, Test, Test_Type, User |
| 21 | +from dojo.product.views import finding_queries |
| 22 | + |
| 23 | +from .dojo_test_case import DojoTestCase |
| 24 | + |
| 25 | +UTC = zoneinfo.ZoneInfo("UTC") |
| 26 | + |
| 27 | + |
| 28 | +class ProductMetricsClosedCountTest(DojoTestCase): |
| 29 | + |
| 30 | + """Regression tests for closed finding counter in Product Metrics.""" |
| 31 | + |
| 32 | + def setUp(self): |
| 33 | + self.user = User.objects.create_superuser(username="admin_regression", password="test") # noqa: S106 |
| 34 | + self.product_type = Product_Type.objects.create(name="Regression PT") |
| 35 | + self.product = Product.objects.create( |
| 36 | + name="Regression Product", |
| 37 | + prod_type=self.product_type, |
| 38 | + description="Regression test product", |
| 39 | + ) |
| 40 | + self.engagement = Engagement.objects.create( |
| 41 | + name="Regression Eng", |
| 42 | + product=self.product, |
| 43 | + target_start=date(2024, 1, 1), |
| 44 | + target_end=date(2024, 12, 31), |
| 45 | + ) |
| 46 | + self.test_type, _ = Test_Type.objects.get_or_create(name="Manual") |
| 47 | + self.test = Test.objects.create( |
| 48 | + engagement=self.engagement, |
| 49 | + test_type=self.test_type, |
| 50 | + target_start=datetime(2024, 1, 1, tzinfo=UTC), |
| 51 | + target_end=datetime(2024, 12, 31, tzinfo=UTC), |
| 52 | + ) |
| 53 | + # date=7 corresponds to "Any date" in MetricsDateRangeFilter |
| 54 | + self.request = RequestFactory().get(f"/product/{self.product.id}/metrics", {"date": "7"}) |
| 55 | + self.request.user = self.user |
| 56 | + |
| 57 | + def _make_closed(self, title, discovery_date, mitigated_dt, severity="High"): |
| 58 | + return Finding.objects.create( |
| 59 | + title=title, |
| 60 | + test=self.test, |
| 61 | + severity=severity, |
| 62 | + active=False, |
| 63 | + is_mitigated=True, |
| 64 | + date=discovery_date, |
| 65 | + mitigated=mitigated_dt, |
| 66 | + reporter=self.user, |
| 67 | + verified=True, |
| 68 | + ) |
| 69 | + |
| 70 | + def test_closed_finding_same_day_after_midnight_is_counted(self): |
| 71 | + """ |
| 72 | + A finding discovered on day X and closed on day X at 10:00 must appear |
| 73 | + in the closed counter. Before the fix end_date was derived from the latest |
| 74 | + discovery date as midnight, so this finding was silently excluded. |
| 75 | + """ |
| 76 | + discovery = date(2024, 6, 15) |
| 77 | + # closed on the same day but well after midnight |
| 78 | + mitigated = datetime(2024, 6, 15, 10, 30, tzinfo=UTC) |
| 79 | + self._make_closed("Closed same-day after midnight", discovery, mitigated) |
| 80 | + |
| 81 | + filters = finding_queries(self.request, self.product) |
| 82 | + closed_ids = list(filters["closed"].values_list("id", flat=True)) |
| 83 | + self.assertEqual(len(closed_ids), 1, "Expected exactly 1 closed finding in metrics") |
| 84 | + |
| 85 | + def test_closed_finding_on_same_day_as_end_date_is_counted(self): |
| 86 | + """ |
| 87 | + A finding with discovery date in the past but closed recently must appear |
| 88 | + in the closed counter. Before the fix end_date was the latest discovery date |
| 89 | + (midnight), so findings closed after that date were excluded. |
| 90 | + """ |
| 91 | + today = date(2025, 6, 15) |
| 92 | + # open finding - sets end_date to today |
| 93 | + Finding.objects.create( |
| 94 | + title="Open Finding - sets end_date", |
| 95 | + test=self.test, severity="Low", |
| 96 | + active=True, is_mitigated=False, |
| 97 | + date=today, reporter=self.user, |
| 98 | + ) |
| 99 | + # closed finding - mitigated today at 10:00 (after midnight) |
| 100 | + closed = self._make_closed( |
| 101 | + "Closed same day as end_date", |
| 102 | + date(2024, 11, 27), |
| 103 | + datetime(2025, 6, 15, 10, 0, tzinfo=UTC), |
| 104 | + ) |
| 105 | + |
| 106 | + filters = finding_queries(self.request, self.product) |
| 107 | + closed_ids = list(filters["closed"].values_list("id", flat=True)) |
| 108 | + self.assertIn( |
| 109 | + closed.id, closed_ids, |
| 110 | + "Finding mitigated on the same day as end_date (but after midnight) must appear in closed metrics", |
| 111 | + ) |
| 112 | + |
| 113 | + def test_closed_count_matches_findings_list_count(self): |
| 114 | + """ |
| 115 | + All closed findings whose mitigated date falls within [start_date, end_date_eod] |
| 116 | + must be counted. Specifically, findings closed on the same day as end_date |
| 117 | + at any time (including 23:59) must appear. |
| 118 | + """ |
| 119 | + # All three findings have the same discovery date, so end_date = 2024-03-01 23:59:59 |
| 120 | + self._make_closed("F1 - closed at 00:01", date(2024, 3, 1), datetime(2024, 3, 1, 0, 1, tzinfo=UTC)) |
| 121 | + self._make_closed("F2 - closed at 14:00", date(2024, 3, 1), datetime(2024, 3, 1, 14, 0, tzinfo=UTC)) |
| 122 | + self._make_closed("F3 - closed at 23:58", date(2024, 3, 1), datetime(2024, 3, 1, 23, 58, tzinfo=UTC)) |
| 123 | + |
| 124 | + filters = finding_queries(self.request, self.product) |
| 125 | + metrics_count = filters["closed"].count() |
| 126 | + |
| 127 | + self.assertEqual( |
| 128 | + metrics_count, |
| 129 | + 3, |
| 130 | + f"All 3 findings closed on end_date must be counted, got {metrics_count}", |
| 131 | + ) |
0 commit comments