elsa-core/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/EventPublisherTests.cs
Sipke Schoorstra 68a5aa6bd7
fix(runtime): honor transactional outbox for in-workflow PublishEvent (#8177)
* fix(runtime): route PublishEvent through transactional workflow-dispatch outbox

In-workflow async PublishEvent and IEventPublisher now dispatch via
IWorkflowDispatcher so TransactionalWorkflowDispatcher applies when
UseTransactionalOutbox is enabled. Fixes #8150.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* test(runtime): fix PublishEvent outbox conformance tests

Register KeyValueFeature so the dispatch outbox store can persist
TriggerWorkflows items, and avoid expression-tree pattern matching in
the EventPublisher unit tests.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* test(runtime): assert PublishEvent outbox delivery creates consumer

ProcessAsync only queues the trigger; start the background command
processor and wait until ConsumeOrderShippedEventWorkflow is created.

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-15 22:10:29 +02:00

74 lines
3.2 KiB
C#

using Elsa.Workflows.Helpers;
using Elsa.Workflows.Runtime.Activities;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.Runtime.Stimuli;
using NSubstitute;
namespace Elsa.Workflows.Runtime.UnitTests.Services;
public class EventPublisherTests
{
private readonly IStimulusSender _stimulusSender = Substitute.For<IStimulusSender>();
private readonly IWorkflowDispatcher _workflowDispatcher = Substitute.For<IWorkflowDispatcher>();
[Fact]
public async Task PublishAsync_WhenAsynchronous_DispatchesThroughWorkflowDispatcher()
{
var publisher = CreatePublisher();
const string eventName = "OrderShipped";
const string correlationId = "corr-1";
const string workflowInstanceId = "wf-1";
const string activityInstanceId = "act-1";
var payload = new { Status = "Shipped" };
await publisher.PublishAsync(eventName, correlationId, workflowInstanceId, activityInstanceId, payload, asynchronous: true);
await _workflowDispatcher.Received(1).DispatchAsync(
Arg.Is<DispatchTriggerWorkflowsRequest>(request => IsMatchingTriggerRequest(request, eventName, correlationId, workflowInstanceId, activityInstanceId, payload)),
Arg.Any<DispatchWorkflowOptions?>(),
Arg.Any<CancellationToken>());
await _stimulusSender.DidNotReceiveWithAnyArgs().SendAsync(default!, default!, default, default);
}
[Fact]
public async Task PublishAsync_WhenSynchronous_SendsThroughStimulusSender()
{
var publisher = CreatePublisher();
const string eventName = "OrderShipped";
var payload = new Dictionary<string, object> { ["Status"] = "Shipped" };
await publisher.PublishAsync(eventName, payload: payload, asynchronous: false);
await _stimulusSender.Received(1).SendAsync(
ActivityTypeNameHelper.GenerateTypeName<Event>(),
Arg.Is<EventStimulus>(stimulus => stimulus.EventName == eventName),
Arg.Is<StimulusMetadata>(metadata =>
metadata.Input != null &&
metadata.Input[Event.EventInputWorkflowInputKey] == payload),
Arg.Any<CancellationToken>());
await _workflowDispatcher.DidNotReceiveWithAnyArgs().DispatchAsync(default(DispatchTriggerWorkflowsRequest)!, default, default);
}
private EventPublisher CreatePublisher() => new(_stimulusSender, _workflowDispatcher);
private static bool IsMatchingTriggerRequest(
DispatchTriggerWorkflowsRequest request,
string eventName,
string correlationId,
string workflowInstanceId,
string activityInstanceId,
object payload)
{
var stimulus = request.BookmarkPayload as EventStimulus;
return request.ActivityTypeName == ActivityTypeNameHelper.GenerateTypeName<Event>() &&
stimulus != null &&
stimulus.EventName == eventName &&
request.CorrelationId == correlationId &&
request.WorkflowInstanceId == workflowInstanceId &&
request.ActivityInstanceId == activityInstanceId &&
request.Input != null &&
request.Input[Event.EventInputWorkflowInputKey] == payload;
}
}