Coverage for product_risk_suite/product/tests.py: 100%
546 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 12:45 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 12:45 +0000
1from django.contrib import admin
2from django.test import TestCase, RequestFactory
3from collections import OrderedDict
4from unittest.mock import MagicMock
6from guardian.shortcuts import assign_perm
8from .models import *
9from .admin import ProductRiskEntryAdmin
10from risk_assessment.models import *
11from threat_model.shared_models import ThreatModelConnectionName
13# Create your tests here.
14class ProductRiskEntryTest(TestCase):
15 custom_id = "pre-test-001"
16 risk_title = "PRE test risk"
17 def setUp(self):
18 os = Asset.objects.create(name="OS")
19 Stride.objects.create(id=1, name="S")
20 usb = Origin.objects.create(name="USB")
21 lc = LifeCycle.objects.create(name="in use")
22 risk = Risk(id=0, custom_id=self.custom_id, asset=os, origin=usb, life_cycle=lc, title=self.risk_title, description="more info")
23 risk.save()
24 s = Stride.objects.get(id=1)
25 risk.stride.set([s])
26 risk.save()
28 self.risk_rating_init = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=5)
29 self.init_risk = 10
30 self.risk_rating_miti = RiskRating.objects.create(likelihood_of_occurrence=1, severity_of_impact=5)
31 self.mit_risk = 5
33 self.secReq1 = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.1", description_long="details 1.1", slug="cra-1.1")
34 self.secReq2 = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.2", description_long="details 1.2", slug="cra-1.2")
35 rm = RiskMitigation(id=0, mitigation="We fixed it", rational="We fixed it good")
36 rm.save()
37 rm.security_requirements.set([self.secReq1])
38 rm.save()
39 self.rm = rm
41 self.u = User.objects.create(username="testuser")
42 self.s1 = Status.objects.create(id=1, status="On going")
43 self.ev = Evidence.objects.create(id=1, due_date="2026-04-23", responsible=self.u, status=self.s1, evidence="[REQ-001] the evidence req", evidence_link="https://example.com/requirement-001")
45 def test_product_risk_entry_str(self):
46 risk = Risk.objects.get(id=0)
47 acc = ProductRiskEntry.objects.create(id=0, risk=risk, risk_accepted = True, risk_rating_initial=self.risk_rating_init)
48 mit_evidence = ProductRiskEntry.objects.create(id=1, risk=risk, risk_accepted = False, risk_rating_initial=self.risk_rating_init, risk_mitigation=self.rm, risk_rating_after_mitigation=self.risk_rating_miti)
49 mit_evidence.evidences.set([self.ev])
50 mit_no_evidence = ProductRiskEntry.objects.create(id=2, risk=risk, risk_accepted = False, risk_rating_initial=self.risk_rating_init, risk_mitigation=self.rm, risk_rating_after_mitigation=self.risk_rating_miti)
51 unmit = ProductRiskEntry.objects.create(id=3, risk=risk, risk_accepted = False, risk_rating_initial=self.risk_rating_init)
53 self.assertEqual(str(acc), f"Accepted Risk: {self.custom_id} - {self.risk_title}")
54 self.assertEqual(str(mit_evidence), f"Mitigated Risk: {self.custom_id} - {self.risk_title} Risk: {self.mit_risk}")
55 self.assertEqual(str(mit_no_evidence), f"Mitigated Risk: {self.custom_id} - {self.risk_title} Risk: {self.mit_risk}")
56 self.assertEqual(str(unmit), f"*Un*mitigated Risk: {self.custom_id} - {self.risk_title} Open risk: {self.init_risk}")
58 def test_list_svg_ids_empty_when_none_assigned(self):
59 risk = Risk.objects.get(id=0)
60 entry = ProductRiskEntry.objects.create(risk=risk, risk_accepted=False, risk_rating_initial=self.risk_rating_init)
61 self.assertEqual(entry.list_svg_ids, [])
63 def test_list_svg_ids_returns_tech_names_of_assigned_connections(self):
64 product = Product.objects.create(title="SVG Test Product", description="d", slug="svg-test-product")
65 cn1 = ThreatModelConnectionName.objects.create(product=product, tech_name="cell-001")
66 cn2 = ThreatModelConnectionName.objects.create(product=product, tech_name="cell-002")
67 risk = Risk.objects.get(id=0)
68 entry = ProductRiskEntry.objects.create(risk=risk, risk_accepted=False, risk_rating_initial=self.risk_rating_init)
69 entry.svg_id.set([cn1, cn2])
70 self.assertCountEqual(entry.list_svg_ids, ["cell-001", "cell-002"])
72class ProductRiskAnalysisTest(TestCase):
73 custom_id = "pra-test-002"
74 risk_title = "PRA test risk"
76 def setUp(self):
77 os = Asset.objects.create(name="OS")
78 Stride.objects.create(id=1, name="S")
79 usb = Origin.objects.create(name="USB")
80 lc = LifeCycle.objects.create(name="in use")
81 risk = Risk(id=0, custom_id=self.custom_id, asset=os, origin=usb, life_cycle=lc, title=self.risk_title, description="more info")
82 risk.save()
83 s = Stride.objects.get(id=1)
84 risk.stride.set([s])
85 risk.save()
87 self.risk_rating_init = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=5)
88 self.init_risk = 10
89 self.risk_rating_miti = RiskRating.objects.create(likelihood_of_occurrence=1, severity_of_impact=5)
90 self.mit_risk = 5
92 self.secReq1 = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.1", description_long="details 1.1", slug="cra-1.1")
93 self.secReq2 = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.2", description_long="details 1.2", slug="cra-1.2")
94 rm = RiskMitigation(id=0, mitigation="We fixed it", rational="We fixed it good")
95 rm.save()
96 rm.security_requirements.set([self.secReq1])
97 rm.save()
98 self.rm = rm
100 self.u = User.objects.create(username="testuser")
101 self.s1 = Status.objects.create(id=1, status="On going")
102 self.ev = Evidence.objects.create(id=1, due_date="2026-04-23", responsible=self.u, status=self.s1, evidence="[REQ-001] the evidence req", evidence_link="https://example.com/requirement-001")
104 acc = ProductRiskEntry.objects.create(id=0, risk=risk, risk_accepted = True, risk_rating_initial=self.risk_rating_init)
105 mit_evidence = ProductRiskEntry.objects.create(id=1, risk=risk, risk_accepted = False, risk_rating_initial=self.risk_rating_init, risk_mitigation=self.rm, risk_rating_after_mitigation=self.risk_rating_miti)
106 mit_evidence.evidences.set([self.ev])
107 mit_evidence.save()
108 mit_no_evidence = ProductRiskEntry.objects.create(id=2, risk=risk, risk_accepted = False, risk_rating_initial=self.risk_rating_init, risk_mitigation=self.rm, risk_rating_after_mitigation=self.risk_rating_miti)
109 unmit = ProductRiskEntry.objects.create(id=3, risk=risk, risk_accepted = False, risk_rating_initial=self.risk_rating_init)
111 def test_str(self):
112 pra = ProductRiskAnalysis.objects.create(id=0, name="Our first analysis", slug="our-first-analysis")
113 pra.risk_entries.set([ProductRiskEntry.objects.get(id=0), ProductRiskEntry.objects.get(id=1), ProductRiskEntry.objects.get(id=2), ProductRiskEntry.objects.get(id=3)])
114 pra.save()
116 self.assertEqual(str(ProductRiskAnalysis.objects.get(id=0)), "Our first analysis")
118 def test_get_risk_summary(self):
119 pra = ProductRiskAnalysis.objects.create(id=0, name="Our first analysis", slug="our-first-analysis")
120 pra.risk_entries.set([ProductRiskEntry.objects.get(id=0), ProductRiskEntry.objects.get(id=1), ProductRiskEntry.objects.get(id=2), ProductRiskEntry.objects.get(id=3)])
121 pra.save()
123 risk_summary = ProductRiskAnalysis.get_risk_summary(pra.risk_entries)
124 self.assertEqual(risk_summary["risk_severities_before_mitigation"][Risk5x5.LOW.name], 0)
125 self.assertEqual(risk_summary["risk_severities_before_mitigation"][Risk5x5.MID.name], 4)
126 self.assertEqual(risk_summary["risk_severities_before_mitigation"][Risk5x5.HIGH.name], 0)
128 self.assertEqual(risk_summary["risk_severities_after_mitigation"][Risk5x5.LOW.name], 2)
129 self.assertEqual(risk_summary["risk_severities_after_mitigation"][Risk5x5.MID.name], 2)
130 self.assertEqual(risk_summary["risk_severities_after_mitigation"][Risk5x5.HIGH.name], 0)
132 self.assertEqual(risk_summary["risk_severity_colors"][Risk5x5.LOW.name], "#d0e6dc")
133 self.assertEqual(risk_summary["risk_severity_colors"][Risk5x5.MID.name], "#fff3cc")
134 self.assertEqual(risk_summary["risk_severity_colors"][Risk5x5.HIGH.name], "#f8d6d9")
136 self.assertEqual(risk_summary["n_risks"], 4)
137 self.assertEqual(len(risk_summary["unmitigated_risks"]), 2)
138 self.assertEqual(risk_summary["unmitigated_risks"][0], "pra-test-002")
139 self.assertEqual(risk_summary["unmitigated_risks"][1], "pra-test-002")
140 self.assertEqual(risk_summary["mitigation_stati"], {
141 "open": 0,
142 "on_going": 1,
143 "finished": 0,
144 "rejected": 0,
145 })
147 self.assertEqual(risk_summary["covered_sec_reqs"], OrderedDict(
148 {"CRA: Part 1.1": "cra-1.1"},
149 ))
151class ProductTest(TestCase):
152 def setUp(self):
153 Product.objects.create(id=0, title="My test project", description="A real cool test project")
155 def test_str(self):
156 p = Product.objects.get(id=0)
157 self.assertEqual(str(p), "My test project")
160class ProductRiskEntryAdminTest(TestCase):
161 custom_id = "admin-test-001"
162 risk_title = "Admin test risk"
164 def setUp(self):
165 asset = Asset.objects.create(name="OS")
166 Stride.objects.create(id=1, name="S")
167 usb = Origin.objects.create(name="USB")
168 lc = LifeCycle.objects.create(name="in use")
169 risk = Risk(custom_id=self.custom_id, asset=asset, origin=usb, life_cycle=lc, title=self.risk_title, description="info")
170 risk.save()
171 risk.stride.set([Stride.objects.get(id=1)])
172 risk.save()
173 self.risk = risk
175 self.rr_init = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=5)
176 self.rr_miti = RiskRating.objects.create(likelihood_of_occurrence=1, severity_of_impact=5)
178 rm = RiskMitigation(mitigation="We fixed it", rational="We fixed it good")
179 rm.save()
180 self.rm = rm
182 u = User.objects.create(username="testuser")
183 s1 = Status.objects.create(id=1, status="On going")
184 self.ev = Evidence.objects.create(due_date="2026-04-23", responsible=u, status=s1, evidence="[REQ-001]", evidence_link="https://example.com/req")
186 self.entry_unmit = ProductRiskEntry.objects.create(risk=risk, risk_accepted=False, risk_rating_initial=self.rr_init)
187 self.entry_mit = ProductRiskEntry.objects.create(risk=risk, risk_accepted=False, risk_rating_initial=self.rr_init, risk_mitigation=rm, risk_rating_after_mitigation=self.rr_miti)
188 self.entry_acc = ProductRiskEntry.objects.create(risk=risk, risk_accepted=True, risk_rating_initial=self.rr_init)
190 self.superuser = User.objects.create_superuser(username="admin", password="admin123")
191 self.client.force_login(self.superuser)
193 self.product = Product.objects.create(title="Test Product", description="desc", slug="test-product")
194 pra = ProductRiskAnalysis.objects.create(name="Test Analysis", slug="test-analysis")
195 pra.risk_entries.set([self.entry_unmit])
196 self.product.analyzes.set([pra])
198 self.cn1 = ThreatModelConnectionName.objects.create(product=self.product, tech_name="conn-a")
199 self.cn2 = ThreatModelConnectionName.objects.create(product=self.product, tech_name="conn-b")
200 other_product = Product.objects.create(title="Other Product", description="d", slug="other-product")
201 self.cn_other = ThreatModelConnectionName.objects.create(product=other_product, tech_name="conn-other")
203 def _admin(self):
204 return ProductRiskEntryAdmin(ProductRiskEntry, admin.site)
206 # --- HTTP view smoke tests ---
208 def test_changelist_view_returns_200(self):
209 response = self.client.get('/admin/product/productriskentry/')
210 self.assertEqual(response.status_code, 200)
212 def test_add_view_returns_200(self):
213 response = self.client.get('/admin/product/productriskentry/add/')
214 self.assertEqual(response.status_code, 200)
216 def test_change_view_returns_200(self):
217 response = self.client.get(f'/admin/product/productriskentry/{self.entry_unmit.pk}/change/')
218 self.assertEqual(response.status_code, 200)
220 def test_add_view_help_text_for_new_entry(self):
221 response = self.client.get('/admin/product/productriskentry/add/')
222 self.assertContains(response, "Here all available IDs are listed")
224 def test_change_view_help_text_for_existing_entry(self):
225 response = self.client.get(f'/admin/product/productriskentry/{self.entry_unmit.pk}/change/')
226 self.assertContains(response, "Select one or more threat model connections")
228 # --- Custom list_display methods ---
230 def test_display_risk_id(self):
231 self.assertEqual(self._admin().risk_Id(self.entry_unmit), self.custom_id)
233 def test_display_risk_title(self):
234 self.assertEqual(self._admin().risk_title(self.entry_unmit), self.risk_title)
236 def test_display_risk_asset(self):
237 self.assertEqual(str(self._admin().risk_asset(self.entry_unmit)), "OS")
239 def test_display_risk_origin(self):
240 self.assertEqual(str(self._admin().risk_origin(self.entry_unmit)), "USB")
242 def test_display_risk_stride(self):
243 self.assertEqual(self._admin().risk_stride(self.entry_unmit), "S")
245 def test_display_risk_initial(self):
246 self.assertEqual(self._admin().risk_initial(self.entry_unmit), 10)
248 def test_display_risk_accepted_false(self):
249 self.assertFalse(self._admin().risk_accepted(self.entry_unmit))
251 def test_display_risk_accepted_true(self):
252 self.assertTrue(self._admin().risk_accepted(self.entry_acc))
254 def test_display_risk_mitigated_false(self):
255 self.assertFalse(self._admin().risk_mitigated(self.entry_unmit))
257 def test_display_risk_mitigated_true(self):
258 self.assertTrue(self._admin().risk_mitigated(self.entry_mit))
260 def test_display_risk_after_mitigation_none_when_unmitigated(self):
261 self.assertIsNone(self._admin().risk_after_mitigation(self.entry_unmit))
263 def test_display_risk_after_mitigation_value_when_mitigated(self):
264 self.assertEqual(self._admin().risk_after_mitigation(self.entry_mit), 5)
266 def test_display_tm_linked_false_without_svg_id(self):
267 self.assertFalse(self._admin().tm_linked(self.entry_unmit))
269 def test_display_tm_linked_true_with_svg_id(self):
270 self.entry_unmit.svg_id.set([self.cn1])
271 self.assertTrue(self._admin().tm_linked(self.entry_unmit))
273 # --- formfield_for_manytomany: svg_id queryset filtering ---
275 def test_svg_id_queryset_filtered_to_product_connection_names(self):
276 from threat_model.models import ThreatModel
277 tm = ThreatModel.objects.create(product=self.product, slug="test-tm", title="TM", svg='')
278 tm.connection_names.set([self.cn1, self.cn2])
280 request = RequestFactory().get(f'/admin/product/productriskentry/{self.entry_unmit.pk}/change/')
281 request.resolver_match = MagicMock()
282 request.resolver_match.kwargs = {'object_id': str(self.entry_unmit.pk)}
284 svg_id_field = ProductRiskEntry._meta.get_field('svg_id')
285 formfield = self._admin().formfield_for_manytomany(svg_id_field, request)
287 qs = list(formfield.queryset)
288 self.assertIn(self.cn1, qs)
289 self.assertIn(self.cn2, qs)
290 self.assertNotIn(self.cn_other, qs)
292 def test_svg_id_queryset_unfiltered_for_new_entry(self):
293 request = RequestFactory().get('/admin/product/productriskentry/add/')
294 request.resolver_match = MagicMock()
295 request.resolver_match.kwargs = {}
297 svg_id_field = ProductRiskEntry._meta.get_field('svg_id')
298 formfield = self._admin().formfield_for_manytomany(svg_id_field, request)
300 qs = list(formfield.queryset)
301 self.assertIn(self.cn1, qs)
302 self.assertIn(self.cn2, qs)
303 self.assertIn(self.cn_other, qs)
306class ProductAnalysisViewTest(TestCase):
307 URL = "/product/{product_slug}/{analysis_slug}/"
309 def setUp(self):
310 asset = Asset.objects.create(name="OS")
311 Stride.objects.create(id=1, name="S")
312 usb = Origin.objects.create(name="USB")
313 lc = LifeCycle.objects.create(name="in use")
315 def make_risk(custom_id, title):
316 r = Risk(custom_id=custom_id, asset=asset, origin=usb, life_cycle=lc, title=title, description="d")
317 r.save()
318 r.stride.set([Stride.objects.get(id=1)])
319 r.save()
320 return r
322 rr_high = RiskRating.objects.create(likelihood_of_occurrence=5, severity_of_impact=5)
323 rr_low = RiskRating.objects.create(likelihood_of_occurrence=1, severity_of_impact=1)
325 self.entry_a = ProductRiskEntry.objects.create(risk=make_risk("ZZZ-002", "Risk Z"), risk_accepted=False, risk_rating_initial=rr_high)
326 self.entry_b = ProductRiskEntry.objects.create(risk=make_risk("AAA-001", "Risk A"), risk_accepted=True, risk_rating_initial=rr_low)
328 self.analysis = ProductRiskAnalysis.objects.create(name="Test Analysis", slug="test-analysis")
329 self.analysis.risk_entries.set([self.entry_a, self.entry_b])
331 self.product = Product.objects.create(title="Test Product", description="desc", slug="test-product")
332 self.product.analyzes.set([self.analysis])
334 self.viewer = User.objects.create_user(username="viewer", password="pw")
335 self.editor = User.objects.create_user(username="editor", password="pw")
336 assign_perm("product.view_product", self.viewer, self.product)
337 assign_perm("product.view_product", self.editor, self.product)
338 assign_perm("product.change_product", self.editor, self.product)
340 def _url(self):
341 return self.URL.format(product_slug=self.product.slug, analysis_slug=self.analysis.slug)
343 # --- Authentication and permissions ---
345 def test_unauthenticated_redirects_to_login(self):
346 response = self.client.get(self._url())
347 self.assertRedirects(response, f"/login?next={self._url()}", fetch_redirect_response=False)
349 def test_no_permission_returns_403(self):
350 no_perm_user = User.objects.create_user(username="noperm", password="pw")
351 self.client.force_login(no_perm_user)
352 self.assertEqual(self.client.get(self._url()).status_code, 403)
354 def test_with_view_permission_returns_200(self):
355 self.client.force_login(self.viewer)
356 self.assertEqual(self.client.get(self._url()).status_code, 200)
358 def test_nonexistent_product_returns_404(self):
359 self.client.force_login(self.viewer)
360 url = self.URL.format(product_slug="does-not-exist", analysis_slug=self.analysis.slug)
361 self.assertEqual(self.client.get(url).status_code, 404)
363 def test_nonexistent_analysis_returns_404(self):
364 self.client.force_login(self.viewer)
365 url = self.URL.format(product_slug=self.product.slug, analysis_slug="does-not-exist")
366 self.assertEqual(self.client.get(url).status_code, 404)
368 # --- Context variables ---
370 def test_context_contains_correct_product(self):
371 self.client.force_login(self.viewer)
372 response = self.client.get(self._url())
373 self.assertEqual(response.context["product"], self.product)
375 def test_context_contains_correct_analysis(self):
376 self.client.force_login(self.viewer)
377 response = self.client.get(self._url())
378 self.assertEqual(response.context["product_analysis"], self.analysis)
380 def test_context_entries_match_analysis(self):
381 self.client.force_login(self.viewer)
382 response = self.client.get(self._url())
383 entries = list(response.context["product_analysis_entries"])
384 self.assertIn(self.entry_a, entries)
385 self.assertIn(self.entry_b, entries)
386 self.assertEqual(len(entries), 2)
388 def test_context_entries_ordered_by_custom_id(self):
389 self.client.force_login(self.viewer)
390 response = self.client.get(self._url())
391 entries = list(response.context["product_analysis_entries"])
392 self.assertEqual(entries[0], self.entry_b) # AAA-001 before ZZZ-002
393 self.assertEqual(entries[1], self.entry_a)
395 def test_context_risk_summary_has_expected_keys(self):
396 self.client.force_login(self.viewer)
397 response = self.client.get(self._url())
398 summary = response.context["risk_summery"]
399 for key in ("risk_severities_before_mitigation", "risk_severities_after_mitigation", "n_risks", "unmitigated_risks", "mitigation_stati", "covered_sec_reqs"):
400 self.assertIn(key, summary)
402 def test_context_risk_summary_counts_entries(self):
403 self.client.force_login(self.viewer)
404 response = self.client.get(self._url())
405 self.assertEqual(response.context["risk_summery"]["n_risks"], 2)
407 # --- product_edit flag ---
409 def test_product_edit_false_for_viewer(self):
410 self.client.force_login(self.viewer)
411 response = self.client.get(self._url())
412 self.assertFalse(response.context["product_edit"])
414 def test_product_edit_true_for_editor(self):
415 self.client.force_login(self.editor)
416 response = self.client.get(self._url())
417 self.assertTrue(response.context["product_edit"])
420class ProductRiskAnalysisAdminTest(TestCase):
421 def setUp(self):
422 asset = Asset.objects.create(name="OS")
423 Stride.objects.create(id=1, name="S")
424 usb = Origin.objects.create(name="USB")
425 lc = LifeCycle.objects.create(name="in use")
426 risk = Risk(custom_id="pra-admin-001", asset=asset, origin=usb, life_cycle=lc, title="Admin PRA risk", description="d")
427 risk.save()
428 risk.stride.set([Stride.objects.get(id=1)])
429 risk.save()
431 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=3)
432 self.entry1 = ProductRiskEntry.objects.create(risk=risk, risk_accepted=False, risk_rating_initial=rr)
433 self.entry2 = ProductRiskEntry.objects.create(risk=risk, risk_accepted=True, risk_rating_initial=rr)
435 self.analysis = ProductRiskAnalysis.objects.create(name="Admin Test Analysis", slug="admin-test-analysis")
436 self.analysis.risk_entries.set([self.entry1, self.entry2])
438 self.superuser = User.objects.create_superuser(username="admin", password="admin123")
439 self.client.force_login(self.superuser)
441 def _admin(self):
442 from product.admin import ProductRiskAnalysisAdmin
443 return ProductRiskAnalysisAdmin(ProductRiskAnalysis, admin.site)
445 # --- HTTP smoke tests ---
447 def test_changelist_returns_200(self):
448 self.assertEqual(self.client.get('/admin/product/productriskanalysis/').status_code, 200)
450 def test_add_view_returns_200(self):
451 self.assertEqual(self.client.get('/admin/product/productriskanalysis/add/').status_code, 200)
453 def test_change_view_returns_200(self):
454 url = f'/admin/product/productriskanalysis/{self.analysis.pk}/change/'
455 self.assertEqual(self.client.get(url).status_code, 200)
457 # --- n_entries display method ---
459 def test_n_entries_with_two_entries(self):
460 self.assertEqual(self._admin().n_entries(self.analysis), 2)
462 def test_n_entries_with_empty_analysis(self):
463 empty = ProductRiskAnalysis.objects.create(name="Empty Analysis", slug="empty-analysis")
464 self.assertEqual(self._admin().n_entries(empty), 0)
466 def test_n_entries_reflected_in_changelist(self):
467 response = self.client.get('/admin/product/productriskanalysis/')
468 self.assertContains(response, "2")
471# ---------------------------------------------------------------------------
472# Template coverage tests
473# ---------------------------------------------------------------------------
475class ProductIndexTemplateTest(TestCase):
476 def setUp(self):
477 self.user = User.objects.create_user(username="viewer", password="pw")
478 self.product = Product.objects.create(title="Index Product", description="d", slug="index-product")
479 assign_perm("product.view_product", self.user, self.product)
481 def test_product_index_template_no_missing_vars(self):
482 self.client.force_login(self.user)
483 response = self.client.get('/product/')
484 self.assertEqual(response.status_code, 200)
485 self.assertNotContains(response, 'MISSING_VAR')
488class ProductViewTemplateTest(TestCase):
489 def setUp(self):
490 self.user = User.objects.create_user(username="viewer", password="pw")
491 self.product = Product.objects.create(title="View Product", description="A product", slug="view-product")
492 self.analysis = ProductRiskAnalysis.objects.create(name="View Analysis", slug="view-analysis")
493 self.product.analyzes.set([self.analysis])
494 assign_perm("product.view_product", self.user, self.product)
496 def test_product_template_no_missing_vars(self):
497 self.client.force_login(self.user)
498 response = self.client.get(f'/product/{self.product.slug}/')
499 self.assertEqual(response.status_code, 200)
500 self.assertNotContains(response, 'MISSING_VAR')
503class ProductAnalysisTemplateTest(TestCase):
504 """Template coverage test: every variable access in product_analysis.html executes."""
506 def setUp(self):
507 asset = Asset.objects.create(name="TmplAsset")
508 origin = Origin.objects.create(name="TmplOrigin")
509 lc = LifeCycle.objects.create(name="TmplLC")
510 stride = Stride.objects.create(name="S")
512 sug = SuggestedMitigationValidation.objects.create(
513 suggested_mitigation="Use firmware signature",
514 suggested_validation="<ol><li>Verify signature</li></ol>",
515 )
516 sec_req = SecurityRequirement.objects.create(
517 norm_short="CRA", norm_long="CRA long",
518 description_short="1.1", description_long="Details 1.1", slug="tmpl-cra-1-1",
519 )
520 risk = Risk(custom_id="TMPL-001", asset=asset, origin=origin, life_cycle=lc,
521 title="Template risk", description="Full coverage",
522 suggested_mitigation_validation=sug)
523 risk.save()
524 risk.stride.set([stride])
525 risk.save()
527 rr_init = RiskRating.objects.create(likelihood_of_occurrence=3, severity_of_impact=4)
528 rr_after = RiskRating.objects.create(likelihood_of_occurrence=1, severity_of_impact=2)
530 rm = RiskMitigation(mitigation="We patched it", rational="Security first", title="Patch")
531 rm.save()
532 rm.security_requirements.set([sec_req])
533 rm.save()
535 resp_user = User.objects.create_user(username="responsible", password="pw")
536 status = Status.objects.create(status="On going")
537 evidence = Evidence.objects.create(
538 due_date="2026-12-31", responsible=resp_user, status=status,
539 evidence="[CERT-001] Evidence text", evidence_link="https://example.com/cert",
540 )
542 self.product = Product.objects.create(title="Tmpl Product", description="Full desc", slug="tmpl-product")
543 cn = ThreatModelConnectionName.objects.create(product=self.product, tech_name="tmpl-cn-1")
544 entry = ProductRiskEntry.objects.create(
545 risk=risk, risk_accepted=False,
546 risk_rating_initial=rr_init,
547 risk_mitigation=rm,
548 risk_rating_after_mitigation=rr_after,
549 )
550 entry.evidences.set([evidence])
551 entry.svg_id.set([cn])
553 self.analysis = ProductRiskAnalysis.objects.create(name="Tmpl Analysis", slug="tmpl-analysis")
554 self.analysis.risk_entries.set([entry])
555 self.product.analyzes.set([self.analysis])
557 self.editor = User.objects.create_user(username="editor", password="pw")
558 assign_perm("product.view_product", self.editor, self.product)
559 assign_perm("product.change_product", self.editor, self.product)
561 def test_product_analysis_template_no_missing_vars(self):
562 self.client.force_login(self.editor)
563 response = self.client.get(f'/product/{self.product.slug}/{self.analysis.slug}/')
564 self.assertEqual(response.status_code, 200)
565 self.assertNotContains(response, 'MISSING_VAR')
568# ---------------------------------------------------------------------------
569# Risk versioning — shared setUp helper
570# ---------------------------------------------------------------------------
572def _make_versioned_risk(custom_id, version, is_current, asset, origin, lc, stride):
573 r = Risk(custom_id=custom_id, version=version, is_current=is_current,
574 asset=asset, origin=origin, life_cycle=lc,
575 title=f"{custom_id} v{version}", description="d")
576 r.save()
577 r.stride.set([stride])
578 r.save()
579 return r
582# ---------------------------------------------------------------------------
583# Risk versioning — admin action: upgrade_to_latest_risk_version
584# ---------------------------------------------------------------------------
586class UpgradeRiskVersionAdminActionTest(TestCase):
587 def setUp(self):
588 self.asset = Asset.objects.create(name="UpgAsset")
589 self.origin = Origin.objects.create(name="UpgOrigin")
590 self.lc = LifeCycle.objects.create(name="UpgLC")
591 self.stride = Stride.objects.create(name="S")
592 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=3)
594 self.old_risk = _make_versioned_risk("R-UPG-001", 1, False, self.asset, self.origin, self.lc, self.stride)
595 self.new_risk = _make_versioned_risk("R-UPG-001", 2, True, self.asset, self.origin, self.lc, self.stride)
596 self.current_risk = _make_versioned_risk("R-CUR-001", 1, True, self.asset, self.origin, self.lc, self.stride)
598 self.entry_outdated = ProductRiskEntry.objects.create(
599 risk=self.old_risk, risk_accepted=False, risk_rating_initial=rr)
600 self.entry_current = ProductRiskEntry.objects.create(
601 risk=self.current_risk, risk_accepted=False, risk_rating_initial=rr)
603 self.superuser = User.objects.create_superuser(username="admin", password="pw")
604 self.client.force_login(self.superuser)
606 def _post_action(self, pks):
607 return self.client.post('/admin/product/productriskentry/', {
608 'action': 'upgrade_to_latest_risk_version',
609 '_selected_action': pks,
610 })
612 def test_upgrades_entry_pointing_to_outdated_risk(self):
613 self._post_action([self.entry_outdated.pk])
614 self.entry_outdated.refresh_from_db()
615 self.assertEqual(self.entry_outdated.risk, self.new_risk)
617 def test_leaves_entry_unchanged_when_already_current(self):
618 self._post_action([self.entry_current.pk])
619 self.entry_current.refresh_from_db()
620 self.assertEqual(self.entry_current.risk, self.current_risk)
622 def test_handles_mix_of_outdated_and_current_entries(self):
623 self._post_action([self.entry_outdated.pk, self.entry_current.pk])
624 self.entry_outdated.refresh_from_db()
625 self.entry_current.refresh_from_db()
626 self.assertEqual(self.entry_outdated.risk, self.new_risk)
627 self.assertEqual(self.entry_current.risk, self.current_risk)
630# ---------------------------------------------------------------------------
631# Risk versioning — admin display: risk_outdated column
632# ---------------------------------------------------------------------------
634class RiskOutdatedAdminDisplayTest(TestCase):
635 def setUp(self):
636 asset = Asset.objects.create(name="DispAsset")
637 origin = Origin.objects.create(name="DispOrigin")
638 lc = LifeCycle.objects.create(name="DispLC")
639 stride = Stride.objects.create(name="S")
640 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=3)
642 old_risk = _make_versioned_risk("R-DISP-001", 1, False, asset, origin, lc, stride)
643 current_risk = _make_versioned_risk("R-DISP-001", 2, True, asset, origin, lc, stride)
645 self.entry_outdated = ProductRiskEntry.objects.create(
646 risk=old_risk, risk_accepted=False, risk_rating_initial=rr)
647 self.entry_current = ProductRiskEntry.objects.create(
648 risk=current_risk, risk_accepted=False, risk_rating_initial=rr)
650 def _admin(self):
651 from product.admin import ProductRiskEntryAdmin
652 return ProductRiskEntryAdmin(ProductRiskEntry, admin.site)
654 def test_risk_outdated_false_when_not_current(self):
655 self.assertFalse(self._admin().risk_outdated(self.entry_outdated))
657 def test_risk_outdated_true_when_current(self):
658 self.assertTrue(self._admin().risk_outdated(self.entry_current))
661# ---------------------------------------------------------------------------
662# Risk versioning — template: outdated icon visibility
663# ---------------------------------------------------------------------------
665class RiskOutdatedIconTemplateTest(TestCase):
666 def setUp(self):
667 asset = Asset.objects.create(name="IconAsset")
668 origin = Origin.objects.create(name="IconOrigin")
669 lc = LifeCycle.objects.create(name="IconLC")
670 stride = Stride.objects.create(name="S")
671 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=3)
673 old_risk = _make_versioned_risk("R-ICON-001", 1, False, asset, origin, lc, stride)
674 _new_risk = _make_versioned_risk("R-ICON-001", 2, True, asset, origin, lc, stride)
675 current_risk = _make_versioned_risk("R-CUR-ICON", 1, True, asset, origin, lc, stride)
677 entry_outdated = ProductRiskEntry.objects.create(
678 risk=old_risk, risk_accepted=False, risk_rating_initial=rr)
679 self.entry_current = ProductRiskEntry.objects.create(
680 risk=current_risk, risk_accepted=False, risk_rating_initial=rr)
682 self.analysis_mixed = ProductRiskAnalysis.objects.create(
683 name="Mixed Analysis", slug="mixed-analysis")
684 self.analysis_mixed.risk_entries.set([entry_outdated, self.entry_current])
686 self.analysis_current_only = ProductRiskAnalysis.objects.create(
687 name="Current Only", slug="current-only")
688 self.analysis_current_only.risk_entries.set([self.entry_current])
690 self.product = Product.objects.create(
691 title="Icon Product", description="d", slug="icon-product")
692 self.product.analyzes.set([self.analysis_mixed, self.analysis_current_only])
694 self.user = User.objects.create_user(username="viewer", password="pw")
695 assign_perm("product.view_product", self.user, self.product)
696 self.client.force_login(self.user)
698 def test_outdated_icon_shown_when_entry_has_outdated_risk(self):
699 response = self.client.get(f'/product/{self.product.slug}/mixed-analysis/')
700 self.assertContains(response, 'fa-circle-exclamation')
702 def test_outdated_icon_is_a_link_to_diff_view(self):
703 response = self.client.get(f'/product/{self.product.slug}/mixed-analysis/')
704 self.assertContains(response, 'risk-diff/')
706 def test_outdated_icon_absent_when_all_risks_are_current(self):
707 response = self.client.get(f'/product/{self.product.slug}/current-only/')
708 self.assertNotContains(response, 'fa-circle-exclamation')
711# ---------------------------------------------------------------------------
712# Risk versioning — risk diff view
713# ---------------------------------------------------------------------------
715class RiskDiffViewTest(TestCase):
716 def setUp(self):
717 asset = Asset.objects.create(name="DiffAsset")
718 origin = Origin.objects.create(name="DiffOrigin")
719 lc = LifeCycle.objects.create(name="DiffLC")
720 stride = Stride.objects.create(name="S")
721 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=3)
723 self.old_risk = _make_versioned_risk("R-DIFF-001", 1, False, asset, origin, lc, stride)
724 self.new_risk = _make_versioned_risk("R-DIFF-001", 2, True, asset, origin, lc, stride)
726 self.old_risk.title = "Old title"
727 self.old_risk.save()
728 self.new_risk.title = "New title"
729 self.new_risk.save()
731 self.current_risk = _make_versioned_risk("R-CUR-DIFF", 1, True, asset, origin, lc, stride)
733 self.entry_outdated = ProductRiskEntry.objects.create(
734 risk=self.old_risk, risk_accepted=False, risk_rating_initial=rr)
735 self.entry_current = ProductRiskEntry.objects.create(
736 risk=self.current_risk, risk_accepted=False, risk_rating_initial=rr)
738 self.product = Product.objects.create(
739 title="Diff Product", description="d", slug="diff-product")
740 analysis = ProductRiskAnalysis.objects.create(name="Diff Analysis", slug="diff-analysis")
741 analysis.risk_entries.set([self.entry_outdated, self.entry_current])
742 self.product.analyzes.set([analysis])
744 self.viewer = User.objects.create_user(username="viewer", password="pw")
745 self.noperm = User.objects.create_user(username="noperm", password="pw")
746 assign_perm("product.view_product", self.viewer, self.product)
748 def _url(self, entry=None):
749 entry = entry or self.entry_outdated
750 return f'/product/{self.product.slug}/risk-diff/{entry.pk}/'
752 def test_unauthenticated_redirects_to_login(self):
753 response = self.client.get(self._url())
754 self.assertRedirects(response, f'/login?next={self._url()}',
755 fetch_redirect_response=False)
757 def test_no_product_permission_returns_403(self):
758 self.client.force_login(self.noperm)
759 self.assertEqual(self.client.get(self._url()).status_code, 403)
761 def test_unknown_entry_returns_404(self):
762 self.client.force_login(self.viewer)
763 self.assertEqual(
764 self.client.get(f'/product/{self.product.slug}/risk-diff/99999/').status_code,
765 404,
766 )
768 def test_authorized_user_gets_200(self):
769 self.client.force_login(self.viewer)
770 self.assertEqual(self.client.get(self._url()).status_code, 200)
772 def test_shows_both_version_numbers(self):
773 self.client.force_login(self.viewer)
774 response = self.client.get(self._url())
775 self.assertContains(response, f'v{self.old_risk.version}')
776 self.assertContains(response, f'v{self.new_risk.version}')
778 def test_shows_old_and_new_title(self):
779 self.client.force_login(self.viewer)
780 response = self.client.get(self._url())
781 self.assertContains(response, 'Old title')
782 self.assertContains(response, 'New title')
784 def test_changed_field_marker_present(self):
785 self.client.force_login(self.viewer)
786 response = self.client.get(self._url())
787 self.assertContains(response, 'class="diff-changed"')
789 def test_entry_already_current_shows_no_diff_message(self):
790 self.client.force_login(self.viewer)
791 response = self.client.get(self._url(self.entry_current))
792 self.assertNotContains(response, 'class="diff-changed"')
793 self.assertContains(response, 'latest version')