Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/DaemonTests/EventProjections/scenario_trailing_action_tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Threading.Tasks;
using JasperFx.Events.Projections;
using Marten;
using Marten.Testing.Documents;
using Marten.Testing.Harness;
using Shouldly;
using Xunit;

namespace DaemonTests.EventProjections;

// #5126: a ScenarioAction only flushed when the NEXT queued step was an assertion, so anything a
// trailing action appended was still in the session when Execute's finally disposed it. The events
// were silently dropped, and an arrange-only scenario was a no-op that passed.
public class scenario_trailing_action_tests: OneOffConfigurationsContext
{
[Fact]
public async Task trailing_append_is_committed()
{
StoreOptions(opts => opts.Projections.Add(new UserProjection(), ProjectionLifecycle.Inline));

var flushed = Guid.NewGuid();
var trailing = Guid.NewGuid();

await theStore.Advanced.EventProjectionScenario(scenario =>
{
// An assertion follows this one, so it flushes through the pre-existing path.
scenario.Append(Guid.NewGuid(), new CreateUser { UserId = flushed, UserName = "Flushed" });
scenario.DocumentShouldExist<User>(flushed);

// Nothing follows this one.
scenario.Append(Guid.NewGuid(), new CreateUser { UserId = trailing, UserName = "Trailing" });
});

await using var query = theStore.QuerySession();
(await query.LoadAsync<User>(trailing)).ShouldNotBeNull();
}

[Fact]
public async Task arrange_only_scenario_actually_writes()
{
StoreOptions(opts => opts.Projections.Add(new UserProjection(), ProjectionLifecycle.Inline));

var id = Guid.NewGuid();

// No assertions at all -- every step is an action, so nothing used to be committed.
await theStore.Advanced.EventProjectionScenario(scenario =>
{
scenario.Append(Guid.NewGuid(), new CreateUser { UserId = id, UserName = "Arranged" });
});

await using var query = theStore.QuerySession();
var user = await query.LoadAsync<User>(id);
user.ShouldNotBeNull();
user.UserName.ShouldBe("Arranged");
}
}
19 changes: 19 additions & 0 deletions src/Marten/Events/TestSupport/ProjectionScenario.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,25 @@ internal async Task Execute(CancellationToken ct = default)
}
}

// A ScenarioAction only flushes when the step AFTER it is an assertion, so whatever a
// trailing action queued is still sitting in the session -- and the finally below disposes
// that session without committing. An append with no assertion after it is still an append,
// and an arrange-only scenario should not be a silent no-op that passes. See #5126.
//
// Unconditional on purpose: SaveChangesAsync returns immediately when the unit of work is
// empty, and WaitForNonStaleData is already a no-op when no daemon is running.
try
{
await Session.SaveChangesAsync(ct).ConfigureAwait(false);
await WaitForNonStaleData().ConfigureAwait(false);
}
catch (Exception e)
{
descriptions.Add($"FAILED: committing the events queued by the final step");
descriptions.Add(e.ToString());
exceptions.Add(e);
}

if (exceptions.Any())
{
throw new ProjectionScenarioException(descriptions, exceptions);
Expand Down
Loading