11using SIL . Harmony . Sample . Models ;
2+ using SIL . Harmony . Sample . Changes ;
3+ using SIL . Harmony . Changes ;
24using Microsoft . EntityFrameworkCore ;
35
46namespace SIL . Harmony . Tests ;
@@ -19,7 +21,7 @@ public async Task MultipleChangesPreservesRootSnapshot()
1921 {
2022 var entityId = Guid . NewGuid ( ) ;
2123 var commits = new List < Commit > ( ) ;
22- for ( int i = 0 ; i < 4 ; i ++ )
24+ for ( var i = 0 ; i < 4 ; i ++ )
2325 {
2426 commits . Add ( await WriteChange ( _localClientId ,
2527 new DateTimeOffset ( 2000 , 1 , 1 , 0 , 0 , 0 , TimeSpan . Zero ) . AddHours ( i ) ,
@@ -39,7 +41,7 @@ public async Task MultipleChangesPreservesSomeIntermediateSnapshots()
3941 {
4042 var entityId = Guid . NewGuid ( ) ;
4143 var commits = new List < Commit > ( ) ;
42- for ( int i = 0 ; i < 6 ; i ++ )
44+ for ( var i = 0 ; i < 6 ; i ++ )
4345 {
4446 commits . Add ( await WriteChange ( _localClientId ,
4547 new DateTimeOffset ( 2000 , 1 , 1 , 0 , 0 , 0 , TimeSpan . Zero ) . AddHours ( i ) ,
@@ -135,7 +137,7 @@ await WriteNextChange(
135137 var tagCreation = await WriteNextChange ( TagWord ( wordId , tagId ) ) ;
136138 await WriteChangeBefore ( tagCreation , TagWord ( wordId , tagId ) ) ;
137139
138- var word = await DataModel . QueryLatest < Word > ( q=> q . Include ( w => w . Tags )
140+ var word = await DataModel . QueryLatest < Word > ( q => q . Include ( w => w . Tags )
139141 . Where ( w => w . Id == wordId ) ) . FirstOrDefaultAsync ( ) ;
140142 word . Should ( ) . NotBeNull ( ) ;
141143 word . Tags . Should ( ) . BeEquivalentTo ( [ new Tag { Id = tagId , Text = "tag-1" } ] ) ;
@@ -161,4 +163,70 @@ public async Task RegenerateSnapshots_WillArriveAtTheSameState()
161163 //we probably won't have the same number of snapshots, which is ok. but none of the ids should be the same
162164 afterSnapshotsIds . Should ( ) . NotIntersectWith ( beforeSnapshotIds ) ;
163165 }
166+
167+ [ Fact ]
168+ public async Task CanApplyChangeToDeletedEntityWithSupportsNewEntity ( )
169+ {
170+ var entityId = Guid . NewGuid ( ) ;
171+
172+ // Create a word with SetWordTextChange (which supports both NewEntity and ApplyChange)
173+ await WriteNextChange ( new SetWordTextChange ( entityId , "original" ) ) ;
174+ var word = await DataModel . GetLatest < Word > ( entityId ) ;
175+ word . Should ( ) . NotBeNull ( ) ;
176+ word ! . Text . Should ( ) . Be ( "original" ) ;
177+ word . DeletedAt . Should ( ) . BeNull ( ) ;
178+
179+ // Delete the word
180+ await WriteNextChange ( DeleteWord ( entityId ) ) ;
181+ var deletedWord = await DataModel . GetLatest < Word > ( entityId ) ;
182+ deletedWord . Should ( ) . NotBeNull ( "deleted entities should still exist in snapshots" ) ;
183+ deletedWord ! . DeletedAt . Should ( ) . NotBeNull ( ) ;
184+
185+ // Apply change to deleted entity using SetWordTextChange (which SupportsNewEntity)
186+ await WriteNextChange ( new SetWordTextChange ( entityId , "modified while deleted" ) ) ;
187+ var modifiedWord = await DataModel . GetLatest < Word > ( entityId ) ;
188+ modifiedWord . Should ( ) . NotBeNull ( "change should be applied to deleted entity" ) ;
189+ modifiedWord ! . Text . Should ( ) . Be ( "modified while deleted" , "text should be updated" ) ;
190+ modifiedWord . DeletedAt . Should ( ) . NotBeNull ( "entity should still be marked as deleted since SetWordTextChange doesn't clear DeletedAt" ) ;
191+ }
192+
193+ [ Fact ]
194+ public async Task SupportsNewEntityAllowsUpdatingDeletedEntities ( )
195+ {
196+ var entityId = Guid . NewGuid ( ) ;
197+
198+ // Create and delete
199+ await WriteNextChange ( new SetWordTextChange ( entityId , "original" ) ) ;
200+ await WriteNextChange ( DeleteWord ( entityId ) ) ;
201+
202+ var deletedWord = await DataModel . GetLatest < Word > ( entityId ) ;
203+ deletedWord . Should ( ) . NotBeNull ( "deleted entity should exist" ) ;
204+ deletedWord ! . DeletedAt . Should ( ) . NotBeNull ( "entity should be deleted" ) ;
205+
206+ // Update deleted entity using SetWordTextChange (which SupportsNewEntity)
207+ await WriteNextChange ( new SetWordTextChange ( entityId , "updated while deleted" ) ) ;
208+
209+ var finalWord = await DataModel . GetLatest < Word > ( entityId ) ;
210+ finalWord . Should ( ) . NotBeNull ( ) ;
211+ finalWord ! . Text . Should ( ) . Be ( "updated while deleted" , "text should be updated" ) ;
212+ finalWord . DeletedAt . Should ( ) . NotBeNull ( "entity should still be marked as deleted" ) ;
213+ }
214+
215+ [ Fact ]
216+ public void SupportsNewEntityMethodIndicatesChangeCapability ( )
217+ {
218+ // Test that different change types report their SupportsNewEntity capability correctly
219+ var setWordChange = new SetWordTextChange ( Guid . NewGuid ( ) , "test" ) ;
220+ var newWordChange = new NewWordChange ( Guid . NewGuid ( ) , "test" ) ;
221+ var deleteChange = new DeleteChange < Word > ( Guid . NewGuid ( ) ) ;
222+
223+ // SetWordTextChange is NOT a CreateChange<T> so it returns false even though it can create entities
224+ setWordChange . SupportsNewEntity ( ) . Should ( ) . BeFalse ( "SetWordTextChange is not a CreateChange<T>" ) ;
225+
226+ // NewWordChange (CreateChange) supports new entity creation but not applying to existing entities
227+ newWordChange . SupportsNewEntity ( ) . Should ( ) . BeTrue ( "NewWordChange can create new entities" ) ;
228+
229+ // DeleteChange doesn't support new entity creation (only applies to existing entities)
230+ deleteChange . SupportsNewEntity ( ) . Should ( ) . BeFalse ( "DeleteChange cannot create new entities" ) ;
231+ }
164232}
0 commit comments