Coverage for product_risk_suite/risk_assessment/tests.py: 99%
716 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:44 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-16 14:44 +0000
1from django.contrib import admin
2from django.test import TestCase
3from django.db.utils import IntegrityError
4from django.core.exceptions import ValidationError
5from django.urls import reverse
7from unittest.mock import Mock, patch
9from .models import *
10from .admin import (
11 SecurityRequirementAdmin,
12 RiskAdmin, RiskRatingAdmin,
13 EvidenceAdmin, run_scraper_now,
14)
16class FreeMethodTests(TestCase):
17 def test_truncate(self):
18 td = [
19 {"str": "Hello World", "len": 20, "expected": "Hello World"},
20 {"str": "Hello World", "len": 5, "expected": "He..."},
21 {"str": None, "len": 20, "expected": ""},
22 ]
24 for t in td:
25 self.assertEqual(truncate(t["str"], t["len"]), t["expected"])
27class AssetTest(TestCase):
28 test_name = "Operating System"
30 def test_simple(self):
31 exp = Asset.objects.create(name=self.test_name)
33 act = Asset.objects.get(name=self.test_name)
34 self.assertEqual(act, exp)
36 def test_unique(self):
37 exp = Asset.objects.create(name=self.test_name)
38 unique_check_ok = False
39 try:
40 item2 = Asset.objects.create(name=self.test_name)
41 except IntegrityError:
42 unique_check_ok = True
44 self.assertTrue(unique_check_ok)
46 def test_str(self):
47 exp = Asset.objects.create(name=self.test_name)
49 self.assertEqual(str(exp), self.test_name)
51class LifeCycleTest(TestCase):
52 test_name = "in use"
54 def test_simple(self):
55 exp = LifeCycle.objects.create(name=self.test_name)
57 act = LifeCycle.objects.get(name=self.test_name)
58 self.assertEqual(act, exp)
60 def test_unique(self):
61 exp = LifeCycle.objects.create(name=self.test_name)
62 unique_check_ok = False
63 try:
64 item2 = LifeCycle.objects.create(name=self.test_name)
65 except IntegrityError:
66 unique_check_ok = True
68 self.assertTrue(unique_check_ok)
70 def test_str(self):
71 exp = LifeCycle.objects.create(name=self.test_name)
73 self.assertEqual(str(exp), self.test_name)
75class OriginTest(TestCase):
76 test_name = "USB"
78 def test_simple(self):
79 exp = Origin.objects.create(name=self.test_name)
81 act = Origin.objects.get(name=self.test_name)
82 self.assertEqual(act, exp)
84 def test_unique(self):
85 exp = Origin.objects.create(name=self.test_name)
86 unique_check_ok = False
87 try:
88 item2 = Origin.objects.create(name=self.test_name)
89 except IntegrityError:
90 unique_check_ok = True
92 self.assertTrue(unique_check_ok)
94 def test_str(self):
95 exp = Origin.objects.create(name=self.test_name)
97 self.assertEqual(str(exp), self.test_name)
99class SecurityRequirementTest(TestCase):
100 def test_simple(self):
101 exp = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.1", description_long="details 1.1")
103 act = SecurityRequirement.objects.get(norm_short="CRA", description_short="Part 1.1")
104 self.assertEqual(act, exp)
106 def test_unique(self):
107 exp = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.1", description_long="details 1.1")
108 unique_check_ok = False
109 try:
110 item2 = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.1", description_long="details 1.1")
111 except IntegrityError:
112 unique_check_ok = True
114 self.assertTrue(unique_check_ok)
116 def test_str(self):
117 exp = SecurityRequirement.objects.create(norm_short="CRA", norm_long="CRA long", description_short="Part 1.1", description_long="details 1.1")
119 self.assertEqual(str(exp), f"CRA: Part 1.1")
121class StrideTest(TestCase):
122 def test_simple(self):
123 exp = Stride.objects.create(name="S")
125 act = Stride.objects.get(name="S")
126 self.assertEqual(act, exp)
128 def test_no_create_unallowed(self):
129 allowed_to_add = True
130 try:
131 unallowed = Stride.objects.create(name="F")
132 except ValidationError:
133 allowed_to_add = False
135 self.assertFalse(allowed_to_add)
137 def test_unique(self):
138 exp = Stride.objects.create(name="S")
139 unique_check_ok = False
140 try:
141 item2 = Stride.objects.create(name="S")
142 except ValidationError:
143 unique_check_ok = True
144 except IntegrityError:
145 unique_check_ok = True
147 self.assertTrue(unique_check_ok)
149 def test_str(self):
150 exp = Stride.objects.create(name="S")
151 self.assertEqual(str(exp), f"S")
152 text = Stride.STRIDE_CHOICES["S"]
153 self.assertEqual(text, "Spoofing")
155class RiskTest(TestCase):
156 def setUp(self):
157 ass = Asset.objects.create(name="OS")
158 lc = LifeCycle.objects.create(name="in use")
159 orig = Origin.objects.create(name="USB")
160 spo = Stride.objects.create(name="S")
161 risk = Risk(id=1,
162 custom_id="Test-risk-01",
163 asset=ass, origin=orig, life_cycle=lc,
164 title="Risk title", description="Risk description")
165 risk.stride.set([spo])
166 risk.save()
168 def test_simple(self):
169 act = Risk.objects.get(id=1)
170 self.assertEqual(act.custom_id, "Test-risk-01")
171 self.assertEqual(act.asset.name, "OS")
172 self.assertEqual(act.origin.name, "USB")
173 self.assertEqual(act.life_cycle.name, "in use")
174 self.assertEqual(act.title, "Risk title")
175 self.assertEqual(act.description, "Risk description")
176 self.assertEqual(len(act.stride.all()), 1)
177 for i in act.stride.all():
178 self.assertEqual(i.name, "S")
180 def test_stride_str(self):
181 act = Risk.objects.get(id=1)
182 stride_str = act.stride_str
183 self.assertEqual(stride_str, "S")
185 def test_list_strid(self):
186 act = Risk.objects.get(id=1)
187 list_stride = act.list_stride
188 self.assertEqual(list_stride, ["Spoofing"])
190class SeverityNameTest(TestCase):
191 def test_human_str(self):
192 data = [
193 { "value": SeverityName.Negligible, "str": "Negligible"},
194 { "value": SeverityName.Minor, "str": "Minor"},
195 { "value": SeverityName.Moderate, "str": "Moderate"},
196 { "value": SeverityName.Major, "str": "Major"},
197 { "value": SeverityName.Critical, "str": "Critical"},
198 { "value": -1, "str": "-1"},
199 { "value": 6, "str": "6"},
200 ]
202 for tc in data:
203 t = SeverityName.to_human_str(tc["value"])
204 self.assertEqual(t, tc["str"])
206class LikelihoodNameTest(TestCase):
207 def test_human_str(self):
208 data = [
209 { "value": LikelihoodName.VeryLow, "str": "Very low"},
210 { "value": LikelihoodName.Low, "str": "Low"},
211 { "value": LikelihoodName.Medium, "str": "Medium"},
212 { "value": LikelihoodName.High, "str": "High"},
213 { "value": LikelihoodName.VeryHigh, "str": "Very high"},
214 ]
216 for tc in data:
217 t = LikelihoodName.to_human_str(tc["value"])
218 self.assertEqual(t, tc["str"])
220class RiskRatingTest(TestCase):
221 def setUp(self):
222 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=4)
224 def test_risk(self):
225 act = RiskRating.objects.get(id=1)
226 self.assertEqual(act.risk, 8)
228 def test_risk_level(self):
229 td = [
230 {"in": 20, "expected": Risk5x5.HIGH},
232 {"in": 16, "expected": Risk5x5.HIGH},
233 {"in": 15, "expected": Risk5x5.HIGH},
234 {"in": 14, "expected": Risk5x5.MID},
236 {"in": 8, "expected": Risk5x5.MID},
237 {"in": 7, "expected": Risk5x5.MID},
238 {"in": 6, "expected": Risk5x5.LOW},
240 {"in": 1, "expected": Risk5x5.LOW},
241 ]
243 for tr in td:
244 self.assertEqual(RiskRating.risk_level(tr["in"]), tr["expected"])
246 def test_risk_level_to_color(self):
247 # Risk5x5
248 td = [
249 {"in": 3, "expected": "danger"},
250 {"in": Risk5x5.HIGH, "expected": "danger"},
251 {"in": Risk5x5.MID, "expected": "warning"},
252 {"in": Risk5x5.LOW, "expected": "success"},
253 {"in": -1, "expected": "danger"},
254 ]
255 for tr in td:
256 self.assertEqual(RiskRating.risk_level_to_color(tr["in"]), tr["expected"])
258 def test_color_class(self):
259 td = [
260 {"in": 0, "expected": "danger"},
261 {"in": 1, "expected": "success"},
262 {"in": 2, "expected": "success"},
263 {"in": 3, "expected": "warning"},
264 {"in": 4, "expected": "warning"},
265 {"in": 5, "expected": "danger"},
266 {"in": 6, "expected": "danger"},
267 ]
268 rr = RiskRating()
269 for tr in td:
270 self.assertEqual(rr.color_class(tr["in"]), tr["expected"])
272 def test_risk_color_class(self):
273 td = [
274 {"in": 20, "expected": "danger"},
276 {"in": 16, "expected": "danger"},
277 {"in": 15, "expected": "danger"},
278 {"in": 14, "expected": "warning"},
280 {"in": 8, "expected": "warning"},
281 {"in": 7, "expected": "warning"},
282 {"in": 6, "expected": "success"},
284 {"in": 1, "expected": "success"},
285 ]
287 rr = RiskRating()
288 for tr in td:
289 self.assertEqual(rr.risk_color_class(tr["in"]), tr["expected"])
291 def test_str(self):
292 act = RiskRating.objects.get(id=1)
293 self.assertEqual(act.likelihood_of_occurrence, 2)
294 self.assertEqual(act.severity_of_impact, 4)
295 self.assertEqual(str(act), "Low (2) * Major (4) = 8")
297 def test_color_props(self):
298 act = RiskRating.objects.get(id=1)
299 self.assertEqual(act.likelihood_of_occurrence, 2)
300 self.assertEqual(act.severity_of_impact, 4)
302 self.assertEqual(act.color_likelihood_of_occurrence, "success")
303 self.assertEqual(act.color_severity_of_impact, "warning")
304 self.assertEqual(act.color_risk, "warning")
306class StatusTest(TestCase):
307 def setUp(self):
308 Status.objects.create(id=0, status="Open")
309 Status.objects.create(id=1, status="On going")
310 Status.objects.create(id=2, status="Finished")
311 Status.objects.create(id=3, status="Rejected")
313 def test_str(self):
314 td = [
315 {"id": 0, "expected": "Open"},
316 {"id": 1, "expected": "On going"},
317 {"id": 2, "expected": "Finished"},
318 {"id": 3, "expected": "Rejected"},
319 ]
321 for tr in td:
322 stat = Status.objects.get(id=tr["id"])
323 self.assertEqual(str(stat), tr["expected"])
325 def test_is_todo(self):
326 td = [
327 {"id": 0, "expected": True},
328 {"id": 1, "expected": True},
329 {"id": 2, "expected": False},
330 {"id": 3, "expected": False},
331 ]
333 for tr in td:
334 stat = Status.objects.get(id=tr["id"])
335 self.assertEqual(stat.is_todo, tr["expected"])
337 def test_web_idx_name(self):
338 td = [
339 {"in": "Open", "expected": "open"},
340 {"in": "On going", "expected": "on_going"},
341 {"in": "Finished", "expected": "finished"},
342 {"in": "Rejected", "expected": "rejected"},
343 {"in": "foobar", "expected": "unknown"},
344 {"in": "", "expected": "unknown"},
345 ]
347 for tr in td:
348 self.assertEqual(Status.web_idx_name(tr["in"]), tr["expected"])
350 def test_invalid(self):
351 td = [
352 "", "Foobar"
353 ]
354 for tr in td:
355 failed_create = False
356 try:
357 Status.objects.create(status=tr)
358 except ValidationError:
359 failed_create = True
360 self.assertTrue(failed_create)
362class EvidenceTest(TestCase):
363 def setUp(self):
364 self.u = User.objects.create(username="testuser")
365 self.s0 = Status.objects.create(id=0, status="Open")
366 self.s1 = Status.objects.create(id=1, status="On going")
367 self.s2 = Status.objects.create(id=2, status="Finished")
368 self.s3 = Status.objects.create(id=3, status="Rejected")
369 Evidence.objects.create(id=0, due_date="2026-04-23", responsible=self.u, status=self.s0)
370 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")
371 Evidence.objects.create(id=2, due_date="2026-04-23", responsible=self.u, status=self.s2, evidence="[REQ-001] the evidence req", evidence_link="https://example.com/requirement-001")
372 Evidence.objects.create(id=3, due_date="2026-04-23", responsible=self.u, status=self.s3, evidence="[REQ-001] the evidence req", evidence_link="https://example.com/requirement-001")
373 Evidence.objects.create(id=4, due_date="2026-04-23", responsible=self.u, status=self.s3)
375 def test_str(self):
376 td = [
377 {"evidence": Evidence.objects.get(id=0), "expected": "Open responsible: testuser due: 2026-04-23"},
378 {"evidence": Evidence.objects.get(id=1), "expected": "On going responsible: testuser due: 2026-04-23 evidence: [REQ-001] the evidence req"},
379 {"evidence": Evidence.objects.get(id=2), "expected": "Finished evidence: [REQ-001] the evidence req"},
380 {"evidence": Evidence.objects.get(id=3), "expected": "Rejected evidence: [REQ-001] the evidence req"},
381 {"evidence": Evidence.objects.get(id=4), "expected": "Rejected evidence: None"},
382 ]
383 for tr in td:
384 self.assertEqual(str(tr["evidence"]), tr["expected"])
386 def test_color_due_date(self):
387 yesterday = Evidence(due_date=(datetime.today() - timedelta(days=1)).date(), responsible=self.u, status=self.s0)
388 finished = Evidence(due_date=(datetime.today() - timedelta(days=1)).date(), responsible=self.u, status=self.s3)
389 in_a_week = Evidence(due_date=(datetime.today() + timedelta(weeks=1)).date(), responsible=self.u, status=self.s0)
390 in_5_weeks = Evidence(due_date=(datetime.today() + timedelta(weeks=5)).date(), responsible=self.u, status=self.s0)
392 self.assertEqual(yesterday.color_due_date, "danger")
393 self.assertEqual(finished.color_due_date, "success")
394 self.assertEqual(in_a_week.color_due_date, "danger")
395 self.assertEqual(in_5_weeks.color_due_date, "warning")
397 def test_scrape_icon_no_scraper(self):
398 ev = Evidence.objects.get(id=0)
399 self.assertIn("text-muted", ev.scrape_icon_class)
400 self.assertEqual(ev.scrape_icon_title, "No scraper configured")
402 def test_scrape_icon_ok(self):
403 from scraper.models import RemoteCredential, Scraper
405 credential = RemoteCredential(name="cred-ok", url="https://example.com", auth_type=RemoteCredential.BASIC, username="u")
406 credential.password = "pw"
407 credential.save()
408 scraper = Scraper.objects.create(
409 name="s-ok", credential=credential, response_format=Scraper.JSON,
410 assignee_path="a", status_path="b",
411 )
412 ev = Evidence.objects.get(id=0)
413 ev.scraper = scraper
414 ev.last_scrape_failed = False
415 ev.last_successful_scrape_at = datetime(2026, 4, 20, 10, 0)
416 self.assertIn("text-success", ev.scrape_icon_class)
417 self.assertIn("2026-04-20", ev.scrape_icon_title)
419 def test_scrape_icon_failed(self):
420 from scraper.models import RemoteCredential, Scraper
422 credential = RemoteCredential(name="cred-fail", url="https://example.com", auth_type=RemoteCredential.BASIC, username="u")
423 credential.password = "pw"
424 credential.save()
425 scraper = Scraper.objects.create(
426 name="s-fail", credential=credential, response_format=Scraper.JSON,
427 assignee_path="a", status_path="b",
428 )
429 ev = Evidence.objects.get(id=0)
430 ev.scraper = scraper
431 ev.last_scrape_failed = True
432 self.assertIn("text-danger", ev.scrape_icon_class)
433 self.assertIn("most recent attempt failed", ev.scrape_icon_title)
436class EvidenceScraperAdminActionTest(TestCase):
437 def setUp(self):
438 self.u = User.objects.create(username="testuser")
439 self.s0 = Status.objects.create(status="Open")
440 self.evidence_with_scraper = Evidence.objects.create(id=0, due_date="2026-04-23", responsible=self.u, status=self.s0)
441 self.evidence_without_scraper = Evidence.objects.create(id=1, due_date="2026-04-23", responsible=self.u, status=self.s0)
443 from scraper.models import RemoteCredential, Scraper
444 credential = RemoteCredential(name="cred", url="https://example.com", auth_type=RemoteCredential.BASIC, username="u")
445 credential.password = "pw"
446 credential.save()
447 self.scraper = Scraper.objects.create(
448 name="s", credential=credential, response_format=Scraper.JSON,
449 assignee_path="a", status_path="b",
450 )
451 self.evidence_with_scraper.scraper = self.scraper
452 self.evidence_with_scraper.save()
454 self.admin_instance = EvidenceAdmin(Evidence, admin.site)
455 superuser = User.objects.create_superuser(username="admin2", password="admin123")
457 from django.contrib.messages.storage.fallback import FallbackStorage
458 from django.test import RequestFactory
460 request = RequestFactory().post("/admin/risk_assessment/evidence/")
461 request.user = superuser
462 request.session = {}
463 self.messages_storage = FallbackStorage(request)
464 request._messages = self.messages_storage
465 self.request = request
467 def test_run_scraper_now_only_processes_evidences_with_scraper(self):
468 def fake_run(evidence):
469 return True if evidence.scraper_id else None
471 with patch("scraper.services.run_scraper_for_evidence", side_effect=fake_run) as mocked:
472 run_scraper_now(self.admin_instance, self.request, Evidence.objects.filter(id__in=[0, 1]))
474 called_evidences = [call.args[0] for call in mocked.call_args_list]
475 self.assertCountEqual(called_evidences, [self.evidence_with_scraper, self.evidence_without_scraper])
477 messages = [m.message for m in self.messages_storage]
478 self.assertEqual(len(messages), 1)
479 self.assertIn("1 succeeded, 0 failed, 1 skipped", messages[0])
482class EvidenceAdminScrapeStatusTest(TestCase):
483 def setUp(self):
484 self.u = User.objects.create(username="testuser")
485 self.s0 = Status.objects.create(status="Open")
486 self.admin_instance = EvidenceAdmin(Evidence, admin.site)
488 from scraper.models import RemoteCredential, Scraper
489 credential = RemoteCredential(name="cred", url="https://example.com", auth_type=RemoteCredential.BASIC, username="u")
490 credential.password = "pw"
491 credential.save()
492 self.scraper = Scraper.objects.create(
493 name="s", credential=credential, response_format=Scraper.JSON,
494 assignee_path="a", status_path="b",
495 )
497 def test_no_scraper_renders_nothing(self):
498 ev = Evidence.objects.create(due_date="2026-04-23", responsible=self.u, status=self.s0)
499 self.assertEqual(self.admin_instance.scrape_status(ev), "")
501 def test_successful_scrape_renders_yes_icon(self):
502 ev = Evidence.objects.create(due_date="2026-04-23", responsible=self.u, status=self.s0, scraper=self.scraper, last_scrape_failed=False)
503 self.assertIn("icon-yes.svg", self.admin_instance.scrape_status(ev))
505 def test_failed_scrape_renders_no_icon(self):
506 ev = Evidence.objects.create(due_date="2026-04-23", responsible=self.u, status=self.s0, scraper=self.scraper, last_scrape_failed=True)
507 self.assertIn("icon-no.svg", self.admin_instance.scrape_status(ev))
509 def test_scrape_bookkeeping_fields_are_readonly(self):
510 readonly = self.admin_instance.get_readonly_fields(request=None)
511 self.assertIn("last_successful_scrape_at", readonly)
512 self.assertIn("last_scrape_failed", readonly)
515class EvidenceAdminReadonlyScrapeFieldsTest(TestCase):
516 def setUp(self):
517 self.u = User.objects.create(username="testuser2")
518 self.s0 = Status.objects.create(status="Open")
519 self.evidence = Evidence.objects.create(due_date="2026-04-23", responsible=self.u, status=self.s0, last_scrape_failed=True)
520 superuser = User.objects.create_superuser(username="admin3", password="admin123")
521 self.client.force_login(superuser)
523 def test_change_form_does_not_expose_editable_inputs(self):
524 response = self.client.get(f"/admin/risk_assessment/evidence/{self.evidence.pk}/change/")
525 self.assertEqual(response.status_code, 200)
526 self.assertNotContains(response, 'name="last_scrape_failed"')
527 self.assertNotContains(response, 'name="last_successful_scrape_at"')
529 def test_post_cannot_change_readonly_scrape_fields(self):
530 self.client.post(
531 f"/admin/risk_assessment/evidence/{self.evidence.pk}/change/",
532 {"due_date": "2026-04-23", "responsible": self.u.pk, "status": self.s0.pk},
533 )
534 self.evidence.refresh_from_db()
535 self.assertTrue(self.evidence.last_scrape_failed)
537 def test_change_form_has_test_scraper_button(self):
538 response = self.client.get(f"/admin/risk_assessment/evidence/{self.evidence.pk}/change/")
539 self.assertContains(response, 'name="_test_scraper"')
540 self.assertContains(response, reverse("admin:risk_assessment_evidence_test_scraper"))
543class EvidenceAdminTestScraperViewTest(TestCase):
544 def setUp(self):
545 self.u = User.objects.create(username="testuser3", email="testuser3@example.com")
546 self.s0 = Status.objects.create(status="Open")
547 self.s1 = Status.objects.create(status="On going")
548 superuser = User.objects.create_superuser(username="admin4", password="admin123")
549 self.client.force_login(superuser)
551 from scraper.models import RemoteCredential, Scraper, ScraperStatusMapping
552 credential = RemoteCredential(name="cred-preview", url="https://example.com", auth_type=RemoteCredential.BASIC, username="u")
553 credential.password = "pw"
554 credential.save()
555 self.scraper = Scraper.objects.create(
556 name="preview-scraper", credential=credential, response_format=Scraper.JSON,
557 assignee_path="assignee.email", status_path="status.name",
558 )
559 ScraperStatusMapping.objects.create(scraper=self.scraper, remote_value="In progress", status=self.s1)
561 def test_get_is_not_allowed(self):
562 response = self.client.get(reverse("admin:risk_assessment_evidence_test_scraper"))
563 self.assertEqual(response.status_code, 403)
565 def test_no_scraper_selected_shows_error(self):
566 response = self.client.post(reverse("admin:risk_assessment_evidence_test_scraper"), {})
567 self.assertEqual(response.status_code, 200)
568 self.assertContains(response, "No scraper selected")
570 @patch("scraper.services.requests.get")
571 def test_successful_dry_run_shows_extracted_values(self, mock_get):
572 response = Mock()
573 response.status_code = 200
574 response.raise_for_status = Mock()
575 response.json = Mock(return_value={"assignee": {"email": "testuser3@example.com"}, "status": {"name": "In progress"}})
576 mock_get.return_value = response
578 result = self.client.post(
579 reverse("admin:risk_assessment_evidence_test_scraper"),
580 {"scraper": self.scraper.pk, "scrape_url": "https://example.com/tickets/1", "evidence_link": ""},
581 )
583 self.assertEqual(result.status_code, 200)
584 self.assertContains(result, "testuser3")
585 self.assertContains(result, "On going")
586 self.assertContains(result, "In progress")
588 def test_unauthenticated_is_redirected_to_login(self):
589 self.client.logout()
590 response = self.client.post(reverse("admin:risk_assessment_evidence_test_scraper"), {"scraper": self.scraper.pk})
591 self.assertEqual(response.status_code, 302)
594class RiskMitigationTest(TestCase):
595 def setUp(self):
596 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")
597 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")
598 rm = RiskMitigation(id=0, mitigation="We fixed it")
599 rm.save()
600 rm.security_requirements.set([self.secReq1])
601 self.rm_no_rat = rm.save()
603 rm = RiskMitigation(id=1, mitigation="Extremely long mitigation that bores us and clutters the ui", rational="We fixed it in a very good manor")
604 rm.save()
605 rm.security_requirements.set([self.secReq1, self.secReq2])
606 self.rm_rat = rm.save()
608 rm = RiskMitigation(id=2, mitigation="Extremely long mitigation that bores us and clutters the ui", title="short catchy")
609 self.rm_tit = rm.save()
611 def test_str(self):
612 self.assertEqual(str(RiskMitigation.objects.get(id=0)), "We fixed it")
613 self.assertEqual(str(RiskMitigation.objects.get(id=1)), "Extremely long mitigation that bores us and clutters the ui")
614 self.assertEqual(str(RiskMitigation.objects.get(id=2)), "short catchy")
616 def test_list_sec_req(self):
617 rm0 = RiskMitigation.objects.get(id=0)
618 rm1 = RiskMitigation.objects.get(id=1)
620 secReqs = rm0.list_security_requirements
621 self.assertEqual(len(secReqs), 1)
622 self.assertEqual(secReqs, [
623 {"str": "CRA: Part 1.1", "slug": "cra-1.1"}
624 ])
626 secReqs = rm1.list_security_requirements
627 self.assertEqual(len(secReqs), 2)
628 self.assertEqual(secReqs, [
629 {"str": "CRA: Part 1.1", "slug": "cra-1.1"},
630 {"str": "CRA: Part 1.2", "slug": "cra-1.2"},
631 ])
633class SeverityExampleTest(TestCase):
634 def test_simple(self):
635 SeverityExample.objects.create(severity_of_impact=1, examples="Hello severity world")
637 act = SeverityExample.objects.get(severity_of_impact=1)
638 self.assertEqual(act.severity_of_impact, 1)
639 self.assertEqual(act.examples, "Hello severity world")
640 self.assertEqual(str(act), "Negligible - Examples")
642class LikelihoodExampleTest(TestCase):
643 def test_simple(self):
644 LikelihoodExample.objects.create(likelihood_of_occurrence=1, examples="Hello like world")
646 act = LikelihoodExample.objects.get(likelihood_of_occurrence=1)
647 self.assertEqual(act.likelihood_of_occurrence, 1)
648 self.assertEqual(act.examples, "Hello like world")
650class SuggestedMitigationValidationTest(TestCase):
651 def test_simple(self):
652 td = [
653 {"id": 1, "mit": "Verification of signature before installation", "vali": "<ol><li>Modify update file and verify detection</li><li>Ensure only signed files are installed</li></ol>", "expected_str": "Verification of signature bef... - <ol><li>Modify update file an..."},
654 {"id": 2, "mit": None, "vali": "<ol><li>Modify update file and verify detection</li><li>Ensure only signed files are installed</li></ol>", "expected_str": " - <ol><li>Modify update file an..."},
655 {"id": 3, "mit": "Verification of signature before installation", "vali": None, "expected_str": "Verification of signature bef... - "},
656 ]
658 for t in td:
659 SuggestedMitigationValidation.objects.create(id=t["id"], suggested_mitigation=t["mit"], suggested_validation=t["vali"])
661 act = SuggestedMitigationValidation.objects.get(id=t["id"])
662 self.assertEqual(act.id, t["id"])
663 self.assertEqual(act.suggested_mitigation, t["mit"])
664 self.assertEqual(act.suggested_validation, t["vali"])
665 self.assertEqual(str(act), t["expected_str"])
668# --- Admin tests ---
670class RiskAssessmentAdminChangelistTest(TestCase):
671 """Smoke tests: every registered admin changelist returns HTTP 200."""
673 def setUp(self):
674 superuser = User.objects.create_superuser(username="admin", password="admin123")
675 self.client.force_login(superuser)
677 def _get(self, model_name):
678 return self.client.get(f'/admin/risk_assessment/{model_name}/')
680 def test_asset_changelist(self):
681 self.assertEqual(self._get('asset').status_code, 200)
683 def test_origin_changelist(self):
684 self.assertEqual(self._get('origin').status_code, 200)
686 def test_lifecycle_changelist(self):
687 self.assertEqual(self._get('lifecycle').status_code, 200)
689 def test_securityrequirement_changelist(self):
690 self.assertEqual(self._get('securityrequirement').status_code, 200)
692 def test_risk_changelist(self):
693 self.assertEqual(self._get('risk').status_code, 200)
695 def test_riskmitigation_changelist(self):
696 self.assertEqual(self._get('riskmitigation').status_code, 200)
698 def test_riskrating_changelist(self):
699 self.assertEqual(self._get('riskrating').status_code, 200)
701 def test_evidence_changelist(self):
702 self.assertEqual(self._get('evidence').status_code, 200)
704 def test_severityexample_changelist(self):
705 self.assertEqual(self._get('severityexample').status_code, 200)
707 def test_likelihoodexample_changelist(self):
708 self.assertEqual(self._get('likelihoodexample').status_code, 200)
710 def test_suggestedmitigationvalidation_changelist(self):
711 self.assertEqual(self._get('suggestedmitigationvalidation').status_code, 200)
714class SecurityRequirementAdminTest(TestCase):
715 def setUp(self):
716 self.sr = SecurityRequirement.objects.create(
717 norm_short="CRA", norm_long="Cyber Resilience Act",
718 description_short="Part 1.1", description_long="Details about Part 1.1",
719 slug="cra-1.1",
720 )
721 self.admin = SecurityRequirementAdmin(SecurityRequirement, admin.site)
723 def test_norm_display(self):
724 self.assertEqual(self.admin.norm(self.sr), "CRA")
726 def test_short_display(self):
727 self.assertEqual(self.admin.short(self.sr), "Part 1.1")
729 def test_description_display(self):
730 self.assertEqual(self.admin.description(self.sr), "Details about Part 1.1")
733class RiskAdminTest(TestCase):
734 def setUp(self):
735 asset = Asset.objects.create(name="OS")
736 origin = Origin.objects.create(name="USB")
737 lc = LifeCycle.objects.create(name="in use")
738 self.stride_s = Stride.objects.create(name="S")
739 self.stride_t = Stride.objects.create(name="T")
740 self.admin = RiskAdmin(Risk, admin.site)
742 risk = Risk(custom_id="R-001", asset=asset, origin=origin, life_cycle=lc, title="Test risk", description="d")
743 risk.save()
744 risk.stride.set([self.stride_s])
745 risk.save()
746 self.risk_single = risk
748 risk2 = Risk(custom_id="R-002", asset=asset, origin=origin, life_cycle=lc, title="Multi stride risk", description="d")
749 risk2.save()
750 risk2.stride.set([self.stride_s, self.stride_t])
751 risk2.save()
752 self.risk_multi = risk2
754 def test_stride_str_display_single(self):
755 self.assertEqual(self.admin.stride_str(self.risk_single), "S")
757 def test_stride_str_display_multiple(self):
758 result = self.admin.stride_str(self.risk_multi)
759 self.assertIn("S", result)
760 self.assertIn("T", result)
763class RiskRatingAdminTest(TestCase):
764 def setUp(self):
765 self.rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=4)
766 self.admin = RiskRatingAdmin(RiskRating, admin.site)
768 def test_risk_str_display(self):
769 self.assertEqual(self.admin.risk_str(self.rr), str(self.rr))
770 self.assertEqual(self.admin.risk_str(self.rr), "Low (2) * Major (4) = 8")
773# ---------------------------------------------------------------------------
774# View tests
775# ---------------------------------------------------------------------------
777class SecurityRequirementsViewTest(TestCase):
778 def setUp(self):
779 self.user = User.objects.create_user(username="viewer", password="pw")
780 self.sr1 = SecurityRequirement.objects.create(
781 norm_short="CRA", norm_long="CRA long",
782 description_short="Part 1.1", description_long="Details 1.1", slug="cra-1-1",
783 )
784 self.sr1_sum = {"description_short": "Part 1.1", "description_long": "Details 1.1"}
785 self.sr2 = SecurityRequirement.objects.create(
786 norm_short="CRA", norm_long="CRA long",
787 description_short="Part 1.2", description_long="Details 1.2", slug="cra-1-2",
788 )
789 self.sr2_sum = {"description_short": "Part 1.2", "description_long": "Details 1.2"}
791 def test_unauthenticated_redirects_to_login(self):
792 response = self.client.get('/security-requirements/')
793 self.assertRedirects(response, '/login?next=/security-requirements/', fetch_redirect_response=False)
795 def test_authenticated_returns_200(self):
796 self.client.force_login(self.user)
797 self.assertEqual(self.client.get('/security-requirements/').status_code, 200)
799 def test_context_contains_all_requirements(self):
800 self.client.force_login(self.user)
801 response = self.client.get('/security-requirements/')
802 req_list = dict(response.context['security_req_map'])
803 self.assertIn(self.sr1.norm_long, req_list)
804 self.assertEqual(len(req_list[self.sr1.norm_long]), 2)
805 self.assertIn(self.sr1_sum, req_list[self.sr1.norm_long])
806 self.assertIn(self.sr2_sum, req_list[self.sr1.norm_long])
808 def test_context_empty_when_no_requirements_exist(self):
809 SecurityRequirement.objects.all().delete()
810 self.client.force_login(self.user)
811 response = self.client.get('/security-requirements/')
812 self.assertEqual(dict(response.context['security_req_map']), {})
815class SecurityRequirementViewTest(TestCase):
816 def setUp(self):
817 self.user = User.objects.create_user(username="viewer", password="pw")
818 self.sr = SecurityRequirement.objects.create(
819 norm_short="CRA", norm_long="CRA long",
820 description_short="Part 1.1", description_long="Details 1.1", slug="cra-1-1",
821 )
823 def test_unauthenticated_redirects_to_login(self):
824 response = self.client.get('/security-requirements/cra-1-1/')
825 self.assertRedirects(response, '/login?next=/security-requirements/cra-1-1/', fetch_redirect_response=False)
827 def test_authenticated_returns_200(self):
828 self.client.force_login(self.user)
829 self.assertEqual(self.client.get('/security-requirements/cra-1-1/').status_code, 200)
831 def test_nonexistent_slug_returns_404(self):
832 self.client.force_login(self.user)
833 self.assertEqual(self.client.get('/security-requirements/does-not-exist/').status_code, 404)
835 def test_context_contains_correct_requirement(self):
836 self.client.force_login(self.user)
837 response = self.client.get('/security-requirements/cra-1-1/')
838 self.assertEqual(response.context['req'], self.sr)
841class RiskInfoViewTest(TestCase):
842 def setUp(self):
843 self.user = User.objects.create_user(username="viewer", password="pw")
844 SeverityExample.objects.create(severity_of_impact=1, examples="Minor damage only")
845 SeverityExample.objects.create(severity_of_impact=5, examples="Catastrophic damage")
846 LikelihoodExample.objects.create(likelihood_of_occurrence=1, examples="Very rare")
847 LikelihoodExample.objects.create(likelihood_of_occurrence=5, examples="Almost certain")
849 def test_unauthenticated_redirects_to_login(self):
850 response = self.client.get('/help/risk-info/')
851 self.assertRedirects(response, '/login?next=/help/risk-info/', fetch_redirect_response=False)
853 def test_authenticated_returns_200(self):
854 self.client.force_login(self.user)
855 self.assertEqual(self.client.get('/help/risk-info/').status_code, 200)
857 def test_context_severities_has_five_entries(self):
858 self.client.force_login(self.user)
859 response = self.client.get('/help/risk-info/')
860 self.assertEqual(len(response.context['severities']), 5)
862 def test_context_severities_have_str_and_value_keys(self):
863 self.client.force_login(self.user)
864 response = self.client.get('/help/risk-info/')
865 for entry in response.context['severities']:
866 self.assertIn('str', entry)
867 self.assertIn('value', entry)
869 def test_context_loos_has_five_entries(self):
870 self.client.force_login(self.user)
871 response = self.client.get('/help/risk-info/')
872 self.assertEqual(len(response.context['loos']), 5)
874 def test_context_risk_is_complete_5x5_matrix(self):
875 self.client.force_login(self.user)
876 response = self.client.get('/help/risk-info/')
877 risk = response.context['risk']
878 self.assertEqual(len(risk), 5)
879 for row in risk.values():
880 self.assertEqual(len(row), 5)
882 def test_context_risk_corner_values(self):
883 self.client.force_login(self.user)
884 response = self.client.get('/help/risk-info/')
885 risk = response.context['risk']
886 self.assertEqual(risk['1']['1'], RiskRating.calc_risk(1, 1))
887 self.assertEqual(risk['5']['5'], RiskRating.calc_risk(5, 5))
889 def test_context_risk_color_is_complete_5x5_matrix(self):
890 self.client.force_login(self.user)
891 response = self.client.get('/help/risk-info/')
892 risk_color = response.context['risk_color']
893 self.assertEqual(len(risk_color), 5)
894 for row in risk_color.values():
895 self.assertEqual(len(row), 5)
897 def test_context_example_severities_populated(self):
898 self.client.force_login(self.user)
899 response = self.client.get('/help/risk-info/')
900 self.assertEqual(response.context['example_severities']['1'], "Minor damage only")
901 self.assertEqual(response.context['example_severities']['5'], "Catastrophic damage")
903 def test_context_example_loos_populated(self):
904 self.client.force_login(self.user)
905 response = self.client.get('/help/risk-info/')
906 self.assertEqual(response.context['example_loos']['1'], "Very rare")
907 self.assertEqual(response.context['example_loos']['5'], "Almost certain")
909 def test_context_example_severities_empty_without_data(self):
910 SeverityExample.objects.all().delete()
911 self.client.force_login(self.user)
912 response = self.client.get('/help/risk-info/')
913 self.assertEqual(response.context['example_severities'], {})
915 def test_context_example_loos_empty_without_data(self):
916 LikelihoodExample.objects.all().delete()
917 self.client.force_login(self.user)
918 response = self.client.get('/help/risk-info/')
919 self.assertEqual(response.context['example_loos'], {})
922class SuggestionViewTest(TestCase):
923 def setUp(self):
924 self.user = User.objects.create_user(username="viewer", password="pw")
925 self.sug = SuggestedMitigationValidation.objects.create(
926 id=1,
927 suggested_mitigation="Use signed firmware",
928 suggested_validation="<ol><li>Check signature</li></ol>",
929 )
931 def test_unauthenticated_redirects_to_login(self):
932 response = self.client.get('/suggestion/1')
933 self.assertRedirects(response, '/login?next=/suggestion/1', fetch_redirect_response=False)
935 def test_authenticated_returns_200(self):
936 self.client.force_login(self.user)
937 self.assertEqual(self.client.get('/suggestion/1').status_code, 200)
939 def test_nonexistent_id_returns_404(self):
940 self.client.force_login(self.user)
941 self.assertEqual(self.client.get('/suggestion/9999').status_code, 404)
943 def test_context_contains_correct_suggestion(self):
944 self.client.force_login(self.user)
945 response = self.client.get('/suggestion/1')
946 self.assertEqual(response.context['sug'], self.sug)
949# ---------------------------------------------------------------------------
950# Risk versioning — model
951# ---------------------------------------------------------------------------
953class RiskVersioningModelTest(TestCase):
954 def setUp(self):
955 asset = Asset.objects.create(name="VER-Asset")
956 origin = Origin.objects.create(name="VER-Origin")
957 lc = LifeCycle.objects.create(name="VER-LC")
958 self.stride = Stride.objects.create(name="S")
959 self.kwargs = dict(asset=asset, origin=origin, life_cycle=lc,
960 title="Title", description="Desc")
962 def test_default_version_is_1(self):
963 risk = Risk.objects.create(custom_id="R-VER-001", **self.kwargs)
964 self.assertEqual(risk.version, 1)
966 def test_default_is_current_is_true(self):
967 risk = Risk.objects.create(custom_id="R-VER-001", **self.kwargs)
968 self.assertTrue(risk.is_current)
970 def test_has_newer_version_false_when_current(self):
971 risk = Risk.objects.create(custom_id="R-VER-001", **self.kwargs)
972 self.assertFalse(risk.has_newer_version)
974 def test_has_newer_version_true_when_not_current(self):
975 risk = Risk.objects.create(custom_id="R-VER-001", is_current=False, **self.kwargs)
976 self.assertTrue(risk.has_newer_version)
978 def test_str_includes_version_number(self):
979 risk = Risk.objects.create(custom_id="R-VER-001", version=3, **self.kwargs)
980 self.assertIn("v3", str(risk))
982 def test_str_includes_custom_id(self):
983 risk = Risk.objects.create(custom_id="R-VER-001", **self.kwargs)
984 self.assertIn("R-VER-001", str(risk))
986 def test_same_custom_id_different_versions_allowed(self):
987 Risk.objects.create(custom_id="R-VER-001", version=1, **self.kwargs)
988 risk_v2 = Risk.objects.create(custom_id="R-VER-001", version=2, **self.kwargs)
989 self.assertEqual(risk_v2.version, 2)
991 def test_duplicate_custom_id_and_version_raises_integrity_error(self):
992 Risk.objects.create(custom_id="R-VER-001", version=1, **self.kwargs)
993 constraint_violated = False
994 try:
995 Risk.objects.create(custom_id="R-VER-001", version=1, **self.kwargs)
996 except IntegrityError:
997 constraint_violated = True
998 self.assertTrue(constraint_violated)
1001# ---------------------------------------------------------------------------
1002# Risk versioning — admin form: save as new version
1003# ---------------------------------------------------------------------------
1005class RiskSaveAsNewVersionFormTest(TestCase):
1006 def setUp(self):
1007 self.superuser = User.objects.create_superuser(username="admin", password="pw")
1008 self.client.force_login(self.superuser)
1010 self.asset = Asset.objects.create(name="FrmAsset")
1011 self.origin = Origin.objects.create(name="FrmOrigin")
1012 self.lc = LifeCycle.objects.create(name="FrmLC")
1013 self.stride = Stride.objects.create(name="S")
1015 self.risk = Risk(custom_id="R-FRM-001", asset=self.asset, origin=self.origin,
1016 life_cycle=self.lc, title="Original", description="Original desc")
1017 self.risk.save()
1018 self.risk.stride.set([self.stride])
1019 self.risk.save()
1021 def _post(self, extra=None):
1022 data = {
1023 'custom_id': 'R-FRM-001',
1024 'asset': self.asset.pk,
1025 'origin': self.origin.pk,
1026 'life_cycle': self.lc.pk,
1027 'stride': [self.stride.pk],
1028 'title': 'Updated title',
1029 'description': 'Updated desc',
1030 '_save': 'Save',
1031 }
1032 if extra:
1033 data.update(extra)
1034 return self.client.post(
1035 f'/admin/risk_assessment/risk/{self.risk.pk}/change/',
1036 data,
1037 )
1039 # --- without checkbox: in-place update ---
1041 def test_no_checkbox_updates_title_in_place(self):
1042 self._post()
1043 self.risk.refresh_from_db()
1044 self.assertEqual(self.risk.title, 'Updated title')
1046 def test_no_checkbox_does_not_create_new_version(self):
1047 self._post()
1048 self.assertFalse(Risk.objects.filter(custom_id='R-FRM-001', version=2).exists())
1050 def test_no_checkbox_keeps_risk_current(self):
1051 self._post()
1052 self.risk.refresh_from_db()
1053 self.assertTrue(self.risk.is_current)
1055 # --- with checkbox: new version ---
1057 def test_checkbox_creates_version_2(self):
1058 self._post({'create_new_version': True})
1059 self.assertTrue(Risk.objects.filter(custom_id='R-FRM-001', version=2).exists())
1061 def test_checkbox_new_version_has_updated_data(self):
1062 self._post({'create_new_version': True})
1063 new = Risk.objects.get(custom_id='R-FRM-001', version=2)
1064 self.assertEqual(new.title, 'Updated title')
1065 self.assertEqual(new.description, 'Updated desc')
1067 def test_checkbox_new_version_is_current(self):
1068 self._post({'create_new_version': True})
1069 new = Risk.objects.get(custom_id='R-FRM-001', version=2)
1070 self.assertTrue(new.is_current)
1072 def test_checkbox_old_version_becomes_not_current(self):
1073 self._post({'create_new_version': True})
1074 self.risk.refresh_from_db()
1075 self.assertFalse(self.risk.is_current)
1077 def test_checkbox_old_version_data_is_unchanged(self):
1078 self._post({'create_new_version': True})
1079 self.risk.refresh_from_db()
1080 self.assertEqual(self.risk.title, 'Original')
1081 self.assertEqual(self.risk.description, 'Original desc')
1083 def test_checkbox_copies_stride_to_new_version(self):
1084 self._post({'create_new_version': True})
1085 new = Risk.objects.get(custom_id='R-FRM-001', version=2)
1086 self.assertIn(self.stride, new.stride.all())
1088 def test_checkbox_redirects_to_new_version_change_form(self):
1089 response = self._post({'create_new_version': True})
1090 new = Risk.objects.get(custom_id='R-FRM-001', version=2)
1091 self.assertRedirects(
1092 response,
1093 f'/admin/risk_assessment/risk/{new.pk}/change/',
1094 fetch_redirect_response=False,
1095 )