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

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 

6 

7from unittest.mock import Mock, patch 

8 

9from .models import * 

10from .admin import ( 

11 SecurityRequirementAdmin, 

12 RiskAdmin, RiskRatingAdmin, 

13 EvidenceAdmin, run_scraper_now, 

14) 

15 

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 ] 

23 

24 for t in td: 

25 self.assertEqual(truncate(t["str"], t["len"]), t["expected"]) 

26 

27class AssetTest(TestCase): 

28 test_name = "Operating System" 

29 

30 def test_simple(self): 

31 exp = Asset.objects.create(name=self.test_name) 

32 

33 act = Asset.objects.get(name=self.test_name) 

34 self.assertEqual(act, exp) 

35 

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 

43 

44 self.assertTrue(unique_check_ok) 

45 

46 def test_str(self): 

47 exp = Asset.objects.create(name=self.test_name) 

48 

49 self.assertEqual(str(exp), self.test_name) 

50 

51class LifeCycleTest(TestCase): 

52 test_name = "in use" 

53 

54 def test_simple(self): 

55 exp = LifeCycle.objects.create(name=self.test_name) 

56 

57 act = LifeCycle.objects.get(name=self.test_name) 

58 self.assertEqual(act, exp) 

59 

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 

67 

68 self.assertTrue(unique_check_ok) 

69 

70 def test_str(self): 

71 exp = LifeCycle.objects.create(name=self.test_name) 

72 

73 self.assertEqual(str(exp), self.test_name) 

74 

75class OriginTest(TestCase): 

76 test_name = "USB" 

77 

78 def test_simple(self): 

79 exp = Origin.objects.create(name=self.test_name) 

80 

81 act = Origin.objects.get(name=self.test_name) 

82 self.assertEqual(act, exp) 

83 

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 

91 

92 self.assertTrue(unique_check_ok) 

93 

94 def test_str(self): 

95 exp = Origin.objects.create(name=self.test_name) 

96 

97 self.assertEqual(str(exp), self.test_name) 

98 

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") 

102 

103 act = SecurityRequirement.objects.get(norm_short="CRA", description_short="Part 1.1") 

104 self.assertEqual(act, exp) 

105 

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 

113 

114 self.assertTrue(unique_check_ok) 

115 

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") 

118 

119 self.assertEqual(str(exp), f"CRA: Part 1.1") 

120 

121class StrideTest(TestCase): 

122 def test_simple(self): 

123 exp = Stride.objects.create(name="S") 

124 

125 act = Stride.objects.get(name="S") 

126 self.assertEqual(act, exp) 

127 

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 

134 

135 self.assertFalse(allowed_to_add) 

136 

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 

146 

147 self.assertTrue(unique_check_ok) 

148 

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") 

154 

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() 

167 

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") 

179 

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") 

184 

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"]) 

189 

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 ] 

201 

202 for tc in data: 

203 t = SeverityName.to_human_str(tc["value"]) 

204 self.assertEqual(t, tc["str"]) 

205 

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 ] 

215 

216 for tc in data: 

217 t = LikelihoodName.to_human_str(tc["value"]) 

218 self.assertEqual(t, tc["str"]) 

219 

220class RiskRatingTest(TestCase): 

221 def setUp(self): 

222 rr = RiskRating.objects.create(likelihood_of_occurrence=2, severity_of_impact=4) 

223 

224 def test_risk(self): 

225 act = RiskRating.objects.get(id=1) 

226 self.assertEqual(act.risk, 8) 

227 

228 def test_risk_level(self): 

229 td = [ 

230 {"in": 20, "expected": Risk5x5.HIGH}, 

231 

232 {"in": 16, "expected": Risk5x5.HIGH}, 

233 {"in": 15, "expected": Risk5x5.HIGH}, 

234 {"in": 14, "expected": Risk5x5.MID}, 

235 

236 {"in": 8, "expected": Risk5x5.MID}, 

237 {"in": 7, "expected": Risk5x5.MID}, 

238 {"in": 6, "expected": Risk5x5.LOW}, 

239 

240 {"in": 1, "expected": Risk5x5.LOW}, 

241 ] 

242 

243 for tr in td: 

244 self.assertEqual(RiskRating.risk_level(tr["in"]), tr["expected"]) 

245 

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"]) 

257 

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"]) 

271 

272 def test_risk_color_class(self): 

273 td = [ 

274 {"in": 20, "expected": "danger"}, 

275 

276 {"in": 16, "expected": "danger"}, 

277 {"in": 15, "expected": "danger"}, 

278 {"in": 14, "expected": "warning"}, 

279 

280 {"in": 8, "expected": "warning"}, 

281 {"in": 7, "expected": "warning"}, 

282 {"in": 6, "expected": "success"}, 

283 

284 {"in": 1, "expected": "success"}, 

285 ] 

286 

287 rr = RiskRating() 

288 for tr in td: 

289 self.assertEqual(rr.risk_color_class(tr["in"]), tr["expected"]) 

290 

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") 

296 

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) 

301 

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") 

305 

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") 

312 

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 ] 

320 

321 for tr in td: 

322 stat = Status.objects.get(id=tr["id"]) 

323 self.assertEqual(str(stat), tr["expected"]) 

324 

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 ] 

332 

333 for tr in td: 

334 stat = Status.objects.get(id=tr["id"]) 

335 self.assertEqual(stat.is_todo, tr["expected"]) 

336 

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 ] 

346 

347 for tr in td: 

348 self.assertEqual(Status.web_idx_name(tr["in"]), tr["expected"]) 

349 

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) 

361 

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) 

374 

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"]) 

385 

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) 

391 

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") 

396 

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") 

401 

402 def test_scrape_icon_ok(self): 

403 from scraper.models import RemoteCredential, Scraper 

404 

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) 

418 

419 def test_scrape_icon_failed(self): 

420 from scraper.models import RemoteCredential, Scraper 

421 

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) 

434 

435 

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) 

442 

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() 

453 

454 self.admin_instance = EvidenceAdmin(Evidence, admin.site) 

455 superuser = User.objects.create_superuser(username="admin2", password="admin123") 

456 

457 from django.contrib.messages.storage.fallback import FallbackStorage 

458 from django.test import RequestFactory 

459 

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 

466 

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 

470 

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])) 

473 

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]) 

476 

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]) 

480 

481 

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) 

487 

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 ) 

496 

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), "") 

500 

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)) 

504 

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)) 

508 

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) 

513 

514 

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) 

522 

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"') 

528 

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) 

536 

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")) 

541 

542 

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) 

550 

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) 

560 

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) 

564 

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") 

569 

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 

577 

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 ) 

582 

583 self.assertEqual(result.status_code, 200) 

584 self.assertContains(result, "testuser3") 

585 self.assertContains(result, "On going") 

586 self.assertContains(result, "In progress") 

587 

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) 

592 

593 

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() 

602 

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() 

607 

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() 

610 

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") 

615 

616 def test_list_sec_req(self): 

617 rm0 = RiskMitigation.objects.get(id=0) 

618 rm1 = RiskMitigation.objects.get(id=1) 

619 

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 ]) 

625 

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 ]) 

632 

633class SeverityExampleTest(TestCase): 

634 def test_simple(self): 

635 SeverityExample.objects.create(severity_of_impact=1, examples="Hello severity world") 

636 

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") 

641 

642class LikelihoodExampleTest(TestCase): 

643 def test_simple(self): 

644 LikelihoodExample.objects.create(likelihood_of_occurrence=1, examples="Hello like world") 

645 

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") 

649 

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 ] 

657 

658 for t in td: 

659 SuggestedMitigationValidation.objects.create(id=t["id"], suggested_mitigation=t["mit"], suggested_validation=t["vali"]) 

660 

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"]) 

666 

667 

668# --- Admin tests --- 

669 

670class RiskAssessmentAdminChangelistTest(TestCase): 

671 """Smoke tests: every registered admin changelist returns HTTP 200.""" 

672 

673 def setUp(self): 

674 superuser = User.objects.create_superuser(username="admin", password="admin123") 

675 self.client.force_login(superuser) 

676 

677 def _get(self, model_name): 

678 return self.client.get(f'/admin/risk_assessment/{model_name}/') 

679 

680 def test_asset_changelist(self): 

681 self.assertEqual(self._get('asset').status_code, 200) 

682 

683 def test_origin_changelist(self): 

684 self.assertEqual(self._get('origin').status_code, 200) 

685 

686 def test_lifecycle_changelist(self): 

687 self.assertEqual(self._get('lifecycle').status_code, 200) 

688 

689 def test_securityrequirement_changelist(self): 

690 self.assertEqual(self._get('securityrequirement').status_code, 200) 

691 

692 def test_risk_changelist(self): 

693 self.assertEqual(self._get('risk').status_code, 200) 

694 

695 def test_riskmitigation_changelist(self): 

696 self.assertEqual(self._get('riskmitigation').status_code, 200) 

697 

698 def test_riskrating_changelist(self): 

699 self.assertEqual(self._get('riskrating').status_code, 200) 

700 

701 def test_evidence_changelist(self): 

702 self.assertEqual(self._get('evidence').status_code, 200) 

703 

704 def test_severityexample_changelist(self): 

705 self.assertEqual(self._get('severityexample').status_code, 200) 

706 

707 def test_likelihoodexample_changelist(self): 

708 self.assertEqual(self._get('likelihoodexample').status_code, 200) 

709 

710 def test_suggestedmitigationvalidation_changelist(self): 

711 self.assertEqual(self._get('suggestedmitigationvalidation').status_code, 200) 

712 

713 

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) 

722 

723 def test_norm_display(self): 

724 self.assertEqual(self.admin.norm(self.sr), "CRA") 

725 

726 def test_short_display(self): 

727 self.assertEqual(self.admin.short(self.sr), "Part 1.1") 

728 

729 def test_description_display(self): 

730 self.assertEqual(self.admin.description(self.sr), "Details about Part 1.1") 

731 

732 

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) 

741 

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 

747 

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 

753 

754 def test_stride_str_display_single(self): 

755 self.assertEqual(self.admin.stride_str(self.risk_single), "S") 

756 

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) 

761 

762 

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) 

767 

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") 

771 

772 

773# --------------------------------------------------------------------------- 

774# View tests 

775# --------------------------------------------------------------------------- 

776 

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"} 

790 

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) 

794 

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) 

798 

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]) 

807 

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']), {}) 

813 

814 

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 ) 

822 

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) 

826 

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) 

830 

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) 

834 

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) 

839 

840 

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") 

848 

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) 

852 

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) 

856 

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) 

861 

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) 

868 

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) 

873 

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) 

881 

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)) 

888 

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) 

896 

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") 

902 

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") 

908 

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'], {}) 

914 

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'], {}) 

920 

921 

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 ) 

930 

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) 

934 

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) 

938 

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) 

942 

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) 

947 

948 

949# --------------------------------------------------------------------------- 

950# Risk versioning — model 

951# --------------------------------------------------------------------------- 

952 

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") 

961 

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) 

965 

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) 

969 

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) 

973 

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) 

977 

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)) 

981 

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)) 

985 

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) 

990 

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) 

999 

1000 

1001# --------------------------------------------------------------------------- 

1002# Risk versioning — admin form: save as new version 

1003# --------------------------------------------------------------------------- 

1004 

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) 

1009 

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") 

1014 

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() 

1020 

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 ) 

1038 

1039 # --- without checkbox: in-place update --- 

1040 

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') 

1045 

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()) 

1049 

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) 

1054 

1055 # --- with checkbox: new version --- 

1056 

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()) 

1060 

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') 

1066 

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) 

1071 

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) 

1076 

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') 

1082 

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()) 

1087 

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 )