Both tests read a value that is usually one thing and occasionally another, with a race deciding which. ReloadTests: EndpointSecurityOptions.SecurityIsEnabled is a process- global static, and ShellsApiTestBase saved/set/restored it per test method. ReloadTests and ReloadAllTests carry no [Collection], so xUnit runs them in parallel. FastEndpoints reads that global once per host while UseFastEndpoints() configures the endpoints, so when one class's DisposeAsync restores true inside another class's set-false -> UseFastEndpoints() window, that host's endpoints get authorization metadata in a pipeline with no UseAuthorization, and every request to them throws. Every test in the assembly wants security off, so set it once in a module initializer and stop mutating it per test. PublishEvent_WithPayload_TransmitsPayloadToConsumer: the payload's representation is not stable. While it is still the original CLR object its properties are PascalCase; once it has been through JsonWorkflowStateSerializer it is an ExpandoObject whose keys were camelCased by that serializer's naming policy. Which one the test sees depends on whether GetSingleWorkflowInstanceAsync returned the live in-memory instance or one read back from the store, and TryGetProperty is case-sensitive. Assert the payload's content through a DTO with PropertyNameCaseInsensitive instead of one of the two representations. Also require a terminal instance at both exits of GetSingleWorkflowInstanceAsync: it accepted any save, and an instance is saved several times over its lifetime, so it could hand a caller that asserts Finished an instance that is still running. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.4 KiB
C#
66 lines
2.4 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using CShells.Lifecycle;
|
|
using Elsa.Shells.Api.ShellFeatures;
|
|
using Elsa.Workflows;
|
|
using FastEndpoints;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NSubstitute;
|
|
|
|
namespace Elsa.Shells.Api.Tests;
|
|
|
|
public abstract class ShellsApiTestBase : IAsyncLifetime
|
|
{
|
|
private WebApplication? _app;
|
|
|
|
protected IShellRegistry ShellRegistry { get; } = Substitute.For<IShellRegistry>();
|
|
protected HttpClient HttpClient { get; private set; } = null!;
|
|
|
|
// Shared options that match the mock IApiSerializer — used when deserializing responses in tests.
|
|
protected static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
Converters = { new JsonStringEnumConverter() }
|
|
};
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
var apiSerializer = Substitute.For<IApiSerializer>();
|
|
apiSerializer.GetOptions().Returns(JsonOptions);
|
|
|
|
var builder = WebApplication.CreateSlimBuilder();
|
|
builder.WebHost.UseTestServer();
|
|
|
|
builder.Services.AddFastEndpoints(o => o.Assemblies = [typeof(ShellsApiFeature).Assembly]);
|
|
builder.Services.AddSingleton(ShellRegistry);
|
|
builder.Services.AddSingleton(apiSerializer);
|
|
builder.Services.AddLogging();
|
|
|
|
// Default behavior: every reload succeeds. Tests override per-call by re-stubbing on the substitute.
|
|
ShellRegistry.ReloadActiveAsync(Arg.Any<ReloadOptions?>(), Arg.Any<CancellationToken>())
|
|
.Returns(_ => Task.FromResult<IReadOnlyList<ReloadResult>>(Array.Empty<ReloadResult>()));
|
|
ShellRegistry.ReloadAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(ci => Task.FromResult(new ReloadResult(ci.Arg<string>(), null, null, null)));
|
|
|
|
_app = builder.Build();
|
|
_app.UseFastEndpoints();
|
|
|
|
await _app.StartAsync();
|
|
HttpClient = _app.GetTestClient();
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
HttpClient.Dispose();
|
|
if (_app != null)
|
|
{
|
|
await _app.StopAsync();
|
|
await _app.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
// Local DTO mirroring the shape of the internal ShellReloadResponse returned by the endpoints.
|
|
protected record ShellReloadResult(string Status, string? Message, string? RequestedShellId);
|
|
}
|