@@ -140,4 +140,42 @@ public async Task DeleteOldEntriesAsync_CompletesGracefully_WhenNothingToDelete(
140140
141141 deleted . Should ( ) . Be ( 0 ) ;
142142 }
143+
144+ [ Fact ]
145+ public async Task DeleteOldEntriesAsync_WithNonUtcCutoff_DeletesCorrectEntries ( )
146+ {
147+ // Regression test for: DateTimeOffset with non-zero UTC offset must be normalized
148+ // to UTC before formatting as a SQLite timestamp string. Without normalization,
149+ // a cutoff of e.g. 2026-01-01T00:00:00-05:00 would be formatted as
150+ // "2026-01-01 00:00:00+00:00" instead of "2026-01-01 05:00:00+00:00",
151+ // causing entries in the wrong five-hour window to be kept or deleted.
152+ using var scope = _factory . Services . CreateScope ( ) ;
153+ var db = scope . ServiceProvider . GetRequiredService < TaskdeckDbContext > ( ) ;
154+ var repo = scope . ServiceProvider . GetRequiredService < IAuditLogRepository > ( ) ;
155+
156+ var oldEntry = new AuditLog ( "Board" , Guid . NewGuid ( ) , AuditAction . Created ) ;
157+ var recentEntry = new AuditLog ( "Board" , Guid . NewGuid ( ) , AuditAction . Created ) ;
158+ db . AuditLogs . AddRange ( oldEntry , recentEntry ) ;
159+ await db . SaveChangesAsync ( ) ;
160+
161+ // Backdate the old entry to well before the cutoff
162+ var oldTimestampStr = DateTimeOffset . UtcNow . AddDays ( - 100 ) . ToString ( "yyyy-MM-dd HH:mm:ss.fffffff+00:00" ) ;
163+ await db . Database . ExecuteSqlRawAsync (
164+ "UPDATE AuditLogs SET Timestamp = {0} WHERE Id = {1}" ,
165+ oldTimestampStr , oldEntry . Id ) ;
166+
167+ // Build a non-UTC cutoff that represents the same instant as "30 days ago UTC"
168+ // expressed in Eastern Standard Time (-05:00). The repository must convert to
169+ // UTC before building the SQLite comparison string.
170+ var utcBase = DateTimeOffset . UtcNow . AddDays ( - 30 ) ;
171+ var nonUtcCutoff = utcBase . ToOffset ( TimeSpan . FromHours ( - 5 ) ) ;
172+ var deleted = await repo . DeleteOldEntriesAsync ( nonUtcCutoff , batchSize : 1000 ) ;
173+
174+ deleted . Should ( ) . BeGreaterOrEqualTo ( 1 ,
175+ "entries older than the UTC-equivalent cutoff should be deleted regardless of the cutoff offset" ) ;
176+
177+ // The recent entry (created just now) must survive
178+ var preserved = await repo . GetByIdAsync ( recentEntry . Id ) ;
179+ preserved . Should ( ) . NotBeNull ( "the recent entry must not be deleted when a non-UTC cutoff is used" ) ;
180+ }
143181}
0 commit comments