@@ -50,10 +50,164 @@ def _create_and_provision_branch(self, name='Test Branch'):
5050 branch .refresh_from_db () # Refresh to get updated status
5151 return branch
5252
53+ def test_merge_basic_create (self ):
54+ """
55+ Test basic create operation.
56+ Verifies object is created in main and ObjectChange was tracked.
57+ """
58+ # Create branch
59+ branch = self ._create_and_provision_branch ()
60+
61+ # Create a request context for event tracking
62+ request = RequestFactory ().get (reverse ('home' ))
63+ request .id = uuid .uuid4 ()
64+ request .user = self .user
65+
66+ # In branch: create site
67+ with activate_branch (branch ), event_tracking (request ):
68+ site = Site .objects .create (name = 'Test Site' , slug = 'test-site' )
69+ site_id = site .id
70+
71+ # Verify ObjectChange was created in branch
72+ from django .contrib .contenttypes .models import ContentType
73+ site_ct = ContentType .objects .get_for_model (Site )
74+ changes = branch .get_unmerged_changes ().filter (
75+ changed_object_type = site_ct ,
76+ changed_object_id = site_id
77+ )
78+ self .assertEqual (changes .count (), 1 )
79+ self .assertEqual (changes .first ().action , 'create' )
80+
81+ # Merge branch
82+ branch .merge (user = self .user , commit = True )
83+
84+ # Verify site exists in main
85+ self .assertTrue (Site .objects .filter (id = site_id ).exists ())
86+ site = Site .objects .get (id = site_id )
87+ self .assertEqual (site .name , 'Test Site' )
88+ self .assertEqual (site .slug , 'test-site' )
89+
90+ def test_merge_basic_update (self ):
91+ """
92+ Test basic update operation.
93+ Verifies object is updated in main and ObjectChange was tracked.
94+ """
95+ # Create site in main
96+ site = Site .objects .create (name = 'Original Site' , slug = 'test-site' , description = 'Original' )
97+ site_id = site .id
98+
99+ # Create branch
100+ branch = self ._create_and_provision_branch ()
101+
102+ # Create a request context for event tracking
103+ request = RequestFactory ().get (reverse ('home' ))
104+ request .id = uuid .uuid4 ()
105+ request .user = self .user
106+
107+ # In branch: update site
108+ with activate_branch (branch ), event_tracking (request ):
109+ site = Site .objects .get (id = site_id )
110+ site .description = 'Updated'
111+ site .save ()
112+
113+ # Verify ObjectChange was created in branch
114+ from django .contrib .contenttypes .models import ContentType
115+ site_ct = ContentType .objects .get_for_model (Site )
116+ changes = branch .get_unmerged_changes ().filter (
117+ changed_object_type = site_ct ,
118+ changed_object_id = site_id
119+ )
120+ self .assertEqual (changes .count (), 1 )
121+ self .assertEqual (changes .first ().action , 'update' )
122+
123+ # Merge branch
124+ branch .merge (user = self .user , commit = True )
125+
126+ # Verify site is updated in main
127+ site = Site .objects .get (id = site_id )
128+ self .assertEqual (site .description , 'Updated' )
129+
130+ def test_merge_basic_delete (self ):
131+ """
132+ Test basic delete operation.
133+ Verifies object is deleted in main and ObjectChange was tracked.
134+ """
135+ # Create site in main
136+ site = Site .objects .create (name = 'Test Site' , slug = 'test-site' )
137+ site_id = site .id
138+
139+ # Create branch
140+ branch = self ._create_and_provision_branch ()
141+
142+ # Create a request context for event tracking
143+ request = RequestFactory ().get (reverse ('home' ))
144+ request .id = uuid .uuid4 ()
145+ request .user = self .user
146+
147+ # In branch: delete site
148+ with activate_branch (branch ), event_tracking (request ):
149+ Site .objects .get (id = site_id ).delete ()
150+
151+ # Verify ObjectChange was created in branch
152+ from django .contrib .contenttypes .models import ContentType
153+ site_ct = ContentType .objects .get_for_model (Site )
154+ changes = branch .get_unmerged_changes ().filter (
155+ changed_object_type = site_ct ,
156+ changed_object_id = site_id
157+ )
158+ self .assertEqual (changes .count (), 1 )
159+ self .assertEqual (changes .first ().action , 'delete' )
160+
161+ # Merge branch
162+ branch .merge (user = self .user , commit = True )
163+
164+ # Verify site is deleted in main
165+ self .assertFalse (Site .objects .filter (id = site_id ).exists ())
166+
167+ def test_merge_basic_create_update_delete (self ):
168+ """
169+ Test create, update, then delete same object.
170+ Verifies object is skipped (not in main) after collapsing.
171+ """
172+ # Create branch
173+ branch = self ._create_and_provision_branch ()
174+
175+ # Create a request context for event tracking
176+ request = RequestFactory ().get (reverse ('home' ))
177+ request .id = uuid .uuid4 ()
178+ request .user = self .user
179+
180+ # In branch: create, update, then delete site
181+ with activate_branch (branch ), event_tracking (request ):
182+ site = Site .objects .create (name = 'Temp Site' , slug = 'temp-site' )
183+ site_id = site .id
184+
185+ site .description = 'Modified'
186+ site .save ()
187+
188+ site .delete ()
189+
190+ # Verify 3 ObjectChanges were created in branch
191+ from django .contrib .contenttypes .models import ContentType
192+ site_ct = ContentType .objects .get_for_model (Site )
193+ changes = branch .get_unmerged_changes ().filter (
194+ changed_object_type = site_ct ,
195+ changed_object_id = site_id
196+ )
197+ self .assertEqual (changes .count (), 3 )
198+ actions = [c .action for c in changes .order_by ('time' )]
199+ self .assertEqual (actions , ['create' , 'update' , 'delete' ])
200+
201+ # Merge branch
202+ branch .merge (user = self .user , commit = True )
203+
204+ # Verify site does not exist in main (skipped during merge)
205+ self .assertFalse (Site .objects .filter (id = site_id ).exists ())
206+
53207 def test_merge_delete_then_create_same_slug (self ):
54208 """
55- Test merging when a site is deleted and a new site with the same slug is created .
56- This was the original bug: deletes must happen before creates to free up unique constraints.
209+ Test delete object, then create object with same unique constraint value (slug) .
210+ Verifies deletes free up unique constraints before creates .
57211 """
58212 # Create site in main
59213 site1 = Site .objects .create (name = 'Site 1' , slug = 'site-1' )
@@ -94,8 +248,8 @@ def test_merge_delete_then_create_same_slug(self):
94248
95249 def test_merge_create_device_and_delete_old (self ):
96250 """
97- Test merging when a new device is created and an old device is deleted .
98- Tests ordering with dependencies.
251+ Test create new object, then delete old object .
252+ Verifies proper ordering with cascade delete dependencies.
99253 """
100254 # Create device with interface in main
101255 site = Site .objects .create (name = 'Site 1' , slug = 'site-1' )
@@ -152,38 +306,10 @@ def test_merge_create_device_and_delete_old(self):
152306 self .assertTrue (Device .objects .filter (id = device_b_id ).exists ())
153307 self .assertTrue (Interface .objects .filter (id = interface_b_id ).exists ())
154308
155- def test_merge_create_and_delete_same_object (self ):
156- """
157- Test that creating and deleting the same object in a branch results in no change (skip).
158- """
159- # Create branch
160- branch = self ._create_and_provision_branch ()
161-
162- # Create a request context for event tracking
163- request = RequestFactory ().get (reverse ('home' ))
164- request .id = uuid .uuid4 () # Set request id for ObjectChange tracking
165- request .user = self .user
166-
167- # In branch: create and delete a site
168- with activate_branch (branch ), event_tracking (request ):
169- site = Site .objects .create (name = 'Temp Site' , slug = 'temp-site' )
170- site_id = site .id
171- site .delete ()
172-
173- # Merge branch
174- branch .merge (user = self .user , commit = True )
175-
176- # Verify main schema - site should not exist
177- self .assertFalse (Site .objects .filter (id = site_id ).exists ())
178-
179- # Verify merge succeeded
180- branch .refresh_from_db ()
181- self .assertEqual (branch .status , BranchStatusChoices .MERGED )
182-
183309 def test_merge_slug_rename_then_create (self ):
184310 """
185- Test merging when a site 's slug is changed , then a new site is created with the old slug .
186- Updates should happen before creates to free up the slug .
311+ Test update object 's unique field , then create new object with old value .
312+ Verifies updates free up unique constraints before creates .
187313 """
188314 # Create site in main
189315 site1 = Site .objects .create (name = 'Site 1' , slug = 'site-1' )
@@ -220,7 +346,8 @@ def test_merge_slug_rename_then_create(self):
220346
221347 def test_merge_multiple_updates_collapsed (self ):
222348 """
223- Test that multiple updates to the same object are collapsed into a single update.
349+ Test multiple updates to same object.
350+ Verifies consecutive non-referencing updates are collapsed.
224351 """
225352 # Create site in main
226353 site = Site .objects .create (name = 'Site 1' , slug = 'site-1' , description = 'Original' )
@@ -257,8 +384,8 @@ def test_merge_multiple_updates_collapsed(self):
257384
258385 def test_merge_create_with_multiple_updates (self ):
259386 """
260- Test that creating an object and then updating it multiple times
261- results in a single create with the final state .
387+ Test create object then update it multiple times.
388+ Verifies create is kept separate from updates .
262389 """
263390 # Create branch
264391 branch = self ._create_and_provision_branch ()
@@ -291,7 +418,8 @@ def test_merge_create_with_multiple_updates(self):
291418
292419 def test_merge_complex_dependency_chain (self ):
293420 """
294- Test a complex scenario with creates, updates, and deletes with dependencies.
421+ Test complex scenario with multiple creates, updates, and deletes.
422+ Verifies correct ordering with FK dependencies and references.
295423 """
296424 # Create initial devices in main
297425 site = Site .objects .create (name = 'Site 1' , slug = 'site-1' )
@@ -364,39 +492,90 @@ def test_merge_complex_dependency_chain(self):
364492 self .assertEqual (device_b .name , 'Device B Updated' )
365493 self .assertEqual (device_b .interfaces .count (), 2 )
366494
367- def test_merge_delete_ordering_by_time (self ):
495+ def test_merge_conflicting_slug_create_update_delete (self ):
368496 """
369- Test that deletes maintain time order when there are no dependencies.
497+ Test create object with conflicting unique constraint, update it, then delete it.
498+ Verifies skipped object doesn't cause constraint violations.
370499 """
371- # Create sites in main
372- site1 = Site .objects .create (name = 'Site 1' , slug = 'site-1' )
373- site2 = Site .objects .create (name = 'Site 2' , slug = 'site-2' )
374- site3 = Site .objects .create (name = 'Site 3' , slug = 'site-3' )
500+ # Create branch
501+ branch = self ._create_and_provision_branch ()
375502
376- site1_id = site1 .id
377- site2_id = site2 .id
378- site3_id = site3 .id
503+ # In main: create site with slug that will conflict
504+ site_main = Site .objects .create (name = 'Main Site' , slug = 'conflict-slug' )
505+ site_main_id = site_main .id
506+
507+ # Create a request context for event tracking
508+ request = RequestFactory ().get (reverse ('home' ))
509+ request .id = uuid .uuid4 ()
510+ request .user = self .user
511+
512+ # In branch: create site with same slug (conflicts), update it, then delete it
513+ with activate_branch (branch ), event_tracking (request ):
514+ site_branch = Site .objects .create (name = 'Branch Site' , slug = 'conflict-slug' )
515+ site_branch_id = site_branch .id
516+
517+ # Update description
518+ site_branch .description = 'Updated in branch'
519+ site_branch .save ()
520+
521+ # Delete the site
522+ site_branch .delete ()
523+
524+ # Merge branch - should succeed (branch site is skipped)
525+ branch .merge (user = self .user , commit = True )
379526
527+ # Verify main schema - only main site exists
528+ self .assertTrue (Site .objects .filter (id = site_main_id ).exists ())
529+ self .assertFalse (Site .objects .filter (id = site_branch_id ).exists ())
530+ self .assertEqual (Site .objects .filter (slug = 'conflict-slug' ).count (), 1 )
531+
532+ # Verify branch status
533+ branch .refresh_from_db ()
534+ self .assertEqual (branch .status , BranchStatusChoices .MERGED )
535+
536+ def test_merge_slug_update_causes_then_resolves_conflict (self ):
537+ """
538+ Test create object, update to conflicting unique constraint, then update to resolve.
539+ Verifies final non-conflicting state merges successfully.
540+ """
380541 # Create branch
381542 branch = self ._create_and_provision_branch ()
382543
544+ # In main: create site that will have slug conflict
545+ site_main = Site .objects .create (name = 'Main Site' , slug = 'conflict-slug' )
546+ site_main_id = site_main .id
547+
383548 # Create a request context for event tracking
384549 request = RequestFactory ().get (reverse ('home' ))
385- request .id = uuid .uuid4 () # Set request id for ObjectChange tracking
550+ request .id = uuid .uuid4 ()
386551 request .user = self .user
387552
388- # In branch: delete sites in specific order
553+ # In branch: create site with non-conflicting slug, update to conflict, then resolve
389554 with activate_branch (branch ), event_tracking (request ):
390- Site .objects .get (id = site1_id ).delete ()
391- Site .objects .get (id = site3_id ).delete ()
392- Site .objects .get (id = site2_id ).delete ()
555+ site_branch = Site .objects .create (name = 'Branch Site' , slug = 'no-conflict' )
556+ site_branch_id = site_branch .id
393557
394- # Merge branch
558+ # Update to conflicting slug
559+ site_branch .slug = 'conflict-slug'
560+ site_branch .save ()
561+
562+ # Update again to resolve conflict
563+ site_branch .slug = 'resolved-slug'
564+ site_branch .save ()
565+
566+ # Merge branch - should succeed (final state has no conflict)
395567 branch .merge (user = self .user , commit = True )
396568
397- # Verify all deleted
398- self .assertEqual (Site .objects .count (), 0 )
569+ # Verify main schema
570+ self .assertTrue (Site .objects .filter (id = site_main_id ).exists ())
571+ self .assertTrue (Site .objects .filter (id = site_branch_id ).exists ())
572+
573+ site_main = Site .objects .get (id = site_main_id )
574+ self .assertEqual (site_main .slug , 'conflict-slug' )
399575
400- # Verify merge succeeded
576+ site_branch = Site .objects .get (id = site_branch_id )
577+ self .assertEqual (site_branch .slug , 'resolved-slug' )
578+
579+ # Verify branch status
401580 branch .refresh_from_db ()
402581 self .assertEqual (branch .status , BranchStatusChoices .MERGED )
0 commit comments