From eab023d2f11f478db4d95baef66018b767f2e607 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 2 Jul 2026 10:22:26 +0000 Subject: [PATCH] Add unit test for note duplication --- src/backend/InvenTree/common/tests.py | 62 +++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index 69d39d3a6f..aade04e30f 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -1754,6 +1754,68 @@ class NotesImageTest(InvenTreeAPITestCase): self.assertTrue(NotesImage.objects.filter(pk=ni1.pk).exists()) self.assertTrue(default_storage.exists(name1)) + def test_copy_notes_with_images(self): + """Images are duplicated (file + DB record) when copy_notes_from is called. + + Specifically: + - New NotesImage records are created pointing to the new notes + - The image files are physically copied (independent from the source) + - The new note content references the new image URLs, not the old ones + - Deleting the source note does not affect the copied note's images + """ + # Build a minimal valid PNG in memory + img_obj = Image.new('RGB', (10, 10), color='green') + with io.BytesIO() as buf: + img_obj.save(buf, format='PNG') + png_bytes = buf.getvalue() + + part_ct = ContentType.objects.get_for_model(Part) + + src_part = Part.objects.create( + name='Copy Notes Source Part', + description='Source part for copy_notes_from test', + ) + dst_part = Part.objects.create( + name='Copy Notes Dest Part', + description='Destination part for copy_notes_from test', + ) + + src_note = Note( + model_type=part_ct, model_id=src_part.pk, title='Src Note', content='' + ) + src_note.save() + + ni = NotesImage(note=src_note) + ni.image.save('copy_test.png', ContentFile(png_bytes)) + old_url = ni.image.url + old_name = ni.image.name + + src_note.content = f'![img]({old_url})' + src_note.save() + + dst_part.copy_notes_from(src_part) + + dst_note = dst_part.notes_list.get(title='Src Note') + + # A new NotesImage must exist for the destination note + self.assertEqual(dst_note.images.count(), 1) + new_img = dst_note.images.first() + + # The file must be a distinct copy + self.assertNotEqual(new_img.image.name, old_name) + self.assertTrue(default_storage.exists(new_img.image.name)) + + # The new note content must reference the new URL, not the old one + self.assertIn(new_img.image.url, dst_note.content) + self.assertNotIn(old_url, dst_note.content) + + # Deleting the source NotesImage must not remove the copied image + # (files are independent; Django cascade does not call Python delete()) + ni.delete() + self.assertFalse(default_storage.exists(old_name)) + self.assertTrue(default_storage.exists(new_img.image.name)) + self.assertTrue(NotesImage.objects.filter(pk=new_img.pk).exists()) + class ProjectCodesTest(InvenTreeAPITestCase): """Units tests for the ProjectCodes model and API endpoints."""