test(auditlog): assert ExecutionId threading hops; defensive Guid parse on S&F read

This commit is contained in:
Joseph Doherty
2026-05-21 15:27:37 -04:00
parent 6f5a35f222
commit 705ae95404
6 changed files with 195 additions and 17 deletions

View File

@@ -396,6 +396,46 @@ public class StoreAndForwardStorageTests : IAsyncLifetime, IDisposable
Assert.Null(retrieved.SourceScript);
}
[Fact]
public async Task MalformedExecutionId_ReadsBackAsNull_DoesNotAbortRetrySweep()
{
// Defensive read path: a corrupt (non-null, non-GUID) execution_id must
// be treated as "no execution id" rather than throwing FormatException
// — a single bad row must not abort the whole GetMessagesForRetryAsync
// sweep, which reads many rows. Persist two due rows, then corrupt the
// execution_id of one directly in the DB.
var goodId = Guid.NewGuid();
var good = CreateMessage("good1", StoreAndForwardCategory.ExternalSystem);
good.ExecutionId = goodId;
good.LastAttemptAt = null; // due immediately
await _storage.EnqueueAsync(good);
var bad = CreateMessage("bad1", StoreAndForwardCategory.ExternalSystem);
bad.ExecutionId = Guid.NewGuid();
bad.LastAttemptAt = null; // due immediately
await _storage.EnqueueAsync(bad);
await using (var conn = new SqliteConnection($"Data Source={_dbName};Mode=Memory;Cache=Shared"))
{
await conn.OpenAsync();
await using var corrupt = conn.CreateCommand();
corrupt.CommandText =
"UPDATE sf_messages SET execution_id = 'not-a-guid' WHERE id = 'bad1';";
await corrupt.ExecuteNonQueryAsync();
}
// The sweep must not throw; the corrupt row reads back with a null
// ExecutionId, the well-formed row keeps its value.
var due = await _storage.GetMessagesForRetryAsync();
Assert.Null(Assert.Single(due, m => m.Id == "bad1").ExecutionId);
Assert.Equal(goodId, Assert.Single(due, m => m.Id == "good1").ExecutionId);
// The single-row read path is equally defensive.
var retrieved = await _storage.GetMessageByIdAsync("bad1");
Assert.NotNull(retrieved);
Assert.Null(retrieved!.ExecutionId);
}
[Fact]
public async Task InitializeAsync_IsIdempotent_WhenColumnsAlreadyExist()
{