@@ -295,3 +295,64 @@ def test_found_by_copied_to_new_original(self):
295295 found_by_ids = set (outside_dupe .found_by .values_list ("id" , flat = True ))
296296 self .assertIn (self .test_type .id , found_by_ids )
297297 self .assertIn (test_type_2 .id , found_by_ids )
298+
299+ def test_delete_finding_reconfigures_cross_engagement_duplicate (self ):
300+ """Deleting an original finding makes its cross-engagement duplicate standalone.
301+
302+ Setup: product with eng A (finding A, original) and eng B (finding B, duplicate of A).
303+ Action: delete finding A.
304+ Expected: finding B becomes a standalone finding (not duplicate, active, no duplicate_finding).
305+ """
306+ finding_a = self ._create_finding (self .test1 , "Original A" )
307+ finding_a .active = True
308+ finding_a .is_mitigated = False
309+ super (Finding , finding_a ).save (skip_validation = True )
310+
311+ finding_b = self ._create_finding (self .test3 , "Duplicate B" )
312+ self ._make_duplicate (finding_b , finding_a )
313+
314+ # Verify setup
315+ finding_b .refresh_from_db ()
316+ self .assertTrue (finding_b .duplicate )
317+ self .assertEqual (finding_b .duplicate_finding_id , finding_a .id )
318+
319+ # Delete finding A — triggers finding_delete signal -> reconfigure_duplicate_cluster
320+ with impersonate (self .testuser ):
321+ finding_a .delete ()
322+
323+ # Finding B should now be standalone
324+ finding_b .refresh_from_db ()
325+ self .assertFalse (finding_b .duplicate )
326+ self .assertIsNone (finding_b .duplicate_finding )
327+ self .assertTrue (finding_b .active )
328+ self .assertFalse (finding_b .is_mitigated )
329+
330+ def test_delete_product_with_cross_engagement_duplicates (self ):
331+ """Deleting a product with cross-engagement duplicates succeeds without FK violations.
332+
333+ Setup: product with eng A (finding A, original) and eng B (finding B, duplicate of A).
334+ Action: delete the entire product via async_delete_crawl_task.
335+ Expected: product and all findings are deleted without errors.
336+ """
337+ from dojo .utils import ASYNC_DELETE_MAPPING , async_delete_crawl_task
338+
339+ finding_a = self ._create_finding (self .test1 , "Original A" )
340+ finding_a .active = True
341+ finding_a .is_mitigated = False
342+ super (Finding , finding_a ).save (skip_validation = True )
343+
344+ finding_b = self ._create_finding (self .test3 , "Duplicate B" )
345+ self ._make_duplicate (finding_b , finding_a )
346+
347+ product_id = self .product .id
348+ finding_a_id = finding_a .id
349+ finding_b_id = finding_b .id
350+
351+ model_list = ASYNC_DELETE_MAPPING ["Product" ]
352+ with impersonate (self .testuser ):
353+ async_delete_crawl_task (self .product , model_list )
354+
355+ # Everything should be gone
356+ self .assertFalse (Product .objects .filter (id = product_id ).exists ())
357+ self .assertFalse (Finding .objects .filter (id = finding_a_id ).exists ())
358+ self .assertFalse (Finding .objects .filter (id = finding_b_id ).exists ())
0 commit comments