From 79d0291e218d5d2718e10dcbc8b064955a61082b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Fri, 10 Apr 2026 12:20:14 +0200 Subject: [PATCH 01/25] test: Added Integration Tests for Outbox --- Directory.Packages.props | 10 ++++-- .../EntityFrameworkExtensions.cs | 17 +++------- .../Internals/DatabaseType.cs | 10 ++++++ .../Internals/IDatabaseServiceFixture.cs | 25 +++++++++++++++ .../InternalTestWebApplicationFactory.cs | 6 ++++ .../Internals/SQLiteDatabaseServiceFixture.cs | 31 +++++++++++++++++++ .../SqlServerDatabaseServiceFixture.cs | 21 +++++++++++++ .../Internals/WebApplicationTestBase.cs | 7 +++++ .../NetEvolve.Pulse.Tests.Integration.csproj | 6 ++++ .../Outbox/OutboxTests.cs | 31 +++++++++++++++++++ 10 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/DatabaseType.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 6473ee32..7279b228 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,9 +44,10 @@ - - - + + + + @@ -54,6 +55,7 @@ + @@ -61,6 +63,7 @@ + @@ -68,6 +71,7 @@ + diff --git a/src/NetEvolve.Pulse.EntityFramework/EntityFrameworkExtensions.cs b/src/NetEvolve.Pulse.EntityFramework/EntityFrameworkExtensions.cs index 6e53c000..f73c5bd4 100644 --- a/src/NetEvolve.Pulse.EntityFramework/EntityFrameworkExtensions.cs +++ b/src/NetEvolve.Pulse.EntityFramework/EntityFrameworkExtensions.cs @@ -58,29 +58,22 @@ public static IMediatorBuilder AddEntityFrameworkOutbox( { ArgumentNullException.ThrowIfNull(configurator); - var services = configurator.Services; - - _ = services.AddOptions(); - - // Register options if configureOptions is provided - if (configureOptions is not null) - { - _ = services.Configure(configureOptions); - } - - // Ensure TimeProvider is registered - services.TryAddSingleton(TimeProvider.System); + var services = configurator.AddOutbox(configureOptions).Services; // Register the repository + _ = services.RemoveAll(); _ = services.AddScoped>(); // Register the event outbox (overrides the default OutboxEventStore) + _ = services.RemoveAll(); _ = services.AddScoped>(); // Register the transaction scope + _ = services.RemoveAll(); _ = services.AddScoped>(); // Register the management API + _ = services.RemoveAll(); _ = services.AddScoped>(); return configurator; diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/DatabaseType.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/DatabaseType.cs new file mode 100644 index 00000000..5d63f126 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/DatabaseType.cs @@ -0,0 +1,10 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +public enum DatabaseType +{ + InMemory, + SqlServer, + SQLite, + PostgreSQL, + MySql, +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs new file mode 100644 index 00000000..250ea5b4 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs @@ -0,0 +1,25 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using TUnit.Core.Interfaces; + +public interface IDatabaseServiceFixture : IAsyncDisposable, IAsyncInitializer +{ + string ConnectionString { get; } + + DatabaseType DatabaseType { get; } +} + +public interface IDatabaseInitializer +{ + ValueTask InitializeAsync(IDatabaseServiceFixture databaseService); +} + +public sealed class EntityFrameworkInitializer : IDatabaseInitializer +{ + public ValueTask InitializeAsync(IDatabaseServiceFixture databaseService) => throw new NotImplementedException(); +} + +public sealed class AdoNetDatabaseInitializer : IDatabaseInitializer +{ + public ValueTask InitializeAsync(IDatabaseServiceFixture databaseService) => throw new NotImplementedException(); +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs new file mode 100644 index 00000000..9a6eb0c7 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs @@ -0,0 +1,6 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using TUnit.AspNetCore; + +public sealed class InternalTestWebApplicationFactory : TestWebApplicationFactory + where TEntryPoint : class { } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs new file mode 100644 index 00000000..9bd47432 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs @@ -0,0 +1,31 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +public sealed class SQLiteDatabaseServiceFixture : IDatabaseServiceFixture +{ + public string ConnectionString => $"Data Source={DatabaseFile};"; + + public DatabaseType DatabaseType => DatabaseType.SQLite; + + private string DatabaseFile { get; } = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".sqlite"); + + public ValueTask DisposeAsync() + { + if (!File.Exists(DatabaseFile)) + { + return ValueTask.CompletedTask; + } + + try + { + File.Delete(DatabaseFile); + } + catch (IOException) + { + // Best-effort cleanup for temporary test database files. + } + + return ValueTask.CompletedTask; + } + + public Task InitializeAsync() => Task.CompletedTask; +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs new file mode 100644 index 00000000..0e05b42f --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs @@ -0,0 +1,21 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using Microsoft.Extensions.Logging.Abstractions; +using Testcontainers.MsSql; + +public sealed class SqlServerDatabaseServiceFixture : IDatabaseServiceFixture +{ + private readonly MsSqlContainer _container = new MsSqlBuilder( + /*dockerimage*/"mcr.microsoft.com/mssql/server:2022-RTM-ubuntu-20.04" + ) + .WithLogger(NullLogger.Instance) + .Build(); + + public string ConnectionString => _container.GetConnectionString(); + + public DatabaseType DatabaseType => DatabaseType.SqlServer; + + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + public Task InitializeAsync() => _container.StartAsync(); +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs new file mode 100644 index 00000000..dc779739 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs @@ -0,0 +1,7 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using TUnit.AspNetCore; + +public abstract class WebApplicationTestBase + : WebApplicationTest, TEntryPoint> + where TEntryPoint : class { } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj index 33f48f6d..e8ef9d63 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj +++ b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj @@ -15,6 +15,7 @@ + @@ -28,6 +29,7 @@ + @@ -65,4 +67,8 @@ + + + + diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs new file mode 100644 index 00000000..36759643 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs @@ -0,0 +1,31 @@ +namespace NetEvolve.Pulse.Tests.Integration.EntityFramework; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[TestGroup("Outbox")] +[ClassDataSource( + Shared = [SharedType.PerClass, SharedType.PerTestSession] +)] +[ClassDataSource( + Shared = [SharedType.PerClass, SharedType.PerTestSession] +)] +public class OutboxTests +{ + public IDatabaseServiceFixture DatabaseServiceFixture { get; } + public IDatabaseInitializer DatabaseInitializer { get; } + + public OutboxTests(IDatabaseServiceFixture databaseServiceFixture, IDatabaseInitializer databaseInitializer) + { + DatabaseServiceFixture = databaseServiceFixture; + DatabaseInitializer = databaseInitializer; + } + + [Test] + [CombinedDataSources] + public async Task Can_Combine_Outbox_Setup() + { + await Assert.That(DatabaseServiceFixture).IsNotNull(); + await Assert.That(DatabaseInitializer).IsNotNull(); + } +} From 675af0c2d6341242347c92fadf54b8c9cebd32f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Fri, 10 Apr 2026 23:40:49 +0200 Subject: [PATCH 02/25] refactor: outbox transport, EF tests, and SQLite storage - Replace InMemoryMessageTransport with NullMessageTransport as the default; remove in-memory transport and its tests - Register NullMessageTransport in DI and update docs - Add unit tests for NullMessageTransport - Refactor EF test infrastructure: new IDatabaseInitializer, improved EntityFrameworkInitializer, internalize fixtures, and add PulseTestsBase/OutboxTestsBase - Add SQLite+EF outbox integration tests and verified output - Store DateTimeOffset columns as INTEGER (UTC ticks) in SQLite for correct ordering - Add test dependencies: EFCore.InMemory, TimeProvider.Testing, Verify.ParametersHashing - Remove obsolete OutboxTests and related code --- Directory.Packages.props | 4 +- .../SqliteOutboxMessageConfiguration.cs | 34 +- .../Outbox/InMemoryMessageTransport.cs | 100 ------ .../Outbox/NullMessageTransport.cs | 18 ++ src/NetEvolve.Pulse/OutboxExtensions.cs | 6 +- .../Internals/EntityFrameworkInitializer.cs | 85 +++++ .../Internals/IDatabaseInitializer.cs | 13 + .../Internals/IDatabaseServiceFixture.cs | 15 - .../InMemoryDatabaseServiceFixture.cs | 12 + .../Internals/PulseTestsBase.cs | 67 ++++ .../Internals/SQLiteDatabaseServiceFixture.cs | 2 +- .../NetEvolve.Pulse.Tests.Integration.csproj | 3 + .../Outbox/OutboxTests.cs | 31 -- .../Outbox/OutboxTestsBase.cs | 68 ++++ .../SQLiteEntityFrameworkOutboxTests.cs | 15 + ...ted_Messages_dda0938409f8d1d7.verified.txt | 26 ++ .../Outbox/InMemoryMessageTransportTests.cs | 300 ------------------ .../Outbox/NullMessageTransportTests.cs | 64 ++++ 18 files changed, 407 insertions(+), 456 deletions(-) delete mode 100644 src/NetEvolve.Pulse/Outbox/InMemoryMessageTransport.cs create mode 100644 src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs delete mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SQLiteEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_dda0938409f8d1d7.verified.txt delete mode 100644 tests/NetEvolve.Pulse.Tests.Unit/Outbox/InMemoryMessageTransportTests.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7279b228..2422d26b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -30,6 +30,7 @@ + @@ -48,6 +49,7 @@ + @@ -74,4 +76,4 @@ - + \ No newline at end of file diff --git a/src/NetEvolve.Pulse.EntityFramework/Configurations/SqliteOutboxMessageConfiguration.cs b/src/NetEvolve.Pulse.EntityFramework/Configurations/SqliteOutboxMessageConfiguration.cs index 74b5dd74..537c44c0 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Configurations/SqliteOutboxMessageConfiguration.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Configurations/SqliteOutboxMessageConfiguration.cs @@ -73,11 +73,35 @@ protected override void ApplyColumnTypes(EntityTypeBuilder builde _ = builder.Property(m => m.EventType).HasColumnType("TEXT"); _ = builder.Property(m => m.Payload).HasColumnType("TEXT"); _ = builder.Property(m => m.CorrelationId).HasColumnType("TEXT"); - // DateTimeOffset is stored as ISO-8601 TEXT by EF Core SQLite. - _ = builder.Property(m => m.CreatedAt).HasColumnType("TEXT"); - _ = builder.Property(m => m.UpdatedAt).HasColumnType("TEXT"); - _ = builder.Property(m => m.ProcessedAt).HasColumnType("TEXT"); - _ = builder.Property(m => m.NextRetryAt).HasColumnType("TEXT"); + + // DateTimeOffset columns are stored as INTEGER (UTC ticks) in SQLite. + // EF Core SQLite refuses to translate DateTimeOffset comparisons and ORDER BY + // when the column is TEXT because ISO-8601 string ordering is incorrect for + // values with non-UTC offsets. Storing as long (UTC ticks) allows EF Core to + // generate correct INTEGER comparisons and orderings in SQL. + _ = builder + .Property(m => m.CreatedAt) + .HasColumnType("INTEGER") + .HasConversion(v => v.UtcTicks, v => new DateTimeOffset(v, TimeSpan.Zero)); + _ = builder + .Property(m => m.UpdatedAt) + .HasColumnType("INTEGER") + .HasConversion(v => v.UtcTicks, v => new DateTimeOffset(v, TimeSpan.Zero)); + _ = builder + .Property(m => m.ProcessedAt) + .HasColumnType("INTEGER") + .HasConversion( + v => v.HasValue ? (long?)v.Value.UtcTicks : null, + v => v.HasValue ? (DateTimeOffset?)new DateTimeOffset(v.Value, TimeSpan.Zero) : null + ); + _ = builder + .Property(m => m.NextRetryAt) + .HasColumnType("INTEGER") + .HasConversion( + v => v.HasValue ? (long?)v.Value.UtcTicks : null, + v => v.HasValue ? (DateTimeOffset?)new DateTimeOffset(v.Value, TimeSpan.Zero) : null + ); + _ = builder.Property(m => m.RetryCount).HasColumnType("INTEGER"); _ = builder.Property(m => m.Error).HasColumnType("TEXT"); _ = builder.Property(m => m.Status).HasColumnType("INTEGER"); diff --git a/src/NetEvolve.Pulse/Outbox/InMemoryMessageTransport.cs b/src/NetEvolve.Pulse/Outbox/InMemoryMessageTransport.cs deleted file mode 100644 index 0dca9e71..00000000 --- a/src/NetEvolve.Pulse/Outbox/InMemoryMessageTransport.cs +++ /dev/null @@ -1,100 +0,0 @@ -namespace NetEvolve.Pulse.Outbox; - -using System.Collections.Concurrent; -using System.Linq.Expressions; -using System.Text.Json; -using Microsoft.Extensions.Options; -using NetEvolve.Pulse.Extensibility; -using NetEvolve.Pulse.Extensibility.Outbox; - -/// -/// Default message transport that dispatches outbox messages through the mediator. -/// Deserializes events and publishes them in-process for handler execution. -/// -/// -/// Use Case: -/// Use this transport when events should be handled in the same process that stored them. -/// The outbox pattern still provides reliability by persisting events before processing. -/// Serialization: -/// Events are deserialized using System.Text.Json with optional custom settings from . -/// The property contains the runtime type of the event. -/// Error Handling: -/// Exceptions from event handlers propagate to the outbox processor for retry handling. -/// -internal sealed class InMemoryMessageTransport : IMessageTransport -{ - /// Cache of compiled publish delegates keyed by concrete event type, avoiding per-message reflection overhead. - private static readonly ConcurrentDictionary< - Type, - Func - > PublishDelegates = new(); - - /// The mediator used to publish deserialized events in-process. - private readonly IMediator _mediator; - - /// The resolved outbox options, providing JSON serialization settings for event deserialization. - private readonly OutboxOptions _options; - - /// - /// Initializes a new instance of the class. - /// - /// The mediator for publishing events. - /// The outbox options containing serialization settings. - public InMemoryMessageTransport(IMediator mediator, IOptions options) - { - ArgumentNullException.ThrowIfNull(mediator); - ArgumentNullException.ThrowIfNull(options); - - _mediator = mediator; - _options = options.Value; - } - - /// - public async Task SendAsync(OutboxMessage message, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(message); - - var eventType = message.EventType; - - var @event = - JsonSerializer.Deserialize(message.Payload, eventType, _options.JsonSerializerOptions) - ?? throw new InvalidOperationException($"Failed to deserialize event payload for type: {eventType}"); - - if (@event is not IEvent typedEvent) - { - throw new InvalidOperationException($"Deserialized object is not an IEvent: {eventType}"); - } - - await PublishEventAsync(typedEvent, cancellationToken).ConfigureAwait(false); - } - - /// - /// Resolves a compiled delegate for the concrete event type (cached after first call) and invokes - /// , eliminating per-message reflection overhead. - /// - /// The deserialized event to publish through the mediator. - /// A token to monitor for cancellation requests. - /// A task representing the asynchronous publish operation. - private Task PublishEventAsync(IEvent @event, CancellationToken cancellationToken) => - PublishDelegates.GetOrAdd(@event.GetType(), CreatePublishDelegate)(_mediator, @event, cancellationToken); - - /// - /// Builds a compiled expression-tree delegate that calls - /// with the given concrete . Called once per event type and cached. - /// - /// The concrete event type for which to create a publish delegate. - /// A compiled delegate that can publish events of the specified type through the mediator. - private static Func CreatePublishDelegate(Type eventType) - { - var mediatorParam = Expression.Parameter(typeof(IMediator), "mediator"); - var eventParam = Expression.Parameter(typeof(IEvent), "event"); - var tokenParam = Expression.Parameter(typeof(CancellationToken), "cancellationToken"); - - var method = typeof(IMediator).GetMethod(nameof(IMediator.PublishAsync))!.MakeGenericMethod(eventType); - var call = Expression.Call(mediatorParam, method, Expression.Convert(eventParam, eventType), tokenParam); - - return Expression - .Lambda>(call, mediatorParam, eventParam, tokenParam) - .Compile(); - } -} diff --git a/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs b/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs new file mode 100644 index 00000000..2efd56e0 --- /dev/null +++ b/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs @@ -0,0 +1,18 @@ +namespace NetEvolve.Pulse.Outbox; + +using NetEvolve.Pulse.Extensibility.Outbox; + +/// +/// A no-op implementation that silently discards every message. +/// +/// +/// Use Case: +/// Registered as the default transport by AddOutbox. +/// Replace it by calling with a concrete +/// transport such as the Dapr or RabbitMQ transport. +/// +internal sealed class NullMessageTransport : IMessageTransport +{ + /// + public Task SendAsync(OutboxMessage message, CancellationToken cancellationToken = default) => Task.CompletedTask; +} diff --git a/src/NetEvolve.Pulse/OutboxExtensions.cs b/src/NetEvolve.Pulse/OutboxExtensions.cs index 9e807a2c..c4971cd8 100644 --- a/src/NetEvolve.Pulse/OutboxExtensions.cs +++ b/src/NetEvolve.Pulse/OutboxExtensions.cs @@ -14,7 +14,7 @@ public static class OutboxExtensions { /// /// Adds core outbox services including the implementation, - /// , and . + /// , and . /// /// The mediator configurator. /// Optional action to configure . @@ -27,7 +27,7 @@ public static class OutboxExtensions /// - Processor options (Singleton) /// as (Scoped) /// as (Scoped, open-generic) - /// as (Singleton) + /// as (Singleton) /// as (Singleton) /// (Hosted service) /// - System time provider (Singleton) @@ -66,7 +66,7 @@ public static IMediatorBuilder AddOutbox( // Register core services services.TryAddScoped(); services.TryAddEnumerable(ServiceDescriptor.Scoped(typeof(IEventHandler<>), typeof(OutboxEventHandler<>))); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); // Register background processor (TryAddEnumerable prevents duplicate registrations) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs new file mode 100644 index 00000000..017f1816 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -0,0 +1,85 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using System.Data.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Pulse.Extensibility; +using NetEvolve.Pulse.Extensibility.Outbox; +using NetEvolve.Pulse.Outbox; + +internal sealed class EntityFrameworkInitializer : IDatabaseInitializer +{ + public void Configure(IMediatorBuilder mediatorBuilder, IDatabaseServiceFixture databaseService) => + mediatorBuilder.AddEntityFrameworkOutbox(); + + public async ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider) + { + using (var scope = serviceProvider.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + + if (await context.Database.CanConnectAsync().ConfigureAwait(false)) + { + _ = await context.Database.EnsureDeletedAsync().ConfigureAwait(false); + } + return await context.Database.EnsureCreatedAsync().ConfigureAwait(false); + } + } + + public void Initialize(IServiceCollection services, IDatabaseServiceFixture databaseService) => + _ = services.AddDbContext(options => + { + _ = databaseService.DatabaseType switch + { + DatabaseType.InMemory => options.UseInMemoryDatabase(databaseService.ConnectionString), + // Add a busy-timeout interceptor so that concurrent SaveChangesAsync calls from + // parallel PublishAsync tasks wait and retry instead of failing with SQLITE_BUSY. + DatabaseType.SQLite => options + .UseSqlite(databaseService.ConnectionString) + .AddInterceptors(new SQLiteBusyTimeoutInterceptor()), + _ => throw new NotSupportedException($"Database type {databaseService.DatabaseType} is not supported."), + }; + }); + + /// + /// Sets PRAGMA busy_timeout on every SQLite connection when it is opened so that + /// concurrent write operations wait and retry rather than immediately failing with SQLITE_BUSY. + /// + private sealed class SQLiteBusyTimeoutInterceptor : DbConnectionInterceptor + { + private const string BusyTimeoutPragma = "PRAGMA busy_timeout = 60000;"; // 60 s + + public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) + { + using var command = connection.CreateCommand(); + command.CommandText = BusyTimeoutPragma; + _ = command.ExecuteNonQuery(); + } + + public override async Task ConnectionOpenedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default + ) + { + await using var command = connection.CreateCommand(); + command.CommandText = BusyTimeoutPragma; + _ = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + } + + private sealed class TestDbContext : DbContext, IOutboxDbContext + { + public DbSet OutboxMessages => Set(); + + public TestDbContext(DbContextOptions configuration) + : base(configuration) { } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + _ = modelBuilder.ApplyPulseConfiguration(this); + } + } +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs new file mode 100644 index 00000000..40c7d827 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs @@ -0,0 +1,13 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Pulse.Extensibility; + +public interface IDatabaseInitializer +{ + void Configure(IMediatorBuilder mediatorBuilder, IDatabaseServiceFixture databaseService); + + ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider); + + void Initialize(IServiceCollection services, IDatabaseServiceFixture databaseService); +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs index 250ea5b4..2431f197 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseServiceFixture.cs @@ -8,18 +8,3 @@ public interface IDatabaseServiceFixture : IAsyncDisposable, IAsyncInitializer DatabaseType DatabaseType { get; } } - -public interface IDatabaseInitializer -{ - ValueTask InitializeAsync(IDatabaseServiceFixture databaseService); -} - -public sealed class EntityFrameworkInitializer : IDatabaseInitializer -{ - public ValueTask InitializeAsync(IDatabaseServiceFixture databaseService) => throw new NotImplementedException(); -} - -public sealed class AdoNetDatabaseInitializer : IDatabaseInitializer -{ - public ValueTask InitializeAsync(IDatabaseServiceFixture databaseService) => throw new NotImplementedException(); -} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs new file mode 100644 index 00000000..741dddb4 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs @@ -0,0 +1,12 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +internal sealed class InMemoryDatabaseServiceFixture : IDatabaseServiceFixture +{ + public string ConnectionString => Guid.NewGuid().ToString("N"); + + public DatabaseType DatabaseType => DatabaseType.InMemory; + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + public Task InitializeAsync() => Task.CompletedTask; +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs new file mode 100644 index 00000000..de68dbcb --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs @@ -0,0 +1,67 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NetEvolve.Pulse.Extensibility; +using NetEvolve.Pulse.Outbox; + +public abstract class PulseTestsBase +{ + protected IDatabaseServiceFixture DatabaseServiceFixture { get; } + protected IDatabaseInitializer DatabaseInitializer { get; } + + protected static DateTimeOffset TestDateTime { get; } = new DateTimeOffset(2025, 1, 1, 12, 0, 0, 0, TimeSpan.Zero); + + protected PulseTestsBase(IDatabaseServiceFixture databaseServiceFixture, IDatabaseInitializer databaseInitializer) + { + DatabaseServiceFixture = databaseServiceFixture; + DatabaseInitializer = databaseInitializer; + } + + protected async ValueTask RunAndVerify( + Func testableCode, + CancellationToken cancellationToken, + Action? configureServices = null, + [CallerMemberName] string tableName = null! + ) + { + ArgumentNullException.ThrowIfNull(testableCode); + + using var host = new HostBuilder() + .ConfigureAppConfiguration((hostContext, configBuilder) => { }) + .ConfigureServices(services => + { + DatabaseInitializer.Initialize(services, DatabaseServiceFixture); + configureServices?.Invoke(services); + _ = services + .AddPulse(mediatorBuilder => DatabaseInitializer.Configure(mediatorBuilder, DatabaseServiceFixture)) + .Configure(options => options.TableName = tableName); + }) + .ConfigureWebHost(webBuilder => _ = webBuilder.UseTestServer().Configure(applicationBuilder => { })) + .Build(); + + await host.StartAsync(cancellationToken).ConfigureAwait(false); + + using var server = host.GetTestServer(); + + var databaseCreated = await DatabaseInitializer.CreateDatabaseAsync(host.Services).ConfigureAwait(false); + + using (Assert.Multiple()) + { + _ = await Assert.That(databaseCreated).IsTrue(); + + if (databaseCreated) + { + await testableCode.Invoke(server.Services, cancellationToken).ConfigureAwait(false); + } + } + + await host.StopAsync(cancellationToken).ConfigureAwait(false); + } + + protected static Task[] PublishEvents(IMediator mediator, int count, Func eventFactory) + where TEvent : IEvent => [.. Enumerable.Range(0, count).Select(x => mediator.PublishAsync(eventFactory(x)))]; +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs index 9bd47432..76d6482d 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs @@ -1,6 +1,6 @@ namespace NetEvolve.Pulse.Tests.Integration.Internals; -public sealed class SQLiteDatabaseServiceFixture : IDatabaseServiceFixture +internal sealed class SQLiteDatabaseServiceFixture : IDatabaseServiceFixture { public string ConnectionString => $"Data Source={DatabaseFile};"; diff --git a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj index e8ef9d63..bdc9926c 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj +++ b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj @@ -15,8 +15,10 @@ + + @@ -30,6 +32,7 @@ + diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs deleted file mode 100644 index 36759643..00000000 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace NetEvolve.Pulse.Tests.Integration.EntityFramework; - -using NetEvolve.Extensions.TUnit; -using NetEvolve.Pulse.Tests.Integration.Internals; - -[TestGroup("Outbox")] -[ClassDataSource( - Shared = [SharedType.PerClass, SharedType.PerTestSession] -)] -[ClassDataSource( - Shared = [SharedType.PerClass, SharedType.PerTestSession] -)] -public class OutboxTests -{ - public IDatabaseServiceFixture DatabaseServiceFixture { get; } - public IDatabaseInitializer DatabaseInitializer { get; } - - public OutboxTests(IDatabaseServiceFixture databaseServiceFixture, IDatabaseInitializer databaseInitializer) - { - DatabaseServiceFixture = databaseServiceFixture; - DatabaseInitializer = databaseInitializer; - } - - [Test] - [CombinedDataSources] - public async Task Can_Combine_Outbox_Setup() - { - await Assert.That(DatabaseServiceFixture).IsNotNull(); - await Assert.That(DatabaseInitializer).IsNotNull(); - } -} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs new file mode 100644 index 00000000..d26e9b85 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs @@ -0,0 +1,68 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Extensibility; +using NetEvolve.Pulse.Extensibility.Outbox; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[TestGroup("Outbox")] +public abstract class OutboxTestsBase( + IDatabaseServiceFixture databaseServiceFixture, + IDatabaseInitializer databaseInitializer +) : PulseTestsBase(databaseServiceFixture, databaseInitializer) +{ + [Test] + public async Task Should_Persist_ExpectedMessageCount(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + var publishTasks = PublishEvents(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }); + await Task.WhenAll(publishTasks); + + var outbox = services.GetRequiredService(); + + var result = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(result).IsEqualTo(3); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Persist_Expected_Messages(CancellationToken cancellationToken) + { + var timeProvider = new FakeTimeProvider(); + timeProvider.AdjustTime(TestDateTime); + + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + var publishTasks = PublishEvents(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }); + await Task.WhenAll(publishTasks); + + var outbox = services.GetRequiredService(); + var result = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Verify(result.OrderBy(x => x.Payload)).HashParameters().ConfigureAwait(false); + }, + cancellationToken, + configureServices: services => services.AddSingleton(timeProvider) + ); + } + + private sealed class TestEvent : IEvent + { + public string? CorrelationId { get; set; } + + public required string Id { get; init; } + + public DateTimeOffset? PublishedAt { get; set; } + } +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs new file mode 100644 index 00000000..c7a05861 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs @@ -0,0 +1,15 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[ClassDataSource( + Shared = [SharedType.None, SharedType.PerTestSession] +)] +[TestGroup("SQLite")] +[TestGroup("EntityFramework")] +[InheritsTests] +public class SQLiteEntityFrameworkOutboxTests( + IDatabaseServiceFixture databaseServiceFixture, + IDatabaseInitializer databaseInitializer +) : OutboxTestsBase(databaseServiceFixture, databaseInitializer); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SQLiteEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_dda0938409f8d1d7.verified.txt b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SQLiteEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_dda0938409f8d1d7.verified.txt new file mode 100644 index 00000000..4ee756e4 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SQLiteEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_dda0938409f8d1d7.verified.txt @@ -0,0 +1,26 @@ +[ + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_1, + Payload: {"CorrelationId":null,"Id":"Test000","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_2, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_3, + Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/InMemoryMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/InMemoryMessageTransportTests.cs deleted file mode 100644 index abb7526a..00000000 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/InMemoryMessageTransportTests.cs +++ /dev/null @@ -1,300 +0,0 @@ -namespace NetEvolve.Pulse.Tests.Unit.Outbox; - -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.Extensions.Options; -using NetEvolve.Extensions.TUnit; -using NetEvolve.Pulse.Extensibility; -using NetEvolve.Pulse.Extensibility.Outbox; -using NetEvolve.Pulse.Outbox; -using TUnit.Core; - -/// -/// Unit tests for . -/// Tests constructor validation, event deserialization, and mediator dispatch. -/// -[TestGroup("Outbox")] -public sealed class InMemoryMessageTransportTests -{ - [Test] - public async Task Constructor_WithNullMediator_ThrowsArgumentNullException() - { - IMediator? mediator = null; - var options = Options.Create(new OutboxOptions()); - - _ = Assert.Throws( - "mediator", - () => _ = new InMemoryMessageTransport(mediator!, options) - ); - } - - [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() - { - var mediator = new TestMediator(); - IOptions? options = null; - - _ = Assert.Throws("options", () => _ = new InMemoryMessageTransport(mediator, options!)); - } - - [Test] - public async Task Constructor_WithValidParameters_CreatesInstance() - { - var mediator = new TestMediator(); - var options = Options.Create(new OutboxOptions()); - - var transport = new InMemoryMessageTransport(mediator, options); - - _ = await Assert.That(transport).IsNotNull(); - } - - [Test] - public async Task SendAsync_WithNullMessage_ThrowsArgumentNullException() - { - var mediator = new TestMediator(); - var options = Options.Create(new OutboxOptions()); - var transport = new InMemoryMessageTransport(mediator, options); - - _ = await Assert.ThrowsAsync(() => transport.SendAsync(null!)); - } - - [Test] - public async Task SendAsync_WithValidMessage_DeserializesAndPublishesEvent() - { - var mediator = new TestMediator(); - var options = Options.Create(new OutboxOptions()); - var transport = new InMemoryMessageTransport(mediator, options); - - var originalEvent = new TestTransportEvent("test-id", "test data"); - var message = CreateOutboxMessage(originalEvent); - - await transport.SendAsync(message).ConfigureAwait(false); - - using (Assert.Multiple()) - { - _ = await Assert.That(mediator.PublishedEvents).HasSingleItem(); - _ = await Assert.That(mediator.PublishedEvents[0]).IsTypeOf(); - - var publishedEvent = (TestTransportEvent)mediator.PublishedEvents[0]; - _ = await Assert.That(publishedEvent.Id).IsEqualTo(originalEvent.Id); - _ = await Assert.That(publishedEvent.Data).IsEqualTo(originalEvent.Data); - } - } - - [Test] - public async Task SendAsync_WithInvalidPayload_ThrowsInvalidOperationException() - { - var mediator = new TestMediator(); - var options = Options.Create(new OutboxOptions()); - var transport = new InMemoryMessageTransport(mediator, options); - - var message = new OutboxMessage - { - Id = Guid.NewGuid(), - EventType = typeof(TestTransportEvent), - Payload = "null", // This will deserialize to null - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Status = OutboxMessageStatus.Processing, - }; - - var exception = await Assert.ThrowsAsync(() => transport.SendAsync(message)); - - _ = await Assert.That(exception!.Message).Contains("Failed to deserialize"); - } - - [Test] - public async Task SendAsync_WithNonEventType_ThrowsInvalidOperationException() - { - var mediator = new TestMediator(); - var options = Options.Create(new OutboxOptions()); - var transport = new InMemoryMessageTransport(mediator, options); - - // Use a type that is not an IEvent - var message = new OutboxMessage - { - Id = Guid.NewGuid(), - EventType = typeof(NonEventClass), - Payload = """{"Value":"test"}""", - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Status = OutboxMessageStatus.Processing, - }; - - var exception = await Assert.ThrowsAsync(() => transport.SendAsync(message)); - - _ = await Assert.That(exception!.Message).Contains("is not an IEvent"); - } - - [Test] - public async Task SendAsync_WithCustomJsonOptions_UsesProvidedOptions() - { - var mediator = new TestMediator(); - var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - var options = Options.Create(new OutboxOptions { JsonSerializerOptions = jsonOptions }); - var transport = new InMemoryMessageTransport(mediator, options); - - // Create payload with camelCase property names - const string payload = """{"id":"custom-id","data":"custom data","correlationId":null,"publishedAt":null}"""; - var message = new OutboxMessage - { - Id = Guid.NewGuid(), - EventType = typeof(TestTransportEvent), - Payload = payload, - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Status = OutboxMessageStatus.Processing, - }; - - await transport.SendAsync(message).ConfigureAwait(false); - - using (Assert.Multiple()) - { - _ = await Assert.That(mediator.PublishedEvents).HasSingleItem(); - var publishedEvent = (TestTransportEvent)mediator.PublishedEvents[0]; - _ = await Assert.That(publishedEvent.Id).IsEqualTo("custom-id"); - _ = await Assert.That(publishedEvent.Data).IsEqualTo("custom data"); - } - } - - [Test] - public async Task SendAsync_WithCancellationToken_PropagatesToken() - { - var mediator = new TestMediator(); - var options = Options.Create(new OutboxOptions()); - var transport = new InMemoryMessageTransport(mediator, options); - - using var cts = new CancellationTokenSource(); - var originalEvent = new TestTransportEvent("test-id", "test data"); - var message = CreateOutboxMessage(originalEvent); - - await transport.SendAsync(message, cts.Token).ConfigureAwait(false); - - _ = await Assert.That(mediator.LastCancellationToken).IsEqualTo(cts.Token); - } - - [Test] - public async Task SendAsync_WhenMediatorThrows_PropagatesException() - { - var mediator = new ThrowingMediator(new InvalidOperationException("Mediator error")); - var options = Options.Create(new OutboxOptions()); - var transport = new InMemoryMessageTransport(mediator, options); - - var originalEvent = new TestTransportEvent("test-id", "test data"); - var message = CreateOutboxMessage(originalEvent); - - // Reflection-based invocation wraps exceptions in TargetInvocationException - var exception = await Assert.ThrowsAsync(() => transport.SendAsync(message)); - - using (Assert.Multiple()) - { - _ = await Assert.That(exception).IsNotNull(); - _ = await Assert.That(exception.Message).IsEqualTo("Mediator error"); - } - } - - private static OutboxMessage CreateOutboxMessage(TestTransportEvent @event) - { - var payload = JsonSerializer.Serialize(@event, @event.GetType()); - - return new OutboxMessage - { - Id = Guid.NewGuid(), - EventType = @event.GetType(), - Payload = payload, - CorrelationId = @event.CorrelationId, - CreatedAt = DateTimeOffset.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Status = OutboxMessageStatus.Processing, - }; - } - -#pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match - private sealed class TestMediator : IMediator - { - public List PublishedEvents { get; } = []; - public CancellationToken LastCancellationToken { get; private set; } - - public Task PublishAsync(TEvent message, CancellationToken cancellationToken = default) - where TEvent : notnull, IEvent - { - PublishedEvents.Add(message); - LastCancellationToken = cancellationToken; - return Task.CompletedTask; - } - - public Task QueryAsync( - TQuery query, - CancellationToken cancellationToken = default - ) - where TQuery : notnull, IQuery => throw new NotSupportedException(); - - public IAsyncEnumerable StreamQueryAsync( - TQuery query, - CancellationToken cancellationToken = default - ) - where TQuery : notnull, IStreamQuery => throw new NotSupportedException(); - - public Task SendAsync( - TCommand command, - CancellationToken cancellationToken = default - ) - where TCommand : notnull, ICommand => throw new NotSupportedException(); - - public Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : notnull, ICommand => throw new NotSupportedException(); - } - - private sealed class ThrowingMediator : IMediator - { - private readonly Exception _exception; - - public ThrowingMediator(Exception exception) => _exception = exception; - - public Task PublishAsync(TEvent message, CancellationToken cancellationToken = default) - where TEvent : notnull, IEvent => throw _exception; - - public Task QueryAsync( - TQuery query, - CancellationToken cancellationToken = default - ) - where TQuery : notnull, IQuery => throw new NotSupportedException(); - - public IAsyncEnumerable StreamQueryAsync( - TQuery query, - CancellationToken cancellationToken = default - ) - where TQuery : notnull, IStreamQuery => throw new NotSupportedException(); - - public Task SendAsync( - TCommand command, - CancellationToken cancellationToken = default - ) - where TCommand : notnull, ICommand => throw new NotSupportedException(); - - public Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : notnull, ICommand => throw new NotSupportedException(); - } -#pragma warning restore CS8767 - - private sealed class TestTransportEvent : IEvent - { - public TestTransportEvent() { } - - public TestTransportEvent(string id, string data) - { - Id = id; - Data = data; - } - - public string Id { get; set; } = string.Empty; - public string Data { get; set; } = string.Empty; - public string? CorrelationId { get; set; } - public DateTimeOffset? PublishedAt { get; set; } - } - - private sealed class NonEventClass - { - public string Value { get; set; } = string.Empty; - } -} diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs new file mode 100644 index 00000000..b8055b66 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs @@ -0,0 +1,64 @@ +namespace NetEvolve.Pulse.Tests.Unit.Outbox; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Extensibility.Outbox; +using NetEvolve.Pulse.Outbox; +using TUnit.Core; + +/// +/// Unit tests for . +/// Verifies that the no-op transport completes silently for any input. +/// +[TestGroup("Outbox")] +public sealed class NullMessageTransportTests +{ + [Test] + public async Task SendAsync_WithValidMessage_CompletesSuccessfully() + { + var transport = new NullMessageTransport(); + var message = new OutboxMessage + { + Id = Guid.NewGuid(), + EventType = typeof(object), + Payload = "{}", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Status = OutboxMessageStatus.Processing, + }; + + await transport.SendAsync(message).ConfigureAwait(false); + + _ = await Assert.That(true).IsTrue(); // No exception means success + } + + [Test] + public async Task SendAsync_WithNullMessage_CompletesSuccessfully() + { + var transport = new NullMessageTransport(); + + await transport.SendAsync(null!).ConfigureAwait(false); + + _ = await Assert.That(true).IsTrue(); // NullMessageTransport discards all messages without validation + } + + [Test] + public async Task SendAsync_WithCancelledToken_CompletesSuccessfully() + { + var transport = new NullMessageTransport(); + var message = new OutboxMessage + { + Id = Guid.NewGuid(), + EventType = typeof(object), + Payload = "{}", + CreatedAt = DateTimeOffset.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Status = OutboxMessageStatus.Processing, + }; + using var cts = new CancellationTokenSource(); + await cts.CancelAsync().ConfigureAwait(false); + + await transport.SendAsync(message, cts.Token).ConfigureAwait(false); + + _ = await Assert.That(true).IsTrue(); // NullMessageTransport ignores cancellation + } +} From f4c73e8e03543b9a773f4942a874c67473dc79a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 11 Apr 2026 00:10:49 +0200 Subject: [PATCH 03/25] fix: Remove extraneous '-' from Directory.Packages.props Clean up file formatting by deleting a stray '-' character before the closing tag. No functional changes. --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2422d26b..7853fac5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -76,4 +76,4 @@ - \ No newline at end of file + From e338f9673b2324b34f466b238bca8ee5c00ddddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 11 Apr 2026 00:17:20 +0200 Subject: [PATCH 04/25] fix: Remove redundant assertions from NullMessageTransportTests Removed unnecessary Assert.That(true).IsTrue() statements from unit tests. The tests now rely on the absence of exceptions to verify correct behavior, simplifying the test code. --- .../Outbox/NullMessageTransportTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs index b8055b66..97ec58e6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs @@ -27,8 +27,6 @@ public async Task SendAsync_WithValidMessage_CompletesSuccessfully() }; await transport.SendAsync(message).ConfigureAwait(false); - - _ = await Assert.That(true).IsTrue(); // No exception means success } [Test] @@ -37,8 +35,6 @@ public async Task SendAsync_WithNullMessage_CompletesSuccessfully() var transport = new NullMessageTransport(); await transport.SendAsync(null!).ConfigureAwait(false); - - _ = await Assert.That(true).IsTrue(); // NullMessageTransport discards all messages without validation } [Test] @@ -58,7 +54,5 @@ public async Task SendAsync_WithCancelledToken_CompletesSuccessfully() await cts.CancelAsync().ConfigureAwait(false); await transport.SendAsync(message, cts.Token).ConfigureAwait(false); - - _ = await Assert.That(true).IsTrue(); // NullMessageTransport ignores cancellation } } From f0a80cc75b29141e793d59e459330ffd8979de4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 11 Apr 2026 00:57:42 +0200 Subject: [PATCH 05/25] fix: Update src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Martin Stühmer --- src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs b/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs index 2efd56e0..eaaaff29 100644 --- a/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs +++ b/src/NetEvolve.Pulse/Outbox/NullMessageTransport.cs @@ -14,5 +14,9 @@ internal sealed class NullMessageTransport : IMessageTransport { /// - public Task SendAsync(OutboxMessage message, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task SendAsync(OutboxMessage message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + return Task.CompletedTask; + } } From 985518bbf53ac2ede7a124a8a4ad53e551814add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 11 Apr 2026 00:59:02 +0200 Subject: [PATCH 06/25] fix: Update tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Martin Stühmer --- .../Internals/InMemoryDatabaseServiceFixture.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs index 741dddb4..14949afc 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs @@ -2,7 +2,11 @@ internal sealed class InMemoryDatabaseServiceFixture : IDatabaseServiceFixture { - public string ConnectionString => Guid.NewGuid().ToString("N"); + internal sealed class InMemoryDatabaseServiceFixture : IDatabaseServiceFixture + { + private readonly string _connectionString = Guid.NewGuid().ToString("N"); + public string ConnectionString => _connectionString; + } public DatabaseType DatabaseType => DatabaseType.InMemory; From 1969b11ba32a5eda0b0e808e1d9f98bce9ec07f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 11 Apr 2026 01:14:41 +0200 Subject: [PATCH 07/25] test: Add comprehensive EF Outbox integration tests - Replace pre-compiled queries in EntityFrameworkOutboxRepository with direct LINQ using AsNoTracking and ToArrayAsync for simplicity and testability - Make EntityFrameworkInitializer and InMemoryDatabaseServiceFixture public; improve SQLite concurrency with WAL mode - Add InMemoryEntityFrameworkOutboxTests and expand OutboxTestsBase with extensive scenarios (pending, batch size, completion, failure, dead-letter, retry scheduling, deletion) - Use FakeTimeProvider for time-dependent tests - Ensure database creation before host start and proper service scope management in tests - Add PublishEventsAsync helper for event publishing with cancellation support - Improve thread-safety in OutboxProcessorHostedService - Overall, greatly increase test coverage and reliability for Outbox operations, especially for in-memory and SQLite scenarios --- .../Outbox/EntityFrameworkOutboxRepository.cs | 78 ++-- .../Outbox/OutboxProcessorHostedService.cs | 2 +- .../Internals/EntityFrameworkInitializer.cs | 10 +- .../InMemoryDatabaseServiceFixture.cs | 6 +- .../Internals/PulseTestsBase.cs | 24 +- .../InMemoryEntityFrameworkOutboxTests.cs | 16 + .../Outbox/OutboxTestsBase.cs | 422 +++++++++++++++++- 7 files changed, 489 insertions(+), 69 deletions(-) create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs index f4174a21..fc4a5721 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs @@ -20,34 +20,6 @@ internal sealed class EntityFrameworkOutboxRepository : IOutboxRepository where TContext : DbContext, IOutboxDbContext { - /// Pre-compiled pending ID-selection query; eliminates expression-tree overhead on every call. - private static readonly Func> GetPendingIdsQuery = - EF.CompileAsyncQuery( - (TContext ctx, int batchSize, DateTimeOffset now) => - ctx - .OutboxMessages.Where(m => - m.Status == OutboxMessageStatus.Pending && (m.NextRetryAt == null || m.NextRetryAt <= now) - ) - .OrderBy(m => m.CreatedAt) - .Take(batchSize) - .Select(m => m.Id) - ); - - /// Pre-compiled failed-for-retry ID-selection query; eliminates expression-tree overhead on every call. - private static readonly Func> GetFailedForRetryIdsQuery = - EF.CompileAsyncQuery( - (TContext ctx, int maxRetryCount, int batchSize, DateTimeOffset now) => - ctx - .OutboxMessages.Where(m => - m.Status == OutboxMessageStatus.Failed - && m.RetryCount < maxRetryCount - && (m.NextRetryAt == null || m.NextRetryAt <= now) - ) - .OrderBy(m => m.UpdatedAt) - .Take(batchSize) - .Select(m => m.Id) - ); - /// The DbContext used for all LINQ-to-SQL query and update operations. private readonly TContext _context; @@ -85,17 +57,16 @@ public async Task> GetPendingAsync( { var now = _timeProvider.GetUtcNow(); - var ids = new List(batchSize); - await foreach ( - var id in GetPendingIdsQuery(_context, batchSize, now) - .WithCancellation(cancellationToken) - .ConfigureAwait(false) - ) - { - ids.Add(id); - } + var ids = await _context + .OutboxMessages.AsNoTracking() + .Where(m => m.Status == OutboxMessageStatus.Pending && (m.NextRetryAt == null || m.NextRetryAt <= now)) + .OrderBy(m => m.CreatedAt) + .Take(batchSize) + .Select(m => m.Id) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); - if (ids.Count == 0) + if (ids.Length == 0) { return []; } @@ -109,8 +80,8 @@ var id in GetPendingIdsQuery(_context, batchSize, now) .ConfigureAwait(false); return await _context - .OutboxMessages.Where(m => ids.Contains(m.Id)) - .AsNoTracking() + .OutboxMessages.AsNoTracking() + .Where(m => ids.Contains(m.Id)) .ToArrayAsync(cancellationToken) .ConfigureAwait(false); } @@ -134,17 +105,20 @@ public async Task> GetFailedForRetryAsync( { var now = _timeProvider.GetUtcNow(); - var ids = new List(batchSize); - await foreach ( - var id in GetFailedForRetryIdsQuery(_context, maxRetryCount, batchSize, now) - .WithCancellation(cancellationToken) - .ConfigureAwait(false) - ) - { - ids.Add(id); - } + var ids = await _context + .OutboxMessages.AsNoTracking() + .Where(m => + m.Status == OutboxMessageStatus.Failed + && m.RetryCount < maxRetryCount + && (m.NextRetryAt == null || m.NextRetryAt <= now) + ) + .OrderBy(m => m.UpdatedAt) + .Take(batchSize) + .Select(m => m.Id) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); - if (ids.Count == 0) + if (ids.Length == 0) { return []; } @@ -158,8 +132,8 @@ var id in GetFailedForRetryIdsQuery(_context, maxRetryCount, batchSize, now) .ConfigureAwait(false); return await _context - .OutboxMessages.Where(m => ids.Contains(m.Id)) - .AsNoTracking() + .OutboxMessages.AsNoTracking() + .Where(m => ids.Contains(m.Id)) .ToArrayAsync(cancellationToken) .ConfigureAwait(false); } diff --git a/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs b/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs index e9e602f6..cbc9c9df 100644 --- a/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs +++ b/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs @@ -240,7 +240,7 @@ private async Task ProcessBatchAsync(CancellationToken cancellationToken) IReadOnlyList messages = []; - if (_pendingCount > 0) + if (Volatile.Read(ref _pendingCount) > 0) { messages = await _repository.GetPendingAsync(batchSize, cancellationToken).ConfigureAwait(false); batchSize -= messages.Count; diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index 017f1816..19bfca02 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -3,12 +3,14 @@ using System.Data.Common; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NetEvolve.Pulse.Extensibility; using NetEvolve.Pulse.Extensibility.Outbox; using NetEvolve.Pulse.Outbox; -internal sealed class EntityFrameworkInitializer : IDatabaseInitializer +public sealed class EntityFrameworkInitializer : IDatabaseInitializer { public void Configure(IMediatorBuilder mediatorBuilder, IDatabaseServiceFixture databaseService) => mediatorBuilder.AddEntityFrameworkOutbox(); @@ -48,12 +50,12 @@ public void Initialize(IServiceCollection services, IDatabaseServiceFixture data /// private sealed class SQLiteBusyTimeoutInterceptor : DbConnectionInterceptor { - private const string BusyTimeoutPragma = "PRAGMA busy_timeout = 60000;"; // 60 s + private const string Pragmas = "PRAGMA busy_timeout = 60000; PRAGMA journal_mode = WAL;"; public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) { using var command = connection.CreateCommand(); - command.CommandText = BusyTimeoutPragma; + command.CommandText = Pragmas; _ = command.ExecuteNonQuery(); } @@ -64,7 +66,7 @@ public override async Task ConnectionOpenedAsync( ) { await using var command = connection.CreateCommand(); - command.CommandText = BusyTimeoutPragma; + command.CommandText = Pragmas; _ = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs index 14949afc..d2956987 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InMemoryDatabaseServiceFixture.cs @@ -2,11 +2,7 @@ internal sealed class InMemoryDatabaseServiceFixture : IDatabaseServiceFixture { - internal sealed class InMemoryDatabaseServiceFixture : IDatabaseServiceFixture - { - private readonly string _connectionString = Guid.NewGuid().ToString("N"); - public string ConnectionString => _connectionString; - } + public string ConnectionString { get; } = Guid.NewGuid().ToString("N"); public DatabaseType DatabaseType => DatabaseType.InMemory; diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs index de68dbcb..e53aad32 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs @@ -43,19 +43,20 @@ protected async ValueTask RunAndVerify( .ConfigureWebHost(webBuilder => _ = webBuilder.UseTestServer().Configure(applicationBuilder => { })) .Build(); + var databaseCreated = await DatabaseInitializer.CreateDatabaseAsync(host.Services).ConfigureAwait(false); + await host.StartAsync(cancellationToken).ConfigureAwait(false); using var server = host.GetTestServer(); - var databaseCreated = await DatabaseInitializer.CreateDatabaseAsync(host.Services).ConfigureAwait(false); - using (Assert.Multiple()) { _ = await Assert.That(databaseCreated).IsTrue(); if (databaseCreated) { - await testableCode.Invoke(server.Services, cancellationToken).ConfigureAwait(false); + await using var scope = server.Services.CreateAsyncScope(); + await testableCode.Invoke(scope.ServiceProvider, cancellationToken).ConfigureAwait(false); } } @@ -64,4 +65,21 @@ protected async ValueTask RunAndVerify( protected static Task[] PublishEvents(IMediator mediator, int count, Func eventFactory) where TEvent : IEvent => [.. Enumerable.Range(0, count).Select(x => mediator.PublishAsync(eventFactory(x)))]; + + protected static async Task PublishEventsAsync( + IMediator mediator, + int count, + Func eventFactory, + CancellationToken cancellationToken = default + ) + where TEvent : IEvent + { + ArgumentNullException.ThrowIfNull(mediator); + ArgumentNullException.ThrowIfNull(eventFactory); + + for (var i = 0; i < count; i++) + { + await mediator.PublishAsync(eventFactory(i), cancellationToken).ConfigureAwait(false); + } + } } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs new file mode 100644 index 00000000..69750d4a --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs @@ -0,0 +1,16 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[ClassDataSource( + Shared = [SharedType.None, SharedType.PerTestSession] +)] +[TestGroup("InMemory")] +[TestGroup("EntityFramework")] +[Explicit] +[InheritsTests] +public class InMemoryEntityFrameworkOutboxTests( + IDatabaseServiceFixture databaseServiceFixture, + IDatabaseInitializer databaseInitializer +) : OutboxTestsBase(databaseServiceFixture, databaseInitializer); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs index d26e9b85..2495cc0d 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs @@ -20,8 +20,7 @@ await RunAndVerify( { var mediator = services.GetRequiredService(); - var publishTasks = PublishEvents(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }); - await Task.WhenAll(publishTasks); + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); var outbox = services.GetRequiredService(); @@ -44,8 +43,7 @@ await RunAndVerify( { var mediator = services.GetRequiredService(); - var publishTasks = PublishEvents(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }); - await Task.WhenAll(publishTasks); + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); var outbox = services.GetRequiredService(); var result = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); @@ -57,6 +55,422 @@ await RunAndVerify( ); } + [Test] + public async Task Should_Return_Zero_PendingCount_When_Empty(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var outbox = services.GetRequiredService(); + + var result = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(result).IsEqualTo(0); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Return_Empty_When_GetPending_NoMessages(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var outbox = services.GetRequiredService(); + + var result = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(result).IsEmpty(); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_GetPendingAsync_Respects_BatchSize(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 5, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(3, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(3); + + var remainingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(remainingCount).IsEqualTo(2); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Mark_Single_Message_AsCompleted(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await mediator.PublishAsync(new TestEvent { Id = "Test001" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(1); + + await outbox.MarkAsCompletedAsync(pending[0].Id, token).ConfigureAwait(false); + + var pendingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(pendingCount).IsEqualTo(0); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Mark_Multiple_Messages_AsCompleted(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(3); + + var messageIds = pending.Select(m => m.Id).ToArray(); + await outbox.MarkAsCompletedAsync(messageIds, token).ConfigureAwait(false); + + var pendingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(pendingCount).IsEqualTo(0); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Mark_Single_Message_AsFailed(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await mediator.PublishAsync(new TestEvent { Id = "Test001" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(1); + + await outbox.MarkAsFailedAsync(pending[0].Id, "Test error", token).ConfigureAwait(false); + + var pendingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(pendingCount).IsEqualTo(0); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry.Count).IsEqualTo(1); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Mark_Multiple_Messages_AsFailed(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(3); + + var messageIds = pending.Select(m => m.Id).ToArray(); + await outbox.MarkAsFailedAsync(messageIds, "Test error", token).ConfigureAwait(false); + + var pendingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(pendingCount).IsEqualTo(0); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry.Count).IsEqualTo(3); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Mark_Single_Message_AsFailed_WithRetryScheduling(CancellationToken cancellationToken) + { + var timeProvider = new FakeTimeProvider(); + timeProvider.AdjustTime(TestDateTime); + + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await mediator.PublishAsync(new TestEvent { Id = "Test001" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(1); + + await outbox + .MarkAsFailedAsync(pending[0].Id, "Test error", TestDateTime.AddHours(1), token) + .ConfigureAwait(false); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry).IsEmpty(); + }, + cancellationToken, + configureServices: services => services.AddSingleton(timeProvider) + ); + } + + [Test] + public async Task Should_Mark_Single_Message_AsDeadLetter(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await mediator.PublishAsync(new TestEvent { Id = "Test001" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(1); + + await outbox.MarkAsDeadLetterAsync(pending[0].Id, "Fatal error", token).ConfigureAwait(false); + + var pendingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(pendingCount).IsEqualTo(0); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry).IsEmpty(); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_Mark_Multiple_Messages_AsDeadLetter(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(3); + + var messageIds = pending.Select(m => m.Id).ToArray(); + await outbox.MarkAsDeadLetterAsync(messageIds, "Fatal error", token).ConfigureAwait(false); + + var pendingCount = await outbox.GetPendingCountAsync(token).ConfigureAwait(false); + + _ = await Assert.That(pendingCount).IsEqualTo(0); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry).IsEmpty(); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_GetFailedForRetry_ExcludesScheduledMessages(CancellationToken cancellationToken) + { + var timeProvider = new FakeTimeProvider(); + timeProvider.AdjustTime(TestDateTime); + + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 2, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(2); + + await outbox + .MarkAsFailedAsync(pending[0].Id, "Scheduled error", TestDateTime.AddHours(1), token) + .ConfigureAwait(false); + await outbox.MarkAsFailedAsync(pending[1].Id, "Immediate error", token).ConfigureAwait(false); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry.Count).IsEqualTo(1); + }, + cancellationToken, + configureServices: services => services.AddSingleton(timeProvider) + ); + } + + [Test] + public async Task Should_DeleteCompleted_ReturnsCorrectCount(CancellationToken cancellationToken) + { + var timeProvider = new FakeTimeProvider(); + timeProvider.AdjustTime(TestDateTime); + + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(3); + + var messageIds = pending.Select(m => m.Id).ToArray(); + await outbox.MarkAsCompletedAsync(messageIds, token).ConfigureAwait(false); + + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + var deleted = await outbox.DeleteCompletedAsync(TimeSpan.FromSeconds(30), token).ConfigureAwait(false); + + _ = await Assert.That(deleted).IsEqualTo(3); + }, + cancellationToken, + configureServices: services => services.AddSingleton(timeProvider) + ); + } + + [Test] + public async Task Should_GetPendingAsync_ExcludesProcessingMessages(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 3, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + _ = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + var secondBatch = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(secondBatch).IsEmpty(); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_GetFailedForRetry_Returns_Empty_When_NoFailedMessages( + CancellationToken cancellationToken + ) => + await RunAndVerify( + async (services, token) => + { + var outbox = services.GetRequiredService(); + + var result = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(result).IsEmpty(); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_GetFailedForRetry_Excludes_MaxRetryCount_Exceeded(CancellationToken cancellationToken) => + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await mediator.PublishAsync(new TestEvent { Id = "Test001" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(1); + + await outbox.MarkAsFailedAsync(pending[0].Id, "Error 1", token).ConfigureAwait(false); + + var firstRetry = await outbox.GetFailedForRetryAsync(3, 50, token).ConfigureAwait(false); + + _ = await Assert.That(firstRetry.Count).IsEqualTo(1); + + await outbox.MarkAsFailedAsync(firstRetry[0].Id, "Error 2", token).ConfigureAwait(false); + + var secondRetry = await outbox.GetFailedForRetryAsync(2, 50, token).ConfigureAwait(false); + + _ = await Assert.That(secondRetry).IsEmpty(); + }, + cancellationToken + ) + .ConfigureAwait(false); + + [Test] + public async Task Should_DeleteCompleted_DoesNotDelete_NonCompletedMessages(CancellationToken cancellationToken) + { + var timeProvider = new FakeTimeProvider(); + timeProvider.AdjustTime(TestDateTime); + + await RunAndVerify( + async (services, token) => + { + var mediator = services.GetRequiredService(); + + await PublishEventsAsync(mediator, 4, x => new TestEvent { Id = $"Test{x:D3}" }, token); + + var outbox = services.GetRequiredService(); + var pending = await outbox.GetPendingAsync(50, token).ConfigureAwait(false); + + _ = await Assert.That(pending.Count).IsEqualTo(4); + + var completedIds = pending.Take(2).Select(m => m.Id).ToArray(); + await outbox.MarkAsCompletedAsync(completedIds, token).ConfigureAwait(false); + + var failedIds = pending.Skip(2).Select(m => m.Id).ToArray(); + await outbox.MarkAsFailedAsync(failedIds, "Test error", token).ConfigureAwait(false); + + timeProvider.Advance(TimeSpan.FromMinutes(1)); + + var deleted = await outbox.DeleteCompletedAsync(TimeSpan.FromSeconds(30), token).ConfigureAwait(false); + + _ = await Assert.That(deleted).IsEqualTo(2); + + var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); + + _ = await Assert.That(failedForRetry.Count).IsEqualTo(2); + }, + cancellationToken, + configureServices: services => services.AddSingleton(timeProvider) + ); + } + private sealed class TestEvent : IEvent { public string? CorrelationId { get; set; } From 7d2db6a13f1cc088690ea9611327dfdfa4012e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 11 Apr 2026 01:29:23 +0200 Subject: [PATCH 08/25] test: Support EF Core InMemory provider for Outbox repository EntityFrameworkOutboxRepository now detects and adapts to the InMemory provider, using tracked entity updates/deletes instead of bulk operations. Added helper methods for this logic. InMemory integration tests now run by default. Added a verified snapshot for InMemory persistence. Removed obsolete null message test from NullMessageTransportTests. --- .../Outbox/EntityFrameworkOutboxRepository.cs | 289 ++++++++++++++---- .../InMemoryEntityFrameworkOutboxTests.cs | 1 - ...ted_Messages_b0c5fb5f6a10ddef.verified.txt | 26 ++ .../Outbox/NullMessageTransportTests.cs | 8 - 4 files changed, 251 insertions(+), 73 deletions(-) create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/InMemoryEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_b0c5fb5f6a10ddef.verified.txt diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs index fc4a5721..94e0ece5 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs @@ -26,6 +26,12 @@ internal sealed class EntityFrameworkOutboxRepository : IOutboxReposit /// The time provider used to generate consistent update and cutoff timestamps. private readonly TimeProvider _timeProvider; + /// + /// when the current EF Core provider is the in-memory provider, + /// which does not support ExecuteUpdate / ExecuteDelete. + /// + private readonly bool _isInMemory; + /// /// Initializes a new instance of the class. /// @@ -38,6 +44,7 @@ public EntityFrameworkOutboxRepository(TContext context, TimeProvider timeProvid _context = context; _timeProvider = timeProvider; + _isInMemory = context.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory"; } /// @@ -71,10 +78,20 @@ public async Task> GetPendingAsync( return []; } - _ = await _context - .OutboxMessages.Where(m => ids.Contains(m.Id) && m.Status == OutboxMessageStatus.Pending) - .ExecuteUpdateAsync( - m => m.SetProperty(m => m.Status, OutboxMessageStatus.Processing).SetProperty(m => m.UpdatedAt, now), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => ids.Contains(m.Id) && m.Status == OutboxMessageStatus.Pending), + msg => + { + msg.Status = OutboxMessageStatus.Processing; + msg.UpdatedAt = now; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Processing) + .SetProperty(m => m.UpdatedAt, now), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -123,10 +140,20 @@ public async Task> GetFailedForRetryAsync( return []; } - _ = await _context - .OutboxMessages.Where(m => ids.Contains(m.Id) && m.Status == OutboxMessageStatus.Failed) - .ExecuteUpdateAsync( - m => m.SetProperty(m => m.Status, OutboxMessageStatus.Processing).SetProperty(m => m.UpdatedAt, now), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => ids.Contains(m.Id) && m.Status == OutboxMessageStatus.Failed), + msg => + { + msg.Status = OutboxMessageStatus.Processing; + msg.UpdatedAt = now; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Processing) + .SetProperty(m => m.UpdatedAt, now), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -143,13 +170,22 @@ public async Task MarkAsCompletedAsync(Guid messageId, CancellationToken cancell { var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Completed) - .SetProperty(m => m.ProcessedAt, now) - .SetProperty(m => m.UpdatedAt, now), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), + msg => + { + msg.Status = OutboxMessageStatus.Completed; + msg.ProcessedAt = now; + msg.UpdatedAt = now; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Completed) + .SetProperty(m => m.ProcessedAt, now) + .SetProperty(m => m.UpdatedAt, now), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -168,13 +204,24 @@ public async Task MarkAsCompletedAsync( var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Completed) - .SetProperty(m => m.ProcessedAt, now) - .SetProperty(m => m.UpdatedAt, now), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => + messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing + ), + msg => + { + msg.Status = OutboxMessageStatus.Completed; + msg.ProcessedAt = now; + msg.UpdatedAt = now; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Completed) + .SetProperty(m => m.ProcessedAt, now) + .SetProperty(m => m.UpdatedAt, now), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -189,14 +236,24 @@ public async Task MarkAsFailedAsync( { var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now) - .SetProperty(m => m.RetryCount, m => m.RetryCount + 1), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), + msg => + { + msg.Status = OutboxMessageStatus.Failed; + msg.Error = errorMessage; + msg.UpdatedAt = now; + msg.RetryCount++; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) + .SetProperty(m => m.Error, errorMessage) + .SetProperty(m => m.UpdatedAt, now) + .SetProperty(m => m.RetryCount, m => m.RetryCount + 1), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -212,15 +269,26 @@ public async Task MarkAsFailedAsync( { var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now) - .SetProperty(m => m.RetryCount, m => m.RetryCount + 1) - .SetProperty(m => m.NextRetryAt, nextRetryAt), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), + msg => + { + msg.Status = OutboxMessageStatus.Failed; + msg.Error = errorMessage; + msg.UpdatedAt = now; + msg.RetryCount++; + msg.NextRetryAt = nextRetryAt; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) + .SetProperty(m => m.Error, errorMessage) + .SetProperty(m => m.UpdatedAt, now) + .SetProperty(m => m.RetryCount, m => m.RetryCount + 1) + .SetProperty(m => m.NextRetryAt, nextRetryAt), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -240,14 +308,26 @@ public async Task MarkAsFailedAsync( var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now) - .SetProperty(m => m.RetryCount, m => m.RetryCount + 1), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => + messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing + ), + msg => + { + msg.Status = OutboxMessageStatus.Failed; + msg.Error = errorMessage; + msg.UpdatedAt = now; + msg.RetryCount++; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) + .SetProperty(m => m.Error, errorMessage) + .SetProperty(m => m.UpdatedAt, now) + .SetProperty(m => m.RetryCount, m => m.RetryCount + 1), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -262,13 +342,22 @@ public async Task MarkAsDeadLetterAsync( { var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.DeadLetter) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), + msg => + { + msg.Status = OutboxMessageStatus.DeadLetter; + msg.Error = errorMessage; + msg.UpdatedAt = now; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.DeadLetter) + .SetProperty(m => m.Error, errorMessage) + .SetProperty(m => m.UpdatedAt, now), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -288,13 +377,24 @@ public async Task MarkAsDeadLetterAsync( var now = _timeProvider.GetUtcNow(); - _ = await _context - .OutboxMessages.Where(m => messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing) - .ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.DeadLetter) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now), + await UpdateEntitiesAsync( + _context.OutboxMessages.Where(m => + messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing + ), + msg => + { + msg.Status = OutboxMessageStatus.DeadLetter; + msg.Error = errorMessage; + msg.UpdatedAt = now; + }, + (q, ct) => + q.ExecuteUpdateAsync( + m => + m.SetProperty(m => m.Status, OutboxMessageStatus.DeadLetter) + .SetProperty(m => m.Error, errorMessage) + .SetProperty(m => m.UpdatedAt, now), + ct + ), cancellationToken ) .ConfigureAwait(false); @@ -305,9 +405,70 @@ public async Task DeleteCompletedAsync(TimeSpan olderThan, CancellationToke { var cutoffTime = _timeProvider.GetUtcNow().Subtract(olderThan); - return await _context - .OutboxMessages.Where(m => m.Status == OutboxMessageStatus.Completed && m.ProcessedAt < cutoffTime) - .ExecuteDeleteAsync(cancellationToken) + return await DeleteEntitiesAsync( + _context.OutboxMessages.Where(m => + m.Status == OutboxMessageStatus.Completed && m.ProcessedAt < cutoffTime + ), + cancellationToken + ) .ConfigureAwait(false); } + + /// + /// Updates a set of entities by either tracking-and-saving (InMemory provider) + /// or issuing a single bulk UPDATE statement via ExecuteUpdateAsync (all other providers). + /// + private async Task UpdateEntitiesAsync( + IQueryable query, + Action applyChanges, + Func, CancellationToken, Task> executeBulkUpdate, + CancellationToken cancellationToken + ) + { + if (_isInMemory) + { + var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); + + if (entities.Length == 0) + { + return; + } + + foreach (var entity in entities) + { + applyChanges(entity); + } + + _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _ = await executeBulkUpdate(query, cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Deletes a set of entities by either tracking-and-removing (InMemory provider) + /// or issuing a single bulk DELETE statement via ExecuteDeleteAsync (all other providers). + /// + /// The number of deleted rows. + private async Task DeleteEntitiesAsync(IQueryable query, CancellationToken cancellationToken) + { + if (_isInMemory) + { + var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); + + if (entities.Length == 0) + { + return 0; + } + + _context.OutboxMessages.RemoveRange(entities); + _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return entities.Length; + } + + return await query.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs index 69750d4a..812130f3 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/InMemoryEntityFrameworkOutboxTests.cs @@ -8,7 +8,6 @@ )] [TestGroup("InMemory")] [TestGroup("EntityFramework")] -[Explicit] [InheritsTests] public class InMemoryEntityFrameworkOutboxTests( IDatabaseServiceFixture databaseServiceFixture, diff --git a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/InMemoryEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_b0c5fb5f6a10ddef.verified.txt b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/InMemoryEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_b0c5fb5f6a10ddef.verified.txt new file mode 100644 index 00000000..4ee756e4 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/InMemoryEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_b0c5fb5f6a10ddef.verified.txt @@ -0,0 +1,26 @@ +[ + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_1, + Payload: {"CorrelationId":null,"Id":"Test000","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_2, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_3, + Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs index 97ec58e6..150d3ce8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs @@ -29,14 +29,6 @@ public async Task SendAsync_WithValidMessage_CompletesSuccessfully() await transport.SendAsync(message).ConfigureAwait(false); } - [Test] - public async Task SendAsync_WithNullMessage_CompletesSuccessfully() - { - var transport = new NullMessageTransport(); - - await transport.SendAsync(null!).ConfigureAwait(false); - } - [Test] public async Task SendAsync_WithCancelledToken_CompletesSuccessfully() { From 3418b20ddd545d2d200e0105db45af6feed43791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 13:42:25 +0200 Subject: [PATCH 09/25] refactor(test): DB setup for isolation and reliability - Improve Entity Framework Outbox test isolation by generating unique database/schema names per test run using target framework and GUID. - Update IDatabaseInitializer and EntityFrameworkInitializer for robust, thread-safe database/table creation with CancellationToken support. - Configure OutboxOptions.Schema for test isolation; disable OutboxProcessor during tests. - Add [Timeout(60_000)] to OutboxTestsBase for CI reliability. - Enhance SQL Server fixture with unique DB names and container startup timeout/error handling. - Remove unused InternalTestWebApplicationFactory and WebApplicationTestBase. - Add TestHelper for target framework detection. - Update test data sources for proper fixture/initializer sharing. - Add verified snapshot for SQL Server Outbox tests. --- .../Internals/EntityFrameworkInitializer.cs | 50 ++++++++++++++++--- .../Internals/IDatabaseInitializer.cs | 2 +- .../InternalTestWebApplicationFactory.cs | 6 --- .../Internals/PulseTestsBase.cs | 18 +++---- .../Internals/SQLiteDatabaseServiceFixture.cs | 3 +- .../SqlServerDatabaseServiceFixture.cs | 20 +++++++- .../Internals/TestHelper.cs | 13 +++++ .../Internals/WebApplicationTestBase.cs | 7 --- .../Outbox/OutboxTestsBase.cs | 7 ++- .../SQLiteEntityFrameworkOutboxTests.cs | 4 +- .../SqlServerEntityFrameworkOutboxTests.cs | 15 ++++++ ...ted_Messages_cde079fe90d49e58.verified.txt | 34 +++++++++++++ 12 files changed, 141 insertions(+), 38 deletions(-) delete mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs delete mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index 19bfca02..15107d97 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -1,11 +1,12 @@ namespace NetEvolve.Pulse.Tests.Integration.Internals; using System.Data.Common; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using NetEvolve.Pulse.Extensibility; using NetEvolve.Pulse.Extensibility.Outbox; using NetEvolve.Pulse.Outbox; @@ -15,31 +16,66 @@ public sealed class EntityFrameworkInitializer : IDatabaseInitializer public void Configure(IMediatorBuilder mediatorBuilder, IDatabaseServiceFixture databaseService) => mediatorBuilder.AddEntityFrameworkOutbox(); - public async ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider) + private static readonly SemaphoreSlim _gate = new(1, 1); + + public async ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) { using (var scope = serviceProvider.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService(); + var databaseCreator = context.GetService(); - if (await context.Database.CanConnectAsync().ConfigureAwait(false)) + if (databaseCreator is IRelationalDatabaseCreator relationalDatabaseCreator) + { + if (!await relationalDatabaseCreator.CanConnectAsync(cancellationToken).ConfigureAwait(false)) + { + await relationalDatabaseCreator.CreateAsync(cancellationToken).ConfigureAwait(false); + } + } + else { - _ = await context.Database.EnsureDeletedAsync().ConfigureAwait(false); + if (await databaseCreator.CanConnectAsync(cancellationToken).ConfigureAwait(false)) + { + return; + } + + await _gate.WaitAsync(cancellationToken); + + try + { + _ = await databaseCreator.EnsureCreatedAsync(cancellationToken).ConfigureAwait(false); + return; + } + finally + { + _ = _gate.Release(); + } + } + + if (databaseCreator is IRelationalDatabaseCreator relationalTableCreator) + { + await relationalTableCreator.CreateTablesAsync(cancellationToken); } - return await context.Database.EnsureCreatedAsync().ConfigureAwait(false); } } public void Initialize(IServiceCollection services, IDatabaseServiceFixture databaseService) => _ = services.AddDbContext(options => { + var connectionString = databaseService.ConnectionString; + _ = databaseService.DatabaseType switch { - DatabaseType.InMemory => options.UseInMemoryDatabase(databaseService.ConnectionString), + DatabaseType.InMemory => options.UseInMemoryDatabase(connectionString), // Add a busy-timeout interceptor so that concurrent SaveChangesAsync calls from // parallel PublishAsync tasks wait and retry instead of failing with SQLITE_BUSY. DatabaseType.SQLite => options - .UseSqlite(databaseService.ConnectionString) + .UseSqlite(connectionString) .AddInterceptors(new SQLiteBusyTimeoutInterceptor()), + DatabaseType.SqlServer => options.UseSqlServer( + connectionString, + sqlOptions => sqlOptions.EnableRetryOnFailure(maxRetryCount: 5) + ), _ => throw new NotSupportedException($"Database type {databaseService.DatabaseType} is not supported."), }; }); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs index 40c7d827..ab863e87 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/IDatabaseInitializer.cs @@ -7,7 +7,7 @@ public interface IDatabaseInitializer { void Configure(IMediatorBuilder mediatorBuilder, IDatabaseServiceFixture databaseService); - ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider); + ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken); void Initialize(IServiceCollection services, IDatabaseServiceFixture databaseService); } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs deleted file mode 100644 index 9a6eb0c7..00000000 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/InternalTestWebApplicationFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NetEvolve.Pulse.Tests.Integration.Internals; - -using TUnit.AspNetCore; - -public sealed class InternalTestWebApplicationFactory : TestWebApplicationFactory - where TEntryPoint : class { } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs index e53aad32..ea3714cd 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PulseTestsBase.cs @@ -38,26 +38,24 @@ protected async ValueTask RunAndVerify( configureServices?.Invoke(services); _ = services .AddPulse(mediatorBuilder => DatabaseInitializer.Configure(mediatorBuilder, DatabaseServiceFixture)) - .Configure(options => options.TableName = tableName); + .Configure(options => + { + options.TableName = tableName; + options.Schema = TestHelper.TargetFramework; + }); }) .ConfigureWebHost(webBuilder => _ = webBuilder.UseTestServer().Configure(applicationBuilder => { })) .Build(); - var databaseCreated = await DatabaseInitializer.CreateDatabaseAsync(host.Services).ConfigureAwait(false); - + await DatabaseInitializer.CreateDatabaseAsync(host.Services, cancellationToken).ConfigureAwait(false); await host.StartAsync(cancellationToken).ConfigureAwait(false); using var server = host.GetTestServer(); using (Assert.Multiple()) { - _ = await Assert.That(databaseCreated).IsTrue(); - - if (databaseCreated) - { - await using var scope = server.Services.CreateAsyncScope(); - await testableCode.Invoke(scope.ServiceProvider, cancellationToken).ConfigureAwait(false); - } + await using var scope = server.Services.CreateAsyncScope(); + await testableCode.Invoke(scope.ServiceProvider, cancellationToken).ConfigureAwait(false); } await host.StopAsync(cancellationToken).ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs index 76d6482d..c3198143 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SQLiteDatabaseServiceFixture.cs @@ -6,7 +6,8 @@ internal sealed class SQLiteDatabaseServiceFixture : IDatabaseServiceFixture public DatabaseType DatabaseType => DatabaseType.SQLite; - private string DatabaseFile { get; } = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".sqlite"); + private string DatabaseFile { get; } = + Path.Combine(Path.GetTempPath(), $"{TestHelper.TargetFramework}{Guid.NewGuid():N}.sqlite"); public ValueTask DisposeAsync() { diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs index 0e05b42f..99553a35 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs @@ -11,11 +11,27 @@ public sealed class SqlServerDatabaseServiceFixture : IDatabaseServiceFixture .WithLogger(NullLogger.Instance) .Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => + _container + .GetConnectionString() + .Replace("master", $"{TestHelper.TargetFramework}{Guid.NewGuid():N}", StringComparison.Ordinal); public DatabaseType DatabaseType => DatabaseType.SqlServer; public ValueTask DisposeAsync() => _container.DisposeAsync(); - public Task InitializeAsync() => _container.StartAsync(); + public async Task InitializeAsync() + { + try + { + await _container.StartAsync().WaitAsync(TimeSpan.FromMinutes(2)); + } + catch (Exception ex) + { + throw new InvalidOperationException( + "SQL Server container failed to start within the expected time frame. Try restarting Rancher Desktop.", + ex + ); + } + } } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs new file mode 100644 index 00000000..d976119e --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs @@ -0,0 +1,13 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +internal static class TestHelper +{ + internal static string TargetFramework => +#if NET10_0 + "net10"; +#elif NET9_0 + "net9"; +#elif NET8_0 + "net8"; +#endif +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs deleted file mode 100644 index dc779739..00000000 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/WebApplicationTestBase.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NetEvolve.Pulse.Tests.Integration.Internals; - -using TUnit.AspNetCore; - -public abstract class WebApplicationTestBase - : WebApplicationTest, TEntryPoint> - where TEntryPoint : class { } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs index 2495cc0d..c3428f38 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs @@ -5,9 +5,11 @@ using NetEvolve.Extensions.TUnit; using NetEvolve.Pulse.Extensibility; using NetEvolve.Pulse.Extensibility.Outbox; +using NetEvolve.Pulse.Outbox; using NetEvolve.Pulse.Tests.Integration.Internals; [TestGroup("Outbox")] +[Timeout(60_000)] // Increased timeout to accommodate potential delays in CI environments, especially when using SQL Server containers. public abstract class OutboxTestsBase( IDatabaseServiceFixture databaseServiceFixture, IDatabaseInitializer databaseInitializer @@ -467,7 +469,10 @@ await RunAndVerify( _ = await Assert.That(failedForRetry.Count).IsEqualTo(2); }, cancellationToken, - configureServices: services => services.AddSingleton(timeProvider) + configureServices: services => + services + .AddSingleton(timeProvider) + .Configure(options => options.DisableProcessing = true) ); } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs index c7a05861..0db61a12 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SQLiteEntityFrameworkOutboxTests.cs @@ -3,9 +3,7 @@ using NetEvolve.Extensions.TUnit; using NetEvolve.Pulse.Tests.Integration.Internals; -[ClassDataSource( - Shared = [SharedType.None, SharedType.PerTestSession] -)] +[ClassDataSource(Shared = [SharedType.None, SharedType.None])] [TestGroup("SQLite")] [TestGroup("EntityFramework")] [InheritsTests] diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs new file mode 100644 index 00000000..4b168565 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs @@ -0,0 +1,15 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[ClassDataSource( + Shared = [SharedType.PerClass, SharedType.PerClass] +)] +[TestGroup("SQLServer")] +[TestGroup("EntityFramework")] +[InheritsTests] +public class SqlServerEntityFrameworkOutboxTests( + IDatabaseServiceFixture databaseServiceFixture, + IDatabaseInitializer databaseInitializer +) : OutboxTestsBase(databaseServiceFixture, databaseInitializer); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt new file mode 100644 index 00000000..70a06431 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt @@ -0,0 +1,34 @@ +[ + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_1, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_2, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_2, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2026-04-12T10:09:40.9420622+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_3 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_3, + Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_3 + }, + { + CreatedAt: DateTimeOffset_4, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_4, + Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2026-04-12T10:09:41.0637344+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_3 + } +] \ No newline at end of file From c4e13a0619b5526c967d5f3e3d637a15d12efc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 18:49:21 +0200 Subject: [PATCH 10/25] fix: Reformatted testhelper --- .../Internals/TestHelper.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs index d976119e..0ae4229a 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/TestHelper.cs @@ -2,12 +2,17 @@ internal static class TestHelper { - internal static string TargetFramework => + internal static string TargetFramework + { + get + { #if NET10_0 - "net10"; + return "net10"; #elif NET9_0 - "net9"; + return "net9"; #elif NET8_0 - "net8"; + return "net8"; #endif + } + } } From ab7756b81c12899d457c75ce5faaf0071f2032fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 18:49:32 +0200 Subject: [PATCH 11/25] fix(test): Use correct TestGroup --- .../Outbox/SqlServerEntityFrameworkOutboxTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs index 4b168565..cb5a4ad6 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs @@ -6,7 +6,7 @@ [ClassDataSource( Shared = [SharedType.PerClass, SharedType.PerClass] )] -[TestGroup("SQLServer")] +[TestGroup("SqlServer")] [TestGroup("EntityFramework")] [InheritsTests] public class SqlServerEntityFrameworkOutboxTests( From b5cd16fb08d8a29dd81ae8d30b4d8389629e0b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 21:24:11 +0200 Subject: [PATCH 12/25] test: Add PostgreSQL integration tests and index name fixes - Add PostgreSqlDatabaseServiceFixture and PostgreSqlEntityFrameworkOutboxTests using Testcontainers and TUnit for PostgreSQL Outbox integration testing - Add Npgsql and Npgsql.EntityFrameworkCore.PostgreSQL dependencies for all target frameworks - Generate EF Core index and PK names with TruncateIdentifier to enforce DB identifier length limits and uniqueness (esp. for PostgreSQL) - Update tests and configuration to use new index naming convention (including schema/table) - EntityFrameworkInitializer now supports PostgreSQL via UseNpgsql - Add ContainerParallelLimiter for container-based test parallelism - Outbox integration tests now always disable background processing for determinism - SqlServerEntityFrameworkOutboxTests now uses SharedType.None for fixture/initializer - Refactor OutboxProcessorHostedServiceTests to use WaitForMarkingsAsync instead of Task.Delay for reliable async test synchronization - Marking methods in InMemoryOutboxRepository now signal a semaphore for test coordination - Update OutboxMessageConfigurationMetadataTests for new index names - Minor: remove unused using, add verified snapshot for PostgreSQL tests --- Directory.Packages.props | 7 +- .../OutboxMessageConfigurationBase.cs | 57 +++- .../Internals/ContainerParallelLimiter.cs | 8 + .../Internals/EntityFrameworkInitializer.cs | 12 +- .../PostgreSqlDatabaseServiceFixture.cs | 35 +++ .../NetEvolve.Pulse.Tests.Integration.csproj | 1 + .../Outbox/OutboxTestsBase.cs | 56 +++- .../PostgreSqlEntityFrameworkOutboxTests.cs | 16 ++ .../SqlServerEntityFrameworkOutboxTests.cs | 3 +- ...ted_Messages_93e6c214b197efdd.verified.txt | 26 ++ ...OutboxMessageConfigurationMetadataTests.cs | 30 +- .../OutboxProcessorHostedServiceTests.cs | 270 ++++++++---------- 12 files changed, 332 insertions(+), 189 deletions(-) create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/PostgreSqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_93e6c214b197efdd.verified.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index 7853fac5..a09b63d3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,7 +36,6 @@ - @@ -59,6 +58,8 @@ + + @@ -67,6 +68,8 @@ + + @@ -75,5 +78,7 @@ + + diff --git a/src/NetEvolve.Pulse.EntityFramework/Configurations/OutboxMessageConfigurationBase.cs b/src/NetEvolve.Pulse.EntityFramework/Configurations/OutboxMessageConfigurationBase.cs index 4be80220..4822f9d0 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Configurations/OutboxMessageConfigurationBase.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Configurations/OutboxMessageConfigurationBase.cs @@ -28,6 +28,7 @@ /// internal abstract class OutboxMessageConfigurationBase : IEntityTypeConfiguration { + private const uint FnvPrime = 16777619; private readonly OutboxOptions _options; /// @@ -93,10 +94,12 @@ public void Configure(EntityTypeBuilder builder) var schema = string.IsNullOrWhiteSpace(_options.Schema) ? OutboxMessageSchema.DefaultSchema : _options.Schema.Trim(); - _ = builder.ToTable(_options.TableName, schema); + var tableName = _options.TableName; + + _ = builder.ToTable(tableName, schema); // Primary key - _ = builder.HasKey(m => m.Id); + _ = builder.HasKey(m => m.Id).HasName(TruncateIdentifier($"PK_{schema}_{tableName}")); // Id column _ = builder.Property(m => m.Id).HasColumnName(OutboxMessageSchema.Columns.Id).ValueGeneratedNever(); @@ -155,18 +158,62 @@ public void Configure(EntityTypeBuilder builder) _ = builder .HasIndex(m => new { m.Status, m.CreatedAt }) .HasFilter(PendingMessagesFilter) - .HasDatabaseName("IX_OutboxMessage_Status_CreatedAt"); + .HasDatabaseName(TruncateIdentifier($"IX_{schema}_{tableName}_Status_CreatedAt")); // Index for retry-scheduled message polling (with exponential backoff) _ = builder .HasIndex(m => new { m.Status, m.NextRetryAt }) .HasFilter(RetryScheduledMessagesFilter) - .HasDatabaseName("IX_OutboxMessage_Status_NextRetryAt"); + .HasDatabaseName(TruncateIdentifier($"IX_{schema}_{tableName}_Status_NextRetryAt")); // Index for completed message cleanup _ = builder .HasIndex(m => new { m.Status, m.ProcessedAt }) .HasFilter(CompletedMessagesFilter) - .HasDatabaseName("IX_OutboxMessage_Status_ProcessedAt"); + .HasDatabaseName(TruncateIdentifier($"IX_{schema}_{tableName}_Status_ProcessedAt")); + } + + /// + /// Truncates a database identifier to the specified maximum length while maintaining uniqueness + /// by appending a stable hash suffix when the identifier exceeds the limit. + /// This is required for databases such as PostgreSQL that enforce a 63-character identifier limit. + /// + /// The full identifier name to potentially truncate. + /// The maximum allowed identifier length. Defaults to 63 (PostgreSQL limit). + /// + /// The original if it fits within ; + /// otherwise, a truncated prefix combined with an 8-character hexadecimal hash suffix + /// that uniquely identifies the original name. + /// + private static string TruncateIdentifier(string name, int maxLength = 63) + { + if (name.Length <= maxLength) + { + return name; + } + + // Append a stable hash suffix to distinguish otherwise-identical truncated prefixes. + // The hash is computed over the full name, so two names that share a long common prefix + // but differ only in their suffix will produce different hashes. + var hash = ComputeFnv1aHash(name); + var hashSuffix = $"_{hash:x8}"; // "_" + 8 hex chars = 9 chars + var prefixLength = maxLength - hashSuffix.Length; + return name[..prefixLength] + hashSuffix; + } + + /// + /// Computes a stable 32-bit FNV-1a hash of the given string. + /// FNV-1a is chosen for its simplicity, speed, and good distribution, + /// making it suitable for generating short disambiguation suffixes in identifier names. + /// + private static uint ComputeFnv1aHash(string value) + { + var hash = 2166136261; + foreach (var c in value) + { + hash ^= c; + hash *= FnvPrime; + } + return hash; } } diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs new file mode 100644 index 00000000..d089a68e --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs @@ -0,0 +1,8 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using TUnit.Core.Interfaces; + +internal class ContainerParallelLimiter : IParallelLimit +{ + public int Limit => (Math.Max(1, (Environment.ProcessorCount / 3) - 1)); +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index 15107d97..7b0ea881 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -1,7 +1,6 @@ namespace NetEvolve.Pulse.Tests.Integration.Internals; using System.Data.Common; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -54,7 +53,15 @@ public async ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider, Can if (databaseCreator is IRelationalDatabaseCreator relationalTableCreator) { - await relationalTableCreator.CreateTablesAsync(cancellationToken); + await _gate.WaitAsync(cancellationToken); + try + { + await relationalTableCreator.CreateTablesAsync(cancellationToken); + } + finally + { + _ = _gate.Release(); + } } } } @@ -67,6 +74,7 @@ public void Initialize(IServiceCollection services, IDatabaseServiceFixture data _ = databaseService.DatabaseType switch { DatabaseType.InMemory => options.UseInMemoryDatabase(connectionString), + DatabaseType.PostgreSQL => options.UseNpgsql(connectionString), // Add a busy-timeout interceptor so that concurrent SaveChangesAsync calls from // parallel PublishAsync tasks wait and retry instead of failing with SQLITE_BUSY. DatabaseType.SQLite => options diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs new file mode 100644 index 00000000..30b5b9b6 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs @@ -0,0 +1,35 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using Microsoft.Extensions.Logging.Abstractions; +using Testcontainers.PostgreSql; + +public sealed class PostgreSqlDatabaseServiceFixture : IDatabaseServiceFixture +{ + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder( + /*dockerimage*/"postgres:15.17" + ) + .WithLogger(NullLogger.Instance) + .WithDatabase($"{TestHelper.TargetFramework}{Guid.NewGuid():N}") + .Build(); + + public string ConnectionString => _container.GetConnectionString() + ";Include Error Detail=true;"; + + public DatabaseType DatabaseType => DatabaseType.PostgreSQL; + + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + public async Task InitializeAsync() + { + try + { + await _container.StartAsync().WaitAsync(TimeSpan.FromMinutes(2)); + } + catch (Exception ex) + { + throw new InvalidOperationException( + "PostgreSQL container failed to start within the expected time frame. Try restarting Rancher Desktop.", + ex + ); + } + } +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj index bdc9926c..830fa86d 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj +++ b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs index c3428f38..4f3d97b4 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs @@ -30,7 +30,9 @@ await RunAndVerify( _ = await Assert.That(result).IsEqualTo(3); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -53,7 +55,10 @@ await RunAndVerify( _ = await Verify(result.OrderBy(x => x.Payload)).HashParameters().ConfigureAwait(false); }, cancellationToken, - configureServices: services => services.AddSingleton(timeProvider) + configureServices: services => + services + .AddSingleton(timeProvider) + .Configure(options => options.DisableProcessing = true) ); } @@ -105,7 +110,9 @@ await RunAndVerify( _ = await Assert.That(remainingCount).IsEqualTo(2); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -129,7 +136,9 @@ await RunAndVerify( _ = await Assert.That(pendingCount).IsEqualTo(0); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -154,7 +163,9 @@ await RunAndVerify( _ = await Assert.That(pendingCount).IsEqualTo(0); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -182,7 +193,9 @@ await RunAndVerify( _ = await Assert.That(failedForRetry.Count).IsEqualTo(1); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -211,7 +224,9 @@ await RunAndVerify( _ = await Assert.That(failedForRetry.Count).IsEqualTo(3); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -242,7 +257,10 @@ await outbox _ = await Assert.That(failedForRetry).IsEmpty(); }, cancellationToken, - configureServices: services => services.AddSingleton(timeProvider) + configureServices: services => + services + .AddSingleton(timeProvider) + .Configure(options => options.DisableProcessing = true) ); } @@ -270,7 +288,9 @@ await RunAndVerify( _ = await Assert.That(failedForRetry).IsEmpty(); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -299,7 +319,9 @@ await RunAndVerify( _ = await Assert.That(failedForRetry).IsEmpty(); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); @@ -331,7 +353,10 @@ await outbox _ = await Assert.That(failedForRetry.Count).IsEqualTo(1); }, cancellationToken, - configureServices: services => services.AddSingleton(timeProvider) + configureServices: services => + services + .AddSingleton(timeProvider) + .Configure(options => options.DisableProcessing = true) ); } @@ -363,7 +388,10 @@ await RunAndVerify( _ = await Assert.That(deleted).IsEqualTo(3); }, cancellationToken, - configureServices: services => services.AddSingleton(timeProvider) + configureServices: services => + services + .AddSingleton(timeProvider) + .Configure(options => options.DisableProcessing = true) ); } @@ -430,7 +458,9 @@ await RunAndVerify( _ = await Assert.That(secondRetry).IsEmpty(); }, - cancellationToken + cancellationToken, + configureServices: services => + services.Configure(options => options.DisableProcessing = true) ) .ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs new file mode 100644 index 00000000..a844f996 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs @@ -0,0 +1,16 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[ClassDataSource( + Shared = [SharedType.None, SharedType.None] +)] +[TestGroup("PostgreSql")] +[TestGroup("EntityFramework")] +[ParallelLimiter] +[InheritsTests] +public class PostgreSqlEntityFrameworkOutboxTests( + IDatabaseServiceFixture databaseServiceFixture, + IDatabaseInitializer databaseInitializer +) : OutboxTestsBase(databaseServiceFixture, databaseInitializer); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs index cb5a4ad6..ad38bde9 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs @@ -4,10 +4,11 @@ using NetEvolve.Pulse.Tests.Integration.Internals; [ClassDataSource( - Shared = [SharedType.PerClass, SharedType.PerClass] + Shared = [SharedType.None, SharedType.None] )] [TestGroup("SqlServer")] [TestGroup("EntityFramework")] +[ParallelLimiter] [InheritsTests] public class SqlServerEntityFrameworkOutboxTests( IDatabaseServiceFixture databaseServiceFixture, diff --git a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/PostgreSqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_93e6c214b197efdd.verified.txt b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/PostgreSqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_93e6c214b197efdd.verified.txt new file mode 100644 index 00000000..4ee756e4 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/PostgreSqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_93e6c214b197efdd.verified.txt @@ -0,0 +1,26 @@ +[ + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_1, + Payload: {"CorrelationId":null,"Id":"Test000","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_2, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_3, + Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs index 7cdac5c3..ee349b10 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs @@ -19,9 +19,9 @@ public async Task Create_WithSqlServerProvider_AppliesSqlServerFiltersAndColumnT { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.SqlServer"); - var pendingIndex = GetIndex(entityType, "IX_OutboxMessage_Status_CreatedAt"); - var retryIndex = GetIndex(entityType, "IX_OutboxMessage_Status_NextRetryAt"); - var completedIndex = GetIndex(entityType, "IX_OutboxMessage_Status_ProcessedAt"); + var pendingIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_CreatedAt"); + var retryIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_NextRetryAt"); + var completedIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_ProcessedAt"); using (Assert.Multiple()) { @@ -46,9 +46,9 @@ public async Task Create_WithPostgreSqlProvider_AppliesPostgreSqlFiltersAndColum { var entityType = GetConfiguredEntityType("Npgsql.EntityFrameworkCore.PostgreSQL"); - var pendingIndex = GetIndex(entityType, "IX_OutboxMessage_Status_CreatedAt"); - var retryIndex = GetIndex(entityType, "IX_OutboxMessage_Status_NextRetryAt"); - var completedIndex = GetIndex(entityType, "IX_OutboxMessage_Status_ProcessedAt"); + var pendingIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_CreatedAt"); + var retryIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_NextRetryAt"); + var completedIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_ProcessedAt"); using (Assert.Multiple()) { @@ -71,9 +71,9 @@ public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes() { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.Sqlite"); - var pendingIndex = GetIndex(entityType, "IX_OutboxMessage_Status_CreatedAt"); - var retryIndex = GetIndex(entityType, "IX_OutboxMessage_Status_NextRetryAt"); - var completedIndex = GetIndex(entityType, "IX_OutboxMessage_Status_ProcessedAt"); + var pendingIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_CreatedAt"); + var retryIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_NextRetryAt"); + var completedIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_ProcessedAt"); using (Assert.Multiple()) { @@ -96,9 +96,9 @@ public async Task Create_WithMySqlProvider_AppliesMySqlColumnTypesAndNoFilteredI { var entityType = GetConfiguredEntityType("Pomelo.EntityFrameworkCore.MySql"); - var pendingIndex = GetIndex(entityType, "IX_OutboxMessage_Status_CreatedAt"); - var retryIndex = GetIndex(entityType, "IX_OutboxMessage_Status_NextRetryAt"); - var completedIndex = GetIndex(entityType, "IX_OutboxMessage_Status_ProcessedAt"); + var pendingIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_CreatedAt"); + var retryIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_NextRetryAt"); + var completedIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_ProcessedAt"); using (Assert.Multiple()) { @@ -123,9 +123,9 @@ public async Task Create_WithInMemoryProvider_UsesBaseDefaultsWithoutColumnTypeO { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.InMemory"); - var pendingIndex = GetIndex(entityType, "IX_OutboxMessage_Status_CreatedAt"); - var retryIndex = GetIndex(entityType, "IX_OutboxMessage_Status_NextRetryAt"); - var completedIndex = GetIndex(entityType, "IX_OutboxMessage_Status_ProcessedAt"); + var pendingIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_CreatedAt"); + var retryIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_NextRetryAt"); + var completedIndex = GetIndex(entityType, "IX_pulse_OutboxMessage_Status_ProcessedAt"); using (Assert.Multiple()) { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs index 4331a88c..2d1853a8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs @@ -35,7 +35,7 @@ public async Task Constructor_WithNullRepository_ThrowsArgumentNullException() [Test] public async Task Constructor_WithNullTransport_ThrowsArgumentNullException() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); IMessageTransport? transport = null; var options = Options.Create(new OutboxProcessorOptions()); var logger = CreateLogger(); @@ -49,7 +49,7 @@ public async Task Constructor_WithNullTransport_ThrowsArgumentNullException() [Test] public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); IOptions? options = null; var logger = CreateLogger(); @@ -63,7 +63,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() [Test] public async Task Constructor_WithNullLogger_ThrowsArgumentNullException() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions()); ILogger? logger = null; @@ -77,7 +77,7 @@ public async Task Constructor_WithNullLogger_ThrowsArgumentNullException() [Test] public async Task Constructor_WithNullLifetime_ThrowsArgumentNullException() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); IHostApplicationLifetime? lifetime = null; var options = Options.Create(new OutboxProcessorOptions()); @@ -92,7 +92,7 @@ public async Task Constructor_WithNullLifetime_ThrowsArgumentNullException() [Test] public async Task Constructor_WithValidParameters_CreatesInstance() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions()); var logger = CreateLogger(); @@ -105,7 +105,7 @@ public async Task Constructor_WithValidParameters_CreatesInstance() [Test] public async Task StartAsync_WithCancellationToken_StartsProcessing() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -128,7 +128,7 @@ public async Task StartAsync_WithCancellationToken_StartsProcessing() [Test] public async Task StopAsync_WhenRunning_StopsGracefully() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -149,7 +149,7 @@ public async Task StopAsync_WhenRunning_StopsGracefully() [Test] public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -159,14 +159,9 @@ public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - - // Wait for processing - await Task.Delay(200).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); using (Assert.Multiple()) @@ -180,7 +175,7 @@ public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages [Test] public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -194,12 +189,9 @@ public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages() await repository.AddAsync(message2).ConfigureAwait(false); await repository.AddAsync(message3).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(300).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(3, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); using (Assert.Multiple()) @@ -212,7 +204,7 @@ public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages() [Test] public async Task ExecuteAsync_WithNoMessages_WaitsForPollingInterval() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(200) }); var logger = CreateLogger(); @@ -233,7 +225,7 @@ public async Task ExecuteAsync_WithNoMessages_WaitsForPollingInterval() [Test] public async Task ExecuteAsync_WithTransportFailure_MarksMessageAsFailed() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), MaxRetryCount = 3 } @@ -244,12 +236,9 @@ public async Task ExecuteAsync_WithTransportFailure_MarksMessageAsFailed() var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); _ = await Assert.That(repository.FailedMessageIds).Contains(message.Id); @@ -258,7 +247,7 @@ public async Task ExecuteAsync_WithTransportFailure_MarksMessageAsFailed() [Test] public async Task ExecuteAsync_WithExceededRetries_MovesToDeadLetter() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), MaxRetryCount = 2 } @@ -271,12 +260,9 @@ public async Task ExecuteAsync_WithExceededRetries_MovesToDeadLetter() message.RetryCount = 1; // One retry already attempted await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); _ = await Assert.That(repository.DeadLetterMessageIds).Contains(message.Id); @@ -285,7 +271,7 @@ public async Task ExecuteAsync_WithExceededRetries_MovesToDeadLetter() [Test] public async Task ExecuteAsync_WithTransientFailure_RetriesAndSucceeds() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: 1); // Fail once, then succeed var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), MaxRetryCount = 3 } @@ -296,12 +282,9 @@ public async Task ExecuteAsync_WithTransientFailure_RetriesAndSucceeds() var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(400).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // Message should eventually be completed after retry @@ -311,7 +294,7 @@ public async Task ExecuteAsync_WithTransientFailure_RetriesAndSucceeds() [Test] public async Task ExecuteAsync_WithBatchSendingEnabled_SendsInBatch() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), EnableBatchSending = true } @@ -324,12 +307,9 @@ public async Task ExecuteAsync_WithBatchSendingEnabled_SendsInBatch() await repository.AddAsync(message1).ConfigureAwait(false); await repository.AddAsync(message2).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(500).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); using (Assert.Multiple()) @@ -340,9 +320,9 @@ public async Task ExecuteAsync_WithBatchSendingEnabled_SendsInBatch() } [Test] - public async Task ExecuteAsync_WithBatchSendingFailure_MarkAsFailedForRetry() + public async Task ExecuteAsync_WithBatchSendingFailure_MarkAsFailedForRetry(CancellationToken cancellationToken) { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new BatchFailingMessageTransport(); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), EnableBatchSending = true } @@ -352,16 +332,16 @@ public async Task ExecuteAsync_WithBatchSendingFailure_MarkAsFailedForRetry() var message1 = CreateMessage(); var message2 = CreateMessage(); - await repository.AddAsync(message1).ConfigureAwait(false); - await repository.AddAsync(message2).ConfigureAwait(false); + await repository.AddAsync(message1, cancellationToken).ConfigureAwait(false); + await repository.AddAsync(message2, cancellationToken).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - // Wait for first polling cycle to process the batch failure and complete marking - await Task.Delay(300).ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + // Wait until at least 2 messages have been marked (failed or dead-letter) instead of + // relying on a fixed delay, which is unreliable under CI thread-pool saturation. + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // Verify that batch send was not followed by individual send (no fallback to ProcessIndividuallyAsync) @@ -380,7 +360,7 @@ public async Task ExecuteAsync_WithBatchSendingFailure_MarkAsFailedForRetry() [Test] public async Task ExecuteAsync_WithBatchSize_RespectsLimit() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), BatchSize = 2 } @@ -394,12 +374,9 @@ public async Task ExecuteAsync_WithBatchSize_RespectsLimit() await repository.AddAsync(CreateMessage()).ConfigureAwait(false); } - using var cts = new CancellationTokenSource(); - - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(100).ConfigureAwait(false); - - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // First batch should have processed 2 messages @@ -409,7 +386,7 @@ public async Task ExecuteAsync_WithBatchSize_RespectsLimit() [Test] public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_UsesOverrideForMatchingEventType() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions @@ -430,10 +407,9 @@ public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_UsesOverrideForMatc message.RetryCount = 0; // First attempt await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // With MaxRetryCount=1, retryCount+1 (1) >= 1, so it should be dead-lettered @@ -462,7 +438,7 @@ public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_FallsBackToGlobalFo [Test] public async Task ExecuteAsync_WithPerEventTypeProcessingTimeout_UsesOverride() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new SlowMessageTransport(delay: TimeSpan.FromMilliseconds(200)); var options = Options.Create( new OutboxProcessorOptions @@ -486,10 +462,9 @@ public async Task ExecuteAsync_WithPerEventTypeProcessingTimeout_UsesOverride() var message = CreateMessage(typeof(SlowEvent)); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(400).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // The message should not have been sent successfully @@ -499,7 +474,7 @@ public async Task ExecuteAsync_WithPerEventTypeProcessingTimeout_UsesOverride() [Test] public async Task ExecuteAsync_WithPerEventTypeBatchSending_UsesOverrideForMatchingEventType() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create( new OutboxProcessorOptions @@ -521,10 +496,9 @@ public async Task ExecuteAsync_WithPerEventTypeBatchSending_UsesOverrideForMatch await repository.AddAsync(message1).ConfigureAwait(false); await repository.AddAsync(message2).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(500).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); using (Assert.Multiple()) @@ -641,7 +615,7 @@ public async Task ExecuteAsync_WithPendingMessages_RecordsProcessedMetric() ); meterListener.Start(); - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -650,10 +624,9 @@ public async Task ExecuteAsync_WithPendingMessages_RecordsProcessedMetric() var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); meterListener.RecordObservableInstruments(); @@ -686,7 +659,7 @@ public async Task ExecuteAsync_WithTransportFailure_RecordsFailedMetric() ); meterListener.Start(); - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), MaxRetryCount = 3 } @@ -697,10 +670,9 @@ public async Task ExecuteAsync_WithTransportFailure_RecordsFailedMetric() var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); _ = await Assert.That(Volatile.Read(ref failedTotal)).IsGreaterThanOrEqualTo(1L); @@ -731,7 +703,7 @@ public async Task ExecuteAsync_WithExceededRetries_RecordsDeadLetterMetric() ); meterListener.Start(); - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50), MaxRetryCount = 2 } @@ -743,10 +715,9 @@ public async Task ExecuteAsync_WithExceededRetries_RecordsDeadLetterMetric() message.RetryCount = 1; await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); _ = await Assert.That(Volatile.Read(ref deadLetterTotal)).IsGreaterThanOrEqualTo(1L); @@ -777,7 +748,7 @@ public async Task ExecuteAsync_AfterProcessingCycle_RecordsProcessingDuration() ); meterListener.Start(); - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -817,7 +788,7 @@ public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendin ); meterListener.Start(); - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -828,8 +799,7 @@ public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendin await repository.AddAsync(CreateMessage()).ConfigureAwait(false); await repository.AddAsync(CreateMessage()).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); // Wait at least one polling cycle so the gauge is refreshed before observing await Task.Delay(75).ConfigureAwait(false); @@ -837,8 +807,8 @@ public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendin var earlyObservation = Volatile.Read(ref pendingObserved); - await Task.Delay(400).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(3, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // After processing all messages, the pending count should be 0 @@ -857,7 +827,7 @@ public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendin [Test] public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions @@ -875,24 +845,12 @@ public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt() var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - // Capture reference time before starting so the assertion is not sensitive to - // how long StopAsync or pre-assertion overhead takes to complete. var startTime = DateTimeOffset.UtcNow; - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await service.StartAsync(cts.Token).ConfigureAwait(false); - // Wait long enough for at least one processing cycle (~50ms poll interval + margin). - await Task.Delay(200).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); - - try - { - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Expected during shutdown - } + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); + await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // Get the failed message from the repository var failedMessage = repository._messages.FirstOrDefault(m => m.Status == OutboxMessageStatus.Failed); @@ -908,7 +866,7 @@ public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt() [Test] public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetryAt() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); var options = Options.Create( new OutboxProcessorOptions @@ -924,19 +882,10 @@ public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetr var message = CreateMessage(); await repository.AddAsync(message).ConfigureAwait(false); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(300).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); - - try - { - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Expected during shutdown - } + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); + await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // Get the failed message from the repository var failedMessage = repository._messages.FirstOrDefault(m => m.Status == OutboxMessageStatus.Failed); @@ -951,7 +900,7 @@ public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetr [Test] public async Task GetPendingAsync_WithFutureNextRetryAt_ExcludesMessage() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var futureTime = DateTimeOffset.UtcNow.AddSeconds(10); var message = CreateMessage(); @@ -966,7 +915,7 @@ public async Task GetPendingAsync_WithFutureNextRetryAt_ExcludesMessage() [Test] public async Task GetPendingAsync_WithPastNextRetryAt_IncludesMessage() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var pastTime = DateTimeOffset.UtcNow.AddSeconds(-10); var message = CreateMessage(); @@ -981,7 +930,7 @@ public async Task GetPendingAsync_WithPastNextRetryAt_IncludesMessage() [Test] public async Task GetFailedForRetryAsync_WithFutureNextRetryAt_ExcludesMessage() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var futureTime = DateTimeOffset.UtcNow.AddSeconds(10); var message = CreateMessage(); @@ -1000,7 +949,7 @@ public async Task GetFailedForRetryAsync_WithFutureNextRetryAt_ExcludesMessage() [Test] public async Task GetFailedForRetryAsync_WithPastNextRetryAt_IncludesMessage() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var pastTime = DateTimeOffset.UtcNow.AddSeconds(-10); var message = CreateMessage(); @@ -1019,7 +968,7 @@ public async Task GetFailedForRetryAsync_WithPastNextRetryAt_IncludesMessage() [Test] public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessages() { - var repository = new InMemoryOutboxRepository(); + using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -1028,8 +977,7 @@ public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessag await repository.AddAsync(CreateMessage()).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); // Give the service ample time to run polling cycles if it were already started. await Task.Delay(200).ConfigureAwait(false); @@ -1043,8 +991,8 @@ public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessag // Now signal that the application has fully started. lifetime.SignalStarted(); - await Task.Delay(200).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); // Processing must have happened after ApplicationStarted fired. @@ -1054,7 +1002,7 @@ public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessag [Test] public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle() { - var repository = new InMemoryOutboxRepository { IsHealthy = false }; + using var repository = new InMemoryOutboxRepository { IsHealthy = false }; var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -1078,7 +1026,7 @@ public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle() [Test] public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing() { - var repository = new InMemoryOutboxRepository { IsHealthy = false }; + using var repository = new InMemoryOutboxRepository { IsHealthy = false }; var transport = new InMemoryMessageTransport(); var options = Options.Create(new OutboxProcessorOptions { PollingInterval = TimeSpan.FromMilliseconds(50) }); var logger = CreateLogger(); @@ -1086,8 +1034,7 @@ public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing() await repository.AddAsync(CreateMessage()).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); - await service.StartAsync(cts.Token).ConfigureAwait(false); + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); // Allow several unhealthy cycles to pass. await Task.Delay(150).ConfigureAwait(false); @@ -1097,8 +1044,8 @@ public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing() // Restore database health and allow processing to resume. repository.IsHealthy = true; - await Task.Delay(300).ConfigureAwait(false); - await cts.CancelAsync().ConfigureAwait(false); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); await service.StopAsync(CancellationToken.None).ConfigureAwait(false); _ = await Assert.That(transport.SentMessages).HasSingleItem(); @@ -1123,10 +1070,23 @@ private static OutboxMessage CreateMessage(Type? eventType = null) => Status = OutboxMessageStatus.Pending, }; - private sealed class InMemoryOutboxRepository : IOutboxRepository + private sealed class InMemoryOutboxRepository : IOutboxRepository, IDisposable { internal readonly List _messages = []; private readonly object _lock = new(); + private readonly SemaphoreSlim _markingEvent = new(0, int.MaxValue); + + /// + /// Waits until at least processing events (completed, failed, or dead-letter) + /// have been recorded, or the is cancelled. + /// + public async Task WaitForMarkingsAsync(int count, CancellationToken cancellationToken = default) + { + for (var i = 0; i < count; i++) + { + await _markingEvent.WaitAsync(cancellationToken).ConfigureAwait(false); + } + } public List CompletedMessageIds { get; } = []; public List FailedMessageIds { get; } = []; @@ -1228,6 +1188,7 @@ public Task MarkAsCompletedAsync(Guid messageId, CancellationToken cancellationT } } + _ = _markingEvent.Release(); return Task.CompletedTask; } @@ -1248,6 +1209,7 @@ public Task MarkAsDeadLetterAsync( } } + _ = _markingEvent.Release(); return Task.CompletedTask; } @@ -1269,6 +1231,7 @@ public Task MarkAsFailedAsync( } } + _ = _markingEvent.Release(); return Task.CompletedTask; } @@ -1292,8 +1255,11 @@ public Task MarkAsFailedAsync( } } + _ = _markingEvent.Release(); return Task.CompletedTask; } + + public void Dispose() => _markingEvent.Dispose(); } private sealed class InMemoryMessageTransport : IMessageTransport From b4828e9609059a724a0fd4a4c2b51cdc3be5b4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 21:48:34 +0200 Subject: [PATCH 13/25] test: Add CancellationToken to all async test methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored all unit and integration tests to accept a CancellationToken parameter and pass it to all async methods under test and framework calls. Updated test signatures, lambda expressions, and helper invocations accordingly. This improves test reliability, enables proper cancellation support, and aligns with .NET async best practices. No production code was changed—only test code is affected. --- .../ActivityMetricsExtensionsTests.cs | 14 +- .../EndpointRouteBuilderExtensionsTests.cs | 49 ++-- .../AssemblyScanningExtensionsTests.cs | 94 ++++--- .../AzureServiceBusExtensionsTests.cs | 30 ++- .../AzureServiceBusMessageTransportTests.cs | 68 ++--- .../Dapr/DaprExtensionsTests.cs | 17 +- .../Dapr/DaprMessageTransportTests.cs | 22 +- .../ParallelEventDispatcherTests.cs | 16 +- .../PrioritizedEventDispatcherTests.cs | 38 +-- .../RateLimitedEventDispatcherTests.cs | 27 +- .../SequentialEventDispatcherTests.cs | 18 +- .../EntityFrameworkEventOutboxTests.cs | 18 +- .../EntityFrameworkExtensionsTests.cs | 22 +- .../EntityFrameworkOutboxManagementTests.cs | 116 +++++--- .../EntityFrameworkOutboxRepositoryTests.cs | 16 +- ...ityFrameworkOutboxTransactionScopeTests.cs | 6 +- .../ModelBuilderExtensionsTests.cs | 16 +- ...OutboxMessageConfigurationMetadataTests.cs | 26 +- .../TypeValueConverterTests.cs | 12 +- .../EventDispatcherExtensionsTests.cs | 48 ++-- .../FluentValidationExtensionsTests.cs | 14 +- ...FluentValidationRequestInterceptorTests.cs | 26 +- .../HandlerRegistrationExtensionsTests.cs | 124 ++++++--- .../HttpCorrelationExtensionsTests.cs | 28 +- .../HttpCorrelationEventInterceptorTests.cs | 31 ++- .../HttpCorrelationRequestInterceptorTests.cs | 35 ++- ...pCorrelationStreamQueryInterceptorTests.cs | 50 ++-- .../IdempotencyExtensionsTests.cs | 17 +- ...ActivityAndMetricsEventInterceptorTests.cs | 36 ++- ...tivityAndMetricsRequestInterceptorTests.cs | 42 +-- ...tyAndMetricsStreamQueryInterceptorTests.cs | 35 +-- .../DistributedCacheQueryInterceptorTests.cs | 76 +++--- .../IdempotencyCommandInterceptorTests.cs | 43 +-- .../LoggingEventInterceptorTests.cs | 38 +-- ...LoggingInterceptorOptionsValidatorTests.cs | 10 +- .../LoggingRequestInterceptorTests.cs | 55 ++-- .../TimeoutRequestInterceptorTests.cs | 63 +++-- .../Internals/MediatorBuilderTests.cs | 6 +- .../Internals/PulseMediatorTests.cs | 88 +++--- .../Kafka/KafkaExtensionsTests.cs | 8 +- .../Kafka/KafkaMessageTransportTests.cs | 40 +-- .../LoggingExtensionsTests.cs | 14 +- .../Outbox/ExponentialBackoffTests.cs | 26 +- .../Outbox/NullMessageTransportTests.cs | 10 +- .../Outbox/OutboxEventHandlerTests.cs | 22 +- .../Outbox/OutboxExtensionsTests.cs | 12 +- .../OutboxProcessorHostedServiceTests.cs | 253 +++++++++--------- .../Outbox/OutboxStatisticsTests.cs | 10 +- .../PollyEventInterceptorTests.cs | 50 ++-- .../PollyRequestInterceptorTests.cs | 51 ++-- .../Polly/PollyExtensionsTests.cs | 90 +++++-- .../PostgreSql/PostgreSqlEventOutboxTests.cs | 20 +- .../PostgreSql/PostgreSqlExtensionsTests.cs | 58 ++-- .../PostgreSqlOutboxManagementTests.cs | 36 ++- .../PostgreSqlOutboxOptionsExtensionsTests.cs | 14 +- .../PostgreSqlOutboxRepositoryTests.cs | 34 ++- .../PostgreSqlOutboxTransactionScopeTests.cs | 8 +- .../QueryCachingExtensionsTests.cs | 12 +- .../RabbitMQ/RabbitMqExtensionsTests.cs | 14 +- .../RabbitMQ/RabbitMqMessageTransportTests.cs | 70 ++--- .../SQLite/SQLiteEventOutboxTests.cs | 46 ++-- .../SQLite/SQLiteExtensionsTests.cs | 44 ++- .../SQLiteOutboxManagementDatabaseTests.cs | 68 ++--- .../SQLite/SQLiteOutboxManagementTests.cs | 30 ++- .../SQLiteOutboxOptionsExtensionsTests.cs | 4 +- .../SQLiteOutboxRepositoryDatabaseTests.cs | 101 ++++--- .../SQLite/SQLiteOutboxRepositoryTests.cs | 26 +- .../SQLiteOutboxTransactionScopeTests.cs | 8 +- .../ServiceCollectionExtensionsTests.cs | 10 +- .../SqlServer/SqlServerEventOutboxTests.cs | 20 +- .../SqlServer/SqlServerExtensionsTests.cs | 56 ++-- .../SqlServerOutboxManagementTests.cs | 36 ++- .../SqlServerOutboxOptionsExtensionsTests.cs | 18 +- .../SqlServerOutboxRepositoryTests.cs | 34 ++- .../SqlServerOutboxTransactionScopeTests.cs | 8 +- 75 files changed, 1704 insertions(+), 1147 deletions(-) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs index f2e0040c..c3a38306 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs @@ -16,13 +16,15 @@ public sealed class ActivityMetricsExtensionsTests { [Test] - public async Task AddActivityAndMetrics_NullBuilder_ThrowsArgumentNullException() => + public async Task AddActivityAndMetrics_NullBuilder_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => ActivityMetricsExtensions.AddActivityAndMetrics(null!)) .Throws(); [Test] - public async Task AddActivityAndMetrics_RegistersEventInterceptor() + public async Task AddActivityAndMetrics_RegistersEventInterceptor(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -44,7 +46,7 @@ public async Task AddActivityAndMetrics_RegistersEventInterceptor() } [Test] - public async Task AddActivityAndMetrics_RegistersRequestInterceptor() + public async Task AddActivityAndMetrics_RegistersRequestInterceptor(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -66,7 +68,9 @@ public async Task AddActivityAndMetrics_RegistersRequestInterceptor() } [Test] - public async Task AddActivityAndMetrics_CalledMultipleTimes_DoesNotDuplicateRegistrations() + public async Task AddActivityAndMetrics_CalledMultipleTimes_DoesNotDuplicateRegistrations( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -96,7 +100,7 @@ public async Task AddActivityAndMetrics_CalledMultipleTimes_DoesNotDuplicateRegi } [Test] - public async Task AddActivityAndMetrics_ReturnsSameBuilder() + public async Task AddActivityAndMetrics_ReturnsSameBuilder(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs index 814776e8..28c79c67 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs @@ -17,11 +17,14 @@ public sealed class EndpointRouteBuilderExtensionsTests // MapCommand — null-argument guards [Test] - public void MapCommand_WithResponseAndNullEndpoints_ThrowsArgumentNullException() => - _ = Assert.Throws(() => PulseEndpoints.MapCommand(null!, "/test")); + public void MapCommand_WithResponseAndNullEndpoints_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = Assert.Throws(() => PulseEndpoints.MapCommand(null!, "/test")); [Test] - public async Task MapCommand_WithResponseAndNullPattern_ThrowsArgumentNullException() + public async Task MapCommand_WithResponseAndNullPattern_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -31,7 +34,9 @@ public async Task MapCommand_WithResponseAndNullPattern_ThrowsArgumentNullExcept // MapCommand — httpMethod validation [Test] - public async Task MapCommand_WithResponse_WithUndefinedMethod_ThrowsArgumentOutOfRangeException() + public async Task MapCommand_WithResponse_WithUndefinedMethod_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -43,7 +48,9 @@ public async Task MapCommand_WithResponse_WithUndefinedMethod_ThrowsArgumentOutO // MapCommand — valid cases [Test] - public async Task MapCommand_WithResponse_DefaultPost_ReturnsRouteHandlerBuilder() + public async Task MapCommand_WithResponse_DefaultPost_ReturnsRouteHandlerBuilder( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -53,7 +60,9 @@ public async Task MapCommand_WithResponse_DefaultPost_ReturnsRouteHandlerBuilder } [Test] - public async Task MapCommand_WithResponse_WithPutMethod_ReturnsRouteHandlerBuilder() + public async Task MapCommand_WithResponse_WithPutMethod_ReturnsRouteHandlerBuilder( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -63,7 +72,9 @@ public async Task MapCommand_WithResponse_WithPutMethod_ReturnsRouteHandlerBuild } [Test] - public async Task MapCommand_WithResponse_WithPatchMethod_ReturnsRouteHandlerBuilder() + public async Task MapCommand_WithResponse_WithPatchMethod_ReturnsRouteHandlerBuilder( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -73,7 +84,9 @@ public async Task MapCommand_WithResponse_WithPatchMethod_ReturnsRouteHandlerBui } [Test] - public async Task MapCommand_WithResponse_WithDeleteMethod_ReturnsRouteHandlerBuilder() + public async Task MapCommand_WithResponse_WithDeleteMethod_ReturnsRouteHandlerBuilder( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -85,11 +98,11 @@ public async Task MapCommand_WithResponse_WithDeleteMethod_ReturnsRouteHandlerBu // MapCommand (void) — null-argument guards [Test] - public void MapCommand_VoidAndNullEndpoints_ThrowsArgumentNullException() => + public void MapCommand_VoidAndNullEndpoints_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = Assert.Throws(() => PulseEndpoints.MapCommand(null!, "/test")); [Test] - public async Task MapCommand_VoidAndNullPattern_ThrowsArgumentNullException() + public async Task MapCommand_VoidAndNullPattern_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -99,7 +112,9 @@ public async Task MapCommand_VoidAndNullPattern_ThrowsArgumentNullException() // MapCommand (void) — httpMethod validation [Test] - public async Task MapCommand_Void_WithUndefinedMethod_ThrowsArgumentOutOfRangeException() + public async Task MapCommand_Void_WithUndefinedMethod_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -111,7 +126,7 @@ public async Task MapCommand_Void_WithUndefinedMethod_ThrowsArgumentOutOfRangeEx // MapCommand (void) — valid cases [Test] - public async Task MapCommand_Void_DefaultPost_ReturnsRouteHandlerBuilder() + public async Task MapCommand_Void_DefaultPost_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -121,7 +136,7 @@ public async Task MapCommand_Void_DefaultPost_ReturnsRouteHandlerBuilder() } [Test] - public async Task MapCommand_Void_WithDeleteMethod_ReturnsRouteHandlerBuilder() + public async Task MapCommand_Void_WithDeleteMethod_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -131,7 +146,7 @@ public async Task MapCommand_Void_WithDeleteMethod_ReturnsRouteHandlerBuilder() } [Test] - public async Task MapCommand_Void_WithPutMethod_ReturnsRouteHandlerBuilder() + public async Task MapCommand_Void_WithPutMethod_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -143,11 +158,11 @@ public async Task MapCommand_Void_WithPutMethod_ReturnsRouteHandlerBuilder() // MapQuery — null-argument guards [Test] - public void MapQuery_WithNullEndpoints_ThrowsArgumentNullException() => + public void MapQuery_WithNullEndpoints_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = Assert.Throws(() => PulseEndpoints.MapQuery(null!, "/test")); [Test] - public async Task MapQuery_WithNullPattern_ThrowsArgumentNullException() + public async Task MapQuery_WithNullPattern_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -155,7 +170,7 @@ public async Task MapQuery_WithNullPattern_ThrowsArgumentNullException() } [Test] - public async Task MapQuery_ReturnsRouteHandlerBuilder() + public async Task MapQuery_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) { await using var endpoints = WebApplication.CreateBuilder().Build(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs index f62740cd..e5e4496c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs @@ -12,7 +12,9 @@ public class AssemblyScanningExtensionsTests { [Test] - public void AddHandlersFromAssembly_WithNullConfigurator_ThrowsArgumentNullException() + public void AddHandlersFromAssembly_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -21,7 +23,9 @@ public void AddHandlersFromAssembly_WithNullConfigurator_ThrowsArgumentNullExcep } [Test] - public void AddHandlersFromAssembly_WithNullAssembly_ThrowsArgumentNullException() + public void AddHandlersFromAssembly_WithNullAssembly_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); Assembly? assembly = null; @@ -32,7 +36,7 @@ public void AddHandlersFromAssembly_WithNullAssembly_ThrowsArgumentNullException } [Test] - public async Task AddHandlersFromAssembly_RegistersCommandHandlers() + public async Task AddHandlersFromAssembly_RegistersCommandHandlers(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -52,7 +56,7 @@ public async Task AddHandlersFromAssembly_RegistersCommandHandlers() } [Test] - public async Task AddHandlersFromAssembly_RegistersQueryHandlers() + public async Task AddHandlersFromAssembly_RegistersQueryHandlers(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -70,7 +74,7 @@ public async Task AddHandlersFromAssembly_RegistersQueryHandlers() } [Test] - public async Task AddHandlersFromAssembly_RegistersEventHandlers() + public async Task AddHandlersFromAssembly_RegistersEventHandlers(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -88,7 +92,9 @@ public async Task AddHandlersFromAssembly_RegistersEventHandlers() } [Test] - public async Task AddHandlersFromAssembly_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime() + public async Task AddHandlersFromAssembly_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -112,7 +118,7 @@ public async Task AddHandlersFromAssembly_WithCustomLifetime_RegistersHandlersWi } [Test] - public async Task AddHandlersFromAssembly_DoesNotRegisterAbstractClasses() + public async Task AddHandlersFromAssembly_DoesNotRegisterAbstractClasses(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -125,7 +131,7 @@ public async Task AddHandlersFromAssembly_DoesNotRegisterAbstractClasses() } [Test] - public async Task AddHandlersFromAssembly_ReturnsConfigurator() + public async Task AddHandlersFromAssembly_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -146,7 +152,9 @@ public async Task AddHandlersFromAssembly_ReturnsConfigurator() } [Test] - public void AddHandlersFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException() + public void AddHandlersFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -156,7 +164,9 @@ public void AddHandlersFromAssemblyContaining_WithNullConfigurator_ThrowsArgumen } [Test] - public async Task AddHandlersFromAssemblyContaining_RegistersHandlersFromMarkerTypeAssembly() + public async Task AddHandlersFromAssemblyContaining_RegistersHandlersFromMarkerTypeAssembly( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -174,7 +184,9 @@ public async Task AddHandlersFromAssemblyContaining_RegistersHandlersFromMarkerT } [Test] - public async Task AddHandlersFromAssemblyContaining_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime() + public async Task AddHandlersFromAssemblyContaining_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -194,7 +206,7 @@ public async Task AddHandlersFromAssemblyContaining_WithCustomLifetime_Registers } [Test] - public async Task AddHandlersFromAssemblyContaining_ReturnsConfigurator() + public async Task AddHandlersFromAssemblyContaining_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -214,7 +226,9 @@ public async Task AddHandlersFromAssemblyContaining_ReturnsConfigurator() } [Test] - public void AddHandlersFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException() + public void AddHandlersFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -223,7 +237,9 @@ public void AddHandlersFromAssemblies_WithNullConfigurator_ThrowsArgumentNullExc } [Test] - public void AddHandlersFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException() + public void AddHandlersFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); Assembly[]? assemblies = null; @@ -234,7 +250,7 @@ public void AddHandlersFromAssemblies_WithNullAssemblies_ThrowsArgumentNullExcep } [Test] - public async Task AddHandlersFromAssemblies_RegistersHandlersFromAllAssemblies() + public async Task AddHandlersFromAssemblies_RegistersHandlersFromAllAssemblies(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assemblies = new[] @@ -253,7 +269,9 @@ public async Task AddHandlersFromAssemblies_RegistersHandlersFromAllAssemblies() } [Test] - public async Task AddHandlersFromAssemblies_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime() + public async Task AddHandlersFromAssemblies_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -272,7 +290,7 @@ public async Task AddHandlersFromAssemblies_WithCustomLifetime_RegistersHandlers } [Test] - public async Task AddHandlersFromAssemblies_ReturnsConfigurator() + public async Task AddHandlersFromAssemblies_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -344,7 +362,9 @@ public abstract Task HandleAsync( // Interceptor scanning tests [Test] - public void AddInterceptorsFromAssembly_WithNullConfigurator_ThrowsArgumentNullException() + public void AddInterceptorsFromAssembly_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -353,7 +373,9 @@ public void AddInterceptorsFromAssembly_WithNullConfigurator_ThrowsArgumentNullE } [Test] - public void AddInterceptorsFromAssembly_WithNullAssembly_ThrowsArgumentNullException() + public void AddInterceptorsFromAssembly_WithNullAssembly_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); Assembly? assembly = null; @@ -364,7 +386,7 @@ public void AddInterceptorsFromAssembly_WithNullAssembly_ThrowsArgumentNullExcep } [Test] - public async Task AddInterceptorsFromAssembly_RegistersRequestInterceptors() + public async Task AddInterceptorsFromAssembly_RegistersRequestInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -384,7 +406,7 @@ public async Task AddInterceptorsFromAssembly_RegistersRequestInterceptors() } [Test] - public async Task AddInterceptorsFromAssembly_RegistersCommandInterceptors() + public async Task AddInterceptorsFromAssembly_RegistersCommandInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -404,7 +426,7 @@ public async Task AddInterceptorsFromAssembly_RegistersCommandInterceptors() } [Test] - public async Task AddInterceptorsFromAssembly_RegistersQueryInterceptors() + public async Task AddInterceptorsFromAssembly_RegistersQueryInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -424,7 +446,7 @@ public async Task AddInterceptorsFromAssembly_RegistersQueryInterceptors() } [Test] - public async Task AddInterceptorsFromAssembly_RegistersEventInterceptors() + public async Task AddInterceptorsFromAssembly_RegistersEventInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -442,7 +464,9 @@ public async Task AddInterceptorsFromAssembly_RegistersEventInterceptors() } [Test] - public async Task AddInterceptorsFromAssembly_WithCustomLifetime_RegistersInterceptorsWithSpecifiedLifetime() + public async Task AddInterceptorsFromAssembly_WithCustomLifetime_RegistersInterceptorsWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -474,7 +498,7 @@ public async Task AddInterceptorsFromAssembly_WithCustomLifetime_RegistersInterc } [Test] - public async Task AddInterceptorsFromAssembly_ReturnsConfigurator() + public async Task AddInterceptorsFromAssembly_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -495,7 +519,9 @@ public async Task AddInterceptorsFromAssembly_ReturnsConfigurator() } [Test] - public void AddInterceptorsFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException() + public void AddInterceptorsFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -504,7 +530,9 @@ public void AddInterceptorsFromAssemblies_WithNullConfigurator_ThrowsArgumentNul } [Test] - public void AddInterceptorsFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException() + public void AddInterceptorsFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); Assembly[]? assemblies = null; @@ -515,7 +543,9 @@ public void AddInterceptorsFromAssemblies_WithNullAssemblies_ThrowsArgumentNullE } [Test] - public async Task AddInterceptorsFromAssemblies_RegistersInterceptorsFromAllAssemblies() + public async Task AddInterceptorsFromAssemblies_RegistersInterceptorsFromAllAssemblies( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -540,7 +570,7 @@ public async Task AddInterceptorsFromAssemblies_RegistersInterceptorsFromAllAsse } [Test] - public async Task AddInterceptorsFromAssemblies_ReturnsConfigurator() + public async Task AddInterceptorsFromAssemblies_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -561,7 +591,7 @@ public async Task AddInterceptorsFromAssemblies_ReturnsConfigurator() } [Test] - public async Task AddInterceptorsFromAssemblyContaining_RegistersInterceptors() + public async Task AddInterceptorsFromAssemblyContaining_RegistersInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -585,7 +615,9 @@ public async Task AddInterceptorsFromAssemblyContaining_RegistersInterceptors() } [Test] - public void AddInterceptorsFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException() + public void AddInterceptorsFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs index 1bb2ea64..a5c11d2c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs @@ -19,7 +19,9 @@ public sealed class AzureServiceBusExtensionsTests "Endpoint=sb://localhost/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=Fake="; [Test] - public void UseAzureServiceBusTransport_When_configurator_is_null_throws_ArgumentNullException() + public void UseAzureServiceBusTransport_When_configurator_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -27,7 +29,7 @@ public void UseAzureServiceBusTransport_When_configurator_is_null_throws_Argumen } [Test] - public async Task UseAzureServiceBusTransport_registers_transport() + public async Task UseAzureServiceBusTransport_registers_transport(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => @@ -39,7 +41,9 @@ public async Task UseAzureServiceBusTransport_registers_transport() } [Test] - public async Task UseAzureServiceBusTransport_registers_ServiceBusClient_as_singleton() + public async Task UseAzureServiceBusTransport_registers_ServiceBusClient_as_singleton( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => @@ -51,7 +55,9 @@ public async Task UseAzureServiceBusTransport_registers_ServiceBusClient_as_sing } [Test] - public async Task UseAzureServiceBusTransport_registers_DefaultAzureCredential_as_TokenCredential() + public async Task UseAzureServiceBusTransport_registers_DefaultAzureCredential_as_TokenCredential( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseAzureServiceBusTransport()); @@ -61,7 +67,7 @@ public async Task UseAzureServiceBusTransport_registers_DefaultAzureCredential_a } [Test] - public async Task UseAzureServiceBusTransport_validates_required_options() + public async Task UseAzureServiceBusTransport_validates_required_options(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseAzureServiceBusTransport()); @@ -72,7 +78,9 @@ public async Task UseAzureServiceBusTransport_validates_required_options() } [Test] - public async Task UseAzureServiceBusTransport_with_fully_qualified_namespace_creates_ServiceBusClient() + public async Task UseAzureServiceBusTransport_with_fully_qualified_namespace_creates_ServiceBusClient( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); @@ -93,7 +101,9 @@ public async Task UseAzureServiceBusTransport_with_fully_qualified_namespace_cre } [Test] - public async Task UseAzureServiceBusTransport_with_connection_string_creates_ServiceBusClient() + public async Task UseAzureServiceBusTransport_with_connection_string_creates_ServiceBusClient( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => @@ -107,7 +117,7 @@ public async Task UseAzureServiceBusTransport_with_connection_string_creates_Ser } [Test] - public async Task UseAzureServiceBusTransport_replaces_existing_transport() + public async Task UseAzureServiceBusTransport_replaces_existing_transport(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -120,7 +130,9 @@ public async Task UseAzureServiceBusTransport_replaces_existing_transport() } [Test] - public async Task UseAzureServiceBusTransport_does_not_replace_custom_TokenCredential() + public async Task UseAzureServiceBusTransport_does_not_replace_custom_TokenCredential( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new FakeTokenCredential()); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs index b44b5229..287d7bf1 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs @@ -22,7 +22,7 @@ public sealed class AzureServiceBusMessageTransportTests // ── Constructor guards ──────────────────────────────────────────────────── [Test] - public async Task Constructor_When_client_is_null_throws_ArgumentNullException() + public async Task Constructor_When_client_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) { var resolver = new FakeTopicNameResolver(); var options = Options.Create(new AzureServiceBusTransportOptions()); @@ -33,7 +33,9 @@ public async Task Constructor_When_client_is_null_throws_ArgumentNullException() } [Test] - public async Task Constructor_When_resolver_is_null_throws_ArgumentNullException() + public async Task Constructor_When_resolver_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) { await using var client = new ServiceBusClient(FakeConnectionString); var options = Options.Create(new AzureServiceBusTransportOptions()); @@ -44,7 +46,7 @@ public async Task Constructor_When_resolver_is_null_throws_ArgumentNullException } [Test] - public async Task Constructor_When_options_is_null_throws_ArgumentNullException() + public async Task Constructor_When_options_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) { await using var client = new ServiceBusClient(FakeConnectionString); var resolver = new FakeTopicNameResolver(); @@ -57,7 +59,7 @@ public async Task Constructor_When_options_is_null_throws_ArgumentNullException( // ── IsHealthyAsync ──────────────────────────────────────────────────────── [Test] - public async Task IsHealthyAsync_When_client_not_closed_returns_true() + public async Task IsHealthyAsync_When_client_not_closed_returns_true(CancellationToken cancellationToken) { await using var client = new ServiceBusClient(FakeConnectionString); var resolver = new FakeTopicNameResolver(); @@ -65,13 +67,13 @@ public async Task IsHealthyAsync_When_client_not_closed_returns_true() await using var transport = new AzureServiceBusMessageTransport(client, resolver, options); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsTrue(); } [Test] - public async Task IsHealthyAsync_When_client_is_closed_returns_false() + public async Task IsHealthyAsync_When_client_is_closed_returns_false(CancellationToken cancellationToken) { // Note: DisposeAsync is a no-op (does not dispose injected dependencies). // Close the underlying client directly to test the health check. @@ -83,7 +85,7 @@ public async Task IsHealthyAsync_When_client_is_closed_returns_false() await client.DisposeAsync(); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } @@ -91,7 +93,7 @@ public async Task IsHealthyAsync_When_client_is_closed_returns_false() // ── SendAsync null guard ────────────────────────────────────────────────── [Test] - public async Task SendAsync_When_message_is_null_throws_ArgumentNullException() + public async Task SendAsync_When_message_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) { await using var client = new ServiceBusClient(FakeConnectionString); var resolver = new FakeTopicNameResolver(); @@ -99,13 +101,13 @@ public async Task SendAsync_When_message_is_null_throws_ArgumentNullException() await using var transport = new AzureServiceBusMessageTransport(client, resolver, options); - _ = await Assert.ThrowsAsync(() => transport.SendAsync(null!)); + _ = await Assert.ThrowsAsync(() => transport.SendAsync(null!, cancellationToken)); } // ── SendAsync happy path ────────────────────────────────────────────────── [Test] - public async Task SendAsync_Routes_message_to_resolver_topic() + public async Task SendAsync_Routes_message_to_resolver_topic(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver("orders"); @@ -114,14 +116,14 @@ public async Task SendAsync_Routes_message_to_resolver_topic() await using var transport = new AzureServiceBusMessageTransport(fakeClient, resolver, options); var message = CreateOutboxMessage(); - await transport.SendAsync(message); + await transport.SendAsync(message, cancellationToken); _ = await Assert.That(fakeClient.GetSender("orders")).IsNotNull(); _ = await Assert.That(fakeClient.GetSender("orders")!.SentMessages).HasSingleItem(); } [Test] - public async Task SendAsync_Maps_required_fields_onto_ServiceBusMessage() + public async Task SendAsync_Maps_required_fields_onto_ServiceBusMessage(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver("my-topic"); @@ -130,7 +132,7 @@ public async Task SendAsync_Maps_required_fields_onto_ServiceBusMessage() await using var transport = new AzureServiceBusMessageTransport(fakeClient, resolver, options); var outboxMessage = CreateOutboxMessage(); - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); var sent = fakeClient.GetSender("my-topic")!.SentMessages[0]; using (Assert.Multiple()) @@ -150,7 +152,7 @@ public async Task SendAsync_Maps_required_fields_onto_ServiceBusMessage() } [Test] - public async Task SendAsync_Maps_optional_ProcessedAt_when_set() + public async Task SendAsync_Maps_optional_ProcessedAt_when_set(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver(); @@ -162,14 +164,14 @@ public async Task SendAsync_Maps_optional_ProcessedAt_when_set() var outboxMessage = CreateOutboxMessage(); outboxMessage.ProcessedAt = processedAt; - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); var sent = fakeClient.GetSender("test-topic")!.SentMessages[0]; _ = await Assert.That(sent.ApplicationProperties["processedAt"]).IsEqualTo(processedAt); } [Test] - public async Task SendAsync_Maps_optional_Error_when_set() + public async Task SendAsync_Maps_optional_Error_when_set(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver(); @@ -180,14 +182,14 @@ public async Task SendAsync_Maps_optional_Error_when_set() var outboxMessage = CreateOutboxMessage(); outboxMessage.Error = "Some processing error"; - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); var sent = fakeClient.GetSender("test-topic")!.SentMessages[0]; _ = await Assert.That(sent.ApplicationProperties["error"]).IsEqualTo("Some processing error"); } [Test] - public async Task SendAsync_Does_not_add_processedAt_property_when_null() + public async Task SendAsync_Does_not_add_processedAt_property_when_null(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver(); @@ -197,7 +199,7 @@ public async Task SendAsync_Does_not_add_processedAt_property_when_null() var outboxMessage = CreateOutboxMessage(); // ProcessedAt is null by default - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); var sent = fakeClient.GetSender("test-topic")!.SentMessages[0]; _ = await Assert.That(sent.ApplicationProperties.ContainsKey("processedAt")).IsFalse(); @@ -207,7 +209,9 @@ public async Task SendAsync_Does_not_add_processedAt_property_when_null() // ── SendBatchAsync null/empty guards ────────────────────────────────────── [Test] - public async Task SendBatchAsync_When_messages_is_null_throws_ArgumentNullException() + public async Task SendBatchAsync_When_messages_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) { await using var client = new ServiceBusClient(FakeConnectionString); var resolver = new FakeTopicNameResolver(); @@ -215,11 +219,11 @@ public async Task SendBatchAsync_When_messages_is_null_throws_ArgumentNullExcept await using var transport = new AzureServiceBusMessageTransport(client, resolver, options); - _ = await Assert.ThrowsAsync(() => transport.SendBatchAsync(null!)); + _ = await Assert.ThrowsAsync(() => transport.SendBatchAsync(null!, cancellationToken)); } [Test] - public async Task SendBatchAsync_When_messages_is_empty_does_not_throw() + public async Task SendBatchAsync_When_messages_is_empty_does_not_throw(CancellationToken cancellationToken) { await using var client = new ServiceBusClient(FakeConnectionString); var resolver = new FakeTopicNameResolver(); @@ -227,13 +231,15 @@ public async Task SendBatchAsync_When_messages_is_empty_does_not_throw() await using var transport = new AzureServiceBusMessageTransport(client, resolver, options); - await transport.SendBatchAsync([]); + await transport.SendBatchAsync([], cancellationToken); } // ── SendBatchAsync – batching disabled ──────────────────────────────────── [Test] - public async Task SendBatchAsync_BatchingDisabled_Sends_each_message_individually() + public async Task SendBatchAsync_BatchingDisabled_Sends_each_message_individually( + CancellationToken cancellationToken + ) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver("queue1"); @@ -242,7 +248,7 @@ public async Task SendBatchAsync_BatchingDisabled_Sends_each_message_individuall await using var transport = new AzureServiceBusMessageTransport(fakeClient, resolver, options); var messages = new[] { CreateOutboxMessage(), CreateOutboxMessage(), CreateOutboxMessage() }; - await transport.SendBatchAsync(messages); + await transport.SendBatchAsync(messages, cancellationToken); var sender = fakeClient.GetSender("queue1")!; _ = await Assert.That(sender.SentMessages.Count).IsEqualTo(3); @@ -250,7 +256,7 @@ public async Task SendBatchAsync_BatchingDisabled_Sends_each_message_individuall } [Test] - public async Task SendBatchAsync_BatchingDisabled_Groups_messages_by_topic() + public async Task SendBatchAsync_BatchingDisabled_Groups_messages_by_topic(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new TopicPerEventTypeResolver(); @@ -265,7 +271,7 @@ public async Task SendBatchAsync_BatchingDisabled_Groups_messages_by_topic() CreateOutboxMessage(typeof(TopicBEvent)), }; - await transport.SendBatchAsync(messages); + await transport.SendBatchAsync(messages, cancellationToken); _ = await Assert.That(fakeClient.GetSender(nameof(TopicAEvent))!.SentMessages.Count).IsEqualTo(2); _ = await Assert.That(fakeClient.GetSender(nameof(TopicBEvent))!.SentMessages.Count).IsEqualTo(1); @@ -274,7 +280,7 @@ public async Task SendBatchAsync_BatchingDisabled_Groups_messages_by_topic() // ── SendBatchAsync – batching enabled ───────────────────────────────────── [Test] - public async Task SendBatchAsync_BatchingEnabled_Sends_messages_as_batch() + public async Task SendBatchAsync_BatchingEnabled_Sends_messages_as_batch(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new FakeTopicNameResolver("orders"); @@ -283,7 +289,7 @@ public async Task SendBatchAsync_BatchingEnabled_Sends_messages_as_batch() await using var transport = new AzureServiceBusMessageTransport(fakeClient, resolver, options); var messages = new[] { CreateOutboxMessage(), CreateOutboxMessage() }; - await transport.SendBatchAsync(messages); + await transport.SendBatchAsync(messages, cancellationToken); var sender = fakeClient.GetSender("orders")!; _ = await Assert.That(sender.SentMessages.Count).IsEqualTo(0); @@ -292,7 +298,7 @@ public async Task SendBatchAsync_BatchingEnabled_Sends_messages_as_batch() } [Test] - public async Task SendBatchAsync_BatchingEnabled_Groups_messages_by_topic() + public async Task SendBatchAsync_BatchingEnabled_Groups_messages_by_topic(CancellationToken cancellationToken) { await using var fakeClient = new FakeServiceBusClient(); var resolver = new TopicPerEventTypeResolver(); @@ -307,7 +313,7 @@ public async Task SendBatchAsync_BatchingEnabled_Groups_messages_by_topic() CreateOutboxMessage(typeof(BetaEvent)), }; - await transport.SendBatchAsync(messages); + await transport.SendBatchAsync(messages, cancellationToken); _ = await Assert.That(fakeClient.GetSender(nameof(AlphaEvent))!.BatchedMessages[0].Count).IsEqualTo(2); _ = await Assert.That(fakeClient.GetSender(nameof(BetaEvent))!.BatchedMessages[0].Count).IsEqualTo(1); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs index 559ee9b3..1a5386f4 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs @@ -18,11 +18,12 @@ public sealed class DaprExtensionsTests { [Test] - public async Task UseDaprTransport_When_configurator_is_null_throws_ArgumentNullException() => - _ = await Assert.That(() => DaprExtensions.UseDaprTransport(null!)).Throws(); + public async Task UseDaprTransport_When_configurator_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert.That(() => DaprExtensions.UseDaprTransport(null!)).Throws(); [Test] - public async Task UseDaprTransport_Registers_transport_as_singleton() + public async Task UseDaprTransport_Registers_transport_as_singleton(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseDaprTransport()); @@ -37,7 +38,7 @@ public async Task UseDaprTransport_Registers_transport_as_singleton() } [Test] - public async Task UseDaprTransport_Replaces_existing_transport() + public async Task UseDaprTransport_Replaces_existing_transport(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -48,7 +49,7 @@ public async Task UseDaprTransport_Replaces_existing_transport() } [Test] - public async Task UseDaprTransport_Configures_options() + public async Task UseDaprTransport_Configures_options(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseDaprTransport(o => o.PubSubName = "custom-pubsub")); @@ -60,7 +61,9 @@ public async Task UseDaprTransport_Configures_options() } [Test] - public async Task UseDaprTransport_Without_configureOptions_uses_default_PubSubName() + public async Task UseDaprTransport_Without_configureOptions_uses_default_PubSubName( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseDaprTransport()); @@ -72,7 +75,7 @@ public async Task UseDaprTransport_Without_configureOptions_uses_default_PubSubN } [Test] - public async Task UseDaprTransport_Returns_same_configurator_for_chaining() + public async Task UseDaprTransport_Returns_same_configurator_for_chaining(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); IMediatorBuilder? captured = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs index 07775390..81f64eaf 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Dapr; +namespace NetEvolve.Pulse.Tests.Unit.Dapr; using System; using System.Threading.Tasks; @@ -14,7 +14,9 @@ public sealed class DaprMessageTransportTests { [Test] - public async Task Constructor_When_daprClient_is_null_throws_ArgumentNullException() => + public async Task Constructor_When_daprClient_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new DaprMessageTransport( @@ -26,7 +28,9 @@ public async Task Constructor_When_daprClient_is_null_throws_ArgumentNullExcepti .Throws(); [Test] - public async Task Constructor_When_topicNameResolver_is_null_throws_ArgumentNullException() + public async Task Constructor_When_topicNameResolver_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) { using var daprClient = new DaprClientBuilder().Build(); @@ -36,7 +40,7 @@ public async Task Constructor_When_topicNameResolver_is_null_throws_ArgumentNull } [Test] - public async Task Constructor_When_options_is_null_throws_ArgumentNullException() + public async Task Constructor_When_options_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) { using var daprClient = new DaprClientBuilder().Build(); @@ -46,7 +50,7 @@ public async Task Constructor_When_options_is_null_throws_ArgumentNullException( } [Test] - public async Task Constructor_With_valid_arguments_creates_instance() + public async Task Constructor_With_valid_arguments_creates_instance(CancellationToken cancellationToken) { using var daprClient = new DaprClientBuilder().Build(); var transport = new DaprMessageTransport( @@ -59,7 +63,7 @@ public async Task Constructor_With_valid_arguments_creates_instance() } [Test] - public async Task SendAsync_When_message_is_null_throws_ArgumentNullException() + public async Task SendAsync_When_message_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) { using var daprClient = new DaprClientBuilder().Build(); var transport = new DaprMessageTransport( @@ -68,11 +72,11 @@ public async Task SendAsync_When_message_is_null_throws_ArgumentNullException() Options.Create(new DaprMessageTransportOptions()) ); - _ = await Assert.ThrowsAsync(() => transport.SendAsync(null!)); + _ = await Assert.ThrowsAsync(() => transport.SendAsync(null!, cancellationToken)); } [Test] - public async Task IsHealthyAsync_Delegates_to_DaprClient() + public async Task IsHealthyAsync_Delegates_to_DaprClient(CancellationToken cancellationToken) { using var daprClient = new DaprClientBuilder().Build(); var transport = new DaprMessageTransport( @@ -82,7 +86,7 @@ public async Task IsHealthyAsync_Delegates_to_DaprClient() ); // Without a running Dapr sidecar, CheckHealthAsync returns false (connection refused → false, not throw) - var result = await transport.IsHealthyAsync(); + var result = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(result).IsTypeOf(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/ParallelEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/ParallelEventDispatcherTests.cs index 6a57ea8e..8b8f8728 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/ParallelEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/ParallelEventDispatcherTests.cs @@ -9,7 +9,7 @@ public class ParallelEventDispatcherTests { [Test] - public async Task DispatchAsync_WithMultipleHandlers_InvokesAllHandlers() + public async Task DispatchAsync_WithMultipleHandlers_InvokesAllHandlers(CancellationToken cancellationToken) { var dispatcher = new ParallelEventDispatcher(); var testEvent = new TestEvent(); @@ -26,7 +26,7 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); @@ -40,7 +40,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithNoHandlers_CompletesSuccessfully() + public async Task DispatchAsync_WithNoHandlers_CompletesSuccessfully(CancellationToken cancellationToken) { var dispatcher = new ParallelEventDispatcher(); var testEvent = new TestEvent(); @@ -51,13 +51,13 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); } [Test] - public async Task DispatchAsync_WithSingleHandler_InvokesHandler() + public async Task DispatchAsync_WithSingleHandler_InvokesHandler(CancellationToken cancellationToken) { var dispatcher = new ParallelEventDispatcher(); var testEvent = new TestEvent(); @@ -69,7 +69,7 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); @@ -77,13 +77,13 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithCancellation_RespectsToken() + public async Task DispatchAsync_WithCancellation_RespectsToken(CancellationToken cancellationToken) { var dispatcher = new ParallelEventDispatcher(); var testEvent = new TestEvent(); var invokedHandlers = new List(); var handlers = new List> { new TestEventHandler(1, invokedHandlers) }; - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await cts.CancelAsync().ConfigureAwait(false); _ = await Assert.ThrowsAsync(async () => diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs index 81cf9258..c529b26b 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs @@ -14,7 +14,7 @@ public class PrioritizedEventDispatcherTests { [Test] - public async Task DispatchAsync_WithPrioritizedHandlers_ExecutesInPriorityOrder() + public async Task DispatchAsync_WithPrioritizedHandlers_ExecutesInPriorityOrder(CancellationToken cancellationToken) { var dispatcher = new PrioritizedEventDispatcher(); var message = new TestEvent(); @@ -27,12 +27,7 @@ public async Task DispatchAsync_WithPrioritizedHandlers_ExecutesInPriorityOrder( }; await dispatcher - .DispatchAsync( - message, - handlers, - (handler, msg, ct) => handler.HandleAsync(msg, ct), - CancellationToken.None - ) + .DispatchAsync(message, handlers, (handler, msg, ct) => handler.HandleAsync(msg, ct), cancellationToken) .ConfigureAwait(false); var order = executionOrder.ToArray(); @@ -46,7 +41,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithNonPrioritizedHandlers_ExecutesLast() + public async Task DispatchAsync_WithNonPrioritizedHandlers_ExecutesLast(CancellationToken cancellationToken) { var dispatcher = new PrioritizedEventDispatcher(); var message = new TestEvent(); @@ -60,12 +55,7 @@ public async Task DispatchAsync_WithNonPrioritizedHandlers_ExecutesLast() }; await dispatcher - .DispatchAsync( - message, - handlers, - (handler, msg, ct) => handler.HandleAsync(msg, ct), - CancellationToken.None - ) + .DispatchAsync(message, handlers, (handler, msg, ct) => handler.HandleAsync(msg, ct), cancellationToken) .ConfigureAwait(false); var order = executionOrder.ToArray(); @@ -80,7 +70,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithEqualPriority_PreservesRegistrationOrder() + public async Task DispatchAsync_WithEqualPriority_PreservesRegistrationOrder(CancellationToken cancellationToken) { var dispatcher = new PrioritizedEventDispatcher(); var message = new TestEvent(); @@ -93,12 +83,7 @@ public async Task DispatchAsync_WithEqualPriority_PreservesRegistrationOrder() }; await dispatcher - .DispatchAsync( - message, - handlers, - (handler, msg, ct) => handler.HandleAsync(msg, ct), - CancellationToken.None - ) + .DispatchAsync(message, handlers, (handler, msg, ct) => handler.HandleAsync(msg, ct), cancellationToken) .ConfigureAwait(false); var order = executionOrder.ToArray(); @@ -112,7 +97,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithCancellation_StopsExecution() + public async Task DispatchAsync_WithCancellation_StopsExecution(CancellationToken cancellationToken) { var dispatcher = new PrioritizedEventDispatcher(); var message = new TestEvent(); @@ -142,19 +127,14 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithEmptyHandlers_CompletesSuccessfully() + public async Task DispatchAsync_WithEmptyHandlers_CompletesSuccessfully(CancellationToken cancellationToken) { var dispatcher = new PrioritizedEventDispatcher(); var message = new TestEvent(); var handlers = Enumerable.Empty>(); await dispatcher - .DispatchAsync( - message, - handlers, - (handler, msg, ct) => handler.HandleAsync(msg, ct), - CancellationToken.None - ) + .DispatchAsync(message, handlers, (handler, msg, ct) => handler.HandleAsync(msg, ct), cancellationToken) .ConfigureAwait(false); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs index c500cfa4..1a682c84 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs @@ -14,7 +14,7 @@ public class RateLimitedEventDispatcherTests { [Test] - public async Task Constructor_WithDefaultConcurrency_CreatesWith5() + public async Task Constructor_WithDefaultConcurrency_CreatesWith5(CancellationToken cancellationToken) { var dispatcher = new RateLimitedEventDispatcher(); @@ -22,7 +22,7 @@ public async Task Constructor_WithDefaultConcurrency_CreatesWith5() } [Test] - public async Task Constructor_WithCustomConcurrency_CreatesWithSpecifiedValue() + public async Task Constructor_WithCustomConcurrency_CreatesWithSpecifiedValue(CancellationToken cancellationToken) { var dispatcher = new RateLimitedEventDispatcher(maxConcurrency: 10); @@ -30,15 +30,17 @@ public async Task Constructor_WithCustomConcurrency_CreatesWithSpecifiedValue() } [Test] - public async Task Constructor_WithZeroConcurrency_ThrowsArgumentOutOfRangeException() => - _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: 0)); + public async Task Constructor_WithZeroConcurrency_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) => _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: 0)); [Test] - public async Task Constructor_WithNegativeConcurrency_ThrowsArgumentOutOfRangeException() => - _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: -1)); + public async Task Constructor_WithNegativeConcurrency_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) => _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: -1)); [Test] - public async Task DispatchAsync_WithHandlers_InvokesAllHandlers() + public async Task DispatchAsync_WithHandlers_InvokesAllHandlers(CancellationToken cancellationToken) { var dispatcher = new RateLimitedEventDispatcher(maxConcurrency: 2); var message = new TestEvent(); @@ -51,12 +53,7 @@ public async Task DispatchAsync_WithHandlers_InvokesAllHandlers() }; await dispatcher - .DispatchAsync( - message, - handlers, - (handler, msg, ct) => handler.HandleAsync(msg, ct), - CancellationToken.None - ) + .DispatchAsync(message, handlers, (handler, msg, ct) => handler.HandleAsync(msg, ct), cancellationToken) .ConfigureAwait(false); using (Assert.Multiple()) @@ -69,7 +66,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_LimitsConcurrency() + public async Task DispatchAsync_LimitsConcurrency(CancellationToken cancellationToken) { var dispatcher = new RateLimitedEventDispatcher(maxConcurrency: 2); var message = new TestEvent(); @@ -107,7 +104,7 @@ await dispatcher message, handlers, async (handler, msg, ct) => await handler.HandleAsync(msg, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs index 985a520a..009d2bd4 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs @@ -9,7 +9,7 @@ public class SequentialEventDispatcherTests { [Test] - public async Task DispatchAsync_WithMultipleHandlers_InvokesAllHandlersInOrder() + public async Task DispatchAsync_WithMultipleHandlers_InvokesAllHandlersInOrder(CancellationToken cancellationToken) { var dispatcher = new SequentialEventDispatcher(); var testEvent = new TestEvent(); @@ -26,7 +26,7 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); @@ -40,7 +40,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithNoHandlers_CompletesSuccessfully() + public async Task DispatchAsync_WithNoHandlers_CompletesSuccessfully(CancellationToken cancellationToken) { var dispatcher = new SequentialEventDispatcher(); var testEvent = new TestEvent(); @@ -51,13 +51,13 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); } [Test] - public async Task DispatchAsync_WithSingleHandler_InvokesHandler() + public async Task DispatchAsync_WithSingleHandler_InvokesHandler(CancellationToken cancellationToken) { var dispatcher = new SequentialEventDispatcher(); var testEvent = new TestEvent(); @@ -69,7 +69,7 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); @@ -77,7 +77,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_WithCancellationBetweenHandlers_StopsExecution() + public async Task DispatchAsync_WithCancellationBetweenHandlers_StopsExecution(CancellationToken cancellationToken) { var dispatcher = new SequentialEventDispatcher(); var testEvent = new TestEvent(); @@ -105,7 +105,7 @@ await dispatcher } [Test] - public async Task DispatchAsync_ExecutesSequentially_NotInParallel() + public async Task DispatchAsync_ExecutesSequentially_NotInParallel(CancellationToken cancellationToken) { var dispatcher = new SequentialEventDispatcher(); var testEvent = new TestEvent(); @@ -151,7 +151,7 @@ await dispatcher testEvent, handlers, async (handler, evt, ct) => await handler.HandleAsync(evt, ct).ConfigureAwait(false), - CancellationToken.None + cancellationToken ) .ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs index ca3de877..b877e666 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; +namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; using System; using System.Threading.Tasks; @@ -14,7 +14,7 @@ public sealed class EntityFrameworkEventOutboxTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new EntityFrameworkOutbox( @@ -26,7 +26,7 @@ public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullOptions_ThrowsArgumentNullException)) @@ -39,7 +39,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException)) @@ -52,7 +52,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance)) @@ -69,7 +69,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(StoreAsync_WithNullMessage_ThrowsArgumentNullException)) @@ -87,7 +87,9 @@ public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() } [Test] - public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException() + public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException)) @@ -104,7 +106,7 @@ public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationExcepti }; _ = await Assert - .That(async () => await outbox.StoreAsync(message).ConfigureAwait(false)) + .That(async () => await outbox.StoreAsync(message, cancellationToken).ConfigureAwait(false)) .Throws(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs index 12056704..2aef3b43 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs @@ -18,13 +18,17 @@ public sealed class EntityFrameworkExtensionsTests { [Test] - public async Task AddEntityFrameworkOutbox_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task AddEntityFrameworkOutbox_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => EntityFrameworkExtensions.AddEntityFrameworkOutbox(null!)) .Throws(); [Test] - public async Task AddEntityFrameworkOutbox_WithValidConfigurator_ReturnsConfiguratorForChaining() + public async Task AddEntityFrameworkOutbox_WithValidConfigurator_ReturnsConfiguratorForChaining( + CancellationToken cancellationToken + ) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -35,7 +39,7 @@ public async Task AddEntityFrameworkOutbox_WithValidConfigurator_ReturnsConfigur } [Test] - public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped() + public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -53,7 +57,7 @@ public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped() } [Test] - public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped() + public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -71,7 +75,7 @@ public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped() } [Test] - public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped() + public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -89,7 +93,7 @@ public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped() } [Test] - public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton() + public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddEntityFrameworkOutbox()); @@ -104,7 +108,7 @@ public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton() } [Test] - public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions() + public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -121,7 +125,7 @@ public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions() } [Test] - public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions() + public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -138,7 +142,7 @@ public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions() } [Test] - public async Task AddEntityFrameworkOutbox_RegistersOutboxManagementAsScoped() + public async Task AddEntityFrameworkOutbox_RegistersOutboxManagementAsScoped(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDbContext(o => diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs index 6d2a801e..366ad382 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs @@ -13,13 +13,13 @@ public sealed class EntityFrameworkOutboxManagementTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new EntityFrameworkOutboxManagement(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException)) @@ -32,7 +32,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance)) @@ -45,7 +45,9 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase( @@ -61,7 +63,9 @@ public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgument } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException)) @@ -75,7 +79,9 @@ public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutO } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException)) @@ -84,12 +90,16 @@ public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutO var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); _ = await Assert - .That(async () => await management.GetDeadLetterMessagesAsync(page: -1).ConfigureAwait(false)) + .That(async () => + await management + .GetDeadLetterMessagesAsync(page: -1, cancellationToken: cancellationToken) + .ConfigureAwait(false) + ) .Throws(); } [Test] - public async Task GetDeadLetterMessagesAsync_EmptyDatabase_ReturnsEmptyList() + public async Task GetDeadLetterMessagesAsync_EmptyDatabase_ReturnsEmptyList(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessagesAsync_EmptyDatabase_ReturnsEmptyList)) @@ -97,13 +107,17 @@ public async Task GetDeadLetterMessagesAsync_EmptyDatabase_ReturnsEmptyList() await using var context = new TestDbContext(options); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.GetDeadLetterMessagesAsync().ConfigureAwait(false); + var result = await management + .GetDeadLetterMessagesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(result).IsEmpty(); } [Test] - public async Task GetDeadLetterMessagesAsync_WithDeadLetterMessages_ReturnsMessages() + public async Task GetDeadLetterMessagesAsync_WithDeadLetterMessages_ReturnsMessages( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessagesAsync_WithDeadLetterMessages_ReturnsMessages)) @@ -132,18 +146,22 @@ public async Task GetDeadLetterMessagesAsync_WithDeadLetterMessages_ReturnsMessa Status = OutboxMessageStatus.Pending, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.GetDeadLetterMessagesAsync().ConfigureAwait(false); + var result = await management + .GetDeadLetterMessagesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(result).Count().IsEqualTo(1); _ = await Assert.That(result[0].Status).IsEqualTo(OutboxMessageStatus.DeadLetter); } [Test] - public async Task GetDeadLetterMessageAsync_WithExistingDeadLetterMessage_ReturnsMessage() + public async Task GetDeadLetterMessageAsync_WithExistingDeadLetterMessage_ReturnsMessage( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessageAsync_WithExistingDeadLetterMessage_ReturnsMessage)) @@ -162,18 +180,20 @@ public async Task GetDeadLetterMessageAsync_WithExistingDeadLetterMessage_Return Status = OutboxMessageStatus.DeadLetter, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.GetDeadLetterMessageAsync(messageId).ConfigureAwait(false); + var result = await management.GetDeadLetterMessageAsync(messageId, cancellationToken).ConfigureAwait(false); _ = await Assert.That(result).IsNotNull(); _ = await Assert.That(result!.Id).IsEqualTo(messageId); } [Test] - public async Task GetDeadLetterMessageAsync_WithNonDeadLetterMessage_ReturnsNull() + public async Task GetDeadLetterMessageAsync_WithNonDeadLetterMessage_ReturnsNull( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessageAsync_WithNonDeadLetterMessage_ReturnsNull)) @@ -192,17 +212,17 @@ public async Task GetDeadLetterMessageAsync_WithNonDeadLetterMessage_ReturnsNull Status = OutboxMessageStatus.Completed, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.GetDeadLetterMessageAsync(messageId).ConfigureAwait(false); + var result = await management.GetDeadLetterMessageAsync(messageId, cancellationToken).ConfigureAwait(false); _ = await Assert.That(result).IsNull(); } [Test] - public async Task GetDeadLetterMessageAsync_WithUnknownId_ReturnsNull() + public async Task GetDeadLetterMessageAsync_WithUnknownId_ReturnsNull(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessageAsync_WithUnknownId_ReturnsNull)) @@ -210,13 +230,15 @@ public async Task GetDeadLetterMessageAsync_WithUnknownId_ReturnsNull() await using var context = new TestDbContext(options); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.GetDeadLetterMessageAsync(Guid.NewGuid()).ConfigureAwait(false); + var result = await management + .GetDeadLetterMessageAsync(Guid.NewGuid(), cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(result).IsNull(); } [Test] - public async Task GetDeadLetterCountAsync_EmptyDatabase_ReturnsZero() + public async Task GetDeadLetterCountAsync_EmptyDatabase_ReturnsZero(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterCountAsync_EmptyDatabase_ReturnsZero)) @@ -224,13 +246,15 @@ public async Task GetDeadLetterCountAsync_EmptyDatabase_ReturnsZero() await using var context = new TestDbContext(options); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var count = await management.GetDeadLetterCountAsync().ConfigureAwait(false); + var count = await management.GetDeadLetterCountAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(count).IsEqualTo(0L); } [Test] - public async Task GetDeadLetterCountAsync_WithDeadLetterMessages_ReturnsCorrectCount() + public async Task GetDeadLetterCountAsync_WithDeadLetterMessages_ReturnsCorrectCount( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterCountAsync_WithDeadLetterMessages_ReturnsCorrectCount)) @@ -263,11 +287,11 @@ public async Task GetDeadLetterCountAsync_WithDeadLetterMessages_ReturnsCorrectC Status = OutboxMessageStatus.Pending, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var count = await management.GetDeadLetterCountAsync().ConfigureAwait(false); + var count = await management.GetDeadLetterCountAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(count).IsEqualTo(3L); } @@ -276,7 +300,9 @@ public async Task GetDeadLetterCountAsync_WithDeadLetterMessages_ReturnsCorrectC [Skip( "The EF Core InMemory provider does not support ExecuteUpdateAsync (bulk updates). Covered by integration tests." )] - public async Task ReplayMessageAsync_WithExistingDeadLetterMessage_ReturnsTrueAndResetsMessage() + public async Task ReplayMessageAsync_WithExistingDeadLetterMessage_ReturnsTrueAndResetsMessage( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ReplayMessageAsync_WithExistingDeadLetterMessage_ReturnsTrueAndResetsMessage)) @@ -297,15 +323,15 @@ public async Task ReplayMessageAsync_WithExistingDeadLetterMessage_ReturnsTrueAn Status = OutboxMessageStatus.DeadLetter, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.ReplayMessageAsync(messageId).ConfigureAwait(false); + var result = await management.ReplayMessageAsync(messageId, cancellationToken).ConfigureAwait(false); _ = await Assert.That(result).IsTrue(); - var message = await context.OutboxMessages.FindAsync(messageId).ConfigureAwait(false); + var message = await context.OutboxMessages.FindAsync([messageId], cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { _ = await Assert.That(message!.Status).IsEqualTo(OutboxMessageStatus.Pending); @@ -318,7 +344,7 @@ public async Task ReplayMessageAsync_WithExistingDeadLetterMessage_ReturnsTrueAn [Skip( "The EF Core InMemory provider does not support ExecuteUpdateAsync (bulk updates). Covered by integration tests." )] - public async Task ReplayMessageAsync_WithNonDeadLetterMessage_ReturnsFalse() + public async Task ReplayMessageAsync_WithNonDeadLetterMessage_ReturnsFalse(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ReplayMessageAsync_WithNonDeadLetterMessage_ReturnsFalse)) @@ -337,11 +363,11 @@ public async Task ReplayMessageAsync_WithNonDeadLetterMessage_ReturnsFalse() Status = OutboxMessageStatus.Failed, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.ReplayMessageAsync(messageId).ConfigureAwait(false); + var result = await management.ReplayMessageAsync(messageId, cancellationToken).ConfigureAwait(false); _ = await Assert.That(result).IsFalse(); } @@ -350,7 +376,7 @@ public async Task ReplayMessageAsync_WithNonDeadLetterMessage_ReturnsFalse() [Skip( "The EF Core InMemory provider does not support ExecuteUpdateAsync (bulk updates). Covered by integration tests." )] - public async Task ReplayMessageAsync_WithUnknownId_ReturnsFalse() + public async Task ReplayMessageAsync_WithUnknownId_ReturnsFalse(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ReplayMessageAsync_WithUnknownId_ReturnsFalse)) @@ -358,7 +384,7 @@ public async Task ReplayMessageAsync_WithUnknownId_ReturnsFalse() await using var context = new TestDbContext(options); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var result = await management.ReplayMessageAsync(Guid.NewGuid()).ConfigureAwait(false); + var result = await management.ReplayMessageAsync(Guid.NewGuid(), cancellationToken).ConfigureAwait(false); _ = await Assert.That(result).IsFalse(); } @@ -367,7 +393,7 @@ public async Task ReplayMessageAsync_WithUnknownId_ReturnsFalse() [Skip( "The EF Core InMemory provider does not support ExecuteUpdateAsync (bulk updates). Covered by integration tests." )] - public async Task ReplayAllDeadLetterAsync_EmptyDatabase_ReturnsZero() + public async Task ReplayAllDeadLetterAsync_EmptyDatabase_ReturnsZero(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ReplayAllDeadLetterAsync_EmptyDatabase_ReturnsZero)) @@ -375,7 +401,7 @@ public async Task ReplayAllDeadLetterAsync_EmptyDatabase_ReturnsZero() await using var context = new TestDbContext(options); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var count = await management.ReplayAllDeadLetterAsync().ConfigureAwait(false); + var count = await management.ReplayAllDeadLetterAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(count).IsEqualTo(0); } @@ -384,7 +410,9 @@ public async Task ReplayAllDeadLetterAsync_EmptyDatabase_ReturnsZero() [Skip( "The EF Core InMemory provider does not support ExecuteUpdateAsync (bulk updates). Covered by integration tests." )] - public async Task ReplayAllDeadLetterAsync_WithDeadLetterMessages_ResetsAllAndReturnsCount() + public async Task ReplayAllDeadLetterAsync_WithDeadLetterMessages_ResetsAllAndReturnsCount( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ReplayAllDeadLetterAsync_WithDeadLetterMessages_ResetsAllAndReturnsCount)) @@ -419,17 +447,17 @@ public async Task ReplayAllDeadLetterAsync_WithDeadLetterMessages_ResetsAllAndRe Status = OutboxMessageStatus.Pending, } ); - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var count = await management.ReplayAllDeadLetterAsync().ConfigureAwait(false); + var count = await management.ReplayAllDeadLetterAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(count).IsEqualTo(3); } [Test] - public async Task GetStatisticsAsync_EmptyDatabase_ReturnsZeroStatistics() + public async Task GetStatisticsAsync_EmptyDatabase_ReturnsZeroStatistics(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetStatisticsAsync_EmptyDatabase_ReturnsZeroStatistics)) @@ -437,7 +465,7 @@ public async Task GetStatisticsAsync_EmptyDatabase_ReturnsZeroStatistics() await using var context = new TestDbContext(options); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var statistics = await management.GetStatisticsAsync().ConfigureAwait(false); + var statistics = await management.GetStatisticsAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -451,7 +479,7 @@ public async Task GetStatisticsAsync_EmptyDatabase_ReturnsZeroStatistics() } [Test] - public async Task GetStatisticsAsync_WithMessages_ReturnsCorrectCounts() + public async Task GetStatisticsAsync_WithMessages_ReturnsCorrectCounts(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetStatisticsAsync_WithMessages_ReturnsCorrectCounts)) @@ -487,11 +515,11 @@ public async Task GetStatisticsAsync_WithMessages_ReturnsCorrectCounts() ); } - _ = await context.SaveChangesAsync().ConfigureAwait(false); + _ = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); var management = new EntityFrameworkOutboxManagement(context, TimeProvider.System); - var statistics = await management.GetStatisticsAsync().ConfigureAwait(false); + var statistics = await management.GetStatisticsAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs index f3ce0f1d..4483b9a6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; +namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; using System; using System.Threading.Tasks; @@ -11,13 +11,13 @@ public sealed class EntityFrameworkOutboxRepositoryTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new EntityFrameworkOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException)) @@ -30,7 +30,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance)) @@ -43,7 +43,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(AddAsync_WithNullMessage_ThrowsArgumentNullException)) @@ -52,12 +52,12 @@ public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() var repository = new EntityFrameworkOutboxRepository(context, TimeProvider.System); _ = await Assert - .That(async () => await repository.AddAsync(null!).ConfigureAwait(false)) + .That(async () => await repository.AddAsync(null!, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task IsHealthyAsync_WithInMemoryProvider_ReturnsTrue() + public async Task IsHealthyAsync_WithInMemoryProvider_ReturnsTrue(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(IsHealthyAsync_WithInMemoryProvider_ReturnsTrue)) @@ -65,7 +65,7 @@ public async Task IsHealthyAsync_WithInMemoryProvider_ReturnsTrue() await using var context = new TestDbContext(options); var repository = new EntityFrameworkOutboxRepository(context, TimeProvider.System); - var result = await repository.IsHealthyAsync(CancellationToken.None).ConfigureAwait(false); + var result = await repository.IsHealthyAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(result).IsTrue(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs index 7131a197..efd7ddfd 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs @@ -11,13 +11,13 @@ public sealed class EntityFrameworkOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new EntityFrameworkOutboxTransactionScope(null!)) .Throws(); [Test] - public async Task Constructor_WithValidContext_CreatesInstance() + public async Task Constructor_WithValidContext_CreatesInstance(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidContext_CreatesInstance)) @@ -30,7 +30,7 @@ public async Task Constructor_WithValidContext_CreatesInstance() } [Test] - public async Task GetCurrentTransaction_WithNoActiveTransaction_ReturnsNull() + public async Task GetCurrentTransaction_WithNoActiveTransaction_ReturnsNull(CancellationToken cancellationToken) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetCurrentTransaction_WithNoActiveTransaction_ReturnsNull)) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs index f60b383d..e810ad7e 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs @@ -12,7 +12,9 @@ public sealed class ModelBuilderExtensionsTests { [Test] - public async Task ApplyPulseConfiguration_When_modelBuilder_is_null_throws_ArgumentNullException() + public async Task ApplyPulseConfiguration_When_modelBuilder_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ApplyPulseConfiguration_When_modelBuilder_is_null_throws_ArgumentNullException)) @@ -25,7 +27,9 @@ public async Task ApplyPulseConfiguration_When_modelBuilder_is_null_throws_Argum } [Test] - public async Task ApplyPulseConfiguration_When_context_is_null_throws_ArgumentNullException() + public async Task ApplyPulseConfiguration_When_context_is_null_throws_ArgumentNullException( + CancellationToken cancellationToken + ) { var modelBuilder = new ModelBuilder(); @@ -35,7 +39,9 @@ public async Task ApplyPulseConfiguration_When_context_is_null_throws_ArgumentNu } [Test] - public async Task ApplyPulseConfiguration_When_context_is_not_IOutboxDbContext_returns_same_modelBuilder() + public async Task ApplyPulseConfiguration_When_context_is_not_IOutboxDbContext_returns_same_modelBuilder( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase( @@ -51,7 +57,9 @@ public async Task ApplyPulseConfiguration_When_context_is_not_IOutboxDbContext_r } [Test] - public async Task ApplyPulseConfiguration_When_context_implements_IOutboxDbContext_returns_same_modelBuilder() + public async Task ApplyPulseConfiguration_When_context_implements_IOutboxDbContext_returns_same_modelBuilder( + CancellationToken cancellationToken + ) { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase( diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs index ee349b10..352fb957 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs @@ -15,7 +15,9 @@ public sealed class OutboxMessageConfigurationMetadataTests { [Test] - public async Task Create_WithSqlServerProvider_AppliesSqlServerFiltersAndColumnTypes() + public async Task Create_WithSqlServerProvider_AppliesSqlServerFiltersAndColumnTypes( + CancellationToken cancellationToken + ) { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.SqlServer"); @@ -42,7 +44,9 @@ public async Task Create_WithSqlServerProvider_AppliesSqlServerFiltersAndColumnT } [Test] - public async Task Create_WithPostgreSqlProvider_AppliesPostgreSqlFiltersAndColumnTypes() + public async Task Create_WithPostgreSqlProvider_AppliesPostgreSqlFiltersAndColumnTypes( + CancellationToken cancellationToken + ) { var entityType = GetConfiguredEntityType("Npgsql.EntityFrameworkCore.PostgreSQL"); @@ -67,7 +71,7 @@ public async Task Create_WithPostgreSqlProvider_AppliesPostgreSqlFiltersAndColum } [Test] - public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes() + public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes(CancellationToken cancellationToken) { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.Sqlite"); @@ -92,7 +96,9 @@ public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes() } [Test] - public async Task Create_WithMySqlProvider_AppliesMySqlColumnTypesAndNoFilteredIndexes() + public async Task Create_WithMySqlProvider_AppliesMySqlColumnTypesAndNoFilteredIndexes( + CancellationToken cancellationToken + ) { var entityType = GetConfiguredEntityType("Pomelo.EntityFrameworkCore.MySql"); @@ -119,7 +125,9 @@ public async Task Create_WithMySqlProvider_AppliesMySqlColumnTypesAndNoFilteredI } [Test] - public async Task Create_WithInMemoryProvider_UsesBaseDefaultsWithoutColumnTypeOverrides() + public async Task Create_WithInMemoryProvider_UsesBaseDefaultsWithoutColumnTypeOverrides( + CancellationToken cancellationToken + ) { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.InMemory"); @@ -139,7 +147,9 @@ public async Task Create_WithInMemoryProvider_UsesBaseDefaultsWithoutColumnTypeO } [Test] - public async Task Create_WithWhitespaceSchema_UsesDefaultSchemaAndConfiguredTableName() + public async Task Create_WithWhitespaceSchema_UsesDefaultSchemaAndConfiguredTableName( + CancellationToken cancellationToken + ) { var entityType = GetConfiguredEntityType( "Microsoft.EntityFrameworkCore.SqlServer", @@ -154,7 +164,7 @@ public async Task Create_WithWhitespaceSchema_UsesDefaultSchemaAndConfiguredTabl } [Test] - public async Task Create_WithSchemaContainingWhitespace_StoresTrimmedSchema() + public async Task Create_WithSchemaContainingWhitespace_StoresTrimmedSchema(CancellationToken cancellationToken) { var entityType = GetConfiguredEntityType( "Microsoft.EntityFrameworkCore.SqlServer", @@ -165,7 +175,7 @@ public async Task Create_WithSchemaContainingWhitespace_StoresTrimmedSchema() } [Test] - public async Task Create_WithSqlServerProvider_AppliesOutboxBaseDefaultValues() + public async Task Create_WithSqlServerProvider_AppliesOutboxBaseDefaultValues(CancellationToken cancellationToken) { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.SqlServer"); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs index 0768b4f8..6b932436 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs @@ -11,7 +11,7 @@ public sealed class TypeValueConverterTests { [Test] - public async Task Constructor_Creates_instance() + public async Task Constructor_Creates_instance(CancellationToken cancellationToken) { var converter = new TypeValueConverter(); @@ -19,7 +19,9 @@ public async Task Constructor_Creates_instance() } [Test] - public async Task ConvertToProvider_With_valid_type_returns_assembly_qualified_name() + public async Task ConvertToProvider_With_valid_type_returns_assembly_qualified_name( + CancellationToken cancellationToken + ) { var converter = new TypeValueConverter(); var toProvider = converter.ConvertToProvider; @@ -34,7 +36,7 @@ public async Task ConvertToProvider_With_valid_type_returns_assembly_qualified_n } [Test] - public async Task ConvertFromProvider_With_valid_type_name_returns_type() + public async Task ConvertFromProvider_With_valid_type_name_returns_type(CancellationToken cancellationToken) { var converter = new TypeValueConverter(); var fromProvider = converter.ConvertFromProvider; @@ -45,7 +47,9 @@ public async Task ConvertFromProvider_With_valid_type_name_returns_type() } [Test] - public async Task ConvertFromProvider_With_invalid_type_name_throws_InvalidOperationException() + public async Task ConvertFromProvider_With_invalid_type_name_throws_InvalidOperationException( + CancellationToken cancellationToken + ) { var converter = new TypeValueConverter(); var fromProvider = converter.ConvertFromProvider; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs index 575466e4..ce720b98 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs @@ -16,13 +16,15 @@ public sealed class EventDispatcherExtensionsTests { [Test] - public async Task UseDefaultEventDispatcher_NullBuilder_ThrowsArgumentNullException() => + public async Task UseDefaultEventDispatcher_NullBuilder_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => EventDispatcherExtensions.UseDefaultEventDispatcher(null!)) .Throws(); [Test] - public async Task UseDefaultEventDispatcher_RegistersDispatcherAsSingleton() + public async Task UseDefaultEventDispatcher_RegistersDispatcherAsSingleton(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -43,7 +45,9 @@ public async Task UseDefaultEventDispatcher_RegistersDispatcherAsSingleton() } [Test] - public async Task UseDefaultEventDispatcher_WithCustomLifetime_RegistersWithSpecifiedLifetime() + public async Task UseDefaultEventDispatcher_WithCustomLifetime_RegistersWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -62,7 +66,9 @@ public async Task UseDefaultEventDispatcher_WithCustomLifetime_RegistersWithSpec } [Test] - public async Task UseDefaultEventDispatcher_CalledMultipleTimes_ReplacesRegistration() + public async Task UseDefaultEventDispatcher_CalledMultipleTimes_ReplacesRegistration( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -82,7 +88,7 @@ public async Task UseDefaultEventDispatcher_CalledMultipleTimes_ReplacesRegistra } [Test] - public async Task UseDefaultEventDispatcher_DoesNotRemoveKeyedDispatchers() + public async Task UseDefaultEventDispatcher_DoesNotRemoveKeyedDispatchers(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -109,7 +115,7 @@ public async Task UseDefaultEventDispatcher_DoesNotRemoveKeyedDispatchers() } [Test] - public async Task UseDefaultEventDispatcher_ReturnsSameBuilder() + public async Task UseDefaultEventDispatcher_ReturnsSameBuilder(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -124,7 +130,7 @@ public async Task UseDefaultEventDispatcher_ReturnsSameBuilder() } [Test] - public async Task UseDefaultEventDispatcher_WithFactory_RegistersDispatcher() + public async Task UseDefaultEventDispatcher_WithFactory_RegistersDispatcher(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -150,7 +156,9 @@ public async Task UseDefaultEventDispatcher_WithFactory_RegistersDispatcher() } [Test] - public async Task UseDefaultEventDispatcher_WithFactory_WithNullFactory_ThrowsArgumentNullException() + public async Task UseDefaultEventDispatcher_WithFactory_WithNullFactory_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -160,13 +168,15 @@ public async Task UseDefaultEventDispatcher_WithFactory_WithNullFactory_ThrowsAr } [Test] - public async Task UseEventDispatcherFor_NullBuilder_ThrowsArgumentNullException() => + public async Task UseEventDispatcherFor_NullBuilder_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => EventDispatcherExtensions.UseEventDispatcherFor(null!)) .Throws(); [Test] - public async Task UseEventDispatcherFor_RegistersKeyedDispatcher() + public async Task UseEventDispatcherFor_RegistersKeyedDispatcher(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -189,7 +199,9 @@ public async Task UseEventDispatcherFor_RegistersKeyedDispatcher() } [Test] - public async Task UseEventDispatcherFor_WithCustomLifetime_RegistersWithSpecifiedLifetime() + public async Task UseEventDispatcherFor_WithCustomLifetime_RegistersWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -208,7 +220,9 @@ public async Task UseEventDispatcherFor_WithCustomLifetime_RegistersWithSpecifie } [Test] - public async Task UseEventDispatcherFor_CalledMultipleTimes_ReplacesRegistration() + public async Task UseEventDispatcherFor_CalledMultipleTimes_ReplacesRegistration( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -228,7 +242,7 @@ public async Task UseEventDispatcherFor_CalledMultipleTimes_ReplacesRegistration } [Test] - public async Task UseEventDispatcherFor_DifferentEventTypes_RegistersSeparately() + public async Task UseEventDispatcherFor_DifferentEventTypes_RegistersSeparately(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -257,7 +271,7 @@ public async Task UseEventDispatcherFor_DifferentEventTypes_RegistersSeparately( } [Test] - public async Task UseEventDispatcherFor_DoesNotAffectGlobalDispatcher() + public async Task UseEventDispatcherFor_DoesNotAffectGlobalDispatcher(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -282,7 +296,7 @@ public async Task UseEventDispatcherFor_DoesNotAffectGlobalDispatcher() } [Test] - public async Task UseEventDispatcherFor_WithFactory_RegistersKeyedDispatcher() + public async Task UseEventDispatcherFor_WithFactory_RegistersKeyedDispatcher(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -310,7 +324,9 @@ public async Task UseEventDispatcherFor_WithFactory_RegistersKeyedDispatcher() } [Test] - public async Task UseEventDispatcherFor_WithFactory_WithNullFactory_ThrowsArgumentNullException() + public async Task UseEventDispatcherFor_WithFactory_WithNullFactory_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs index bf8fc192..acd04849 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs @@ -15,13 +15,15 @@ public sealed class FluentValidationExtensionsTests { [Test] - public async Task AddFluentValidation_NullConfigurator_ThrowsArgumentNullException() => + public async Task AddFluentValidation_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => FluentValidationExtensions.AddFluentValidation(null!)) .Throws(); [Test] - public async Task AddFluentValidation_RegistersRequestInterceptor() + public async Task AddFluentValidation_RegistersRequestInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -35,7 +37,9 @@ public async Task AddFluentValidation_RegistersRequestInterceptor() } [Test] - public async Task AddFluentValidation_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor() + public async Task AddFluentValidation_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -53,7 +57,7 @@ public async Task AddFluentValidation_CalledMultipleTimes_DoesNotDuplicateReques } [Test] - public async Task AddFluentValidation_ReturnsSameConfigurator() + public async Task AddFluentValidation_ReturnsSameConfigurator(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -65,7 +69,7 @@ public async Task AddFluentValidation_ReturnsSameConfigurator() } [Test] - public async Task AddFluentValidation_RegistersInterceptorWithScopedLifetime() + public async Task AddFluentValidation_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/Interceptors/FluentValidationRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/Interceptors/FluentValidationRequestInterceptorTests.cs index 126b3762..b0acebd9 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/Interceptors/FluentValidationRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/Interceptors/FluentValidationRequestInterceptorTests.cs @@ -17,7 +17,7 @@ public sealed class FluentValidationRequestInterceptorTests { [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -26,12 +26,12 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() // Act & Assert _ = await Assert - .That(() => interceptor.HandleAsync(new TestCommand("valid"), null!, CancellationToken.None)!) + .That(() => interceptor.HandleAsync(new TestCommand("valid"), null!, cancellationToken)!) .Throws(); } [Test] - public async Task HandleAsync_NoValidatorsRegistered_PassesThroughToHandler() + public async Task HandleAsync_NoValidatorsRegistered_PassesThroughToHandler(CancellationToken cancellationToken) { // Arrange — no IValidator registered var services = new ServiceCollection(); @@ -47,7 +47,7 @@ public async Task HandleAsync_NoValidatorsRegistered_PassesThroughToHandler() handlerCalled = true; return Task.FromResult("ok"); }, - CancellationToken.None + cancellationToken ); // Assert @@ -59,7 +59,7 @@ public async Task HandleAsync_NoValidatorsRegistered_PassesThroughToHandler() } [Test] - public async Task HandleAsync_ValidInput_PassesThroughToHandler() + public async Task HandleAsync_ValidInput_PassesThroughToHandler(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -77,7 +77,7 @@ public async Task HandleAsync_ValidInput_PassesThroughToHandler() handlerCalled = true; return Task.FromResult("success"); }, - CancellationToken.None + cancellationToken ); // Assert @@ -89,7 +89,7 @@ public async Task HandleAsync_ValidInput_PassesThroughToHandler() } [Test] - public async Task HandleAsync_InvalidInput_ThrowsValidationException() + public async Task HandleAsync_InvalidInput_ThrowsValidationException(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -109,7 +109,7 @@ public async Task HandleAsync_InvalidInput_ThrowsValidationException() handlerCalled = true; return Task.FromResult("should not reach"); }, - CancellationToken.None + cancellationToken )! ) .Throws(); @@ -118,7 +118,7 @@ public async Task HandleAsync_InvalidInput_ThrowsValidationException() } [Test] - public async Task HandleAsync_MultipleValidators_AggregatesAllFailures() + public async Task HandleAsync_MultipleValidators_AggregatesAllFailures(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -134,7 +134,7 @@ public async Task HandleAsync_MultipleValidators_AggregatesAllFailures() interceptor.HandleAsync( new TestCommand("invalid"), (_, _) => Task.FromResult("should not reach"), - CancellationToken.None + cancellationToken )! ) .Throws(); @@ -144,7 +144,9 @@ public async Task HandleAsync_MultipleValidators_AggregatesAllFailures() } [Test] - public async Task HandleAsync_MultipleValidatorsOneInvalid_ThrowsValidationException() + public async Task HandleAsync_MultipleValidatorsOneInvalid_ThrowsValidationException( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -160,7 +162,7 @@ public async Task HandleAsync_MultipleValidatorsOneInvalid_ThrowsValidationExcep interceptor.HandleAsync( new TestCommand("input"), (_, _) => Task.FromResult("should not reach"), - CancellationToken.None + cancellationToken )! ) .Throws(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs index d831261c..c6ff00f4 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs @@ -11,7 +11,7 @@ public class HandlerRegistrationExtensionsTests { [Test] - public void AddCommandHandler_WithNullConfigurator_ThrowsArgumentNullException() + public void AddCommandHandler_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) { IMediatorBuilder? configurator = null; @@ -21,7 +21,7 @@ public void AddCommandHandler_WithNullConfigurator_ThrowsArgumentNullException() } [Test] - public async Task AddCommandHandler_RegistersHandlerWithScopedLifetime() + public async Task AddCommandHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -38,7 +38,9 @@ public async Task AddCommandHandler_RegistersHandlerWithScopedLifetime() } [Test] - public async Task AddCommandHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime() + public async Task AddCommandHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -57,7 +59,7 @@ public async Task AddCommandHandler_WithExplicitLifetime_RegistersHandlerWithSpe } [Test] - public async Task AddCommandHandler_ReturnsConfigurator() + public async Task AddCommandHandler_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -77,7 +79,9 @@ public async Task AddCommandHandler_ReturnsConfigurator() } [Test] - public void AddCommandHandler_VoidCommand_WithNullConfigurator_ThrowsArgumentNullException() + public void AddCommandHandler_VoidCommand_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -87,7 +91,9 @@ public void AddCommandHandler_VoidCommand_WithNullConfigurator_ThrowsArgumentNul } [Test] - public async Task AddCommandHandler_VoidCommand_RegistersHandlerWithScopedLifetime() + public async Task AddCommandHandler_VoidCommand_RegistersHandlerWithScopedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -104,7 +110,9 @@ public async Task AddCommandHandler_VoidCommand_RegistersHandlerWithScopedLifeti } [Test] - public async Task AddCommandHandler_VoidCommand_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime() + public async Task AddCommandHandler_VoidCommand_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -123,7 +131,7 @@ public async Task AddCommandHandler_VoidCommand_WithExplicitLifetime_RegistersHa } [Test] - public async Task AddCommandHandler_VoidCommand_ReturnsConfigurator() + public async Task AddCommandHandler_VoidCommand_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -143,7 +151,7 @@ public async Task AddCommandHandler_VoidCommand_ReturnsConfigurator() } [Test] - public void AddQueryHandler_WithNullConfigurator_ThrowsArgumentNullException() + public void AddQueryHandler_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) { IMediatorBuilder? configurator = null; @@ -153,7 +161,7 @@ public void AddQueryHandler_WithNullConfigurator_ThrowsArgumentNullException() } [Test] - public async Task AddQueryHandler_RegistersHandlerWithScopedLifetime() + public async Task AddQueryHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -170,7 +178,9 @@ public async Task AddQueryHandler_RegistersHandlerWithScopedLifetime() } [Test] - public async Task AddQueryHandler_WithTransientLifetime_RegistersHandlerWithTransientLifetime() + public async Task AddQueryHandler_WithTransientLifetime_RegistersHandlerWithTransientLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -189,7 +199,7 @@ public async Task AddQueryHandler_WithTransientLifetime_RegistersHandlerWithTran } [Test] - public async Task AddQueryHandler_ReturnsConfigurator() + public async Task AddQueryHandler_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -209,7 +219,7 @@ public async Task AddQueryHandler_ReturnsConfigurator() } [Test] - public void AddEventHandler_WithNullConfigurator_ThrowsArgumentNullException() + public void AddEventHandler_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) { IMediatorBuilder? configurator = null; @@ -217,7 +227,7 @@ public void AddEventHandler_WithNullConfigurator_ThrowsArgumentNullException() } [Test] - public async Task AddEventHandler_RegistersHandlerWithScopedLifetime() + public async Task AddEventHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -234,7 +244,9 @@ public async Task AddEventHandler_RegistersHandlerWithScopedLifetime() } [Test] - public async Task AddEventHandler_WithSingletonLifetime_RegistersHandlerWithSingletonLifetime() + public async Task AddEventHandler_WithSingletonLifetime_RegistersHandlerWithSingletonLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -251,7 +263,7 @@ public async Task AddEventHandler_WithSingletonLifetime_RegistersHandlerWithSing } [Test] - public async Task AddEventHandler_AllowsMultipleHandlersForSameEvent() + public async Task AddEventHandler_AllowsMultipleHandlersForSameEvent(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -270,7 +282,7 @@ public async Task AddEventHandler_AllowsMultipleHandlersForSameEvent() } [Test] - public async Task AddEventHandler_ReturnsConfigurator() + public async Task AddEventHandler_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -290,7 +302,7 @@ public async Task AddEventHandler_ReturnsConfigurator() } [Test] - public async Task HandlerRegistrationExtensions_SupportMethodChaining() + public async Task HandlerRegistrationExtensions_SupportMethodChaining(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -366,7 +378,9 @@ private sealed partial class AnotherTestEventHandler : IEventHandler // Interceptor registration tests [Test] - public void AddRequestInterceptor_WithNullConfigurator_ThrowsArgumentNullException() + public void AddRequestInterceptor_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -376,7 +390,7 @@ public void AddRequestInterceptor_WithNullConfigurator_ThrowsArgumentNullExcepti } [Test] - public async Task AddRequestInterceptor_RegistersInterceptorWithScopedLifetime() + public async Task AddRequestInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -395,7 +409,9 @@ public async Task AddRequestInterceptor_RegistersInterceptorWithScopedLifetime() } [Test] - public async Task AddRequestInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() + public async Task AddRequestInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -416,7 +432,7 @@ public async Task AddRequestInterceptor_WithExplicitLifetime_RegistersIntercepto } [Test] - public async Task AddRequestInterceptor_ReturnsConfigurator() + public async Task AddRequestInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -436,7 +452,9 @@ public async Task AddRequestInterceptor_ReturnsConfigurator() } [Test] - public void AddCommandInterceptor_WithNullConfigurator_ThrowsArgumentNullException() + public void AddCommandInterceptor_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -446,7 +464,7 @@ public void AddCommandInterceptor_WithNullConfigurator_ThrowsArgumentNullExcepti } [Test] - public async Task AddCommandInterceptor_RegistersInterceptorWithScopedLifetime() + public async Task AddCommandInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -465,7 +483,9 @@ public async Task AddCommandInterceptor_RegistersInterceptorWithScopedLifetime() } [Test] - public async Task AddCommandInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() + public async Task AddCommandInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -486,7 +506,7 @@ public async Task AddCommandInterceptor_WithExplicitLifetime_RegistersIntercepto } [Test] - public async Task AddCommandInterceptor_ReturnsConfigurator() + public async Task AddCommandInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -506,7 +526,9 @@ public async Task AddCommandInterceptor_ReturnsConfigurator() } [Test] - public void AddQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException() + public void AddQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -516,7 +538,7 @@ public void AddQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException } [Test] - public async Task AddQueryInterceptor_RegistersInterceptorWithScopedLifetime() + public async Task AddQueryInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -533,7 +555,9 @@ public async Task AddQueryInterceptor_RegistersInterceptorWithScopedLifetime() } [Test] - public async Task AddQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() + public async Task AddQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -552,7 +576,7 @@ public async Task AddQueryInterceptor_WithExplicitLifetime_RegistersInterceptorW } [Test] - public async Task AddQueryInterceptor_ReturnsConfigurator() + public async Task AddQueryInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -572,7 +596,9 @@ public async Task AddQueryInterceptor_ReturnsConfigurator() } [Test] - public void AddEventInterceptor_WithNullConfigurator_ThrowsArgumentNullException() + public void AddEventInterceptor_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -582,7 +608,7 @@ public void AddEventInterceptor_WithNullConfigurator_ThrowsArgumentNullException } [Test] - public async Task AddEventInterceptor_RegistersInterceptorWithScopedLifetime() + public async Task AddEventInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -599,7 +625,9 @@ public async Task AddEventInterceptor_RegistersInterceptorWithScopedLifetime() } [Test] - public async Task AddEventInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() + public async Task AddEventInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -618,7 +646,7 @@ public async Task AddEventInterceptor_WithExplicitLifetime_RegistersInterceptorW } [Test] - public async Task AddEventInterceptor_ReturnsConfigurator() + public async Task AddEventInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -638,7 +666,7 @@ public async Task AddEventInterceptor_ReturnsConfigurator() } [Test] - public async Task AddEventInterceptor_AllowsMultipleInterceptorsForSameEvent() + public async Task AddEventInterceptor_AllowsMultipleInterceptorsForSameEvent(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -708,7 +736,9 @@ public Task HandleAsync( // Stream query handler registration tests [Test] - public void AddStreamQueryHandler_WithNullConfigurator_ThrowsArgumentNullException() + public void AddStreamQueryHandler_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -718,7 +748,7 @@ public void AddStreamQueryHandler_WithNullConfigurator_ThrowsArgumentNullExcepti } [Test] - public async Task AddStreamQueryHandler_RegistersHandlerWithScopedLifetime() + public async Task AddStreamQueryHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -739,7 +769,9 @@ public async Task AddStreamQueryHandler_RegistersHandlerWithScopedLifetime() } [Test] - public async Task AddStreamQueryHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime() + public async Task AddStreamQueryHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -760,7 +792,7 @@ public async Task AddStreamQueryHandler_WithExplicitLifetime_RegistersHandlerWit } [Test] - public async Task AddStreamQueryHandler_ReturnsConfigurator() + public async Task AddStreamQueryHandler_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -780,7 +812,9 @@ public async Task AddStreamQueryHandler_ReturnsConfigurator() } [Test] - public void AddStreamQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException() + public void AddStreamQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { IMediatorBuilder? configurator = null; @@ -790,7 +824,9 @@ public void AddStreamQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullExc } [Test] - public async Task AddStreamQueryInterceptor_RegistersInterceptorWithScopedLifetime() + public async Task AddStreamQueryInterceptor_RegistersInterceptorWithScopedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -811,7 +847,9 @@ public async Task AddStreamQueryInterceptor_RegistersInterceptorWithScopedLifeti } [Test] - public async Task AddStreamQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() + public async Task AddStreamQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); @@ -834,7 +872,7 @@ public async Task AddStreamQueryInterceptor_WithExplicitLifetime_RegistersInterc } [Test] - public async Task AddStreamQueryInterceptor_ReturnsConfigurator() + public async Task AddStreamQueryInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs index 8ad16746..74fcc19a 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs @@ -14,13 +14,15 @@ public sealed class HttpCorrelationExtensionsTests { [Test] - public async Task AddHttpCorrelationEnrichment_NullConfigurator_ThrowsArgumentNullException() => + public async Task AddHttpCorrelationEnrichment_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => HttpCorrelationExtensions.AddHttpCorrelationEnrichment(null!)) .Throws(); [Test] - public async Task AddHttpCorrelationEnrichment_RegistersRequestInterceptor() + public async Task AddHttpCorrelationEnrichment_RegistersRequestInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -34,7 +36,7 @@ public async Task AddHttpCorrelationEnrichment_RegistersRequestInterceptor() } [Test] - public async Task AddHttpCorrelationEnrichment_RegistersEventInterceptor() + public async Task AddHttpCorrelationEnrichment_RegistersEventInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -48,7 +50,9 @@ public async Task AddHttpCorrelationEnrichment_RegistersEventInterceptor() } [Test] - public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor() + public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -69,7 +73,9 @@ public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplic } [Test] - public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateEventInterceptor() + public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateEventInterceptor( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -90,7 +96,7 @@ public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplic } [Test] - public async Task AddHttpCorrelationEnrichment_ReturnsSameConfigurator() + public async Task AddHttpCorrelationEnrichment_ReturnsSameConfigurator(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -102,7 +108,9 @@ public async Task AddHttpCorrelationEnrichment_ReturnsSameConfigurator() } [Test] - public async Task AddHttpCorrelationEnrichment_WithoutAccessorRegistered_InterceptorResolvesSuccessfully() + public async Task AddHttpCorrelationEnrichment_WithoutAccessorRegistered_InterceptorResolvesSuccessfully( + CancellationToken cancellationToken + ) { // Arrange — IHttpCorrelationAccessor is intentionally NOT registered var services = new ServiceCollection(); @@ -119,7 +127,7 @@ public async Task AddHttpCorrelationEnrichment_WithoutAccessorRegistered_Interce } [Test] - public async Task AddHttpCorrelationEnrichment_RegistersStreamQueryInterceptor() + public async Task AddHttpCorrelationEnrichment_RegistersStreamQueryInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -133,7 +141,9 @@ public async Task AddHttpCorrelationEnrichment_RegistersStreamQueryInterceptor() } [Test] - public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateStreamQueryInterceptor() + public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateStreamQueryInterceptor( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs index f5aff385..85822546 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -23,13 +23,15 @@ public sealed class HttpCorrelationEventInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new HttpCorrelationEventInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoAccessorRegistered_DoesNotThrow() + public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -42,7 +44,7 @@ public async Task Constructor_NoAccessorRegistered_DoesNotThrow() } [Test] - public async Task Constructor_WithAccessorRegistered_DoesNotThrow() + public async Task Constructor_WithAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -60,7 +62,7 @@ public async Task Constructor_WithAccessorRegistered_DoesNotThrow() } [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -69,12 +71,14 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() // Act & Assert _ = await Assert - .That(async () => await interceptor.HandleAsync(message, null!).ConfigureAwait(false)) + .That(async () => await interceptor.HandleAsync(message, null!, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModification() + public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModification( + CancellationToken cancellationToken + ) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -90,7 +94,8 @@ await interceptor { handlerCalled = true; return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -103,7 +108,9 @@ await interceptor } [Test] - public async Task HandleAsync_AccessorHasCorrelationId_MessageAlreadyHasCorrelationId_DoesNotOverwrite() + public async Task HandleAsync_AccessorHasCorrelationId_MessageAlreadyHasCorrelationId_DoesNotOverwrite( + CancellationToken cancellationToken + ) { // Arrange const string existingId = "existing-id"; @@ -118,14 +125,14 @@ public async Task HandleAsync_AccessorHasCorrelationId_MessageAlreadyHasCorrelat var message = new TestEvent { CorrelationId = existingId }; // Act - await interceptor.HandleAsync(message, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(message, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); // Assert _ = await Assert.That(message.CorrelationId).IsEqualTo(existingId); } [Test] - public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyMessage() + public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyMessage(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -139,7 +146,7 @@ public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyMessage( var message = new TestEvent { CorrelationId = null }; // Act - await interceptor.HandleAsync(message, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(message, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); // Assert _ = await Assert.That(message.CorrelationId).IsNull(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs index 10dd212c..d2057f5d 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -23,13 +23,15 @@ public sealed class HttpCorrelationRequestInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new HttpCorrelationRequestInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoAccessorRegistered_DoesNotThrow() + public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -42,7 +44,7 @@ public async Task Constructor_NoAccessorRegistered_DoesNotThrow() } [Test] - public async Task Constructor_WithAccessorRegistered_DoesNotThrow() + public async Task Constructor_WithAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -60,7 +62,7 @@ public async Task Constructor_WithAccessorRegistered_DoesNotThrow() } [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -69,12 +71,14 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() // Act & Assert _ = await Assert - .That(async () => await interceptor.HandleAsync(request, null!).ConfigureAwait(false)) + .That(async () => await interceptor.HandleAsync(request, null!, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModification() + public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModification( + CancellationToken cancellationToken + ) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -90,7 +94,8 @@ public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModificat { handlerCalled = true; return Task.FromResult("response"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -104,7 +109,9 @@ public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModificat } [Test] - public async Task HandleAsync_AccessorHasCorrelationId_RequestAlreadyHasCorrelationId_DoesNotOverwrite() + public async Task HandleAsync_AccessorHasCorrelationId_RequestAlreadyHasCorrelationId_DoesNotOverwrite( + CancellationToken cancellationToken + ) { // Arrange const string existingId = "existing-id"; @@ -119,14 +126,16 @@ public async Task HandleAsync_AccessorHasCorrelationId_RequestAlreadyHasCorrelat var request = new TestCommand { CorrelationId = existingId }; // Act - _ = await interceptor.HandleAsync(request, (_, _) => Task.FromResult("response")).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(request, (_, _) => Task.FromResult("response"), cancellationToken) + .ConfigureAwait(false); // Assert _ = await Assert.That(request.CorrelationId).IsEqualTo(existingId); } [Test] - public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyRequest() + public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyRequest(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -140,7 +149,9 @@ public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyRequest( var request = new TestCommand { CorrelationId = null }; // Act - _ = await interceptor.HandleAsync(request, (_, _) => Task.FromResult("response")).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(request, (_, _) => Task.FromResult("response"), cancellationToken) + .ConfigureAwait(false); // Assert _ = await Assert.That(request.CorrelationId).IsNull(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs index badfe5ce..d436bbb1 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; using System; using System.Collections.Generic; @@ -28,13 +28,15 @@ public sealed class HttpCorrelationStreamQueryInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new HttpCorrelationStreamQueryInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoAccessorRegistered_DoesNotThrow() + public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -47,7 +49,7 @@ public async Task Constructor_NoAccessorRegistered_DoesNotThrow() } [Test] - public async Task Constructor_WithAccessorRegistered_DoesNotThrow() + public async Task Constructor_WithAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -65,7 +67,7 @@ public async Task Constructor_WithAccessorRegistered_DoesNotThrow() } [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -76,7 +78,9 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() _ = await Assert .That(async () => { - await foreach (var _ in interceptor.HandleAsync(request, null!).ConfigureAwait(false)) + await foreach ( + var _ in interceptor.HandleAsync(request, null!, cancellationToken).ConfigureAwait(false) + ) { // consume — we expect the foreach to throw before yielding any items } @@ -85,7 +89,9 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() } [Test] - public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModification() + public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModification( + CancellationToken cancellationToken + ) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -96,7 +102,7 @@ public async Task HandleAsync_NoAccessorRegistered_PassesThroughWithoutModificat var items = new List(); await foreach ( var item in interceptor - .HandleAsync(request, (_, ct) => YieldItemsAsync(["a", "b"], ct)) + .HandleAsync(request, (_, ct) => YieldItemsAsync(["a", "b"], ct), cancellationToken) .ConfigureAwait(false) ) { @@ -112,7 +118,9 @@ var item in interceptor } [Test] - public async Task HandleAsync_AccessorHasCorrelationId_RequestAlreadyHasCorrelationId_DoesNotOverwrite() + public async Task HandleAsync_AccessorHasCorrelationId_RequestAlreadyHasCorrelationId_DoesNotOverwrite( + CancellationToken cancellationToken + ) { // Arrange const string existingId = "existing-id"; @@ -128,7 +136,9 @@ public async Task HandleAsync_AccessorHasCorrelationId_RequestAlreadyHasCorrelat // Act await foreach ( - var _ in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct)).ConfigureAwait(false) + var _ in interceptor + .HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct), cancellationToken) + .ConfigureAwait(false) ) { // consume @@ -139,7 +149,9 @@ var _ in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct)) } [Test] - public async Task HandleAsync_AccessorHasCorrelationId_RequestHasNoCorrelationId_SetsCorrelationId() + public async Task HandleAsync_AccessorHasCorrelationId_RequestHasNoCorrelationId_SetsCorrelationId( + CancellationToken cancellationToken + ) { // Arrange const string httpId = "http-correlation-id"; @@ -162,7 +174,9 @@ public async Task HandleAsync_AccessorHasCorrelationId_RequestHasNoCorrelationId // Act await foreach ( - var _ in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct)).ConfigureAwait(false) + var _ in interceptor + .HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct), cancellationToken) + .ConfigureAwait(false) ) { // consume @@ -173,7 +187,7 @@ var _ in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct)) } [Test] - public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyRequest() + public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyRequest(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -188,7 +202,9 @@ public async Task HandleAsync_AccessorCorrelationIdIsEmpty_DoesNotModifyRequest( // Act await foreach ( - var _ in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct)).ConfigureAwait(false) + var _ in interceptor + .HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct), cancellationToken) + .ConfigureAwait(false) ) { // consume @@ -199,7 +215,7 @@ var _ in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(["x"], ct)) } [Test] - public async Task HandleAsync_YieldsItemsUnchanged() + public async Task HandleAsync_YieldsItemsUnchanged(CancellationToken cancellationToken) { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -210,7 +226,9 @@ public async Task HandleAsync_YieldsItemsUnchanged() // Act var items = new List(); await foreach ( - var item in interceptor.HandleAsync(request, (_, ct) => YieldItemsAsync(expected, ct)).ConfigureAwait(false) + var item in interceptor + .HandleAsync(request, (_, ct) => YieldItemsAsync(expected, ct), cancellationToken) + .ConfigureAwait(false) ) { items.Add(item); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs index 627782eb..64b3f7c9 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs @@ -17,11 +17,12 @@ public sealed class IdempotencyExtensionsTests { [Test] - public async Task AddIdempotency_NullConfigurator_ThrowsArgumentNullException() => - _ = await Assert.That(() => IdempotencyExtensions.AddIdempotency(null!)).Throws(); + public async Task AddIdempotency_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert.That(() => IdempotencyExtensions.AddIdempotency(null!)).Throws(); [Test] - public async Task AddIdempotency_RegistersRequestInterceptor() + public async Task AddIdempotency_RegistersRequestInterceptor(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -43,7 +44,9 @@ public async Task AddIdempotency_RegistersRequestInterceptor() } [Test] - public async Task AddIdempotency_CalledMultipleTimes_DoesNotDuplicateInterceptor() + public async Task AddIdempotency_CalledMultipleTimes_DoesNotDuplicateInterceptor( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -62,7 +65,7 @@ public async Task AddIdempotency_CalledMultipleTimes_DoesNotDuplicateInterceptor } [Test] - public async Task AddIdempotency_ReturnsSameConfigurator() + public async Task AddIdempotency_ReturnsSameConfigurator(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -73,7 +76,9 @@ public async Task AddIdempotency_ReturnsSameConfigurator() } [Test] - public async Task AddIdempotency_WithoutStoreRegistered_InterceptorResolvesSuccessfully() + public async Task AddIdempotency_WithoutStoreRegistered_InterceptorResolvesSuccessfully( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddLogging().AddSingleton(TimeProvider.System); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsEventInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsEventInterceptorTests.cs index 99f07a22..310da935 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsEventInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsEventInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System.Diagnostics; using NetEvolve.Extensions.TUnit; @@ -11,7 +11,7 @@ public class ActivityAndMetricsEventInterceptorTests { [Test] [NotInParallel] - public async Task HandleAsync_CreatesActivityWithCorrectTags() + public async Task HandleAsync_CreatesActivityWithCorrectTags(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -35,7 +35,8 @@ await interceptor { handlerCalled = true; return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -51,7 +52,7 @@ await interceptor [Test] [NotInParallel] - public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk() + public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -67,7 +68,7 @@ public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk() listener.ActivityStopped = activity => capturedActivity = activity; - await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -81,7 +82,7 @@ public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk() [Test] [NotInParallel] - public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() + public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -99,7 +100,9 @@ public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() listener.ActivityStopped = activity => capturedActivity = activity; var exception = await Assert.ThrowsAsync(async () => - await interceptor.HandleAsync(testEvent, (_, _) => throw testException).ConfigureAwait(false) + await interceptor + .HandleAsync(testEvent, (_, _) => throw testException, cancellationToken) + .ConfigureAwait(false) ); using (Assert.Multiple()) @@ -121,7 +124,7 @@ public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() [Test] [NotInParallel] - public async Task HandleAsync_SetsTimestamps() + public async Task HandleAsync_SetsTimestamps(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -137,7 +140,7 @@ public async Task HandleAsync_SetsTimestamps() listener.ActivityStopped = activity => capturedActivity = activity; - await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -148,7 +151,7 @@ public async Task HandleAsync_SetsTimestamps() } [Test] - public async Task HandleAsync_InvokesHandlerWithCorrectEvent() + public async Task HandleAsync_InvokesHandlerWithCorrectEvent(CancellationToken cancellationToken) { var timeProvider = TimeProvider.System; var interceptor = new ActivityAndMetricsEventInterceptor(timeProvider); @@ -162,7 +165,8 @@ await interceptor { receivedEvent = evt; return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -171,7 +175,7 @@ await interceptor [Test] [NotInParallel] - public async Task HandleAsync_WithDifferentEventTypes_CreatesCorrectActivities() + public async Task HandleAsync_WithDifferentEventTypes_CreatesCorrectActivities(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -187,8 +191,12 @@ public async Task HandleAsync_WithDifferentEventTypes_CreatesCorrectActivities() listener.ActivityStarted = activity => activities.Add(activity); - await interceptor1.HandleAsync(new TestEvent(), (_, _) => Task.CompletedTask).ConfigureAwait(false); - await interceptor2.HandleAsync(new AnotherTestEvent(), (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor1 + .HandleAsync(new TestEvent(), (_, _) => Task.CompletedTask, cancellationToken) + .ConfigureAwait(false); + await interceptor2 + .HandleAsync(new AnotherTestEvent(), (_, _) => Task.CompletedTask, cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsRequestInterceptorTests.cs index 405da4fe..776fd6f6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System.Diagnostics; using NetEvolve.Extensions.TUnit; @@ -11,7 +11,7 @@ public class ActivityAndMetricsRequestInterceptorTests { [Test] [NotInParallel] - public async Task HandleAsync_WithCommand_CreatesActivityWithCorrectTags() + public async Task HandleAsync_WithCommand_CreatesActivityWithCorrectTags(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -35,7 +35,8 @@ public async Task HandleAsync_WithCommand_CreatesActivityWithCorrectTags() { handlerCalled = true; return Task.FromResult("test-result"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -53,7 +54,7 @@ public async Task HandleAsync_WithCommand_CreatesActivityWithCorrectTags() [Test] [NotInParallel] - public async Task HandleAsync_WithQuery_CreatesActivityWithCorrectTags() + public async Task HandleAsync_WithQuery_CreatesActivityWithCorrectTags(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -69,7 +70,9 @@ public async Task HandleAsync_WithQuery_CreatesActivityWithCorrectTags() listener.ActivityStarted = activity => capturedActivity = activity; - var result = await interceptor.HandleAsync(query, (_, _) => Task.FromResult(42)).ConfigureAwait(false); + var result = await interceptor + .HandleAsync(query, (_, _) => Task.FromResult(42), cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -84,7 +87,7 @@ public async Task HandleAsync_WithQuery_CreatesActivityWithCorrectTags() [Test] [NotInParallel] - public async Task HandleAsync_WithGenericRequest_CreatesActivityWithCorrectTags() + public async Task HandleAsync_WithGenericRequest_CreatesActivityWithCorrectTags(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -100,7 +103,9 @@ public async Task HandleAsync_WithGenericRequest_CreatesActivityWithCorrectTags( listener.ActivityStarted = activity => capturedActivity = activity; - var result = await interceptor.HandleAsync(request, (_, _) => Task.FromResult(true)).ConfigureAwait(false); + var result = await interceptor + .HandleAsync(request, (_, _) => Task.FromResult(true), cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -115,7 +120,7 @@ public async Task HandleAsync_WithGenericRequest_CreatesActivityWithCorrectTags( [Test] [NotInParallel] - public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk() + public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -131,7 +136,9 @@ public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk() listener.ActivityStopped = activity => capturedActivity = activity; - _ = await interceptor.HandleAsync(command, (_, _) => Task.FromResult("success")).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("success"), cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -145,7 +152,7 @@ public async Task HandleAsync_WhenHandlerSucceeds_SetsActivityStatusToOk() [Test] [NotInParallel] - public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() + public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -163,7 +170,9 @@ public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() listener.ActivityStopped = activity => capturedActivity = activity; var exception = await Assert.ThrowsAsync(async () => - await interceptor.HandleAsync(command, (_, _) => throw testException).ConfigureAwait(false) + await interceptor + .HandleAsync(command, (_, _) => throw testException, cancellationToken) + .ConfigureAwait(false) ); using (Assert.Multiple()) @@ -185,7 +194,7 @@ public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() [Test] [NotInParallel] - public async Task HandleAsync_SetsTimestamps() + public async Task HandleAsync_SetsTimestamps(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -201,7 +210,9 @@ public async Task HandleAsync_SetsTimestamps() listener.ActivityStopped = activity => capturedActivity = activity; - _ = await interceptor.HandleAsync(command, (_, _) => Task.FromResult("result")).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("result"), cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -212,7 +223,7 @@ public async Task HandleAsync_SetsTimestamps() } [Test] - public async Task HandleAsync_InvokesHandlerWithCorrectRequest() + public async Task HandleAsync_InvokesHandlerWithCorrectRequest(CancellationToken cancellationToken) { var timeProvider = TimeProvider.System; var interceptor = new ActivityAndMetricsRequestInterceptor(timeProvider); @@ -226,7 +237,8 @@ public async Task HandleAsync_InvokesHandlerWithCorrectRequest() { receivedCommand = cmd; return Task.FromResult("result"); - } + }, + cancellationToken ) .ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsStreamQueryInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsStreamQueryInterceptorTests.cs index af0db49a..b20c3ad1 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsStreamQueryInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/ActivityAndMetricsStreamQueryInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -12,7 +12,7 @@ public class ActivityAndMetricsStreamQueryInterceptorTests { [Test] [NotInParallel] - public async Task HandleAsync_CreatesActivityWithCorrectTags() + public async Task HandleAsync_CreatesActivityWithCorrectTags(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -28,7 +28,7 @@ public async Task HandleAsync_CreatesActivityWithCorrectTags() listener.ActivityStarted = activity => capturedActivity = activity; - await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items([1, 2, 3], ct))) + await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items([1, 2, 3], ct), cancellationToken)) { // consume items } @@ -45,7 +45,7 @@ public async Task HandleAsync_CreatesActivityWithCorrectTags() [Test] [NotInParallel] - public async Task HandleAsync_WhenStreamCompletes_SetsActivityStatusToOk() + public async Task HandleAsync_WhenStreamCompletes_SetsActivityStatusToOk(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -61,7 +61,7 @@ public async Task HandleAsync_WhenStreamCompletes_SetsActivityStatusToOk() listener.ActivityStopped = activity => capturedActivity = activity; - await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items([1, 2, 3], ct))) + await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items([1, 2, 3], ct), cancellationToken)) { // consume items } @@ -78,7 +78,7 @@ public async Task HandleAsync_WhenStreamCompletes_SetsActivityStatusToOk() [Test] [NotInParallel] - public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() + public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -97,7 +97,9 @@ public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() var exception = await Assert.ThrowsAsync(async () => { - await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => ThrowingItems(testException, ct))) + await foreach ( + var _ in interceptor.HandleAsync(query, (_, ct) => ThrowingItems(testException, ct), cancellationToken) + ) { // consume items until exception } @@ -124,7 +126,7 @@ public async Task HandleAsync_WhenHandlerThrows_SetsActivityStatusToError() [Test] [NotInParallel] - public async Task HandleAsync_WithEmptyStream_SetsActivityStatusToOk() + public async Task HandleAsync_WithEmptyStream_SetsActivityStatusToOk(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -140,7 +142,9 @@ public async Task HandleAsync_WithEmptyStream_SetsActivityStatusToOk() listener.ActivityStopped = activity => capturedActivity = activity; - await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items(Array.Empty(), ct))) + await foreach ( + var _ in interceptor.HandleAsync(query, (_, ct) => Items(Array.Empty(), ct), cancellationToken) + ) { // empty stream } @@ -156,7 +160,7 @@ public async Task HandleAsync_WithEmptyStream_SetsActivityStatusToOk() } [Test] - public async Task HandleAsync_YieldsAllItemsUnchanged() + public async Task HandleAsync_YieldsAllItemsUnchanged(CancellationToken cancellationToken) { var timeProvider = TimeProvider.System; var interceptor = new ActivityAndMetricsStreamQueryInterceptor(timeProvider); @@ -164,7 +168,7 @@ public async Task HandleAsync_YieldsAllItemsUnchanged() var expected = new[] { 10, 20, 30 }; var received = new List(); - await foreach (var item in interceptor.HandleAsync(query, (_, ct) => Items(expected, ct))) + await foreach (var item in interceptor.HandleAsync(query, (_, ct) => Items(expected, ct), cancellationToken)) { received.Add(item); } @@ -174,7 +178,7 @@ public async Task HandleAsync_YieldsAllItemsUnchanged() [Test] [NotInParallel] - public async Task HandleAsync_SetsTimestamps() + public async Task HandleAsync_SetsTimestamps(CancellationToken cancellationToken) { using var listener = new ActivityListener { @@ -190,7 +194,7 @@ public async Task HandleAsync_SetsTimestamps() listener.ActivityStopped = activity => capturedActivity = activity; - await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items([1], ct))) + await foreach (var _ in interceptor.HandleAsync(query, (_, ct) => Items([1], ct), cancellationToken)) { // consume } @@ -204,7 +208,7 @@ public async Task HandleAsync_SetsTimestamps() } [Test] - public async Task HandleAsync_InvokesHandlerWithCorrectQuery() + public async Task HandleAsync_InvokesHandlerWithCorrectQuery(CancellationToken cancellationToken) { var timeProvider = TimeProvider.System; var interceptor = new ActivityAndMetricsStreamQueryInterceptor(timeProvider); @@ -218,7 +222,8 @@ var _ in interceptor.HandleAsync( { receivedQuery = q; return Items([1], ct); - } + }, + cancellationToken ) ) { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/DistributedCacheQueryInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/DistributedCacheQueryInterceptorTests.cs index a38d0357..af038b9f 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/DistributedCacheQueryInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/DistributedCacheQueryInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System.Text.Json; using Microsoft.Extensions.Caching.Distributed; @@ -16,7 +16,7 @@ public class DistributedCacheQueryInterceptorTests private static IOptions DefaultOptions => Options.Create(new QueryCachingOptions()); [Test] - public async Task HandleAsync_QueryNotCacheable_AlwaysCallsHandler() + public async Task HandleAsync_QueryNotCacheable_AlwaysCallsHandler(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -33,7 +33,8 @@ public async Task HandleAsync_QueryNotCacheable_AlwaysCallsHandler() { handlerCallCount++; return Task.FromResult("handler-result"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -45,7 +46,7 @@ public async Task HandleAsync_QueryNotCacheable_AlwaysCallsHandler() } [Test] - public async Task HandleAsync_CacheMiss_CallsHandlerAndStoresResult() + public async Task HandleAsync_CacheMiss_CallsHandlerAndStoresResult(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -62,7 +63,8 @@ public async Task HandleAsync_CacheMiss_CallsHandlerAndStoresResult() { handlerCallCount++; return Task.FromResult("cached-value"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -74,14 +76,14 @@ public async Task HandleAsync_CacheMiss_CallsHandlerAndStoresResult() // Verify the value was written to the cache var cache = provider.GetRequiredService(); - var bytes = await cache.GetAsync("test-key").ConfigureAwait(false); + var bytes = await cache.GetAsync("test-key", cancellationToken).ConfigureAwait(false); _ = await Assert.That(bytes).IsNotNull(); var deserialised = JsonSerializer.Deserialize(bytes!); _ = await Assert.That(deserialised).IsEqualTo("cached-value"); } [Test] - public async Task HandleAsync_CacheHit_ReturnsCachedValueWithoutCallingHandler() + public async Task HandleAsync_CacheHit_ReturnsCachedValueWithoutCallingHandler(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -90,7 +92,7 @@ public async Task HandleAsync_CacheHit_ReturnsCachedValueWithoutCallingHandler() // Pre-populate the cache var cache = provider.GetRequiredService(); var serialized = JsonSerializer.SerializeToUtf8Bytes("cached-result"); - await cache.SetAsync("hit-key", serialized).ConfigureAwait(false); + await cache.SetAsync("hit-key", serialized, cancellationToken).ConfigureAwait(false); var interceptor = new DistributedCacheQueryInterceptor(provider, DefaultOptions); var query = new CacheableQuery("hit-key"); @@ -103,7 +105,8 @@ public async Task HandleAsync_CacheHit_ReturnsCachedValueWithoutCallingHandler() { handlerCallCount++; return Task.FromResult("handler-result"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -115,7 +118,7 @@ public async Task HandleAsync_CacheHit_ReturnsCachedValueWithoutCallingHandler() } [Test] - public async Task HandleAsync_WithExpiry_StoresEntryWithAbsoluteExpiration() + public async Task HandleAsync_WithExpiry_StoresEntryWithAbsoluteExpiration(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -128,18 +131,18 @@ public async Task HandleAsync_WithExpiry_StoresEntryWithAbsoluteExpiration() var query = new CacheableQueryWithExpiry("expiry-key", TimeSpan.FromSeconds(60)); var result = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("expiry-value")) + .HandleAsync(query, (_, _) => Task.FromResult("expiry-value"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("expiry-value"); var cache = provider.GetRequiredService(); - var bytes = await cache.GetAsync("expiry-key").ConfigureAwait(false); + var bytes = await cache.GetAsync("expiry-key", cancellationToken).ConfigureAwait(false); _ = await Assert.That(bytes).IsNotNull(); } [Test] - public async Task HandleAsync_WithNullExpiry_StoresEntryWithoutExpiration() + public async Task HandleAsync_WithNullExpiry_StoresEntryWithoutExpiration(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -149,18 +152,18 @@ public async Task HandleAsync_WithNullExpiry_StoresEntryWithoutExpiration() var query = new CacheableQuery("no-expiry-key"); var result = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("no-expiry-value")) + .HandleAsync(query, (_, _) => Task.FromResult("no-expiry-value"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("no-expiry-value"); var cache = provider.GetRequiredService(); - var bytes = await cache.GetAsync("no-expiry-key").ConfigureAwait(false); + var bytes = await cache.GetAsync("no-expiry-key", cancellationToken).ConfigureAwait(false); _ = await Assert.That(bytes).IsNotNull(); } [Test] - public async Task HandleAsync_NoCacheRegistered_FallsThroughToHandler() + public async Task HandleAsync_NoCacheRegistered_FallsThroughToHandler(CancellationToken cancellationToken) { var services = new ServiceCollection(); // Do NOT register IDistributedCache @@ -177,7 +180,8 @@ public async Task HandleAsync_NoCacheRegistered_FallsThroughToHandler() { handlerCallCount++; return Task.FromResult("fallthrough-result"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -189,7 +193,7 @@ public async Task HandleAsync_NoCacheRegistered_FallsThroughToHandler() } [Test] - public async Task HandleAsync_ExpiredCacheEntry_CallsHandlerAndRefreshesCache() + public async Task HandleAsync_ExpiredCacheEntry_CallsHandlerAndRefreshesCache(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -202,11 +206,12 @@ await cache .SetAsync( "expired-key", serialized, - new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(1) } + new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMilliseconds(1) }, + cancellationToken ) .ConfigureAwait(false); - await Task.Delay(50).ConfigureAwait(false); // Allow the entry to expire + await Task.Delay(50, cancellationToken).ConfigureAwait(false); // Allow the entry to expire var interceptor = new DistributedCacheQueryInterceptor(provider, DefaultOptions); var query = new CacheableQuery("expired-key"); @@ -219,7 +224,8 @@ await cache { handlerCallCount++; return Task.FromResult("fresh-value"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -231,7 +237,9 @@ await cache } [Test] - public async Task HandleAsync_SlidingExpirationMode_StoresEntryWithSlidingExpiration() + public async Task HandleAsync_SlidingExpirationMode_StoresEntryWithSlidingExpiration( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -242,19 +250,21 @@ public async Task HandleAsync_SlidingExpirationMode_StoresEntryWithSlidingExpira var query = new CacheableQueryWithExpiry("sliding-key", TimeSpan.FromSeconds(60)); var result = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("sliding-value")) + .HandleAsync(query, (_, _) => Task.FromResult("sliding-value"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("sliding-value"); // Entry should still be accessible after being stored with sliding expiration var cache = provider.GetRequiredService(); - var bytes = await cache.GetAsync("sliding-key").ConfigureAwait(false); + var bytes = await cache.GetAsync("sliding-key", cancellationToken).ConfigureAwait(false); _ = await Assert.That(bytes).IsNotNull(); } [Test] - public async Task HandleAsync_CustomJsonSerializerOptions_UsedForSerializationAndDeserialization() + public async Task HandleAsync_CustomJsonSerializerOptions_UsedForSerializationAndDeserialization( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -267,21 +277,21 @@ public async Task HandleAsync_CustomJsonSerializerOptions_UsedForSerializationAn var query = new CacheableQuery("custom-json-key"); var result = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("custom-json-value")) + .HandleAsync(query, (_, _) => Task.FromResult("custom-json-value"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("custom-json-value"); // Second call should return from cache using the same custom options var cachedResult = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("should-not-be-returned")) + .HandleAsync(query, (_, _) => Task.FromResult("should-not-be-returned"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(cachedResult).IsEqualTo("custom-json-value"); } [Test] - public async Task HandleAsync_DefaultExpiry_UsedWhenQueryExpiryIsNull() + public async Task HandleAsync_DefaultExpiry_UsedWhenQueryExpiryIsNull(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -292,19 +302,19 @@ public async Task HandleAsync_DefaultExpiry_UsedWhenQueryExpiryIsNull() var query = new CacheableQuery("default-expiry-key"); var result = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("default-expiry-value")) + .HandleAsync(query, (_, _) => Task.FromResult("default-expiry-value"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("default-expiry-value"); // Entry should be present (default expiry applied) var cache = provider.GetRequiredService(); - var bytes = await cache.GetAsync("default-expiry-key").ConfigureAwait(false); + var bytes = await cache.GetAsync("default-expiry-key", cancellationToken).ConfigureAwait(false); _ = await Assert.That(bytes).IsNotNull(); } [Test] - public async Task HandleAsync_DefaultExpiry_NotUsedWhenQueryExpiryIsProvided() + public async Task HandleAsync_DefaultExpiry_NotUsedWhenQueryExpiryIsProvided(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddDistributedMemoryCache(); @@ -316,14 +326,14 @@ public async Task HandleAsync_DefaultExpiry_NotUsedWhenQueryExpiryIsProvided() var query = new CacheableQueryWithExpiry("query-expiry-key", TimeSpan.FromMinutes(10)); var result = await interceptor - .HandleAsync(query, (_, _) => Task.FromResult("query-expiry-value")) + .HandleAsync(query, (_, _) => Task.FromResult("query-expiry-value"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("query-expiry-value"); // Entry should still be present because the query's own expiry (10 min) overrode DefaultExpiry (1 ms) var cache = provider.GetRequiredService(); - var bytes = await cache.GetAsync("query-expiry-key").ConfigureAwait(false); + var bytes = await cache.GetAsync("query-expiry-key", cancellationToken).ConfigureAwait(false); _ = await Assert.That(bytes).IsNotNull(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs index 51793f76..94feb3b8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs @@ -22,13 +22,15 @@ namespace NetEvolve.Pulse.Tests.Unit.Interceptors; public sealed class IdempotencyCommandInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new IdempotencyCommandInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoStoreRegistered_DoesNotThrow() + public async Task Constructor_NoStoreRegistered_DoesNotThrow(CancellationToken cancellationToken) { var provider = new ServiceCollection().BuildServiceProvider(); @@ -38,19 +40,21 @@ public async Task Constructor_NoStoreRegistered_DoesNotThrow() } [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { var provider = new ServiceCollection().BuildServiceProvider(); var interceptor = new IdempotencyCommandInterceptor(provider); var command = new TestCommand(); _ = await Assert - .That(async () => await interceptor.HandleAsync(command, null!).ConfigureAwait(false)) + .That(async () => await interceptor.HandleAsync(command, null!, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task HandleAsync_NonIdempotentCommand_PassesThroughWithoutStoreInteraction() + public async Task HandleAsync_NonIdempotentCommand_PassesThroughWithoutStoreInteraction( + CancellationToken cancellationToken + ) { var store = new TrackingIdempotencyStore(); var services = new ServiceCollection(); @@ -67,7 +71,8 @@ public async Task HandleAsync_NonIdempotentCommand_PassesThroughWithoutStoreInte { handlerCalled = true; return Task.FromResult("response"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -81,7 +86,7 @@ public async Task HandleAsync_NonIdempotentCommand_PassesThroughWithoutStoreInte } [Test] - public async Task HandleAsync_NoStoreRegistered_PassesThroughWithoutError() + public async Task HandleAsync_NoStoreRegistered_PassesThroughWithoutError(CancellationToken cancellationToken) { var provider = new ServiceCollection().BuildServiceProvider(); var interceptor = new IdempotencyCommandInterceptor(provider); @@ -95,7 +100,8 @@ public async Task HandleAsync_NoStoreRegistered_PassesThroughWithoutError() { handlerCalled = true; return Task.FromResult("response"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -107,7 +113,7 @@ public async Task HandleAsync_NoStoreRegistered_PassesThroughWithoutError() } [Test] - public async Task HandleAsync_NewKey_ExecutesHandlerAndStoresKey() + public async Task HandleAsync_NewKey_ExecutesHandlerAndStoresKey(CancellationToken cancellationToken) { var store = new TrackingIdempotencyStore(); var services = new ServiceCollection(); @@ -124,7 +130,8 @@ public async Task HandleAsync_NewKey_ExecutesHandlerAndStoresKey() { handlerCalled = true; return Task.FromResult("response"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -139,7 +146,7 @@ public async Task HandleAsync_NewKey_ExecutesHandlerAndStoresKey() } [Test] - public async Task HandleAsync_ExistingKey_ThrowsIdempotencyConflictException() + public async Task HandleAsync_ExistingKey_ThrowsIdempotencyConflictException(CancellationToken cancellationToken) { var store = new TrackingIdempotencyStore(existingKey: "key-dup"); var services = new ServiceCollection(); @@ -158,7 +165,8 @@ await interceptor { handlerCalled = true; return Task.FromResult("response"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -173,7 +181,7 @@ await interceptor } [Test] - public async Task HandleAsync_ExistingKey_DoesNotCallHandler() + public async Task HandleAsync_ExistingKey_DoesNotCallHandler(CancellationToken cancellationToken) { var store = new TrackingIdempotencyStore(existingKey: "key-exists"); var services = new ServiceCollection(); @@ -184,7 +192,9 @@ public async Task HandleAsync_ExistingKey_DoesNotCallHandler() _ = await Assert .That(async () => - await interceptor.HandleAsync(command, (_, _) => Task.FromResult("response")).ConfigureAwait(false) + await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("response"), cancellationToken) + .ConfigureAwait(false) ) .Throws(); @@ -192,7 +202,7 @@ await interceptor.HandleAsync(command, (_, _) => Task.FromResult("response")).Co } [Test] - public async Task HandleAsync_HandlerThrows_DoesNotStoreKey() + public async Task HandleAsync_HandlerThrows_DoesNotStoreKey(CancellationToken cancellationToken) { var store = new TrackingIdempotencyStore(); var services = new ServiceCollection(); @@ -206,7 +216,8 @@ public async Task HandleAsync_HandlerThrows_DoesNotStoreKey() await interceptor .HandleAsync( command, - (_, _) => Task.FromException(new InvalidOperationException("handler error")) + (_, _) => Task.FromException(new InvalidOperationException("handler error")), + cancellationToken ) .ConfigureAwait(false) ) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingEventInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingEventInterceptorTests.cs index 06c7ce92..a651e125 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingEventInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingEventInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -24,7 +24,7 @@ private static LoggingEventInterceptor CreateInterceptor( } [Test] - public async Task HandleAsync_LogsBeginAndEndAtDebugLevel() + public async Task HandleAsync_LogsBeginAndEndAtDebugLevel(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger, new LoggingInterceptorOptions { LogLevel = LogLevel.Debug }); @@ -38,7 +38,8 @@ await interceptor { handlerCalled = true; return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -52,13 +53,13 @@ await interceptor } [Test] - public async Task HandleAsync_LogsBeginAndEndAtInformationLevel() + public async Task HandleAsync_LogsBeginAndEndAtInformationLevel(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger, new LoggingInterceptorOptions { LogLevel = LogLevel.Information }); var testEvent = new TestEvent(); - await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -69,19 +70,19 @@ public async Task HandleAsync_LogsBeginAndEndAtInformationLevel() } [Test] - public async Task HandleAsync_LogsEventNameInMessage() + public async Task HandleAsync_LogsEventNameInMessage(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); var testEvent = new TestEvent(); - await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); _ = await Assert.That(logger.Entries[0].Message).Contains("TestEvent"); } [Test] - public async Task HandleAsync_WithSlowEvent_LogsWarning() + public async Task HandleAsync_WithSlowEvent_LogsWarning(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor( @@ -91,7 +92,7 @@ public async Task HandleAsync_WithSlowEvent_LogsWarning() var testEvent = new TestEvent(); await interceptor - .HandleAsync(testEvent, async (_, ct) => await Task.Delay(50, ct).ConfigureAwait(false)) + .HandleAsync(testEvent, async (_, ct) => await Task.Delay(50, ct).ConfigureAwait(false), cancellationToken) .ConfigureAwait(false); var warnings = logger.Entries.Where(e => e.LogLevel == LogLevel.Warning).ToList(); @@ -100,14 +101,14 @@ await interceptor } [Test] - public async Task HandleAsync_WithDisabledSlowThreshold_DoesNotLogWarning() + public async Task HandleAsync_WithDisabledSlowThreshold_DoesNotLogWarning(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger, new LoggingInterceptorOptions { SlowRequestThreshold = null }); var testEvent = new TestEvent(); await interceptor - .HandleAsync(testEvent, async (_, ct) => await Task.Delay(50, ct).ConfigureAwait(false)) + .HandleAsync(testEvent, async (_, ct) => await Task.Delay(50, ct).ConfigureAwait(false), cancellationToken) .ConfigureAwait(false); var warnings = logger.Entries.Where(e => e.LogLevel == LogLevel.Warning).ToList(); @@ -115,7 +116,7 @@ await interceptor } [Test] - public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows() + public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); @@ -123,7 +124,9 @@ public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows() var expectedException = new InvalidOperationException("event error"); var exception = await Assert.ThrowsAsync(async () => - await interceptor.HandleAsync(testEvent, (_, _) => throw expectedException).ConfigureAwait(false) + await interceptor + .HandleAsync(testEvent, (_, _) => throw expectedException, cancellationToken) + .ConfigureAwait(false) ); _ = await Assert.That(exception).IsSameReferenceAs(expectedException); @@ -137,19 +140,19 @@ public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows() } [Test] - public async Task HandleAsync_LogsCorrelationId() + public async Task HandleAsync_LogsCorrelationId(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); var testEvent = new TestEvent { CorrelationId = "event-correlation-id" }; - await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask).ConfigureAwait(false); + await interceptor.HandleAsync(testEvent, (_, _) => Task.CompletedTask, cancellationToken).ConfigureAwait(false); _ = await Assert.That(logger.Entries[0].Message).Contains("event-correlation-id"); } [Test] - public async Task HandleAsync_InvokesHandlerWithCorrectEvent() + public async Task HandleAsync_InvokesHandlerWithCorrectEvent(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); @@ -163,7 +166,8 @@ await interceptor { received = evt; return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs index 10e88d59..e716d0b4 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs @@ -11,7 +11,7 @@ public class LoggingInterceptorOptionsValidatorTests private static readonly LoggingInterceptorOptionsValidator _validator = new(); [Test] - public async Task Validate_WithNullThreshold_Succeeds() + public async Task Validate_WithNullThreshold_Succeeds(CancellationToken cancellationToken) { var options = new LoggingInterceptorOptions { SlowRequestThreshold = null }; @@ -21,7 +21,7 @@ public async Task Validate_WithNullThreshold_Succeeds() } [Test] - public async Task Validate_WithZeroThreshold_Succeeds() + public async Task Validate_WithZeroThreshold_Succeeds(CancellationToken cancellationToken) { var options = new LoggingInterceptorOptions { SlowRequestThreshold = TimeSpan.Zero }; @@ -31,7 +31,7 @@ public async Task Validate_WithZeroThreshold_Succeeds() } [Test] - public async Task Validate_WithPositiveThreshold_Succeeds() + public async Task Validate_WithPositiveThreshold_Succeeds(CancellationToken cancellationToken) { var options = new LoggingInterceptorOptions { SlowRequestThreshold = TimeSpan.FromMilliseconds(500) }; @@ -41,7 +41,7 @@ public async Task Validate_WithPositiveThreshold_Succeeds() } [Test] - public async Task Validate_WithNegativeThreshold_Fails() + public async Task Validate_WithNegativeThreshold_Fails(CancellationToken cancellationToken) { var options = new LoggingInterceptorOptions { SlowRequestThreshold = TimeSpan.FromMilliseconds(-1) }; @@ -51,7 +51,7 @@ public async Task Validate_WithNegativeThreshold_Fails() } [Test] - public async Task Validate_DefaultOptions_Succeeds() + public async Task Validate_DefaultOptions_Succeeds(CancellationToken cancellationToken) { var options = new LoggingInterceptorOptions(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingRequestInterceptorTests.cs index 66f6d23a..ef45cc4c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -24,13 +24,15 @@ private static LoggingRequestInterceptor CreateInterceptor< } [Test] - public async Task HandleAsync_WithCommand_LogsBeginAndEndAtDebugLevel() + public async Task HandleAsync_WithCommand_LogsBeginAndEndAtDebugLevel(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger, new LoggingInterceptorOptions { LogLevel = LogLevel.Debug }); var command = new TestCommand { CorrelationId = "corr-123" }; - var result = await interceptor.HandleAsync(command, (_, _) => Task.FromResult("ok")).ConfigureAwait(false); + var result = await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("ok"), cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("ok"); using (Assert.Multiple()) @@ -42,13 +44,15 @@ public async Task HandleAsync_WithCommand_LogsBeginAndEndAtDebugLevel() } [Test] - public async Task HandleAsync_WithCommand_LogsBeginAndEndAtInformationLevel() + public async Task HandleAsync_WithCommand_LogsBeginAndEndAtInformationLevel(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger, new LoggingInterceptorOptions { LogLevel = LogLevel.Information }); var command = new TestCommand(); - _ = await interceptor.HandleAsync(command, (_, _) => Task.FromResult("ok")).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("ok"), cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -59,13 +63,15 @@ public async Task HandleAsync_WithCommand_LogsBeginAndEndAtInformationLevel() } [Test] - public async Task HandleAsync_WithQuery_LogsQueryInMessage() + public async Task HandleAsync_WithQuery_LogsQueryInMessage(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); var query = new TestQuery(); - _ = await interceptor.HandleAsync(query, (_, _) => Task.FromResult(42)).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(query, (_, _) => Task.FromResult(42), cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -75,19 +81,21 @@ public async Task HandleAsync_WithQuery_LogsQueryInMessage() } [Test] - public async Task HandleAsync_WithGenericRequest_LogsRequestInMessage() + public async Task HandleAsync_WithGenericRequest_LogsRequestInMessage(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); var request = new TestRequest(); - _ = await interceptor.HandleAsync(request, (_, _) => Task.FromResult(true)).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(request, (_, _) => Task.FromResult(true), cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(logger.Entries[0].Message).Contains("Request"); } [Test] - public async Task HandleAsync_WithSlowRequest_LogsWarning() + public async Task HandleAsync_WithSlowRequest_LogsWarning(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor( @@ -103,7 +111,8 @@ public async Task HandleAsync_WithSlowRequest_LogsWarning() { await Task.Delay(50, ct).ConfigureAwait(false); return "ok"; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -113,7 +122,9 @@ public async Task HandleAsync_WithSlowRequest_LogsWarning() } [Test] - public async Task HandleAsync_WithDisabledSlowRequestThreshold_DoesNotLogWarning() + public async Task HandleAsync_WithDisabledSlowRequestThreshold_DoesNotLogWarning( + CancellationToken cancellationToken + ) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger, new LoggingInterceptorOptions { SlowRequestThreshold = null }); @@ -126,7 +137,8 @@ public async Task HandleAsync_WithDisabledSlowRequestThreshold_DoesNotLogWarning { await Task.Delay(50, ct).ConfigureAwait(false); return "ok"; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -135,7 +147,7 @@ public async Task HandleAsync_WithDisabledSlowRequestThreshold_DoesNotLogWarning } [Test] - public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows() + public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); @@ -143,7 +155,9 @@ public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows() var expectedException = new InvalidOperationException("test error"); var exception = await Assert.ThrowsAsync(async () => - await interceptor.HandleAsync(command, (_, _) => throw expectedException).ConfigureAwait(false) + await interceptor + .HandleAsync(command, (_, _) => throw expectedException, cancellationToken) + .ConfigureAwait(false) ); _ = await Assert.That(exception).IsSameReferenceAs(expectedException); @@ -157,19 +171,21 @@ public async Task HandleAsync_WhenHandlerThrows_LogsErrorAndRethrows() } [Test] - public async Task HandleAsync_LogsCorrelationId() + public async Task HandleAsync_LogsCorrelationId(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); var command = new TestCommand { CorrelationId = "my-correlation-id" }; - _ = await interceptor.HandleAsync(command, (_, _) => Task.FromResult("ok")).ConfigureAwait(false); + _ = await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("ok"), cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(logger.Entries[0].Message).Contains("my-correlation-id"); } [Test] - public async Task HandleAsync_InvokesHandlerWithCorrectRequest() + public async Task HandleAsync_InvokesHandlerWithCorrectRequest(CancellationToken cancellationToken) { var logger = Mock.Logger>(); var interceptor = CreateInterceptor(logger); @@ -183,7 +199,8 @@ public async Task HandleAsync_InvokesHandlerWithCorrectRequest() { received = cmd; return Task.FromResult("ok"); - } + }, + cancellationToken ) .ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/TimeoutRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/TimeoutRequestInterceptorTests.cs index 88bfa937..c4d928c1 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/TimeoutRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/TimeoutRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System; using System.Threading; @@ -13,31 +13,37 @@ public sealed class TimeoutRequestInterceptorTests { [Test] - public async Task HandleAsync_WithNullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_WithNullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { var options = Options.Create(new TimeoutRequestInterceptorOptions()); var interceptor = new TimeoutRequestInterceptor(options); var command = new TestTimeoutCommand(TimeSpan.FromSeconds(5)); _ = await Assert.ThrowsAsync(async () => - await interceptor.HandleAsync(command, null!).ConfigureAwait(false) + await interceptor.HandleAsync(command, null!, cancellationToken).ConfigureAwait(false) ); } [Test] - public async Task HandleAsync_WithTimeoutRequest_WhenCompletesWithinDeadline_ReturnsResult() + public async Task HandleAsync_WithTimeoutRequest_WhenCompletesWithinDeadline_ReturnsResult( + CancellationToken cancellationToken + ) { var options = Options.Create(new TimeoutRequestInterceptorOptions()); var interceptor = new TimeoutRequestInterceptor(options); var command = new TestTimeoutCommand(TimeSpan.FromSeconds(5)); - var result = await interceptor.HandleAsync(command, (_, _) => Task.FromResult("success")).ConfigureAwait(false); + var result = await interceptor + .HandleAsync(command, (_, _) => Task.FromResult("success"), cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("success"); } [Test] - public async Task HandleAsync_WithTimeoutRequest_WhenExceedsDeadline_ThrowsTimeoutException() + public async Task HandleAsync_WithTimeoutRequest_WhenExceedsDeadline_ThrowsTimeoutException( + CancellationToken cancellationToken + ) { var options = Options.Create(new TimeoutRequestInterceptorOptions()); var interceptor = new TimeoutRequestInterceptor(options); @@ -51,7 +57,8 @@ await interceptor { await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); return "never"; - } + }, + cancellationToken ) .ConfigureAwait(false) ); @@ -62,12 +69,14 @@ await interceptor } [Test] - public async Task HandleAsync_WithOriginalTokenCancelled_ThrowsOperationCanceledException_NotTimeoutException() + public async Task HandleAsync_WithOriginalTokenCancelled_ThrowsOperationCanceledException_NotTimeoutException( + CancellationToken cancellationToken + ) { var options = Options.Create(new TimeoutRequestInterceptorOptions()); var interceptor = new TimeoutRequestInterceptor(options); var command = new TestTimeoutCommand(TimeSpan.FromSeconds(5)); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromMilliseconds(50)); var exception = await Assert.ThrowsAsync(async () => @@ -89,7 +98,9 @@ await interceptor } [Test] - public async Task HandleAsync_WithNonTimeoutRequest_AlwaysPassesThrough_RegardlessOfGlobalTimeout() + public async Task HandleAsync_WithNonTimeoutRequest_AlwaysPassesThrough_RegardlessOfGlobalTimeout( + CancellationToken cancellationToken + ) { var options = Options.Create( new TimeoutRequestInterceptorOptions { GlobalTimeout = TimeSpan.FromMilliseconds(1) } @@ -99,42 +110,48 @@ public async Task HandleAsync_WithNonTimeoutRequest_AlwaysPassesThrough_Regardle // Even though GlobalTimeout is 1ms, the non-ITimeoutRequest should pass through immediately. var result = await interceptor - .HandleAsync(command, (_, _) => Task.FromResult("passed-through")) + .HandleAsync(command, (_, _) => Task.FromResult("passed-through"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("passed-through"); } [Test] - public async Task HandleAsync_WithTimeoutRequest_NullTimeout_AndNoGlobalTimeout_PassesThrough() + public async Task HandleAsync_WithTimeoutRequest_NullTimeout_AndNoGlobalTimeout_PassesThrough( + CancellationToken cancellationToken + ) { var options = Options.Create(new TimeoutRequestInterceptorOptions()); var interceptor = new TimeoutRequestInterceptor(options); var command = new TestTimeoutCommand(null); var result = await interceptor - .HandleAsync(command, (_, _) => Task.FromResult("passed-through")) + .HandleAsync(command, (_, _) => Task.FromResult("passed-through"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("passed-through"); } [Test] - public async Task HandleAsync_WithTimeoutRequest_NullTimeout_AndGlobalTimeout_WhenCompletesWithinDeadline_ReturnsResult() + public async Task HandleAsync_WithTimeoutRequest_NullTimeout_AndGlobalTimeout_WhenCompletesWithinDeadline_ReturnsResult( + CancellationToken cancellationToken + ) { var options = Options.Create(new TimeoutRequestInterceptorOptions { GlobalTimeout = TimeSpan.FromSeconds(5) }); var interceptor = new TimeoutRequestInterceptor(options); var command = new TestTimeoutCommand(null); var result = await interceptor - .HandleAsync(command, (_, _) => Task.FromResult("global-fallback-success")) + .HandleAsync(command, (_, _) => Task.FromResult("global-fallback-success"), cancellationToken) .ConfigureAwait(false); _ = await Assert.That(result).IsEqualTo("global-fallback-success"); } [Test] - public async Task HandleAsync_WithTimeoutRequest_NullTimeout_AndGlobalTimeout_WhenExceedsDeadline_ThrowsTimeoutException() + public async Task HandleAsync_WithTimeoutRequest_NullTimeout_AndGlobalTimeout_WhenExceedsDeadline_ThrowsTimeoutException( + CancellationToken cancellationToken + ) { var options = Options.Create( new TimeoutRequestInterceptorOptions { GlobalTimeout = TimeSpan.FromMilliseconds(50) } @@ -150,7 +167,8 @@ await interceptor { await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); return "never"; - } + }, + cancellationToken ) .ConfigureAwait(false) ); @@ -160,7 +178,9 @@ await interceptor } [Test] - public async Task HandleAsync_WithTimeoutRequest_ExplicitTimeoutOverridesGlobalTimeout() + public async Task HandleAsync_WithTimeoutRequest_ExplicitTimeoutOverridesGlobalTimeout( + CancellationToken cancellationToken + ) { // Per-request timeout (50ms) should take precedence over global (5s), // so the request should time out. @@ -176,7 +196,8 @@ await interceptor { await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false); return "never"; - } + }, + cancellationToken ) .ConfigureAwait(false) ); @@ -185,7 +206,7 @@ await interceptor } [Test] - public async Task HandleAsync_DisposesLinkedCts_EvenWhenHandlerThrows() + public async Task HandleAsync_DisposesLinkedCts_EvenWhenHandlerThrows(CancellationToken cancellationToken) { var options = Options.Create(new TimeoutRequestInterceptorOptions()); var interceptor = new TimeoutRequestInterceptor(options); @@ -193,7 +214,7 @@ public async Task HandleAsync_DisposesLinkedCts_EvenWhenHandlerThrows() _ = await Assert.ThrowsAsync(async () => await interceptor - .HandleAsync(command, (_, _) => throw new InvalidOperationException("handler error")) + .HandleAsync(command, (_, _) => throw new InvalidOperationException("handler error"), cancellationToken) .ConfigureAwait(false) ); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs index b1fddbfe..dabb78f5 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs @@ -10,7 +10,7 @@ public class MediatorBuilderTests { [Test] - public async Task Constructor_WithNullServices_ThrowsArgumentNullException() + public async Task Constructor_WithNullServices_ThrowsArgumentNullException(CancellationToken cancellationToken) { IServiceCollection? services = null; @@ -18,7 +18,7 @@ public async Task Constructor_WithNullServices_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidServices_CreatesInstance() + public async Task Constructor_WithValidServices_CreatesInstance(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -32,7 +32,7 @@ public async Task Constructor_WithValidServices_CreatesInstance() } [Test] - public async Task Services_ReturnsProvidedServiceCollection() + public async Task Services_ReturnsProvidedServiceCollection(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs index 48c058c5..8b392889 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs @@ -13,7 +13,7 @@ public class PulseMediatorTests { [Test] - public async Task Constructor_WithNullLogger_ThrowsArgumentNullException() + public async Task Constructor_WithNullLogger_ThrowsArgumentNullException(CancellationToken cancellationToken) { ILogger? logger = null; var serviceProvider = new ServiceCollection().BuildServiceProvider(); @@ -26,7 +26,9 @@ public async Task Constructor_WithNullLogger_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithNullServiceProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) { var logger = new ServiceCollection() .AddLogging() @@ -42,7 +44,7 @@ public async Task Constructor_WithNullServiceProvider_ThrowsArgumentNullExceptio } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { var logger = new ServiceCollection() .AddLogging() @@ -58,7 +60,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidParameters_CreatesInstance() + public async Task Constructor_WithValidParameters_CreatesInstance(CancellationToken cancellationToken) { var logger = new ServiceCollection() .AddLogging() @@ -77,7 +79,7 @@ public async Task Constructor_WithValidParameters_CreatesInstance() } [Test] - public async Task PublishAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task PublishAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -88,12 +90,12 @@ public async Task PublishAsync_WithNullMessage_ThrowsArgumentNullException() _ = await Assert.ThrowsAsync( "message", - async () => await mediator.PublishAsync(null!).ConfigureAwait(false) + async () => await mediator.PublishAsync(null!, cancellationToken).ConfigureAwait(false) ); } [Test] - public async Task PublishAsync_WithNoHandlers_CompletesSuccessfully() + public async Task PublishAsync_WithNoHandlers_CompletesSuccessfully(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -103,13 +105,13 @@ public async Task PublishAsync_WithNoHandlers_CompletesSuccessfully() var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var testEvent = new TestEvent(); - await mediator.PublishAsync(testEvent).ConfigureAwait(false); + await mediator.PublishAsync(testEvent, cancellationToken).ConfigureAwait(false); _ = await Assert.That(testEvent.PublishedAt).IsNotNull(); } [Test] - public async Task PublishAsync_WithHandlers_InvokesAllHandlers() + public async Task PublishAsync_WithHandlers_InvokesAllHandlers(CancellationToken cancellationToken) { var handler1 = new TestEventHandler(); var handler2 = new TestEventHandler(); @@ -123,7 +125,7 @@ public async Task PublishAsync_WithHandlers_InvokesAllHandlers() var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var testEvent = new TestEvent(); - await mediator.PublishAsync(testEvent).ConfigureAwait(false); + await mediator.PublishAsync(testEvent, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -135,7 +137,9 @@ public async Task PublishAsync_WithHandlers_InvokesAllHandlers() } [Test] - public async Task PublishAsync_WithHandlerException_ContinuesExecutingOtherHandlersAndThrowsAggregate() + public async Task PublishAsync_WithHandlerException_ContinuesExecutingOtherHandlersAndThrowsAggregate( + CancellationToken cancellationToken + ) { var handler1 = new ThrowingEventHandler(); var handler2 = new TestEventHandler(); @@ -151,7 +155,7 @@ public async Task PublishAsync_WithHandlerException_ContinuesExecutingOtherHandl // Act & Assert - PublishAsync throws AggregateException containing the handler failure var exception = await Assert.ThrowsAsync(async () => - await mediator.PublishAsync(testEvent).ConfigureAwait(false) + await mediator.PublishAsync(testEvent, cancellationToken).ConfigureAwait(false) ); // Verify the exception contains the handler failure @@ -163,7 +167,7 @@ await mediator.PublishAsync(testEvent).ConfigureAwait(false) } [Test] - public async Task PublishAsync_SetsPublishedAtTimestamp() + public async Task PublishAsync_SetsPublishedAtTimestamp(CancellationToken cancellationToken) { var services = new ServiceCollection().AddLogging(); var serviceProvider = services.BuildServiceProvider(); @@ -173,7 +177,7 @@ public async Task PublishAsync_SetsPublishedAtTimestamp() var testEvent = new TestEvent(); var beforePublish = timeProvider.GetUtcNow(); - await mediator.PublishAsync(testEvent).ConfigureAwait(false); + await mediator.PublishAsync(testEvent, cancellationToken).ConfigureAwait(false); var afterPublish = timeProvider.GetUtcNow(); var publishedAt = testEvent.PublishedAt; @@ -186,7 +190,7 @@ public async Task PublishAsync_SetsPublishedAtTimestamp() } [Test] - public async Task QueryAsync_WithNullQuery_ThrowsArgumentNullException() + public async Task QueryAsync_WithNullQuery_ThrowsArgumentNullException(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -197,12 +201,12 @@ public async Task QueryAsync_WithNullQuery_ThrowsArgumentNullException() _ = await Assert.ThrowsAsync( "query", - async () => await mediator.QueryAsync(null!).ConfigureAwait(false) + async () => await mediator.QueryAsync(null!, cancellationToken).ConfigureAwait(false) ); } [Test] - public async Task QueryAsync_WithNoHandler_ThrowsInvalidOperationException() + public async Task QueryAsync_WithNoHandler_ThrowsInvalidOperationException(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -213,12 +217,12 @@ public async Task QueryAsync_WithNoHandler_ThrowsInvalidOperationException() var query = new TestQuery(); _ = await Assert.ThrowsAsync(async () => - await mediator.QueryAsync(query).ConfigureAwait(false) + await mediator.QueryAsync(query, cancellationToken).ConfigureAwait(false) ); } [Test] - public async Task QueryAsync_WithHandler_InvokesHandlerAndReturnsResult() + public async Task QueryAsync_WithHandler_InvokesHandlerAndReturnsResult(CancellationToken cancellationToken) { var handler = new TestQueryHandler("test-result"); var services = new ServiceCollection(); @@ -230,7 +234,7 @@ public async Task QueryAsync_WithHandler_InvokesHandlerAndReturnsResult() var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var query = new TestQuery(); - var result = await mediator.QueryAsync(query).ConfigureAwait(false); + var result = await mediator.QueryAsync(query, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -241,7 +245,7 @@ public async Task QueryAsync_WithHandler_InvokesHandlerAndReturnsResult() } [Test] - public async Task SendAsync_WithNullCommand_ThrowsArgumentNullException() + public async Task SendAsync_WithNullCommand_ThrowsArgumentNullException(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -252,12 +256,12 @@ public async Task SendAsync_WithNullCommand_ThrowsArgumentNullException() _ = await Assert.ThrowsAsync( "command", - async () => await mediator.SendAsync(null!).ConfigureAwait(false) + async () => await mediator.SendAsync(null!, cancellationToken).ConfigureAwait(false) ); } [Test] - public async Task SendAsync_WithNoHandler_ThrowsInvalidOperationException() + public async Task SendAsync_WithNoHandler_ThrowsInvalidOperationException(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -268,12 +272,12 @@ public async Task SendAsync_WithNoHandler_ThrowsInvalidOperationException() var command = new TestCommand(); _ = await Assert.ThrowsAsync(async () => - await mediator.SendAsync(command).ConfigureAwait(false) + await mediator.SendAsync(command, cancellationToken).ConfigureAwait(false) ); } [Test] - public async Task SendAsync_WithHandler_InvokesHandlerAndReturnsResult() + public async Task SendAsync_WithHandler_InvokesHandlerAndReturnsResult(CancellationToken cancellationToken) { var handler = new TestCommandHandler("test-result"); var services = new ServiceCollection(); @@ -285,7 +289,7 @@ public async Task SendAsync_WithHandler_InvokesHandlerAndReturnsResult() var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var command = new TestCommand(); - var result = await mediator.SendAsync(command).ConfigureAwait(false); + var result = await mediator.SendAsync(command, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -296,7 +300,7 @@ public async Task SendAsync_WithHandler_InvokesHandlerAndReturnsResult() } [Test] - public async Task SendAsync_WithInterceptor_InvokesInterceptorBeforeHandler() + public async Task SendAsync_WithInterceptor_InvokesInterceptorBeforeHandler(CancellationToken cancellationToken) { var handler = new TestCommandHandler("test-result"); var interceptor = new TestCommandInterceptor(); @@ -310,7 +314,7 @@ public async Task SendAsync_WithInterceptor_InvokesInterceptorBeforeHandler() var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var command = new TestCommand(); - var result = await mediator.SendAsync(command).ConfigureAwait(false); + var result = await mediator.SendAsync(command, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -321,7 +325,7 @@ public async Task SendAsync_WithInterceptor_InvokesInterceptorBeforeHandler() } [Test] - public async Task QueryAsync_WithInterceptor_InvokesInterceptorBeforeHandler() + public async Task QueryAsync_WithInterceptor_InvokesInterceptorBeforeHandler(CancellationToken cancellationToken) { var handler = new TestQueryHandler("test-result"); var interceptor = new TestQueryInterceptor(); @@ -335,7 +339,7 @@ public async Task QueryAsync_WithInterceptor_InvokesInterceptorBeforeHandler() var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var query = new TestQuery(); - var result = await mediator.QueryAsync(query).ConfigureAwait(false); + var result = await mediator.QueryAsync(query, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -346,7 +350,7 @@ public async Task QueryAsync_WithInterceptor_InvokesInterceptorBeforeHandler() } [Test] - public async Task PublishAsync_WithInterceptor_InvokesInterceptorBeforeHandlers() + public async Task PublishAsync_WithInterceptor_InvokesInterceptorBeforeHandlers(CancellationToken cancellationToken) { var handler = new TestEventHandler(); var interceptor = new TestEventInterceptor(); @@ -360,7 +364,7 @@ public async Task PublishAsync_WithInterceptor_InvokesInterceptorBeforeHandlers( var mediator = new PulseMediator(logger, serviceProvider, timeProvider); var testEvent = new TestEvent(); - await mediator.PublishAsync(testEvent).ConfigureAwait(false); + await mediator.PublishAsync(testEvent, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -524,7 +528,7 @@ public async IAsyncEnumerable HandleAsync( // StreamQueryAsync tests [Test] - public async Task StreamQueryAsync_WithNullQuery_ThrowsArgumentNullException() + public async Task StreamQueryAsync_WithNullQuery_ThrowsArgumentNullException(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -535,12 +539,14 @@ public async Task StreamQueryAsync_WithNullQuery_ThrowsArgumentNullException() _ = Assert.Throws( "query", - () => mediator.StreamQueryAsync(null!) + () => mediator.StreamQueryAsync(null!, cancellationToken) ); } [Test] - public async Task StreamQueryAsync_WithNoHandler_ThrowsInvalidOperationException() + public async Task StreamQueryAsync_WithNoHandler_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddLogging(); @@ -552,7 +558,7 @@ public async Task StreamQueryAsync_WithNoHandler_ThrowsInvalidOperationException _ = await Assert.ThrowsAsync(async () => { - await foreach (var _ in mediator.StreamQueryAsync(query)) + await foreach (var _ in mediator.StreamQueryAsync(query, cancellationToken)) { // consume } @@ -560,7 +566,7 @@ public async Task StreamQueryAsync_WithNoHandler_ThrowsInvalidOperationException } [Test] - public async Task StreamQueryAsync_WithHandler_YieldsAllItems() + public async Task StreamQueryAsync_WithHandler_YieldsAllItems(CancellationToken cancellationToken) { var expectedItems = new[] { "first", "second", "third" }; var handler = new TestStreamQueryHandler(expectedItems); @@ -574,7 +580,7 @@ public async Task StreamQueryAsync_WithHandler_YieldsAllItems() var query = new TestStreamQuery(); var results = new List(); - await foreach (var item in mediator.StreamQueryAsync(query)) + await foreach (var item in mediator.StreamQueryAsync(query, cancellationToken)) { results.Add(item); } @@ -587,7 +593,9 @@ public async Task StreamQueryAsync_WithHandler_YieldsAllItems() } [Test] - public async Task StreamQueryAsync_WithInterceptor_InvokesInterceptorAndYieldsAllItems() + public async Task StreamQueryAsync_WithInterceptor_InvokesInterceptorAndYieldsAllItems( + CancellationToken cancellationToken + ) { var expectedItems = new[] { "alpha", "beta" }; var handler = new TestStreamQueryHandler(expectedItems); @@ -603,7 +611,7 @@ public async Task StreamQueryAsync_WithInterceptor_InvokesInterceptorAndYieldsAl var query = new TestStreamQuery(); var results = new List(); - await foreach (var item in mediator.StreamQueryAsync(query)) + await foreach (var item in mediator.StreamQueryAsync(query, cancellationToken)) { results.Add(item); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs index c4143ed6..6d0bb78d 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs @@ -13,7 +13,7 @@ public sealed class KafkaExtensionsTests { [Test] - public async Task UseKafkaTransport_Registers_transport() + public async Task UseKafkaTransport_Registers_transport(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseKafkaTransport()); @@ -23,7 +23,7 @@ public async Task UseKafkaTransport_Registers_transport() } [Test] - public async Task UseKafkaTransport_Replaces_existing_transport() + public async Task UseKafkaTransport_Replaces_existing_transport(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -34,7 +34,7 @@ public async Task UseKafkaTransport_Replaces_existing_transport() } [Test] - public async Task UseKafkaTransport_Returns_same_configurator_for_chaining() + public async Task UseKafkaTransport_Returns_same_configurator_for_chaining(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); IMediatorBuilder? captured = null; @@ -49,7 +49,7 @@ public async Task UseKafkaTransport_Returns_same_configurator_for_chaining() } [Test] - public async Task UseKafkaTransport_Does_not_register_adapters() + public async Task UseKafkaTransport_Does_not_register_adapters(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseKafkaTransport()); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaMessageTransportTests.cs index 8da5b922..71eae4a8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaMessageTransportTests.cs @@ -19,14 +19,14 @@ public sealed class KafkaMessageTransportTests { [Test] - public async Task SendAsync_Maps_outbox_message_to_kafka_message() + public async Task SendAsync_Maps_outbox_message_to_kafka_message(CancellationToken cancellationToken) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { BrokerCount = 1 }; var transport = CreateTransport(producer, admin); var outboxMessage = CreateOutboxMessage(); - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); var kafkaMessage = producer.ProducedMessages.Single(); using (Assert.Multiple()) @@ -42,7 +42,7 @@ public async Task SendAsync_Maps_outbox_message_to_kafka_message() } [Test] - public async Task SendAsync_Propagates_ProduceException_on_delivery_failure() + public async Task SendAsync_Propagates_ProduceException_on_delivery_failure(CancellationToken cancellationToken) { var expectedError = new Error(ErrorCode.BrokerNotAvailable, "broker unavailable"); using var producer = new FakeProducer { ProduceAsyncError = expectedError }; @@ -51,7 +51,7 @@ public async Task SendAsync_Propagates_ProduceException_on_delivery_failure() var outboxMessage = CreateOutboxMessage(); var exception = await Assert.ThrowsAsync>(() => - transport.SendAsync(outboxMessage) + transport.SendAsync(outboxMessage, cancellationToken) ); _ = await Assert.That(exception).IsNotNull(); @@ -59,47 +59,47 @@ public async Task SendAsync_Propagates_ProduceException_on_delivery_failure() } [Test] - public async Task SendAsync_Uses_topic_name_resolver_to_determine_topic() + public async Task SendAsync_Uses_topic_name_resolver_to_determine_topic(CancellationToken cancellationToken) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { BrokerCount = 1 }; var transport = CreateTransport(producer, admin, topicName: "resolved-topic"); var outboxMessage = CreateOutboxMessage(); - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); _ = await Assert.That(producer.ProducedTopics.Single()).IsEqualTo("resolved-topic"); } [Test] - public async Task SendAsync_Routes_to_topic_from_resolver() + public async Task SendAsync_Routes_to_topic_from_resolver(CancellationToken cancellationToken) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { BrokerCount = 1 }; var transport = CreateTransport(producer, admin, topicName: "test-topic"); var outboxMessage = CreateOutboxMessage(); - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); _ = await Assert.That(producer.ProducedTopics.Single()).IsEqualTo("test-topic"); } [Test] - public async Task SendBatchAsync_Enqueues_all_messages_and_flushes() + public async Task SendBatchAsync_Enqueues_all_messages_and_flushes(CancellationToken cancellationToken) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { BrokerCount = 1 }; var transport = CreateTransport(producer, admin); var messages = Enumerable.Range(0, 3).Select(_ => CreateOutboxMessage()).ToArray(); - await transport.SendBatchAsync(messages); + await transport.SendBatchAsync(messages, cancellationToken); _ = await Assert.That(producer.EnqueuedMessages.Count).IsEqualTo(messages.Length); _ = await Assert.That(producer.FlushCallCount).IsEqualTo(1); } [Test] - public async Task SendBatchAsync_Collects_delivery_errors_as_AggregateException() + public async Task SendBatchAsync_Collects_delivery_errors_as_AggregateException(CancellationToken cancellationToken) { var error = new Error(ErrorCode.BrokerNotAvailable, "broker down"); using var producer = new FakeProducer { DeliveryError = error }; @@ -107,45 +107,49 @@ public async Task SendBatchAsync_Collects_delivery_errors_as_AggregateException( var transport = CreateTransport(producer, admin); var messages = new[] { CreateOutboxMessage(), CreateOutboxMessage() }; - var exception = await Assert.ThrowsAsync(() => transport.SendBatchAsync(messages)); + var exception = await Assert.ThrowsAsync(() => + transport.SendBatchAsync(messages, cancellationToken) + ); _ = await Assert.That(exception).IsNotNull(); _ = await Assert.That(exception!.InnerExceptions.Count).IsEqualTo(messages.Length); } [Test] - public async Task IsHealthyAsync_Returns_true_when_broker_metadata_is_available() + public async Task IsHealthyAsync_Returns_true_when_broker_metadata_is_available(CancellationToken cancellationToken) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { BrokerCount = 1 }; var transport = CreateTransport(producer, admin); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsTrue(); _ = await Assert.That(admin.GetMetadataCallCount).IsEqualTo(1); } [Test] - public async Task IsHealthyAsync_Returns_false_when_no_brokers_in_metadata() + public async Task IsHealthyAsync_Returns_false_when_no_brokers_in_metadata(CancellationToken cancellationToken) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { BrokerCount = 0 }; var transport = CreateTransport(producer, admin); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } [Test] - public async Task IsHealthyAsync_Returns_false_without_throwing_when_broker_is_unreachable() + public async Task IsHealthyAsync_Returns_false_without_throwing_when_broker_is_unreachable( + CancellationToken cancellationToken + ) { using var producer = new FakeProducer(); using var admin = new FakeAdminClient { ThrowOnGetMetadata = true }; var transport = CreateTransport(producer, admin); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs index 5d09e6b4..c088fd11 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs @@ -13,7 +13,7 @@ public class LoggingExtensionsTests { [Test] - public async Task AddLogging_WithNullConfigurator_ThrowsArgumentNullException() + public async Task AddLogging_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) { IMediatorBuilder? configurator = null; @@ -21,7 +21,7 @@ public async Task AddLogging_WithNullConfigurator_ThrowsArgumentNullException() } [Test] - public async Task AddLogging_RegistersEventInterceptor() + public async Task AddLogging_RegistersEventInterceptor(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -42,7 +42,7 @@ public async Task AddLogging_RegistersEventInterceptor() } [Test] - public async Task AddLogging_RegistersRequestInterceptor() + public async Task AddLogging_RegistersRequestInterceptor(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -64,7 +64,7 @@ public async Task AddLogging_RegistersRequestInterceptor() } [Test] - public async Task AddLogging_CalledMultipleTimes_DoesNotDuplicateInterceptors() + public async Task AddLogging_CalledMultipleTimes_DoesNotDuplicateInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -94,7 +94,7 @@ public async Task AddLogging_CalledMultipleTimes_DoesNotDuplicateInterceptors() } [Test] - public async Task AddLogging_WithConfigure_AppliesOptions() + public async Task AddLogging_WithConfigure_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -116,7 +116,7 @@ public async Task AddLogging_WithConfigure_AppliesOptions() } [Test] - public async Task AddLogging_WithoutConfigure_UsesDefaultOptions() + public async Task AddLogging_WithoutConfigure_UsesDefaultOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -134,7 +134,7 @@ public async Task AddLogging_WithoutConfigure_UsesDefaultOptions() } [Test] - public async Task AddLogging_ReturnsConfiguratorForChaining() + public async Task AddLogging_ReturnsConfiguratorForChaining(CancellationToken cancellationToken) { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs index 608fc66e..199de0b7 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Outbox; +namespace NetEvolve.Pulse.Tests.Unit.Outbox; using NetEvolve.Extensions.TUnit; using NetEvolve.Pulse.Outbox; @@ -15,7 +15,11 @@ public sealed class ExponentialBackoffTests [Arguments(0, 5)] [Arguments(1, 10)] [Arguments(2, 20)] - public async Task ComputeNextRetryAt_WithNoJitter_ComputesCorrectBackoff(int retryCount, int expectedSeconds) + public async Task ComputeNextRetryAt_WithNoJitter_ComputesCorrectBackoff( + int retryCount, + int expectedSeconds, + CancellationToken cancellationToken + ) { var options = new OutboxProcessorOptions { @@ -37,7 +41,7 @@ public async Task ComputeNextRetryAt_WithNoJitter_ComputesCorrectBackoff(int ret } [Test] - public async Task ComputeNextRetryAt_WithMaxRetryDelayExceeded_ClampsToMax() + public async Task ComputeNextRetryAt_WithMaxRetryDelayExceeded_ClampsToMax(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions { @@ -59,7 +63,7 @@ public async Task ComputeNextRetryAt_WithMaxRetryDelayExceeded_ClampsToMax() } [Test] - public async Task ComputeNextRetryAt_WithJitter_AddsRandomizedDelay() + public async Task ComputeNextRetryAt_WithJitter_AddsRandomizedDelay(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions { @@ -82,7 +86,7 @@ public async Task ComputeNextRetryAt_WithJitter_AddsRandomizedDelay() } [Test] - public async Task ComputeNextRetryAt_WithMultipleSamples_JitterIsRandomized() + public async Task ComputeNextRetryAt_WithMultipleSamples_JitterIsRandomized(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions { @@ -107,7 +111,7 @@ public async Task ComputeNextRetryAt_WithMultipleSamples_JitterIsRandomized() } [Test] - public async Task ComputeNextRetryAt_WithZeroRetryCount_UsesBaseDelay() + public async Task ComputeNextRetryAt_WithZeroRetryCount_UsesBaseDelay(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions { @@ -128,7 +132,7 @@ public async Task ComputeNextRetryAt_WithZeroRetryCount_UsesBaseDelay() } [Test] - public async Task OutboxProcessorOptions_HasCorrectDefaults() + public async Task OutboxProcessorOptions_HasCorrectDefaults(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions(); @@ -140,7 +144,7 @@ public async Task OutboxProcessorOptions_HasCorrectDefaults() } [Test] - public async Task OutboxProcessorOptions_CanConfigureCustomValues() + public async Task OutboxProcessorOptions_CanConfigureCustomValues(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions { @@ -159,7 +163,7 @@ public async Task OutboxProcessorOptions_CanConfigureCustomValues() } [Test] - public async Task ComputeNextRetryAt_WithLargeRetryCount_DoesNotOverflow() + public async Task ComputeNextRetryAt_WithLargeRetryCount_DoesNotOverflow(CancellationToken cancellationToken) { var options = new OutboxProcessorOptions { @@ -182,7 +186,9 @@ public async Task ComputeNextRetryAt_WithLargeRetryCount_DoesNotOverflow() } [Test] - public async Task ComputeNextRetryAt_WithBaseDelayGreaterThanMax_ClampsCorrectly() + public async Task ComputeNextRetryAt_WithBaseDelayGreaterThanMax_ClampsCorrectly( + CancellationToken cancellationToken + ) { var options = new OutboxProcessorOptions { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs index 150d3ce8..58af9c66 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/NullMessageTransportTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Outbox; +namespace NetEvolve.Pulse.Tests.Unit.Outbox; using NetEvolve.Extensions.TUnit; using NetEvolve.Pulse.Extensibility.Outbox; @@ -13,7 +13,7 @@ namespace NetEvolve.Pulse.Tests.Unit.Outbox; public sealed class NullMessageTransportTests { [Test] - public async Task SendAsync_WithValidMessage_CompletesSuccessfully() + public async Task SendAsync_WithValidMessage_CompletesSuccessfully(CancellationToken cancellationToken) { var transport = new NullMessageTransport(); var message = new OutboxMessage @@ -26,11 +26,11 @@ public async Task SendAsync_WithValidMessage_CompletesSuccessfully() Status = OutboxMessageStatus.Processing, }; - await transport.SendAsync(message).ConfigureAwait(false); + await transport.SendAsync(message, cancellationToken).ConfigureAwait(false); } [Test] - public async Task SendAsync_WithCancelledToken_CompletesSuccessfully() + public async Task SendAsync_WithCancelledToken_CompletesSuccessfully(CancellationToken cancellationToken) { var transport = new NullMessageTransport(); var message = new OutboxMessage @@ -42,7 +42,7 @@ public async Task SendAsync_WithCancelledToken_CompletesSuccessfully() UpdatedAt = DateTimeOffset.UtcNow, Status = OutboxMessageStatus.Processing, }; - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await cts.CancelAsync().ConfigureAwait(false); await transport.SendAsync(message, cts.Token).ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxEventHandlerTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxEventHandlerTests.cs index 6cc46066..e1c0cc64 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxEventHandlerTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxEventHandlerTests.cs @@ -15,13 +15,13 @@ namespace NetEvolve.Pulse.Tests.Unit.Outbox; public sealed class OutboxEventHandlerTests { [Test] - public async Task HandleAsync_WithRegularEvent_StoresEventInOutbox() + public async Task HandleAsync_WithRegularEvent_StoresEventInOutbox(CancellationToken cancellationToken) { var outbox = new TrackingEventOutbox(); var handler = new OutboxEventHandler(outbox); var @event = new TestRegularEvent(); - await handler.HandleAsync(@event).ConfigureAwait(false); + await handler.HandleAsync(@event, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -31,25 +31,27 @@ public async Task HandleAsync_WithRegularEvent_StoresEventInOutbox() } [Test] - public async Task HandleAsync_WithInProcessEvent_SkipsOutbox() + public async Task HandleAsync_WithInProcessEvent_SkipsOutbox(CancellationToken cancellationToken) { var outbox = new TrackingEventOutbox(); var handler = new OutboxEventHandler(outbox); var @event = new TestInProcessEvent(); - await handler.HandleAsync(@event).ConfigureAwait(false); + await handler.HandleAsync(@event, cancellationToken).ConfigureAwait(false); _ = await Assert.That(outbox.StoredEvents).IsEmpty(); } [Test] - public async Task HandleAsync_WithInProcessEventAndHandleInProcessFalse_StoresEventInOutbox() + public async Task HandleAsync_WithInProcessEventAndHandleInProcessFalse_StoresEventInOutbox( + CancellationToken cancellationToken + ) { var outbox = new TrackingEventOutbox(); var handler = new OutboxEventHandler(outbox); var @event = new TestOptOutInProcessEvent(); - await handler.HandleAsync(@event).ConfigureAwait(false); + await handler.HandleAsync(@event, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -59,12 +61,12 @@ public async Task HandleAsync_WithInProcessEventAndHandleInProcessFalse_StoresEv } [Test] - public async Task HandleAsync_WithCancellationToken_PassesTokenToOutbox() + public async Task HandleAsync_WithCancellationToken_PassesTokenToOutbox(CancellationToken cancellationToken) { var outbox = new TrackingEventOutbox(); var handler = new OutboxEventHandler(outbox); var @event = new TestRegularEvent(); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await handler.HandleAsync(@event, cts.Token).ConfigureAwait(false); @@ -72,12 +74,12 @@ public async Task HandleAsync_WithCancellationToken_PassesTokenToOutbox() } [Test] - public async Task HandleAsync_WithCancelledToken_PropagatesCancellation() + public async Task HandleAsync_WithCancelledToken_PropagatesCancellation(CancellationToken cancellationToken) { var outbox = new TrackingEventOutbox(); var handler = new OutboxEventHandler(outbox); var @event = new TestRegularEvent(); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await cts.CancelAsync().ConfigureAwait(false); _ = await Assert.That(() => handler.HandleAsync(@event, cts.Token)).Throws(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs index e713c62f..b761a1c7 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs @@ -21,11 +21,11 @@ public sealed class OutboxExtensionsTests { [Test] - public async Task AddOutbox_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task AddOutbox_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert.That(() => OutboxExtensions.AddOutbox(null!)).Throws(); [Test] - public async Task AddOutbox_ReturnsSameBuilder() + public async Task AddOutbox_ReturnsSameBuilder(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -36,7 +36,7 @@ public async Task AddOutbox_ReturnsSameBuilder() } [Test] - public async Task AddOutbox_RegistersOutboxEventHandlerAsOpenGenericScoped() + public async Task AddOutbox_RegistersOutboxEventHandlerAsOpenGenericScoped(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -55,7 +55,9 @@ public async Task AddOutbox_RegistersOutboxEventHandlerAsOpenGenericScoped() } [Test] - public async Task AddOutbox_CalledMultipleTimes_DoesNotDuplicateOutboxEventHandler() + public async Task AddOutbox_CalledMultipleTimes_DoesNotDuplicateOutboxEventHandler( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -73,7 +75,7 @@ public async Task AddOutbox_CalledMultipleTimes_DoesNotDuplicateOutboxEventHandl } [Test] - public async Task AddOutbox_RegistersEventOutboxAsScoped() + public async Task AddOutbox_RegistersEventOutboxAsScoped(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs index 2d1853a8..31c3de58 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs @@ -103,7 +103,7 @@ public async Task Constructor_WithValidParameters_CreatesInstance() } [Test] - public async Task StartAsync_WithCancellationToken_StartsProcessing() + public async Task StartAsync_WithCancellationToken_StartsProcessing(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -116,17 +116,17 @@ public async Task StartAsync_WithCancellationToken_StartsProcessing() await service.StartAsync(cts.Token).ConfigureAwait(false); // Give it a moment to start - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Test passes if no exceptions thrown - verify the service started by checking poll count _ = await Assert.That(repository.GetPendingCallCount).IsGreaterThanOrEqualTo(1); } [Test] - public async Task StopAsync_WhenRunning_StopsGracefully() + public async Task StopAsync_WhenRunning_StopsGracefully(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -137,17 +137,19 @@ public async Task StopAsync_WhenRunning_StopsGracefully() using var cts = new CancellationTokenSource(); await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Test passes if no exceptions thrown during graceful shutdown _ = await Assert.That(repository.GetPendingCallCount).IsGreaterThanOrEqualTo(1); } [Test] - public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages() + public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages( + CancellationToken cancellationToken + ) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -157,12 +159,12 @@ public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages // Add a pending message var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -173,7 +175,7 @@ public async Task ExecuteAsync_WithPendingMessages_ProcessesAndCompletesMessages } [Test] - public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages() + public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -185,14 +187,14 @@ public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages() var message1 = CreateMessage(); var message2 = CreateMessage(); var message3 = CreateMessage(); - await repository.AddAsync(message1).ConfigureAwait(false); - await repository.AddAsync(message2).ConfigureAwait(false); - await repository.AddAsync(message3).ConfigureAwait(false); + await repository.AddAsync(message1, cancellationToken).ConfigureAwait(false); + await repository.AddAsync(message2, cancellationToken).ConfigureAwait(false); + await repository.AddAsync(message3, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(3, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -202,7 +204,7 @@ public async Task ExecuteAsync_WithMultipleMessages_ProcessesAllMessages() } [Test] - public async Task ExecuteAsync_WithNoMessages_WaitsForPollingInterval() + public async Task ExecuteAsync_WithNoMessages_WaitsForPollingInterval(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -213,17 +215,17 @@ public async Task ExecuteAsync_WithNoMessages_WaitsForPollingInterval() using var cts = new CancellationTokenSource(); await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(1000).ConfigureAwait(false); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Should have polled at least twice _ = await Assert.That(repository.GetPendingCallCount).IsGreaterThanOrEqualTo(2); } [Test] - public async Task ExecuteAsync_WithTransportFailure_MarksMessageAsFailed() + public async Task ExecuteAsync_WithTransportFailure_MarksMessageAsFailed(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); @@ -234,18 +236,18 @@ public async Task ExecuteAsync_WithTransportFailure_MarksMessageAsFailed() using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(repository.FailedMessageIds).Contains(message.Id); } [Test] - public async Task ExecuteAsync_WithExceededRetries_MovesToDeadLetter() + public async Task ExecuteAsync_WithExceededRetries_MovesToDeadLetter(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); @@ -258,18 +260,18 @@ public async Task ExecuteAsync_WithExceededRetries_MovesToDeadLetter() // Add a message that has already been retried once var message = CreateMessage(); message.RetryCount = 1; // One retry already attempted - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(repository.DeadLetterMessageIds).Contains(message.Id); } [Test] - public async Task ExecuteAsync_WithTransientFailure_RetriesAndSucceeds() + public async Task ExecuteAsync_WithTransientFailure_RetriesAndSucceeds(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: 1); // Fail once, then succeed @@ -280,19 +282,19 @@ public async Task ExecuteAsync_WithTransientFailure_RetriesAndSucceeds() using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Message should eventually be completed after retry _ = await Assert.That(repository.CompletedMessageIds).Contains(message.Id); } [Test] - public async Task ExecuteAsync_WithBatchSendingEnabled_SendsInBatch() + public async Task ExecuteAsync_WithBatchSendingEnabled_SendsInBatch(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -304,13 +306,13 @@ public async Task ExecuteAsync_WithBatchSendingEnabled_SendsInBatch() var message1 = CreateMessage(); var message2 = CreateMessage(); - await repository.AddAsync(message1).ConfigureAwait(false); - await repository.AddAsync(message2).ConfigureAwait(false); + await repository.AddAsync(message1, cancellationToken).ConfigureAwait(false); + await repository.AddAsync(message2, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -335,14 +337,14 @@ public async Task ExecuteAsync_WithBatchSendingFailure_MarkAsFailedForRetry(Canc await repository.AddAsync(message1, cancellationToken).ConfigureAwait(false); await repository.AddAsync(message2, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); // Wait until at least 2 messages have been marked (failed or dead-letter) instead of // relying on a fixed delay, which is unreliable under CI thread-pool saturation. using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Verify that batch send was not followed by individual send (no fallback to ProcessIndividuallyAsync) // Messages should be marked as failed or deadlettered (depending on retry cycles that ran), @@ -358,7 +360,7 @@ public async Task ExecuteAsync_WithBatchSendingFailure_MarkAsFailedForRetry(Canc } [Test] - public async Task ExecuteAsync_WithBatchSize_RespectsLimit() + public async Task ExecuteAsync_WithBatchSize_RespectsLimit(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -371,20 +373,22 @@ public async Task ExecuteAsync_WithBatchSize_RespectsLimit() // Add more messages than batch size for (var i = 0; i < 5; i++) { - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); } - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // First batch should have processed 2 messages _ = await Assert.That(repository.LastBatchSizeRequested).IsEqualTo(2); } [Test] - public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_UsesOverrideForMatchingEventType() + public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_UsesOverrideForMatchingEventType( + CancellationToken cancellationToken + ) { using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); @@ -405,38 +409,19 @@ public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_UsesOverrideForMatc // Message of type CriticalEvent should use override (MaxRetryCount = 1) var message = CreateMessage(typeof(CriticalEvent)); message.RetryCount = 0; // First attempt - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // With MaxRetryCount=1, retryCount+1 (1) >= 1, so it should be dead-lettered _ = await Assert.That(repository.DeadLetterMessageIds).Contains(message.Id); } [Test] - public async Task ExecuteAsync_WithPerEventTypeMaxRetryCount_FallsBackToGlobalForOtherEventTypes() - { - var options = new OutboxProcessorOptions - { - MaxRetryCount = 3, // Global default - EventTypeOverrides = { [typeof(CriticalEvent)] = new OutboxEventTypeOptions { MaxRetryCount = 1 } }, - }; - - using (Assert.Multiple()) - { - // "CriticalEvent" uses the override - _ = await Assert.That(options.GetEffectiveMaxRetryCount(typeof(CriticalEvent))).IsEqualTo(1); - // Other event types fall back to the global default - _ = await Assert.That(options.GetEffectiveMaxRetryCount(typeof(OtherEvent))).IsEqualTo(3); - _ = await Assert.That(options.GetEffectiveMaxRetryCount(typeof(TestOutboxEvent))).IsEqualTo(3); - } - } - - [Test] - public async Task ExecuteAsync_WithPerEventTypeProcessingTimeout_UsesOverride() + public async Task ExecuteAsync_WithPerEventTypeProcessingTimeout_UsesOverride(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new SlowMessageTransport(delay: TimeSpan.FromMilliseconds(200)); @@ -460,19 +445,21 @@ public async Task ExecuteAsync_WithPerEventTypeProcessingTimeout_UsesOverride() // SlowEvent message should time out and be marked as failed/dead-lettered var message = CreateMessage(typeof(SlowEvent)); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // The message should not have been sent successfully _ = await Assert.That(transport.SentMessages).IsEmpty(); } [Test] - public async Task ExecuteAsync_WithPerEventTypeBatchSending_UsesOverrideForMatchingEventType() + public async Task ExecuteAsync_WithPerEventTypeBatchSending_UsesOverrideForMatchingEventType( + CancellationToken cancellationToken + ) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -493,13 +480,13 @@ public async Task ExecuteAsync_WithPerEventTypeBatchSending_UsesOverrideForMatch // Add two messages of the overridden type: should be batch-sent var message1 = CreateMessage(typeof(BatchEvent)); var message2 = CreateMessage(typeof(BatchEvent)); - await repository.AddAsync(message1).ConfigureAwait(false); - await repository.AddAsync(message2).ConfigureAwait(false); + await repository.AddAsync(message1, cancellationToken).ConfigureAwait(false); + await repository.AddAsync(message2, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(2, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -592,7 +579,7 @@ public async Task OutboxEventTypeOptions_WithNullOverrideProperties_FallsBackToG [Test] [NotInParallel("OutboxMetrics")] - public async Task ExecuteAsync_WithPendingMessages_RecordsProcessedMetric() + public async Task ExecuteAsync_WithPendingMessages_RecordsProcessedMetric(CancellationToken cancellationToken) { using var meterListener = new MeterListener(); meterListener.InstrumentPublished = (instrument, listener) => @@ -622,12 +609,12 @@ public async Task ExecuteAsync_WithPendingMessages_RecordsProcessedMetric() using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); meterListener.RecordObservableInstruments(); @@ -636,7 +623,7 @@ public async Task ExecuteAsync_WithPendingMessages_RecordsProcessedMetric() [Test] [NotInParallel("OutboxMetrics")] - public async Task ExecuteAsync_WithTransportFailure_RecordsFailedMetric() + public async Task ExecuteAsync_WithTransportFailure_RecordsFailedMetric(CancellationToken cancellationToken) { using var meterListener = new MeterListener(); meterListener.InstrumentPublished = (instrument, listener) => @@ -668,19 +655,19 @@ public async Task ExecuteAsync_WithTransportFailure_RecordsFailedMetric() using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(Volatile.Read(ref failedTotal)).IsGreaterThanOrEqualTo(1L); } [Test] [NotInParallel("OutboxMetrics")] - public async Task ExecuteAsync_WithExceededRetries_RecordsDeadLetterMetric() + public async Task ExecuteAsync_WithExceededRetries_RecordsDeadLetterMetric(CancellationToken cancellationToken) { using var meterListener = new MeterListener(); meterListener.InstrumentPublished = (instrument, listener) => @@ -713,19 +700,19 @@ public async Task ExecuteAsync_WithExceededRetries_RecordsDeadLetterMetric() var message = CreateMessage(); message.RetryCount = 1; - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(Volatile.Read(ref deadLetterTotal)).IsGreaterThanOrEqualTo(1L); } [Test] [NotInParallel("OutboxMetrics")] - public async Task ExecuteAsync_AfterProcessingCycle_RecordsProcessingDuration() + public async Task ExecuteAsync_AfterProcessingCycle_RecordsProcessingDuration(CancellationToken cancellationToken) { using var meterListener = new MeterListener(); meterListener.InstrumentPublished = (instrument, listener) => @@ -756,16 +743,18 @@ public async Task ExecuteAsync_AfterProcessingCycle_RecordsProcessingDuration() using var cts = new CancellationTokenSource(); await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(200).ConfigureAwait(false); + await Task.Delay(200, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(Volatile.Read(ref durationRecorded)).IsTrue(); } [Test] [NotInParallel("OutboxMetrics")] - public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendingCount() + public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendingCount( + CancellationToken cancellationToken + ) { using var meterListener = new MeterListener(); meterListener.InstrumentPublished = (instrument, listener) => @@ -795,21 +784,21 @@ public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendin using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); // Add 3 pending messages before starting - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); // Wait at least one polling cycle so the gauge is refreshed before observing - await Task.Delay(75).ConfigureAwait(false); + await Task.Delay(75, cancellationToken).ConfigureAwait(false); meterListener.RecordObservableInstruments(); var earlyObservation = Volatile.Read(ref pendingObserved); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(3, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // After processing all messages, the pending count should be 0 meterListener.RecordObservableInstruments(); @@ -825,7 +814,7 @@ public async Task ExecuteAsync_WithPendingMessages_ObservableGaugeReflectsPendin } [Test] - public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt() + public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); @@ -843,14 +832,14 @@ public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt() using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); var startTime = DateTimeOffset.UtcNow; - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Get the failed message from the repository var failedMessage = repository._messages.FirstOrDefault(m => m.Status == OutboxMessageStatus.Failed); @@ -864,7 +853,9 @@ public async Task ExecuteAsync_WithExponentialBackoffEnabled_SetsNextRetryAt() } [Test] - public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetryAt() + public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetryAt( + CancellationToken cancellationToken + ) { using var repository = new InMemoryOutboxRepository(); var transport = new FailingMessageTransport(failCount: int.MaxValue); @@ -880,12 +871,12 @@ public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetr using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Get the failed message from the repository var failedMessage = repository._messages.FirstOrDefault(m => m.Status == OutboxMessageStatus.Failed); @@ -898,37 +889,37 @@ public async Task ExecuteAsync_WithExponentialBackoffDisabled_DoesNotSetNextRetr } [Test] - public async Task GetPendingAsync_WithFutureNextRetryAt_ExcludesMessage() + public async Task GetPendingAsync_WithFutureNextRetryAt_ExcludesMessage(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var futureTime = DateTimeOffset.UtcNow.AddSeconds(10); var message = CreateMessage(); message.NextRetryAt = futureTime; - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - var pending = await repository.GetPendingAsync(batchSize: 10).ConfigureAwait(false); + var pending = await repository.GetPendingAsync(batchSize: 10, cancellationToken).ConfigureAwait(false); _ = await Assert.That(pending.Count).IsEqualTo(0); } [Test] - public async Task GetPendingAsync_WithPastNextRetryAt_IncludesMessage() + public async Task GetPendingAsync_WithPastNextRetryAt_IncludesMessage(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var pastTime = DateTimeOffset.UtcNow.AddSeconds(-10); var message = CreateMessage(); message.NextRetryAt = pastTime; - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - var pending = await repository.GetPendingAsync(batchSize: 10).ConfigureAwait(false); + var pending = await repository.GetPendingAsync(batchSize: 10, cancellationToken).ConfigureAwait(false); _ = await Assert.That(pending.Count).IsEqualTo(1); } [Test] - public async Task GetFailedForRetryAsync_WithFutureNextRetryAt_ExcludesMessage() + public async Task GetFailedForRetryAsync_WithFutureNextRetryAt_ExcludesMessage(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var futureTime = DateTimeOffset.UtcNow.AddSeconds(10); @@ -937,17 +928,17 @@ public async Task GetFailedForRetryAsync_WithFutureNextRetryAt_ExcludesMessage() message.Status = OutboxMessageStatus.Failed; message.RetryCount = 1; message.NextRetryAt = futureTime; - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); var failedForRetry = await repository - .GetFailedForRetryAsync(maxRetryCount: 3, batchSize: 10) + .GetFailedForRetryAsync(maxRetryCount: 3, batchSize: 10, cancellationToken) .ConfigureAwait(false); _ = await Assert.That(failedForRetry.Count).IsEqualTo(0); } [Test] - public async Task GetFailedForRetryAsync_WithPastNextRetryAt_IncludesMessage() + public async Task GetFailedForRetryAsync_WithPastNextRetryAt_IncludesMessage(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository(); var pastTime = DateTimeOffset.UtcNow.AddSeconds(-10); @@ -956,17 +947,19 @@ public async Task GetFailedForRetryAsync_WithPastNextRetryAt_IncludesMessage() message.Status = OutboxMessageStatus.Failed; message.RetryCount = 1; message.NextRetryAt = pastTime; - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); var failedForRetry = await repository - .GetFailedForRetryAsync(maxRetryCount: 3, batchSize: 10) + .GetFailedForRetryAsync(maxRetryCount: 3, batchSize: 10, cancellationToken) .ConfigureAwait(false); _ = await Assert.That(failedForRetry.Count).IsEqualTo(1); } [Test] - public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessages() + public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessages( + CancellationToken cancellationToken + ) { using var repository = new InMemoryOutboxRepository(); var transport = new InMemoryMessageTransport(); @@ -975,12 +968,12 @@ public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessag using var lifetime = new PendingStartLifetime(); using var service = new OutboxProcessorHostedService(repository, transport, lifetime, options, logger); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); // Give the service ample time to run polling cycles if it were already started. - await Task.Delay(200).ConfigureAwait(false); + await Task.Delay(200, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -993,14 +986,14 @@ public async Task ExecuteAsync_WaitsForApplicationStarted_BeforeProcessingMessag using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); // Processing must have happened after ApplicationStarted fired. _ = await Assert.That(transport.SentMessages).HasSingleItem(); } [Test] - public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle() + public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository { IsHealthy = false }; var transport = new InMemoryMessageTransport(); @@ -1008,13 +1001,13 @@ public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle() var logger = CreateLogger(); using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); using var cts = new CancellationTokenSource(); await service.StartAsync(cts.Token).ConfigureAwait(false); - await Task.Delay(300).ConfigureAwait(false); + await Task.Delay(300, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -1024,7 +1017,7 @@ public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle() } [Test] - public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing() + public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing(CancellationToken cancellationToken) { using var repository = new InMemoryOutboxRepository { IsHealthy = false }; var transport = new InMemoryMessageTransport(); @@ -1032,12 +1025,12 @@ public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing() var logger = CreateLogger(); using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); - await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + await service.StartAsync(cancellationToken).ConfigureAwait(false); // Allow several unhealthy cycles to pass. - await Task.Delay(150).ConfigureAwait(false); + await Task.Delay(150, cancellationToken).ConfigureAwait(false); _ = await Assert.That(transport.SentMessages).IsEmpty(); @@ -1046,7 +1039,7 @@ public async Task ExecuteAsync_WhenDatabaseBecomesHealthy_ResumesProcessing() using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); await repository.WaitForMarkingsAsync(1, timeoutCts.Token).ConfigureAwait(false); - await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + await service.StopAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(transport.SentMessages).HasSingleItem(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs index 1f64f114..c3259447 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs @@ -8,7 +8,7 @@ public sealed class OutboxStatisticsTests { [Test] - public async Task Total_WithDefaultValues_ReturnsZero() + public async Task Total_WithDefaultValues_ReturnsZero(CancellationToken cancellationToken) { var statistics = new OutboxStatistics(); @@ -16,7 +16,7 @@ public async Task Total_WithDefaultValues_ReturnsZero() } [Test] - public async Task Total_WithOnlyPending_ReturnsPendingCount() + public async Task Total_WithOnlyPending_ReturnsPendingCount(CancellationToken cancellationToken) { var statistics = new OutboxStatistics { Pending = 5 }; @@ -24,7 +24,7 @@ public async Task Total_WithOnlyPending_ReturnsPendingCount() } [Test] - public async Task Total_WithAllStatusesSet_ReturnsSumOfAllStatuses() + public async Task Total_WithAllStatusesSet_ReturnsSumOfAllStatuses(CancellationToken cancellationToken) { var statistics = new OutboxStatistics { @@ -39,7 +39,7 @@ public async Task Total_WithAllStatusesSet_ReturnsSumOfAllStatuses() } [Test] - public async Task Total_WithOnlyDeadLetter_ReturnsDeadLetterCount() + public async Task Total_WithOnlyDeadLetter_ReturnsDeadLetterCount(CancellationToken cancellationToken) { var statistics = new OutboxStatistics { DeadLetter = 7 }; @@ -47,7 +47,7 @@ public async Task Total_WithOnlyDeadLetter_ReturnsDeadLetterCount() } [Test] - public async Task AllProperties_WhenSet_TotalReflectsCorrectSum() + public async Task AllProperties_WhenSet_TotalReflectsCorrectSum(CancellationToken cancellationToken) { var statistics = new OutboxStatistics { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs index 395b98cc..de547968 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -45,12 +45,16 @@ private static ServiceProvider CreateServiceProvider( } [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert.That(() => new PollyEventInterceptor(null!)).Throws(); [Test] - public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException() + public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -63,7 +67,7 @@ public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationExcepti } [Test] - public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully() + public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: true); @@ -76,7 +80,7 @@ public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully() } [Test] - public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully() + public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: false); @@ -89,7 +93,7 @@ public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully() } [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(); @@ -98,12 +102,12 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() // Act & Assert _ = await Assert - .That(async () => await interceptor.HandleAsync(message, null!).ConfigureAwait(false)) + .That(async () => await interceptor.HandleAsync(message, null!, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task HandleAsync_WithSuccessfulHandler_CompletesSuccessfully() + public async Task HandleAsync_WithSuccessfulHandler_CompletesSuccessfully(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(); @@ -119,7 +123,8 @@ await interceptor { handlerCalled = true; return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -128,7 +133,7 @@ await interceptor } [Test] - public async Task HandleAsync_WithRetryPolicy_RetriesOnFailure() + public async Task HandleAsync_WithRetryPolicy_RetriesOnFailure(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -159,7 +164,8 @@ await interceptor throw new InvalidOperationException("Transient failure"); } return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -168,7 +174,7 @@ await interceptor } [Test] - public async Task HandleAsync_WithRetryPolicyExhausted_ThrowsException() + public async Task HandleAsync_WithRetryPolicyExhausted_ThrowsException(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -197,7 +203,8 @@ await interceptor { attemptCount++; throw new InvalidOperationException("Persistent failure"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -209,7 +216,7 @@ await interceptor } [Test] - public async Task HandleAsync_WithCombinedPolicies_ExecutesInOrder() + public async Task HandleAsync_WithCombinedPolicies_ExecutesInOrder(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -241,7 +248,8 @@ await interceptor throw new InvalidOperationException("Transient failure"); } return Task.CompletedTask; - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -250,7 +258,7 @@ await interceptor } [Test] - public async Task HandleAsync_WithCircuitBreaker_BlocksAfterFailureThreshold() + public async Task HandleAsync_WithCircuitBreaker_BlocksAfterFailureThreshold(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -280,7 +288,8 @@ await interceptor { attemptCount++; throw new InvalidOperationException("Failure"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -295,7 +304,8 @@ await interceptor { attemptCount++; throw new InvalidOperationException("Failure"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -304,7 +314,9 @@ await interceptor // Circuit should be open now, next request should be rejected immediately _ = await Assert .That(async () => - await interceptor.HandleAsync(message, (_, _) => Task.CompletedTask).ConfigureAwait(false) + await interceptor + .HandleAsync(message, (_, _) => Task.CompletedTask, cancellationToken) + .ConfigureAwait(false) ) .Throws(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs index c0cb6f36..c68b0000 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -45,14 +45,18 @@ private static ServiceProvider CreateServiceProvider( } [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => new PollyRequestInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException() + public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -65,7 +69,7 @@ public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationExcepti } [Test] - public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully() + public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: true); @@ -78,7 +82,7 @@ public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully() } [Test] - public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully() + public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: false); @@ -91,7 +95,7 @@ public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully() } [Test] - public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() + public async Task HandleAsync_NullHandler_ThrowsArgumentNullException(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(); @@ -100,12 +104,12 @@ public async Task HandleAsync_NullHandler_ThrowsArgumentNullException() // Act & Assert _ = await Assert - .That(async () => await interceptor.HandleAsync(request, null!).ConfigureAwait(false)) + .That(async () => await interceptor.HandleAsync(request, null!, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task HandleAsync_WithSuccessfulHandler_ReturnsResult() + public async Task HandleAsync_WithSuccessfulHandler_ReturnsResult(CancellationToken cancellationToken) { // Arrange var serviceProvider = CreateServiceProvider(); @@ -114,14 +118,16 @@ public async Task HandleAsync_WithSuccessfulHandler_ReturnsResult() const string expected = "success"; // Act - var result = await interceptor.HandleAsync(request, (_, _) => Task.FromResult(expected)).ConfigureAwait(false); + var result = await interceptor + .HandleAsync(request, (_, _) => Task.FromResult(expected), cancellationToken) + .ConfigureAwait(false); // Assert _ = await Assert.That(result).IsEqualTo(expected); } [Test] - public async Task HandleAsync_WithRetryPolicy_RetriesOnFailure() + public async Task HandleAsync_WithRetryPolicy_RetriesOnFailure(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -152,7 +158,8 @@ public async Task HandleAsync_WithRetryPolicy_RetriesOnFailure() throw new InvalidOperationException("Transient failure"); } return Task.FromResult("success"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -162,7 +169,7 @@ public async Task HandleAsync_WithRetryPolicy_RetriesOnFailure() } [Test] - public async Task HandleAsync_WithRetryPolicyExhausted_ThrowsException() + public async Task HandleAsync_WithRetryPolicyExhausted_ThrowsException(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -191,7 +198,8 @@ await interceptor { attemptCount++; throw new InvalidOperationException("Persistent failure"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -203,7 +211,7 @@ await interceptor } [Test] - public async Task HandleAsync_WithCombinedPolicies_ExecutesInOrder() + public async Task HandleAsync_WithCombinedPolicies_ExecutesInOrder(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -235,7 +243,8 @@ public async Task HandleAsync_WithCombinedPolicies_ExecutesInOrder() throw new InvalidOperationException("Transient failure"); } return Task.FromResult("success"); - } + }, + cancellationToken ) .ConfigureAwait(false); @@ -245,7 +254,7 @@ public async Task HandleAsync_WithCombinedPolicies_ExecutesInOrder() } [Test] - public async Task HandleAsync_WithCircuitBreaker_BlocksAfterFailureThreshold() + public async Task HandleAsync_WithCircuitBreaker_BlocksAfterFailureThreshold(CancellationToken cancellationToken) { // Arrange var attemptCount = 0; @@ -275,7 +284,8 @@ await interceptor { attemptCount++; throw new InvalidOperationException("Failure"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -290,7 +300,8 @@ await interceptor { attemptCount++; throw new InvalidOperationException("Failure"); - } + }, + cancellationToken ) .ConfigureAwait(false) ) @@ -299,7 +310,9 @@ await interceptor // Circuit should be open now, next request should be rejected immediately _ = await Assert .That(async () => - await interceptor.HandleAsync(request, (_, _) => Task.FromResult("success")).ConfigureAwait(false) + await interceptor + .HandleAsync(request, (_, _) => Task.FromResult("success"), cancellationToken) + .ConfigureAwait(false) ) .Throws(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs index 70d7e0c1..2b431fb3 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs @@ -18,21 +18,25 @@ public sealed class PollyExtensionsTests { [Test] - public async Task AddPollyRequestPolicies_NullConfigurator_ThrowsArgumentNullException() => + public async Task AddPollyRequestPolicies_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyRequestPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyRequestPolicies_NullConfigure_ThrowsArgumentNullException() => + public async Task AddPollyRequestPolicies_NullConfigure_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyRequestPolicies(null!)) .Throws(); [Test] - public async Task AddPollyRequestPolicies_RegistersPipelineAndInterceptor() + public async Task AddPollyRequestPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -55,7 +59,9 @@ public async Task AddPollyRequestPolicies_RegistersPipelineAndInterceptor() } [Test] - public async Task AddPollyRequestPolicies_VoidCommand_RegistersPipelineAndInterceptor() + public async Task AddPollyRequestPolicies_VoidCommand_RegistersPipelineAndInterceptor( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -74,21 +80,25 @@ public async Task AddPollyRequestPolicies_VoidCommand_RegistersPipelineAndInterc } [Test] - public async Task AddPollyEventPolicies_NullConfigurator_ThrowsArgumentNullException() => + public async Task AddPollyEventPolicies_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyEventPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyEventPolicies_NullConfigure_ThrowsArgumentNullException() => + public async Task AddPollyEventPolicies_NullConfigure_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyEventPolicies(null!)) .Throws(); [Test] - public async Task AddPollyEventPolicies_RegistersPipelineAndInterceptor() + public async Task AddPollyEventPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -111,7 +121,9 @@ public async Task AddPollyEventPolicies_RegistersPipelineAndInterceptor() } [Test] - public async Task AddPollyRequestPolicies_WithDifferentLifetimes_RespectsLifetime() + public async Task AddPollyRequestPolicies_WithDifferentLifetimes_RespectsLifetime( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -137,7 +149,9 @@ public async Task AddPollyRequestPolicies_WithDifferentLifetimes_RespectsLifetim } [Test] - public async Task AddPollyRequestPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered() + public async Task AddPollyRequestPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -164,7 +178,9 @@ public async Task AddPollyRequestPolicies_CalledTwice_OnlyOnePipelineDescriptorR } [Test] - public async Task AddPollyRequestPolicies_CalledTwice_SecondConfigurationIsApplied() + public async Task AddPollyRequestPolicies_CalledTwice_SecondConfigurationIsApplied( + CancellationToken cancellationToken + ) { // Arrange var firstFactoryInvoked = false; @@ -196,7 +212,9 @@ public async Task AddPollyRequestPolicies_CalledTwice_SecondConfigurationIsAppli } [Test] - public async Task AddPollyRequestPolicies_VoidCommand_CalledTwice_ReplacesExistingRegistration() + public async Task AddPollyRequestPolicies_VoidCommand_CalledTwice_ReplacesExistingRegistration( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -217,7 +235,9 @@ public async Task AddPollyRequestPolicies_VoidCommand_CalledTwice_ReplacesExisti } [Test] - public async Task AddPollyRequestPolicies_CalledTwice_ReplacesExistingRegistration() + public async Task AddPollyRequestPolicies_CalledTwice_ReplacesExistingRegistration( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -242,7 +262,9 @@ public async Task AddPollyRequestPolicies_CalledTwice_ReplacesExistingRegistrati } [Test] - public async Task AddPollyEventPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered() + public async Task AddPollyEventPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -267,7 +289,9 @@ public async Task AddPollyEventPolicies_CalledTwice_OnlyOnePipelineDescriptorReg } [Test] - public async Task AddPollyEventPolicies_CalledTwice_SecondConfigurationIsApplied() + public async Task AddPollyEventPolicies_CalledTwice_SecondConfigurationIsApplied( + CancellationToken cancellationToken + ) { // Arrange var firstFactoryInvoked = false; @@ -299,7 +323,7 @@ public async Task AddPollyEventPolicies_CalledTwice_SecondConfigurationIsApplied } [Test] - public async Task AddPollyEventPolicies_WithDifferentLifetimes_RespectsLifetime() + public async Task AddPollyEventPolicies_WithDifferentLifetimes_RespectsLifetime(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -325,7 +349,9 @@ public async Task AddPollyEventPolicies_WithDifferentLifetimes_RespectsLifetime( } [Test] - public async Task AddPollyEventPolicies_CalledTwice_ReplacesExistingRegistration() + public async Task AddPollyEventPolicies_CalledTwice_ReplacesExistingRegistration( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -350,21 +376,25 @@ public async Task AddPollyEventPolicies_CalledTwice_ReplacesExistingRegistration } [Test] - public async Task AddPollyQueryPolicies_NullConfigurator_ThrowsArgumentNullException() => + public async Task AddPollyQueryPolicies_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyQueryPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyQueryPolicies_NullConfigure_ThrowsArgumentNullException() => + public async Task AddPollyQueryPolicies_NullConfigure_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyQueryPolicies(null!)) .Throws(); [Test] - public async Task AddPollyQueryPolicies_RegistersPipelineAndInterceptor() + public async Task AddPollyQueryPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -387,7 +417,9 @@ public async Task AddPollyQueryPolicies_RegistersPipelineAndInterceptor() } [Test] - public async Task AddPollyQueryPolicies_CalledTwice_ReplacesExistingRegistration() + public async Task AddPollyQueryPolicies_CalledTwice_ReplacesExistingRegistration( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -412,21 +444,25 @@ public async Task AddPollyQueryPolicies_CalledTwice_ReplacesExistingRegistration } [Test] - public async Task AddPollyCommandPolicies_NullConfigurator_ThrowsArgumentNullException() => + public async Task AddPollyCommandPolicies_NullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyCommandPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyCommandPolicies_NullConfigure_ThrowsArgumentNullException() => + public async Task AddPollyCommandPolicies_NullConfigure_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyCommandPolicies(null!)) .Throws(); [Test] - public async Task AddPollyCommandPolicies_RegistersPipelineAndInterceptor() + public async Task AddPollyCommandPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) { // Arrange var services = new ServiceCollection(); @@ -449,7 +485,9 @@ public async Task AddPollyCommandPolicies_RegistersPipelineAndInterceptor() } [Test] - public async Task AddPollyCommandPolicies_WithDifferentLifetimes_RespectsLifetime() + public async Task AddPollyCommandPolicies_WithDifferentLifetimes_RespectsLifetime( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); @@ -475,7 +513,9 @@ public async Task AddPollyCommandPolicies_WithDifferentLifetimes_RespectsLifetim } [Test] - public async Task AddPollyCommandPolicies_CalledTwice_ReplacesExistingRegistration() + public async Task AddPollyCommandPolicies_CalledTwice_ReplacesExistingRegistration( + CancellationToken cancellationToken + ) { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs index da7d3792..605aa0b9 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; +namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; using System; using System.Threading.Tasks; @@ -14,13 +14,13 @@ public sealed class PostgreSqlEventOutboxTests { [Test] - public async Task Constructor_WithNullConnection_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnection_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new PostgreSqlEventOutbox(null!, Options.Create(new OutboxOptions()), TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -30,7 +30,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -40,7 +40,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -50,7 +50,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithTransaction_CreatesInstance() + public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken cancellationToken) { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -65,7 +65,7 @@ public async Task Constructor_WithTransaction_CreatesInstance() } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new NpgsqlConnection("Host=localhost;"); var outbox = new PostgreSqlEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); @@ -76,7 +76,9 @@ public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() } [Test] - public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException() + public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { await using var connection = new NpgsqlConnection("Host=localhost;"); var outbox = new PostgreSqlEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); @@ -86,7 +88,7 @@ public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationExcepti }; _ = await Assert - .That(async () => await outbox.StoreAsync(message).ConfigureAwait(false)) + .That(async () => await outbox.StoreAsync(message, cancellationToken).ConfigureAwait(false)) .Throws(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs index 929bf307..85eed625 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs @@ -17,31 +17,41 @@ public sealed class PostgreSqlExtensionsTests { [Test] - public async Task AddPostgreSqlOutbox_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task AddPostgreSqlOutbox_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => PostgreSqlExtensions.AddPostgreSqlOutbox(null!, "Host=localhost;Encrypt=true;")) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task AddPostgreSqlOutbox_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox((string)null!)) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task AddPostgreSqlOutbox_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox(string.Empty)) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task AddPostgreSqlOutbox_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox(" ")) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining() + public async Task AddPostgreSqlOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining( + CancellationToken cancellationToken + ) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -52,7 +62,9 @@ public async Task AddPostgreSqlOutbox_WithValidConnectionString_ReturnsConfigura } [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped() + public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox("Host=localhost;Encrypt=true;")); @@ -67,7 +79,9 @@ public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxR } [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton() + public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddPostgreSqlOutbox("Host=localhost;Encrypt=true;")); @@ -82,7 +96,7 @@ public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersTimePro } [Test] - public async Task AddPostgreSqlOutbox_WithConfigureOptions_AppliesOptions() + public async Task AddPostgreSqlOutbox_WithConfigureOptions_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -98,7 +112,9 @@ public async Task AddPostgreSqlOutbox_WithConfigureOptions_AppliesOptions() } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task AddPostgreSqlOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => PostgreSqlExtensions.AddPostgreSqlOutbox( @@ -109,13 +125,17 @@ public async Task AddPostgreSqlOutbox_WithFactory_WithNullConfigurator_ThrowsArg .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException() => + public async Task AddPostgreSqlOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox((Func)null!)) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithFactory_ReturnsConfiguratorForChaining() + public async Task AddPostgreSqlOutbox_WithFactory_ReturnsConfiguratorForChaining( + CancellationToken cancellationToken + ) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -126,7 +146,9 @@ public async Task AddPostgreSqlOutbox_WithFactory_ReturnsConfiguratorForChaining } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxRepositoryAsScoped() + public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxRepositoryAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox(_ => "Host=localhost;Encrypt=true;")); @@ -141,7 +163,9 @@ public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxRepositoryAsSco } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_WithConfigureOptions_AppliesOptions() + public async Task AddPostgreSqlOutbox_WithFactory_WithConfigureOptions_AppliesOptions( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -157,7 +181,9 @@ public async Task AddPostgreSqlOutbox_WithFactory_WithConfigureOptions_AppliesOp } [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped() + public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox("Host=localhost;Encrypt=true;")); @@ -172,7 +198,9 @@ public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxM } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxManagementAsScoped() + public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxManagementAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox(_ => "Host=localhost;Encrypt=true;")); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs index 8e00fbd6..5a28913e 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs @@ -13,13 +13,17 @@ public sealed class PostgreSqlOutboxManagementTests private const string ValidConnectionString = "Host=localhost;Database=Test;Username=postgres;Password=secret;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = null }))) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = string.Empty })) @@ -27,17 +31,19 @@ public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = " " }))) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert.That(() => new PostgreSqlOutboxManagement(null!)).Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -47,7 +53,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithNullSchema_UsesDefaultSchema() + public async Task Constructor_WithNullSchema_UsesDefaultSchema(CancellationToken cancellationToken) { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }); @@ -57,7 +63,7 @@ public async Task Constructor_WithNullSchema_UsesDefaultSchema() } [Test] - public async Task Constructor_WithEmptySchema_UsesDefaultSchema() + public async Task Constructor_WithEmptySchema_UsesDefaultSchema(CancellationToken cancellationToken) { var options = Options.Create( new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty } @@ -69,7 +75,7 @@ public async Task Constructor_WithEmptySchema_UsesDefaultSchema() } [Test] - public async Task Constructor_WithWhitespaceSchema_UsesDefaultSchema() + public async Task Constructor_WithWhitespaceSchema_UsesDefaultSchema(CancellationToken cancellationToken) { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = " " }); @@ -79,7 +85,7 @@ public async Task Constructor_WithWhitespaceSchema_UsesDefaultSchema() } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance() + public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }); @@ -89,7 +95,9 @@ public async Task Constructor_WithCustomSchema_CreatesInstance() } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -101,7 +109,9 @@ public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgument } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -113,7 +123,9 @@ public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutO } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs index 958e96f9..e444060b 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs @@ -11,7 +11,7 @@ public sealed class PostgreSqlOutboxOptionsExtensionsTests { [Test] - public async Task FullTableName_WithDefaultOptions_Returns_correct_quoted_name() + public async Task FullTableName_WithDefaultOptions_Returns_correct_quoted_name(CancellationToken cancellationToken) { var options = new OutboxOptions(); @@ -19,7 +19,7 @@ public async Task FullTableName_WithDefaultOptions_Returns_correct_quoted_name() } [Test] - public async Task FullTableName_WithCustomSchema_Returns_correct_quoted_name() + public async Task FullTableName_WithCustomSchema_Returns_correct_quoted_name(CancellationToken cancellationToken) { var options = new OutboxOptions { Schema = "myschema" }; @@ -27,7 +27,7 @@ public async Task FullTableName_WithCustomSchema_Returns_correct_quoted_name() } [Test] - public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema() + public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema(CancellationToken cancellationToken) { var options = new OutboxOptions { Schema = null! }; @@ -35,7 +35,9 @@ public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema() } [Test] - public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema() + public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema( + CancellationToken cancellationToken + ) { var options = new OutboxOptions { Schema = " " }; @@ -43,7 +45,7 @@ public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schem } [Test] - public async Task FullTableName_WithCustomTableName_Returns_correct_quoted_name() + public async Task FullTableName_WithCustomTableName_Returns_correct_quoted_name(CancellationToken cancellationToken) { var options = new OutboxOptions { TableName = "MyTable" }; @@ -51,7 +53,7 @@ public async Task FullTableName_WithCustomTableName_Returns_correct_quoted_name( } [Test] - public async Task FullTableName_Trims_schema_whitespace() + public async Task FullTableName_Trims_schema_whitespace(CancellationToken cancellationToken) { var options = new OutboxOptions { Schema = " myschema " }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs index f908e730..b8c2f718 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; +namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; using System; using System.Threading.Tasks; @@ -13,7 +13,9 @@ public sealed class PostgreSqlOutboxRepositoryTests private const string ValidConnectionString = "Host=localhost;Database=Test;Username=postgres;Password=secret;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -24,7 +26,9 @@ public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullExcepti .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -35,7 +39,9 @@ public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -46,13 +52,15 @@ public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentExcep .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new PostgreSqlOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -63,7 +71,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() .Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var repository = new PostgreSqlOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }), @@ -74,7 +82,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithTransactionScope_CreatesInstance() + public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationToken cancellationToken) { var transactionScope = new PostgreSqlOutboxTransactionScope(null); @@ -88,7 +96,7 @@ public async Task Constructor_WithTransactionScope_CreatesInstance() } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance() + public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }; @@ -98,7 +106,7 @@ public async Task Constructor_WithCustomSchema_CreatesInstance() } [Test] - public async Task Constructor_WithNullSchema_CreatesInstance() + public async Task Constructor_WithNullSchema_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }; @@ -108,7 +116,7 @@ public async Task Constructor_WithNullSchema_CreatesInstance() } [Test] - public async Task Constructor_WithEmptySchema_CreatesInstance() + public async Task Constructor_WithEmptySchema_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty }; @@ -118,7 +126,7 @@ public async Task Constructor_WithEmptySchema_CreatesInstance() } [Test] - public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { var repository = new PostgreSqlOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }), @@ -126,7 +134,7 @@ public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() ); _ = await Assert - .That(async () => await repository.AddAsync(null!).ConfigureAwait(false)) + .That(async () => await repository.AddAsync(null!, cancellationToken).ConfigureAwait(false)) .Throws(); } } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs index 39d0ad1c..93f08e5a 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs @@ -9,7 +9,7 @@ public sealed class PostgreSqlOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithDefaultParameter_CreatesInstance() + public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationToken cancellationToken) { var scope = new PostgreSqlOutboxTransactionScope(); @@ -17,7 +17,7 @@ public async Task Constructor_WithDefaultParameter_CreatesInstance() } [Test] - public async Task Constructor_WithNullTransaction_CreatesInstance() + public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationToken cancellationToken) { var scope = new PostgreSqlOutboxTransactionScope(null); @@ -25,7 +25,7 @@ public async Task Constructor_WithNullTransaction_CreatesInstance() } [Test] - public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() + public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(CancellationToken cancellationToken) { var scope = new PostgreSqlOutboxTransactionScope(); @@ -35,7 +35,7 @@ public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() } [Test] - public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull() + public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull(CancellationToken cancellationToken) { var scope = new PostgreSqlOutboxTransactionScope(null); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs index 4531aa94..bba531e9 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs @@ -18,11 +18,11 @@ public sealed class QueryCachingExtensionsTests { [Test] - public async Task AddQueryCaching_NullBuilder_ThrowsArgumentNullException() => + public async Task AddQueryCaching_NullBuilder_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert.That(() => QueryCachingExtensions.AddQueryCaching(null!)).Throws(); [Test] - public async Task AddQueryCaching_RegistersRequestInterceptor() + public async Task AddQueryCaching_RegistersRequestInterceptor(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -44,7 +44,7 @@ public async Task AddQueryCaching_RegistersRequestInterceptor() } [Test] - public async Task AddQueryCaching_WithConfigure_AppliesOptions() + public async Task AddQueryCaching_WithConfigure_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -58,7 +58,9 @@ public async Task AddQueryCaching_WithConfigure_AppliesOptions() } [Test] - public async Task AddQueryCaching_CalledMultipleTimes_DoesNotDuplicateInterceptor() + public async Task AddQueryCaching_CalledMultipleTimes_DoesNotDuplicateInterceptor( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -77,7 +79,7 @@ public async Task AddQueryCaching_CalledMultipleTimes_DoesNotDuplicateIntercepto } [Test] - public async Task AddQueryCaching_ReturnsSameBuilder() + public async Task AddQueryCaching_ReturnsSameBuilder(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs index 50ed0d60..76510844 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs @@ -12,7 +12,7 @@ public sealed class RabbitMqExtensionsTests { [Test] - public async Task UseRabbitMqTransport_Registers_transport_service() + public async Task UseRabbitMqTransport_Registers_transport_service(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseRabbitMqTransport()); @@ -23,7 +23,7 @@ public async Task UseRabbitMqTransport_Registers_transport_service() } [Test] - public async Task UseRabbitMqTransport_Configures_options() + public async Task UseRabbitMqTransport_Configures_options(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseRabbitMqTransport(options => options.ExchangeName = "test-exchange")); @@ -35,7 +35,9 @@ public async Task UseRabbitMqTransport_Configures_options() } [Test] - public async Task UseRabbitMqTransport_Without_configureOptions_registers_default_options() + public async Task UseRabbitMqTransport_Without_configureOptions_registers_default_options( + CancellationToken cancellationToken + ) { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseRabbitMqTransport()); @@ -49,7 +51,7 @@ public async Task UseRabbitMqTransport_Without_configureOptions_registers_defaul } [Test] - public async Task UseRabbitMqTransport_Replaces_existing_transport() + public async Task UseRabbitMqTransport_Replaces_existing_transport(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -65,7 +67,7 @@ public async Task UseRabbitMqTransport_Replaces_existing_transport() } [Test] - public async Task UseRabbitMqTransport_When_configurator_null_throws() + public async Task UseRabbitMqTransport_When_configurator_null_throws(CancellationToken cancellationToken) { IMediatorBuilder configurator = null!; @@ -76,7 +78,7 @@ public async Task UseRabbitMqTransport_When_configurator_null_throws() } [Test] - public async Task UseRabbitMqTransport_Returns_configurator_for_chaining() + public async Task UseRabbitMqTransport_Returns_configurator_for_chaining(CancellationToken cancellationToken) { IServiceCollection services = new ServiceCollection(); IMediatorBuilder? returnedConfigurator = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs index 181931e0..3cb8c96a 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.RabbitMQ; +namespace NetEvolve.Pulse.Tests.Unit.RabbitMQ; using System.Text; using global::RabbitMQ.Client; @@ -15,7 +15,7 @@ public sealed class RabbitMqMessageTransportTests { [Test] - public async Task Constructor_When_connectionAdapter_null_throws() + public async Task Constructor_When_connectionAdapter_null_throws(CancellationToken cancellationToken) { IRabbitMqConnectionAdapter connectionAdapter = null!; var topicNameResolver = new FakeTopicNameResolver(); @@ -30,7 +30,7 @@ public async Task Constructor_When_connectionAdapter_null_throws() } [Test] - public async Task Constructor_When_topicNameResolver_null_throws() + public async Task Constructor_When_topicNameResolver_null_throws(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); ITopicNameResolver topicNameResolver = null!; @@ -45,7 +45,7 @@ public async Task Constructor_When_topicNameResolver_null_throws() } [Test] - public async Task Constructor_When_options_null_throws() + public async Task Constructor_When_options_null_throws(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); @@ -60,27 +60,29 @@ public async Task Constructor_When_options_null_throws() } [Test] - public async Task SendAsync_When_message_null_throws() + public async Task SendAsync_When_message_null_throws(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); - var exception = await Assert.ThrowsAsync(() => transport.SendAsync(null!)); + var exception = await Assert.ThrowsAsync(() => + transport.SendAsync(null!, cancellationToken) + ); _ = await Assert.That(exception).IsNotNull(); _ = await Assert.That(exception!.ParamName).IsEqualTo("message"); } [Test] - public async Task SendAsync_Publishes_message_with_correct_properties() + public async Task SendAsync_Publishes_message_with_correct_properties(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver, exchangeName: "test-exchange"); var outboxMessage = CreateOutboxMessage(); - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); _ = await Assert.That(connectionAdapter.CreateChannelCallCount).IsEqualTo(1); var channel = connectionAdapter.CreatedChannels.Single(); @@ -108,7 +110,7 @@ public async Task SendAsync_Publishes_message_with_correct_properties() } [Test] - public async Task SendAsync_Reuses_open_channel() + public async Task SendAsync_Reuses_open_channel(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); @@ -116,8 +118,8 @@ public async Task SendAsync_Reuses_open_channel() var message1 = CreateOutboxMessage(); var message2 = CreateOutboxMessage(); - await transport.SendAsync(message1); - await transport.SendAsync(message2); + await transport.SendAsync(message1, cancellationToken); + await transport.SendAsync(message2, cancellationToken); _ = await Assert.That(connectionAdapter.CreateChannelCallCount).IsEqualTo(1); var channel = connectionAdapter.CreatedChannels.Single(); @@ -125,7 +127,7 @@ public async Task SendAsync_Reuses_open_channel() } [Test] - public async Task SendAsync_Creates_new_channel_when_previous_closed() + public async Task SendAsync_Creates_new_channel_when_previous_closed(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); @@ -133,25 +135,25 @@ public async Task SendAsync_Creates_new_channel_when_previous_closed() var message1 = CreateOutboxMessage(); var message2 = CreateOutboxMessage(); - await transport.SendAsync(message1); + await transport.SendAsync(message1, cancellationToken); var firstChannel = connectionAdapter.CreatedChannels.Single(); firstChannel.IsOpen = false; // Simulate channel closure - await transport.SendAsync(message2); + await transport.SendAsync(message2, cancellationToken); _ = await Assert.That(connectionAdapter.CreateChannelCallCount).IsEqualTo(2); _ = await Assert.That(connectionAdapter.CreatedChannels.Count).IsEqualTo(2); } [Test] - public async Task SendAsync_Uses_topic_name_resolver_for_routing_key() + public async Task SendAsync_Uses_topic_name_resolver_for_routing_key(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver { ResolvedName = "custom-routing-key" }; using var transport = CreateTransport(connectionAdapter, topicNameResolver); var outboxMessage = CreateOutboxMessage(); - await transport.SendAsync(outboxMessage); + await transport.SendAsync(outboxMessage, cancellationToken); var channel = connectionAdapter.CreatedChannels.Single(); var publishCall = channel.PublishCalls.Single(); @@ -160,81 +162,81 @@ public async Task SendAsync_Uses_topic_name_resolver_for_routing_key() } [Test] - public async Task IsHealthyAsync_When_connection_not_open_returns_false() + public async Task IsHealthyAsync_When_connection_not_open_returns_false(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter { IsOpen = false }; var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } [Test] - public async Task IsHealthyAsync_When_channel_not_created_returns_false() + public async Task IsHealthyAsync_When_channel_not_created_returns_false(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter { IsOpen = true }; var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } [Test] - public async Task IsHealthyAsync_When_channel_not_open_returns_false() + public async Task IsHealthyAsync_When_channel_not_open_returns_false(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter { IsOpen = true }; var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); // Create a channel - await transport.SendAsync(CreateOutboxMessage()); + await transport.SendAsync(CreateOutboxMessage(), cancellationToken); var channel = connectionAdapter.CreatedChannels.Single(); channel.IsOpen = false; - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } [Test] - public async Task IsHealthyAsync_When_connection_and_channel_open_returns_true() + public async Task IsHealthyAsync_When_connection_and_channel_open_returns_true(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter { IsOpen = true }; var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); // Create a channel - await transport.SendAsync(CreateOutboxMessage()); + await transport.SendAsync(CreateOutboxMessage(), cancellationToken); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsTrue(); } [Test] - public async Task IsHealthyAsync_When_exception_thrown_returns_false() + public async Task IsHealthyAsync_When_exception_thrown_returns_false(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter { IsOpen = true, ThrowOnIsOpen = true }; var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); - var healthy = await transport.IsHealthyAsync(); + var healthy = await transport.IsHealthyAsync(cancellationToken); _ = await Assert.That(healthy).IsFalse(); } [Test] - public async Task Dispose_Disposes_channel_and_lock() + public async Task Dispose_Disposes_channel_and_lock(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); using var transport = CreateTransport(connectionAdapter, topicNameResolver); - await transport.SendAsync(CreateOutboxMessage()); + await transport.SendAsync(CreateOutboxMessage(), cancellationToken); var channel = connectionAdapter.CreatedChannels.Single(); transport.Dispose(); @@ -243,13 +245,13 @@ public async Task Dispose_Disposes_channel_and_lock() } [Test] - public async Task Dispose_Is_idempotent() + public async Task Dispose_Is_idempotent(CancellationToken cancellationToken) { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); var transport = CreateTransport(connectionAdapter, topicNameResolver); - await transport.SendAsync(CreateOutboxMessage()); + await transport.SendAsync(CreateOutboxMessage(), cancellationToken); var channel = connectionAdapter.CreatedChannels.Single(); transport.Dispose(); @@ -259,7 +261,7 @@ public async Task Dispose_Is_idempotent() } [Test] - public async Task Options_ExchangeName_can_be_configured() + public async Task Options_ExchangeName_can_be_configured(CancellationToken cancellationToken) { var options = new RabbitMqTransportOptions { ExchangeName = "test-exchange" }; @@ -267,7 +269,7 @@ public async Task Options_ExchangeName_can_be_configured() } [Test] - public async Task Options_Default_ExchangeName_is_empty_string() + public async Task Options_Default_ExchangeName_is_empty_string(CancellationToken cancellationToken) { var options = new RabbitMqTransportOptions(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs index a18fb5f7..4938992c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SQLite; +namespace NetEvolve.Pulse.Tests.Unit.SQLite; using System; using System.Reflection; @@ -16,13 +16,13 @@ public sealed class SQLiteEventOutboxTests { [Test] - public async Task Constructor_WithNullConnection_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnection_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new SQLiteEventOutbox(null!, Options.Create(new OutboxOptions()), TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -32,7 +32,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -42,7 +42,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -52,7 +52,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithTransaction_CreatesInstance() + public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken cancellationToken) { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -67,7 +67,7 @@ public async Task Constructor_WithTransaction_CreatesInstance() } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new SqliteConnection("Data Source=:memory:"); var outbox = new SQLiteEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); @@ -78,7 +78,9 @@ public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() } [Test] - public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException() + public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { await using var connection = new SqliteConnection("Data Source=:memory:"); var outbox = new SQLiteEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); @@ -88,16 +90,16 @@ public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationExcepti }; _ = await Assert - .That(async () => await outbox.StoreAsync(message).ConfigureAwait(false)) + .That(async () => await outbox.StoreAsync(message, cancellationToken).ConfigureAwait(false)) .Throws(); } [Test] - public async Task StoreAsync_WithValidEvent_PersistsRow() + public async Task StoreAsync_WithValidEvent_PersistsRow(CancellationToken cancellationToken) { var connectionString = $"Data Source=store_{Guid.NewGuid():N};Mode=Memory;Cache=Shared"; await using var keepAlive = new SqliteConnection(connectionString); - await keepAlive.OpenAsync().ConfigureAwait(false); + await keepAlive.OpenAsync(cancellationToken).ConfigureAwait(false); await using ( var create = new SqliteCommand( @@ -121,11 +123,11 @@ public async Task StoreAsync_WithValidEvent_PersistsRow() ) ) { - _ = await create.ExecuteNonQueryAsync().ConfigureAwait(false); + _ = await create.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } await using var connection = new SqliteConnection(connectionString); - await connection.OpenAsync().ConfigureAwait(false); + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); var outbox = new SQLiteEventOutbox( connection, @@ -135,15 +137,15 @@ public async Task StoreAsync_WithValidEvent_PersistsRow() var evt = new TestEvent { CorrelationId = "corr" }; - await outbox.StoreAsync(evt).ConfigureAwait(false); + await outbox.StoreAsync(evt, cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT \"EventType\",\"CorrelationId\",\"Status\",\"Payload\" FROM \"OutboxMessage\" WHERE \"Id\" = @Id", keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", evt.Id); - await using var reader = await cmd.ExecuteReaderAsync().ConfigureAwait(false); - _ = await reader.ReadAsync().ConfigureAwait(false); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + _ = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -155,11 +157,13 @@ public async Task StoreAsync_WithValidEvent_PersistsRow() } [Test] - public async Task StoreAsync_WithOversizedEventType_ThrowsInvalidOperationException() + public async Task StoreAsync_WithOversizedEventType_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { var connectionString = $"Data Source=type_{Guid.NewGuid():N};Mode=Memory;Cache=Shared"; await using var keepAlive = new SqliteConnection(connectionString); - await keepAlive.OpenAsync().ConfigureAwait(false); + await keepAlive.OpenAsync(cancellationToken).ConfigureAwait(false); await using ( var create = new SqliteCommand( """ @@ -182,11 +186,11 @@ public async Task StoreAsync_WithOversizedEventType_ThrowsInvalidOperationExcept ) ) { - _ = await create.ExecuteNonQueryAsync().ConfigureAwait(false); + _ = await create.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } await using var connection = new SqliteConnection(connectionString); - await connection.OpenAsync().ConfigureAwait(false); + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); var outbox = new SQLiteEventOutbox( connection, @@ -197,7 +201,7 @@ public async Task StoreAsync_WithOversizedEventType_ThrowsInvalidOperationExcept var longEvent = CreateLongTypeEvent(); _ = await Assert - .That(async () => await outbox.StoreAsync(longEvent).ConfigureAwait(false)) + .That(async () => await outbox.StoreAsync(longEvent, cancellationToken).ConfigureAwait(false)) .Throws(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs index 9f29d44b..326068eb 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs @@ -17,37 +17,49 @@ public sealed class SQLiteExtensionsTests { [Test] - public async Task UseSQLiteOutbox_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task UseSQLiteOutbox_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => SQLiteExtensions.UseSQLiteOutbox(null!, "Data Source=:memory:")) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task UseSQLiteOutbox_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox((string)null!)) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task UseSQLiteOutbox_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox(string.Empty)) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task UseSQLiteOutbox_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox(" ")) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithNullConfigureOptions_ThrowsArgumentNullException() => + public async Task UseSQLiteOutbox_WithNullConfigureOptions_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox((Action)null!)) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining() + public async Task UseSQLiteOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining( + CancellationToken cancellationToken + ) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -58,7 +70,9 @@ public async Task UseSQLiteOutbox_WithValidConnectionString_ReturnsConfiguratorF } [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped() + public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().UseSQLiteOutbox("Data Source=:memory:")); @@ -73,7 +87,9 @@ public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxRepos } [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped() + public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().UseSQLiteOutbox("Data Source=:memory:")); @@ -88,7 +104,9 @@ public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxManag } [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton() + public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.UseSQLiteOutbox("Data Source=:memory:")); @@ -103,7 +121,7 @@ public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersTimeProvide } [Test] - public async Task UseSQLiteOutbox_WithConfigureOptions_AppliesTableName() + public async Task UseSQLiteOutbox_WithConfigureOptions_AppliesTableName(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -117,7 +135,7 @@ public async Task UseSQLiteOutbox_WithConfigureOptions_AppliesTableName() } [Test] - public async Task UseSQLiteOutbox_WithConfigureAction_AppliesOptions() + public async Task UseSQLiteOutbox_WithConfigureAction_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -143,7 +161,9 @@ public async Task UseSQLiteOutbox_WithConfigureAction_AppliesOptions() } [Test] - public async Task UseSQLiteOutbox_WithConfigureAction_ReturnsConfiguratorForChaining() + public async Task UseSQLiteOutbox_WithConfigureAction_ReturnsConfiguratorForChaining( + CancellationToken cancellationToken + ) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementDatabaseTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementDatabaseTests.cs index de6d06cf..0cd5c3ba 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementDatabaseTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementDatabaseTests.cs @@ -85,7 +85,7 @@ private static OutboxMessage CreateMessage( Status = status, }; - private async Task InsertAsync(OutboxMessage message) + private async Task InsertAsync(OutboxMessage message, CancellationToken cancellationToken) { await using var cmd = new SqliteCommand( """ @@ -115,31 +115,33 @@ INSERT INTO "OutboxMessage" _ = cmd.Parameters.AddWithValue("@Error", (object?)message.Error ?? DBNull.Value); _ = cmd.Parameters.AddWithValue("@Status", (int)message.Status); - _ = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); + _ = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } [Test] - public async Task GetDeadLetterCountAsync_ReturnsExpectedCount() + public async Task GetDeadLetterCountAsync_ReturnsExpectedCount(CancellationToken cancellationToken) { var management = CreateManagement(); - await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter)).ConfigureAwait(false); - await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter)).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter), cancellationToken).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter), cancellationToken).ConfigureAwait(false); - var count = await management.GetDeadLetterCountAsync().ConfigureAwait(false); + var count = await management.GetDeadLetterCountAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(count).IsEqualTo(2L); } [Test] - public async Task GetDeadLetterMessagesAsync_ReturnsPagedOrdered() + public async Task GetDeadLetterMessagesAsync_ReturnsPagedOrdered(CancellationToken cancellationToken) { var management = CreateManagement(); var older = CreateMessage(OutboxMessageStatus.DeadLetter, createdAt: DateTimeOffset.UtcNow.AddMinutes(-5)); var newer = CreateMessage(OutboxMessageStatus.DeadLetter, createdAt: DateTimeOffset.UtcNow); - await InsertAsync(older).ConfigureAwait(false); - await InsertAsync(newer).ConfigureAwait(false); + await InsertAsync(older, cancellationToken).ConfigureAwait(false); + await InsertAsync(newer, cancellationToken).ConfigureAwait(false); - var messages = await management.GetDeadLetterMessagesAsync(pageSize: 1, page: 0).ConfigureAwait(false); + var messages = await management + .GetDeadLetterMessagesAsync(pageSize: 1, page: 0, cancellationToken: cancellationToken) + .ConfigureAwait(false); using (Assert.Multiple()) { @@ -149,19 +151,19 @@ public async Task GetDeadLetterMessagesAsync_ReturnsPagedOrdered() } [Test] - public async Task GetDeadLetterMessageAsync_ReturnsSingleMessage() + public async Task GetDeadLetterMessageAsync_ReturnsSingleMessage(CancellationToken cancellationToken) { var management = CreateManagement(); var target = CreateMessage(OutboxMessageStatus.DeadLetter); - await InsertAsync(target).ConfigureAwait(false); + await InsertAsync(target, cancellationToken).ConfigureAwait(false); - var message = await management.GetDeadLetterMessageAsync(target.Id).ConfigureAwait(false); + var message = await management.GetDeadLetterMessageAsync(target.Id, cancellationToken).ConfigureAwait(false); _ = await Assert.That(message!.Id).IsEqualTo(target.Id); } [Test] - public async Task ReplayMessageAsync_ResetsDeadLetterFields() + public async Task ReplayMessageAsync_ResetsDeadLetterFields(CancellationToken cancellationToken) { var management = CreateManagement(enableWal: true); var deadLetter = CreateMessage( @@ -171,44 +173,44 @@ public async Task ReplayMessageAsync_ResetsDeadLetterFields() error: "fatal", retryCount: 3 ); - await InsertAsync(deadLetter).ConfigureAwait(false); + await InsertAsync(deadLetter, cancellationToken).ConfigureAwait(false); - var result = await management.ReplayMessageAsync(deadLetter.Id).ConfigureAwait(false); + var result = await management.ReplayMessageAsync(deadLetter.Id, cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT \"Status\",\"Error\",\"ProcessedAt\",\"NextRetryAt\",\"RetryCount\" FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", deadLetter.Id.ToString()); - await using var reader = await cmd.ExecuteReaderAsync().ConfigureAwait(false); - _ = await reader.ReadAsync().ConfigureAwait(false); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + _ = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { _ = await Assert.That(result).IsTrue(); _ = await Assert.That(reader.GetInt64(0)).IsEqualTo((long)OutboxMessageStatus.Pending); - _ = await Assert.That(await reader.IsDBNullAsync(1).ConfigureAwait(false)).IsTrue(); - _ = await Assert.That(await reader.IsDBNullAsync(2).ConfigureAwait(false)).IsTrue(); - _ = await Assert.That(await reader.IsDBNullAsync(3).ConfigureAwait(false)).IsTrue(); + _ = await Assert.That(await reader.IsDBNullAsync(1, cancellationToken).ConfigureAwait(false)).IsTrue(); + _ = await Assert.That(await reader.IsDBNullAsync(2, cancellationToken).ConfigureAwait(false)).IsTrue(); + _ = await Assert.That(await reader.IsDBNullAsync(3, cancellationToken).ConfigureAwait(false)).IsTrue(); _ = await Assert.That(reader.GetInt64(4)).IsEqualTo(0); } } [Test] - public async Task ReplayAllDeadLetterAsync_ResetsAllMessages() + public async Task ReplayAllDeadLetterAsync_ResetsAllMessages(CancellationToken cancellationToken) { var management = CreateManagement(); - await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter)).ConfigureAwait(false); - await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter)).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter), cancellationToken).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter), cancellationToken).ConfigureAwait(false); - var updated = await management.ReplayAllDeadLetterAsync().ConfigureAwait(false); + var updated = await management.ReplayAllDeadLetterAsync(cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT COUNT(*) FROM \"OutboxMessage\" WHERE \"Status\" = @status", _keepAlive ); _ = cmd.Parameters.AddWithValue("@status", (int)OutboxMessageStatus.Pending); - var count = (long)(await cmd.ExecuteScalarAsync().ConfigureAwait(false))!; + var count = (long)(await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!; using (Assert.Multiple()) { @@ -218,16 +220,16 @@ public async Task ReplayAllDeadLetterAsync_ResetsAllMessages() } [Test] - public async Task GetStatisticsAsync_ReturnsAggregatedCounts() + public async Task GetStatisticsAsync_ReturnsAggregatedCounts(CancellationToken cancellationToken) { var management = CreateManagement(); - await InsertAsync(CreateMessage(OutboxMessageStatus.Pending)).ConfigureAwait(false); - await InsertAsync(CreateMessage(OutboxMessageStatus.Processing)).ConfigureAwait(false); - await InsertAsync(CreateMessage(OutboxMessageStatus.Completed)).ConfigureAwait(false); - await InsertAsync(CreateMessage(OutboxMessageStatus.Failed)).ConfigureAwait(false); - await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter)).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.Pending), cancellationToken).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.Processing), cancellationToken).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.Completed), cancellationToken).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.Failed), cancellationToken).ConfigureAwait(false); + await InsertAsync(CreateMessage(OutboxMessageStatus.DeadLetter), cancellationToken).ConfigureAwait(false); - var stats = await management.GetStatisticsAsync().ConfigureAwait(false); + var stats = await management.GetStatisticsAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs index 26757952..53ecac6a 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs @@ -11,19 +11,23 @@ public sealed class SQLiteOutboxManagementTests { [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new SQLiteOutboxManagement(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SQLiteOutboxManagement(Options.Create(new OutboxOptions()), null!)) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SQLiteOutboxManagement( @@ -34,7 +38,9 @@ public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SQLiteOutboxManagement( @@ -45,7 +51,7 @@ public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentExcep .Throws(); [Test] - public async Task Constructor_WithValidOptions_CreatesInstance() + public async Task Constructor_WithValidOptions_CreatesInstance(CancellationToken cancellationToken) { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -56,7 +62,7 @@ public async Task Constructor_WithValidOptions_CreatesInstance() } [Test] - public async Task Constructor_WithCustomTableName_CreatesInstance() + public async Task Constructor_WithCustomTableName_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { @@ -71,7 +77,9 @@ public async Task Constructor_WithCustomTableName_CreatesInstance() } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -84,7 +92,9 @@ public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgument } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -97,7 +107,9 @@ public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutO } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs index 9ea899c9..ddd91c60 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs @@ -11,7 +11,7 @@ public sealed class OutboxOptionsExtensionsTests { [Test] - public async Task FullTableName_WithDefaultOptions_Returns_quoted_table_name() + public async Task FullTableName_WithDefaultOptions_Returns_quoted_table_name(CancellationToken cancellationToken) { var options = new OutboxOptions(); @@ -19,7 +19,7 @@ public async Task FullTableName_WithDefaultOptions_Returns_quoted_table_name() } [Test] - public async Task FullTableName_WithCustomTableName_Returns_quoted_table_name() + public async Task FullTableName_WithCustomTableName_Returns_quoted_table_name(CancellationToken cancellationToken) { var options = new OutboxOptions { TableName = "MyCustomTable" }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryDatabaseTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryDatabaseTests.cs index 91bf8000..6f1b25c8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryDatabaseTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryDatabaseTests.cs @@ -80,33 +80,35 @@ private static OutboxMessage CreateMessage(Type? eventType = null) => }; [Test] - public async Task AddAsync_WithValidMessage_PersistsToDatabase() + public async Task AddAsync_WithValidMessage_PersistsToDatabase(CancellationToken cancellationToken) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT COUNT(*) FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", message.Id.ToString()); - var count = (long)(await cmd.ExecuteScalarAsync().ConfigureAwait(false))!; + var count = (long)(await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!; _ = await Assert.That(count).IsEqualTo(1L); } [Test] - public async Task GetPendingAsync_WithPendingMessages_ReturnsAndMarksAsProcessing() + public async Task GetPendingAsync_WithPendingMessages_ReturnsAndMarksAsProcessing( + CancellationToken cancellationToken + ) { var repository = CreateRepository(); var message1 = CreateMessage(typeof(TestSQLiteRepoEvent)); var message2 = CreateMessage(); - await repository.AddAsync(message1).ConfigureAwait(false); - await repository.AddAsync(message2).ConfigureAwait(false); + await repository.AddAsync(message1, cancellationToken).ConfigureAwait(false); + await repository.AddAsync(message2, cancellationToken).ConfigureAwait(false); - var pending = await repository.GetPendingAsync(10).ConfigureAwait(false); + var pending = await repository.GetPendingAsync(10, cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -116,49 +118,51 @@ public async Task GetPendingAsync_WithPendingMessages_ReturnsAndMarksAsProcessin } [Test] - public async Task GetPendingAsync_WithEmptyTable_ReturnsEmptyList() + public async Task GetPendingAsync_WithEmptyTable_ReturnsEmptyList(CancellationToken cancellationToken) { var repository = CreateRepository(); - var pending = await repository.GetPendingAsync(10).ConfigureAwait(false); + var pending = await repository.GetPendingAsync(10, cancellationToken).ConfigureAwait(false); _ = await Assert.That(pending.Count).IsEqualTo(0); } [Test] - public async Task MarkAsCompletedAsync_SetsStatusToCompleted() + public async Task MarkAsCompletedAsync_SetsStatusToCompleted(CancellationToken cancellationToken) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await repository.MarkAsCompletedAsync(message.Id).ConfigureAwait(false); + await repository.MarkAsCompletedAsync(message.Id, cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT \"Status\" FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", message.Id.ToString()); - var status = (long)(await cmd.ExecuteScalarAsync().ConfigureAwait(false))!; + var status = (long)(await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!; _ = await Assert.That(status).IsEqualTo((long)OutboxMessageStatus.Completed); } [Test] - public async Task MarkAsFailedAsync_SetsStatusToFailed() + public async Task MarkAsFailedAsync_SetsStatusToFailed(CancellationToken cancellationToken) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await repository.MarkAsFailedAsync(message.Id, "Test error").ConfigureAwait(false); + await repository + .MarkAsFailedAsync(message.Id, "Test error", cancellationToken: cancellationToken) + .ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT \"Status\", \"Error\", \"RetryCount\" FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", message.Id.ToString()); - await using var reader = await cmd.ExecuteReaderAsync().ConfigureAwait(false); - _ = await reader.ReadAsync().ConfigureAwait(false); + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + _ = await reader.ReadAsync(cancellationToken).ConfigureAwait(false); using (Assert.Multiple()) { @@ -169,101 +173,110 @@ public async Task MarkAsFailedAsync_SetsStatusToFailed() } [Test] - public async Task MarkAsDeadLetterAsync_SetsStatusToDeadLetter() + public async Task MarkAsDeadLetterAsync_SetsStatusToDeadLetter(CancellationToken cancellationToken) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); - await repository.MarkAsDeadLetterAsync(message.Id, "Fatal error").ConfigureAwait(false); + await repository.MarkAsDeadLetterAsync(message.Id, "Fatal error", cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT \"Status\" FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", message.Id.ToString()); - var status = (long)(await cmd.ExecuteScalarAsync().ConfigureAwait(false))!; + var status = (long)(await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!; _ = await Assert.That(status).IsEqualTo((long)OutboxMessageStatus.DeadLetter); } [Test] - public async Task GetPendingCountAsync_WithPendingMessages_ReturnsCorrectCount() + public async Task GetPendingCountAsync_WithPendingMessages_ReturnsCorrectCount(CancellationToken cancellationToken) { var repository = CreateRepository(); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); - await repository.AddAsync(CreateMessage()).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); + await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); - var count = await repository.GetPendingCountAsync().ConfigureAwait(false); + var count = await repository.GetPendingCountAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(count).IsGreaterThanOrEqualTo(2L); } [Test] - public async Task DeleteCompletedAsync_DeletesOldCompletedMessages() + public async Task DeleteCompletedAsync_DeletesOldCompletedMessages(CancellationToken cancellationToken) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); - await repository.MarkAsCompletedAsync(message.Id).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); + await repository.MarkAsCompletedAsync(message.Id, cancellationToken).ConfigureAwait(false); - var deleted = await repository.DeleteCompletedAsync(TimeSpan.Zero).ConfigureAwait(false); + var deleted = await repository.DeleteCompletedAsync(TimeSpan.Zero, cancellationToken).ConfigureAwait(false); _ = await Assert.That(deleted).IsGreaterThanOrEqualTo(1); } [Test] - public async Task GetFailedForRetryAsync_WithFailedMessages_ReturnsEligibleMessages() + public async Task GetFailedForRetryAsync_WithFailedMessages_ReturnsEligibleMessages( + CancellationToken cancellationToken + ) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); - await repository.MarkAsFailedAsync(message.Id, "First failure").ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); + await repository + .MarkAsFailedAsync(message.Id, "First failure", cancellationToken: cancellationToken) + .ConfigureAwait(false); - var forRetry = await repository.GetFailedForRetryAsync(maxRetryCount: 3, batchSize: 10).ConfigureAwait(false); + var forRetry = await repository + .GetFailedForRetryAsync(maxRetryCount: 3, batchSize: 10, cancellationToken) + .ConfigureAwait(false); _ = await Assert.That(forRetry.Count).IsGreaterThanOrEqualTo(1); } [Test] - public async Task MarkAsFailedAsync_WithNextRetryAt_SetsNextRetryAt() + public async Task MarkAsFailedAsync_WithNextRetryAt_SetsNextRetryAt(CancellationToken cancellationToken) { var repository = CreateRepository(); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); var nextRetry = DateTimeOffset.UtcNow.AddMinutes(5); - await repository.MarkAsFailedAsync(message.Id, "Error with retry", nextRetry).ConfigureAwait(false); + await repository + .MarkAsFailedAsync(message.Id, "Error with retry", nextRetry, cancellationToken) + .ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT \"NextRetryAt\" FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", message.Id.ToString()); - var nextRetryValue = await cmd.ExecuteScalarAsync().ConfigureAwait(false); + var nextRetryValue = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); _ = await Assert.That(nextRetryValue).IsNotNull(); } [Test] - public async Task AddAsync_UsesAmbientTransactionScope() + public async Task AddAsync_UsesAmbientTransactionScope(CancellationToken cancellationToken) { await using var connection = new SqliteConnection(_connectionString); - await connection.OpenAsync().ConfigureAwait(false); - await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync().ConfigureAwait(false); + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var transaction = (SqliteTransaction) + await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); var scope = new StubTransactionScope(transaction); var repository = CreateRepositoryWithScope(scope); var message = CreateMessage(); - await repository.AddAsync(message).ConfigureAwait(false); - await transaction.RollbackAsync().ConfigureAwait(false); + await repository.AddAsync(message, cancellationToken).ConfigureAwait(false); + await transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); await using var cmd = new SqliteCommand( "SELECT COUNT(*) FROM \"OutboxMessage\" WHERE \"Id\" = @Id", _keepAlive ); _ = cmd.Parameters.AddWithValue("@Id", message.Id.ToString()); - var count = (long)(await cmd.ExecuteScalarAsync().ConfigureAwait(false))!; + var count = (long)(await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!; _ = await Assert.That(count).IsEqualTo(0L); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs index 188586c7..ac557330 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SQLite; +namespace NetEvolve.Pulse.Tests.Unit.SQLite; using System; using System.Threading.Tasks; @@ -11,19 +11,23 @@ public sealed class SQLiteOutboxRepositoryTests { [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new SQLiteOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SQLiteOutboxRepository(Options.Create(new OutboxOptions()), null!)) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SQLiteOutboxRepository( @@ -34,7 +38,9 @@ public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SQLiteOutboxRepository( @@ -45,7 +51,7 @@ public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentExcep .Throws(); [Test] - public async Task Constructor_WithValidOptions_CreatesInstance() + public async Task Constructor_WithValidOptions_CreatesInstance(CancellationToken cancellationToken) { var repository = new SQLiteOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -56,7 +62,7 @@ public async Task Constructor_WithValidOptions_CreatesInstance() } [Test] - public async Task Constructor_WithTransactionScope_CreatesInstance() + public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationToken cancellationToken) { var transactionScope = new SQLiteOutboxTransactionScope(null); @@ -70,7 +76,7 @@ public async Task Constructor_WithTransactionScope_CreatesInstance() } [Test] - public async Task Constructor_WithCustomTableName_CreatesInstance() + public async Task Constructor_WithCustomTableName_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { @@ -85,7 +91,7 @@ public async Task Constructor_WithCustomTableName_CreatesInstance() } [Test] - public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { var repository = new SQLiteOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -93,7 +99,7 @@ public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() ); _ = await Assert - .That(async () => await repository.AddAsync(null!).ConfigureAwait(false)) + .That(async () => await repository.AddAsync(null!, cancellationToken).ConfigureAwait(false)) .Throws(); } } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs index 4ca77221..c5f7b1c6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs @@ -9,7 +9,7 @@ public sealed class SQLiteOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithDefaultParameter_CreatesInstance() + public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationToken cancellationToken) { var scope = new SQLiteOutboxTransactionScope(); @@ -17,7 +17,7 @@ public async Task Constructor_WithDefaultParameter_CreatesInstance() } [Test] - public async Task Constructor_WithNullTransaction_CreatesInstance() + public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationToken cancellationToken) { var scope = new SQLiteOutboxTransactionScope(null); @@ -25,7 +25,7 @@ public async Task Constructor_WithNullTransaction_CreatesInstance() } [Test] - public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() + public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(CancellationToken cancellationToken) { var scope = new SQLiteOutboxTransactionScope(); @@ -35,7 +35,7 @@ public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() } [Test] - public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull() + public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull(CancellationToken cancellationToken) { var scope = new SQLiteOutboxTransactionScope(null); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs index 68d68cac..68e97e45 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -12,7 +12,7 @@ public class ServiceCollectionExtensionsTests { [Test] - public void AddPulse_WithNullServices_ThrowsArgumentNullException() + public void AddPulse_WithNullServices_ThrowsArgumentNullException(CancellationToken cancellationToken) { IServiceCollection? services = null; @@ -20,7 +20,7 @@ public void AddPulse_WithNullServices_ThrowsArgumentNullException() } [Test] - public async Task AddPulse_WithoutBuilder_RegistersMediator() + public async Task AddPulse_WithoutBuilder_RegistersMediator(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -38,7 +38,7 @@ public async Task AddPulse_WithoutBuilder_RegistersMediator() } [Test] - public async Task AddPulse_WithBuilder_InvokesBuilderAction() + public async Task AddPulse_WithBuilder_InvokesBuilderAction(CancellationToken cancellationToken) { var services = new ServiceCollection(); var builderInvoked = false; @@ -53,7 +53,7 @@ public async Task AddPulse_WithBuilder_InvokesBuilderAction() } [Test] - public async Task AddPulse_WithActivityAndMetrics_RegistersInterceptors() + public async Task AddPulse_WithActivityAndMetrics_RegistersInterceptors(CancellationToken cancellationToken) { var services = new ServiceCollection(); @@ -80,7 +80,7 @@ public async Task AddPulse_WithActivityAndMetrics_RegistersInterceptors() } [Test] - public async Task AddPulse_CanBeInvokedMultipleTimes() + public async Task AddPulse_CanBeInvokedMultipleTimes(CancellationToken cancellationToken) { var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs index 79cb4727..690879eb 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SqlServer; +namespace NetEvolve.Pulse.Tests.Unit.SqlServer; using System; using System.Threading.Tasks; @@ -14,13 +14,13 @@ public sealed class SqlServerEventOutboxTests { [Test] - public async Task Constructor_WithNullConnection_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnection_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new SqlServerEventOutbox(null!, Options.Create(new OutboxOptions()), TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -30,7 +30,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -40,7 +40,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -50,7 +50,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithTransaction_CreatesInstance() + public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken cancellationToken) { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -65,7 +65,7 @@ public async Task Constructor_WithTransaction_CreatesInstance() } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); var outbox = new SqlServerEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); @@ -76,7 +76,9 @@ public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() } [Test] - public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException() + public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException( + CancellationToken cancellationToken + ) { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); var outbox = new SqlServerEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); @@ -86,7 +88,7 @@ public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationExcepti }; _ = await Assert - .That(async () => await outbox.StoreAsync(message).ConfigureAwait(false)) + .That(async () => await outbox.StoreAsync(message, cancellationToken).ConfigureAwait(false)) .Throws(); } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs index b8618d39..aa9f20ac 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs @@ -17,31 +17,41 @@ public sealed class SqlServerExtensionsTests { [Test] - public async Task AddSqlServerOutbox_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task AddSqlServerOutbox_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => SqlServerExtensions.AddSqlServerOutbox(null!, "Server=.;Encrypt=true;")) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task AddSqlServerOutbox_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox((string)null!)) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task AddSqlServerOutbox_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox(string.Empty)) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task AddSqlServerOutbox_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox(" ")) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining() + public async Task AddSqlServerOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining( + CancellationToken cancellationToken + ) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -52,7 +62,9 @@ public async Task AddSqlServerOutbox_WithValidConnectionString_ReturnsConfigurat } [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped() + public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox("Server=.;Encrypt=true;")); @@ -67,7 +79,9 @@ public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxRe } [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton() + public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddSqlServerOutbox("Server=.;Encrypt=true;")); @@ -82,7 +96,7 @@ public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersTimeProv } [Test] - public async Task AddSqlServerOutbox_WithConfigureOptions_AppliesOptions() + public async Task AddSqlServerOutbox_WithConfigureOptions_AppliesOptions(CancellationToken cancellationToken) { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -96,19 +110,23 @@ public async Task AddSqlServerOutbox_WithConfigureOptions_AppliesOptions() } [Test] - public async Task AddSqlServerOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException() => + public async Task AddSqlServerOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => SqlServerExtensions.AddSqlServerOutbox(null!, _ => "Server=.;Encrypt=true;")) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException() => + public async Task AddSqlServerOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox((Func)null!)) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithFactory_ReturnsConfiguratorForChaining() + public async Task AddSqlServerOutbox_WithFactory_ReturnsConfiguratorForChaining(CancellationToken cancellationToken) { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -119,7 +137,9 @@ public async Task AddSqlServerOutbox_WithFactory_ReturnsConfiguratorForChaining( } [Test] - public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxRepositoryAsScoped() + public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxRepositoryAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox(_ => "Server=.;Encrypt=true;")); @@ -134,7 +154,9 @@ public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxRepositoryAsScop } [Test] - public async Task AddSqlServerOutbox_WithFactory_WithConfigureOptions_AppliesOptions() + public async Task AddSqlServerOutbox_WithFactory_WithConfigureOptions_AppliesOptions( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -150,7 +172,9 @@ public async Task AddSqlServerOutbox_WithFactory_WithConfigureOptions_AppliesOpt } [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped() + public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox("Server=.;Encrypt=true;")); @@ -165,7 +189,9 @@ public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxMa } [Test] - public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxManagementAsScoped() + public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxManagementAsScoped( + CancellationToken cancellationToken + ) { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox(_ => "Server=.;Encrypt=true;")); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs index 76360914..4bc2cb36 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs @@ -13,13 +13,17 @@ public sealed class SqlServerOutboxManagementTests private const string ValidConnectionString = "Server=.;Database=Test;Integrated Security=true;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = null }))) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = string.Empty })) @@ -27,17 +31,19 @@ public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = " " }))) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert.That(() => new SqlServerOutboxManagement(null!)).Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -47,7 +53,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithNullSchema_UsesDefaultDboSchema() + public async Task Constructor_WithNullSchema_UsesDefaultDboSchema(CancellationToken cancellationToken) { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }); @@ -57,7 +63,7 @@ public async Task Constructor_WithNullSchema_UsesDefaultDboSchema() } [Test] - public async Task Constructor_WithEmptySchema_UsesDefaultDboSchema() + public async Task Constructor_WithEmptySchema_UsesDefaultDboSchema(CancellationToken cancellationToken) { var options = Options.Create( new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty } @@ -69,7 +75,7 @@ public async Task Constructor_WithEmptySchema_UsesDefaultDboSchema() } [Test] - public async Task Constructor_WithWhitespaceSchema_UsesDefaultDboSchema() + public async Task Constructor_WithWhitespaceSchema_UsesDefaultDboSchema(CancellationToken cancellationToken) { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = " " }); @@ -79,7 +85,7 @@ public async Task Constructor_WithWhitespaceSchema_UsesDefaultDboSchema() } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance() + public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }); @@ -89,7 +95,9 @@ public async Task Constructor_WithCustomSchema_CreatesInstance() } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -101,7 +109,9 @@ public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgument } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -113,7 +123,9 @@ public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutO } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( + CancellationToken cancellationToken + ) { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs index e15da219..faeb8cc3 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs @@ -11,7 +11,9 @@ public sealed class SqlServerOutboxOptionsExtensionsTests { [Test] - public async Task FullTableName_WithDefaultOptions_Returns_correct_bracketed_name() + public async Task FullTableName_WithDefaultOptions_Returns_correct_bracketed_name( + CancellationToken cancellationToken + ) { var options = new OutboxOptions(); @@ -19,7 +21,7 @@ public async Task FullTableName_WithDefaultOptions_Returns_correct_bracketed_nam } [Test] - public async Task FullTableName_WithCustomSchema_Returns_correct_bracketed_name() + public async Task FullTableName_WithCustomSchema_Returns_correct_bracketed_name(CancellationToken cancellationToken) { var options = new OutboxOptions { Schema = "myschema" }; @@ -27,7 +29,7 @@ public async Task FullTableName_WithCustomSchema_Returns_correct_bracketed_name( } [Test] - public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema() + public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema(CancellationToken cancellationToken) { var options = new OutboxOptions { Schema = null! }; @@ -35,7 +37,9 @@ public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema() } [Test] - public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema() + public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema( + CancellationToken cancellationToken + ) { var options = new OutboxOptions { Schema = " " }; @@ -43,7 +47,9 @@ public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schem } [Test] - public async Task FullTableName_WithCustomTableName_Returns_correct_bracketed_name() + public async Task FullTableName_WithCustomTableName_Returns_correct_bracketed_name( + CancellationToken cancellationToken + ) { var options = new OutboxOptions { TableName = "MyTable" }; @@ -51,7 +57,7 @@ public async Task FullTableName_WithCustomTableName_Returns_correct_bracketed_na } [Test] - public async Task FullTableName_Trims_schema_whitespace() + public async Task FullTableName_Trims_schema_whitespace(CancellationToken cancellationToken) { var options = new OutboxOptions { Schema = " myschema " }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs index d40049bb..3f1da136 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SqlServer; +namespace NetEvolve.Pulse.Tests.Unit.SqlServer; using System; using System.Threading.Tasks; @@ -13,7 +13,9 @@ public sealed class SqlServerOutboxRepositoryTests private const string ValidConnectionString = "Server=.;Database=Test;Integrated Security=true;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -24,7 +26,9 @@ public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullExcepti .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -35,7 +39,9 @@ public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -46,13 +52,15 @@ public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentExcep .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => _ = await Assert .That(() => new SqlServerOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( + CancellationToken cancellationToken + ) => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -63,7 +71,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() .Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance() + public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) { var repository = new SqlServerOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }), @@ -74,7 +82,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance() } [Test] - public async Task Constructor_WithTransactionScope_CreatesInstance() + public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationToken cancellationToken) { var transactionScope = new SqlServerOutboxTransactionScope(null); @@ -88,7 +96,7 @@ public async Task Constructor_WithTransactionScope_CreatesInstance() } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance() + public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }; @@ -98,7 +106,7 @@ public async Task Constructor_WithCustomSchema_CreatesInstance() } [Test] - public async Task Constructor_WithNullSchema_CreatesInstance() + public async Task Constructor_WithNullSchema_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }; @@ -108,7 +116,7 @@ public async Task Constructor_WithNullSchema_CreatesInstance() } [Test] - public async Task Constructor_WithEmptySchema_CreatesInstance() + public async Task Constructor_WithEmptySchema_CreatesInstance(CancellationToken cancellationToken) { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty }; @@ -118,7 +126,7 @@ public async Task Constructor_WithEmptySchema_CreatesInstance() } [Test] - public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() + public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) { var repository = new SqlServerOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }), @@ -126,7 +134,7 @@ public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException() ); _ = await Assert - .That(async () => await repository.AddAsync(null!).ConfigureAwait(false)) + .That(async () => await repository.AddAsync(null!, cancellationToken).ConfigureAwait(false)) .Throws(); } } diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs index 4ad47817..1bc3888f 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs @@ -9,7 +9,7 @@ public sealed class SqlServerOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithDefaultParameter_CreatesInstance() + public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationToken cancellationToken) { var scope = new SqlServerOutboxTransactionScope(); @@ -17,7 +17,7 @@ public async Task Constructor_WithDefaultParameter_CreatesInstance() } [Test] - public async Task Constructor_WithNullTransaction_CreatesInstance() + public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationToken cancellationToken) { var scope = new SqlServerOutboxTransactionScope(null); @@ -25,7 +25,7 @@ public async Task Constructor_WithNullTransaction_CreatesInstance() } [Test] - public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() + public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(CancellationToken cancellationToken) { var scope = new SqlServerOutboxTransactionScope(); @@ -35,7 +35,7 @@ public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() } [Test] - public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull() + public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull(CancellationToken cancellationToken) { var scope = new SqlServerOutboxTransactionScope(null); From 1de2405391a3f792dfbe000f3f5de5fb2cf24bf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 22:21:19 +0200 Subject: [PATCH 14/25] fix(test): Improve SQL Server test DB setup and update outbox tests Refactored SqlServerDatabaseServiceFixture to generate and use a consistent test database name, explicitly create the database during initialization, and ensure reliable connection strings. Updated verified outbox test output for consistency in payloads, timestamps, and event IDs, resulting in more stable and predictable integration tests. --- .../SqlServerDatabaseServiceFixture.cs | 18 +++++++++++++++--- ...cted_Messages_cde079fe90d49e58.verified.txt | 18 +++++------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs index 99553a35..641df89a 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs @@ -1,5 +1,6 @@ namespace NetEvolve.Pulse.Tests.Integration.Internals; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging.Abstractions; using Testcontainers.MsSql; @@ -12,9 +13,9 @@ public sealed class SqlServerDatabaseServiceFixture : IDatabaseServiceFixture .Build(); public string ConnectionString => - _container - .GetConnectionString() - .Replace("master", $"{TestHelper.TargetFramework}{Guid.NewGuid():N}", StringComparison.Ordinal); + _container.GetConnectionString().Replace("master", DatabaseName, StringComparison.Ordinal); + + internal string DatabaseName { get; } = $"{TestHelper.TargetFramework}{Guid.NewGuid():N}"; public DatabaseType DatabaseType => DatabaseType.SqlServer; @@ -25,6 +26,17 @@ public async Task InitializeAsync() try { await _container.StartAsync().WaitAsync(TimeSpan.FromMinutes(2)); + + // Create temporary database to ensure the container is fully initialized and ready to accept connections + await using var con = new SqlConnection(_container.GetConnectionString()); + await con.OpenAsync(); + + await using var cmd = con.CreateCommand(); +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + cmd.CommandText = $"CREATE DATABASE [{DatabaseName}]"; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + + _ = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt index 70a06431..4ee756e4 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt +++ b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/SqlServerEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_cde079fe90d49e58.verified.txt @@ -3,17 +3,17 @@ CreatedAt: DateTimeOffset_1, EventType: OutboxTestsBase.TestEvent, Id: Guid_1, - Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Payload: {"CorrelationId":null,"Id":"Test000","PublishedAt":"2025-01-01T12:00:00+00:00"}, Status: Processing, UpdatedAt: DateTimeOffset_1 }, { - CreatedAt: DateTimeOffset_2, + CreatedAt: DateTimeOffset_1, EventType: OutboxTestsBase.TestEvent, Id: Guid_2, - Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2026-04-12T10:09:40.9420622+00:00"}, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, Status: Processing, - UpdatedAt: DateTimeOffset_3 + UpdatedAt: DateTimeOffset_1 }, { CreatedAt: DateTimeOffset_1, @@ -21,14 +21,6 @@ Id: Guid_3, Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2025-01-01T12:00:00+00:00"}, Status: Processing, - UpdatedAt: DateTimeOffset_3 - }, - { - CreatedAt: DateTimeOffset_4, - EventType: OutboxTestsBase.TestEvent, - Id: Guid_4, - Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2026-04-12T10:09:41.0637344+00:00"}, - Status: Processing, - UpdatedAt: DateTimeOffset_3 + UpdatedAt: DateTimeOffset_1 } ] \ No newline at end of file From d6f94d759a1fe89c4a08e8bb2da41f5b44050e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 12 Apr 2026 22:52:00 +0200 Subject: [PATCH 15/25] refactor(test): DB container management and sharing Refactor integration test infrastructure to introduce shared PostgreSQL and SQL Server container fixtures, reducing container startup overhead. Database service fixtures now create per-test databases within shared containers. Removed custom parallel limiter and updated test classes for improved efficiency and maintainability. --- .../Internals/ContainerParallelLimiter.cs | 8 ----- .../Internals/PostgreSqlContainerFixture.cs | 20 +++++++++++++ .../PostgreSqlDatabaseServiceFixture.cs | 29 +++++++++++-------- .../Internals/SqlServerContainerFixture.cs | 20 +++++++++++++ .../SqlServerDatabaseServiceFixture.cs | 17 ++++------- .../PostgreSqlEntityFrameworkOutboxTests.cs | 1 - .../SqlServerEntityFrameworkOutboxTests.cs | 1 - 7 files changed, 62 insertions(+), 34 deletions(-) delete mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlContainerFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs deleted file mode 100644 index d089a68e..00000000 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/ContainerParallelLimiter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace NetEvolve.Pulse.Tests.Integration.Outbox; - -using TUnit.Core.Interfaces; - -internal class ContainerParallelLimiter : IParallelLimit -{ - public int Limit => (Math.Max(1, (Environment.ProcessorCount / 3) - 1)); -} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlContainerFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlContainerFixture.cs new file mode 100644 index 00000000..69229c77 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlContainerFixture.cs @@ -0,0 +1,20 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using Microsoft.Extensions.Logging.Abstractions; +using Testcontainers.PostgreSql; +using TUnit.Core.Interfaces; + +public sealed class PostgreSqlContainerFixture : IAsyncDisposable, IAsyncInitializer +{ + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder( + /*dockerimage*/"postgres:15.17" + ) + .WithLogger(NullLogger.Instance) + .Build(); + + public string ConnectionString => _container.GetConnectionString() + ";Include Error Detail=true;"; + + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + public async Task InitializeAsync() => await _container.StartAsync().ConfigureAwait(false); +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs index 30b5b9b6..181bddbc 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/PostgreSqlDatabaseServiceFixture.cs @@ -1,28 +1,33 @@ namespace NetEvolve.Pulse.Tests.Integration.Internals; -using Microsoft.Extensions.Logging.Abstractions; -using Testcontainers.PostgreSql; - public sealed class PostgreSqlDatabaseServiceFixture : IDatabaseServiceFixture { - private readonly PostgreSqlContainer _container = new PostgreSqlBuilder( - /*dockerimage*/"postgres:15.17" - ) - .WithLogger(NullLogger.Instance) - .WithDatabase($"{TestHelper.TargetFramework}{Guid.NewGuid():N}") - .Build(); + [ClassDataSource(Shared = SharedType.PerTestSession)] + public PostgreSqlContainerFixture Container { get; set; } = default!; + + public string ConnectionString => + Container.ConnectionString.Replace("Database=postgres;", $"Database={DatabaseName};", StringComparison.Ordinal); - public string ConnectionString => _container.GetConnectionString() + ";Include Error Detail=true;"; + internal string DatabaseName { get; } = $"{TestHelper.TargetFramework}{Guid.NewGuid():N}"; public DatabaseType DatabaseType => DatabaseType.PostgreSQL; - public ValueTask DisposeAsync() => _container.DisposeAsync(); + public ValueTask DisposeAsync() => ValueTask.CompletedTask; public async Task InitializeAsync() { try { - await _container.StartAsync().WaitAsync(TimeSpan.FromMinutes(2)); + // Create temporary database to ensure the container is fully initialized and ready to accept connections + await using var con = new Npgsql.NpgsqlConnection(Container.ConnectionString); + await con.OpenAsync(); + + await using var cmd = con.CreateCommand(); +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + cmd.CommandText = $"CREATE DATABASE \"{DatabaseName}\""; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + + _ = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs new file mode 100644 index 00000000..74cfe441 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs @@ -0,0 +1,20 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using Microsoft.Extensions.Logging.Abstractions; +using Testcontainers.MsSql; +using TUnit.Core.Interfaces; + +public sealed class SqlServerContainerFixture : IAsyncDisposable, IAsyncInitializer +{ + private readonly MsSqlContainer _container = new MsSqlBuilder( + /*dockerimage*/"mcr.microsoft.com/mssql/server:2022-RTM-ubuntu-20.04" + ) + .WithLogger(NullLogger.Instance) + .Build(); + + public string ConnectionString => _container.GetConnectionString(); + + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + public async Task InitializeAsync() => await _container.StartAsync().ConfigureAwait(false); +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs index 641df89a..eca409f8 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerDatabaseServiceFixture.cs @@ -1,34 +1,27 @@ namespace NetEvolve.Pulse.Tests.Integration.Internals; using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging.Abstractions; -using Testcontainers.MsSql; public sealed class SqlServerDatabaseServiceFixture : IDatabaseServiceFixture { - private readonly MsSqlContainer _container = new MsSqlBuilder( - /*dockerimage*/"mcr.microsoft.com/mssql/server:2022-RTM-ubuntu-20.04" - ) - .WithLogger(NullLogger.Instance) - .Build(); + [ClassDataSource(Shared = SharedType.PerTestSession)] + public SqlServerContainerFixture Container { get; set; } = default!; public string ConnectionString => - _container.GetConnectionString().Replace("master", DatabaseName, StringComparison.Ordinal); + Container.ConnectionString.Replace("master", DatabaseName, StringComparison.Ordinal); internal string DatabaseName { get; } = $"{TestHelper.TargetFramework}{Guid.NewGuid():N}"; public DatabaseType DatabaseType => DatabaseType.SqlServer; - public ValueTask DisposeAsync() => _container.DisposeAsync(); + public ValueTask DisposeAsync() => ValueTask.CompletedTask; public async Task InitializeAsync() { try { - await _container.StartAsync().WaitAsync(TimeSpan.FromMinutes(2)); - // Create temporary database to ensure the container is fully initialized and ready to accept connections - await using var con = new SqlConnection(_container.GetConnectionString()); + await using var con = new SqlConnection(Container.ConnectionString); await con.OpenAsync(); await using var cmd = con.CreateCommand(); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs index a844f996..09bd1641 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/PostgreSqlEntityFrameworkOutboxTests.cs @@ -8,7 +8,6 @@ )] [TestGroup("PostgreSql")] [TestGroup("EntityFramework")] -[ParallelLimiter] [InheritsTests] public class PostgreSqlEntityFrameworkOutboxTests( IDatabaseServiceFixture databaseServiceFixture, diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs index ad38bde9..2d1a1c13 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/SqlServerEntityFrameworkOutboxTests.cs @@ -8,7 +8,6 @@ )] [TestGroup("SqlServer")] [TestGroup("EntityFramework")] -[ParallelLimiter] [InheritsTests] public class SqlServerEntityFrameworkOutboxTests( IDatabaseServiceFixture databaseServiceFixture, From ae53067441dc8ea1867a61740caeb888e50ea562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 00:05:25 +0200 Subject: [PATCH 16/25] fix: Skip processing loop when disabled Added a continue statement after the delay when processing is disabled, ensuring the background service loop immediately restarts and avoids executing further logic in that iteration. This prevents unnecessary processing when the service is set to disabled. --- src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs b/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs index cbc9c9df..2f2d2f79 100644 --- a/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs +++ b/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs @@ -133,6 +133,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (_options.DisableProcessing) { await Task.Delay(_options.PollingInterval, stoppingToken).ConfigureAwait(false); + continue; } try From 4cd6e8bd77c37e987baa4e6f129e6b275e758851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 00:17:40 +0200 Subject: [PATCH 17/25] refactor: clarify tracking vs bulk ops in EF outbox repo Rename _isInMemory to _useTrackingOperations to better reflect when tracking operations are required (currently only for the InMemory provider). Update all relevant checks and assignments for improved clarity and maintainability. No change to underlying logic. --- .../Outbox/EntityFrameworkOutboxRepository.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs index 94e0ece5..00c561bb 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs @@ -30,7 +30,7 @@ internal sealed class EntityFrameworkOutboxRepository : IOutboxReposit /// when the current EF Core provider is the in-memory provider, /// which does not support ExecuteUpdate / ExecuteDelete. /// - private readonly bool _isInMemory; + private readonly bool _useTrackingOperations; /// /// Initializes a new instance of the class. @@ -44,7 +44,8 @@ public EntityFrameworkOutboxRepository(TContext context, TimeProvider timeProvid _context = context; _timeProvider = timeProvider; - _isInMemory = context.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory"; + var providerName = context.Database.ProviderName; + _useTrackingOperations = providerName == "Microsoft.EntityFrameworkCore.InMemory"; } /// @@ -425,7 +426,7 @@ private async Task UpdateEntitiesAsync( CancellationToken cancellationToken ) { - if (_isInMemory) + if (_useTrackingOperations) { var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); @@ -454,7 +455,7 @@ CancellationToken cancellationToken /// The number of deleted rows. private async Task DeleteEntitiesAsync(IQueryable query, CancellationToken cancellationToken) { - if (_isInMemory) + if (_useTrackingOperations) { var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); From 8964d6ae66128450c0ab162a88502ddeb33fdbe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 00:18:05 +0200 Subject: [PATCH 18/25] refactor: Disable EF Core service provider caching in tests Ensures each test gets a fresh model by disabling EF Core's internal service provider caching. This prevents tests from reusing cached models when using the same connection string, avoiding interference and improving test isolation. --- .../Internals/EntityFrameworkInitializer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index 7b0ea881..44a1c8bd 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -71,6 +71,11 @@ public void Initialize(IServiceCollection services, IDatabaseServiceFixture data { var connectionString = databaseService.ConnectionString; + // Disable EF Core's global internal service provider cache so that each test + // gets a freshly-built model (with its own table name) instead of reusing a + // cached model from a previous test that used the same connection string. + _ = options.EnableServiceProviderCaching(false); + _ = databaseService.DatabaseType switch { DatabaseType.InMemory => options.UseInMemoryDatabase(connectionString), From 6a9da0dec8a91cc9698226aae62681b95242d3c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 00:39:57 +0200 Subject: [PATCH 19/25] chore: Use custom model cache key for per-test table names Replace EnableServiceProviderCaching(false) with a custom IModelCacheKeyFactory that includes the table name in the EF Core model cache key. This prevents model reuse across tests sharing a connection string, avoiding "Table '...' already exists" errors, while preserving correct type-mapping initialization. --- .../Internals/EntityFrameworkInitializer.cs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index 44a1c8bd..e4b29664 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -71,10 +71,14 @@ public void Initialize(IServiceCollection services, IDatabaseServiceFixture data { var connectionString = databaseService.ConnectionString; - // Disable EF Core's global internal service provider cache so that each test - // gets a freshly-built model (with its own table name) instead of reusing a - // cached model from a previous test that used the same connection string. - _ = options.EnableServiceProviderCaching(false); + // Register a custom model-cache key factory that includes the per-test table name. + // Multiple tests share the same connection string (same container), so EF Core would + // otherwise cache the first test's model (with its table name) and reuse it for all + // subsequent tests — causing "Table '...' already exists" errors on CreateTablesAsync. + // This factory makes the cache key unique per (DbContext type, TableName), so each + // test gets its own model while still sharing the internal EF Core service provider + // (critical for correct type-mapping initialisation on providers like Oracle MySQL). + options.ReplaceService(); _ = databaseService.DatabaseType switch { @@ -120,6 +124,32 @@ public override async Task ConnectionOpenedAsync( } } + /// + /// Custom EF Core model-cache key factory that incorporates + /// into the cache key. Without this, all tests that share the same database connection string + /// receive the same cached EF Core model (keyed only by type), so the + /// second test picks up the first test's table name and CreateTablesAsync fails with + /// "Table '<first-test-name>' already exists". + /// + private sealed class OutboxTableModelCacheKeyFactory : IModelCacheKeyFactory + { + /// + public object Create(DbContext context, bool designTime) + { + string tableName; + try + { + tableName = context.GetService>()?.Value?.TableName ?? string.Empty; + } + catch (InvalidOperationException) + { + tableName = string.Empty; + } + + return (context.GetType(), tableName, designTime); + } + } + private sealed class TestDbContext : DbContext, IOutboxDbContext { public DbSet OutboxMessages => Set(); From f72600dea55f844f62dede38332db3f673363b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 00:41:27 +0200 Subject: [PATCH 20/25] fix: Move disable processing check inside try block The check for `_options.DisableProcessing` and its delay was moved from outside to inside the `try` block in `OutboxProcessorHostedService`. This ensures any exceptions during the check or delay are now handled by the existing exception handling logic. --- .../Outbox/OutboxProcessorHostedService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs b/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs index 2f2d2f79..c26c825e 100644 --- a/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs +++ b/src/NetEvolve.Pulse/Outbox/OutboxProcessorHostedService.cs @@ -130,14 +130,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) while (!stoppingToken.IsCancellationRequested) { - if (_options.DisableProcessing) - { - await Task.Delay(_options.PollingInterval, stoppingToken).ConfigureAwait(false); - continue; - } - try { + if (_options.DisableProcessing) + { + await Task.Delay(_options.PollingInterval, stoppingToken).ConfigureAwait(false); + continue; + } + // Check database health before processing var isDatabaseHealthy = await _repository.IsHealthyAsync(stoppingToken).ConfigureAwait(false); if (!isDatabaseHealthy) From c1b1bc7e320054ddf5acec93b4cd3db876600896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 09:44:06 +0200 Subject: [PATCH 21/25] feat: Add Oracle MySQL EF Core provider support for Outbox - Implements provider-specific outbox operations via IOutboxOperationsExecutor and new executor classes - Maps Guid to binary(16) and DateTimeOffset to bigint for MySQL (Oracle) to work around provider limitations - Adds MySql.EntityFrameworkCore and Testcontainers.MySql dependencies - Adds MySQL integration test fixture and test suite - Updates model metadata tests and documentation for MySQL - Refactors repository to delegate all mutations to the correct executor - Improves SQL Server test container connection string and test clarity --- Directory.Packages.props | 2 + .../MySqlOutboxMessageConfiguration.cs | 63 +++- .../Outbox/BulkOutboxOperationsExecutor.cs | 159 +++++++++ .../Outbox/EntityFrameworkOutboxManagement.cs | 12 +- .../Outbox/EntityFrameworkOutboxRepository.cs | 336 ++++-------------- .../Outbox/IOutboxOperationsExecutor.cs | 46 +++ .../InMemoryOutboxOperationsExecutor.cs | 42 +++ .../Outbox/MySqlOutboxOperationsExecutor.cs | 84 +++++ .../TrackingOutboxOperationsExecutorBase.cs | 145 ++++++++ .../Internals/EntityFrameworkInitializer.cs | 8 +- .../Internals/MySqlContainerFixture.cs | 71 ++++ .../Internals/SqlServerContainerFixture.cs | 2 +- .../NetEvolve.Pulse.Tests.Integration.csproj | 2 + .../Outbox/MySqlEntityFrameworkOutboxTests.cs | 13 + .../Outbox/OutboxTestsBase.cs | 7 +- ...ted_Messages_2f3dd6e39de993fc.verified.txt | 26 ++ .../EntityFrameworkOutboxRepositoryTests.cs | 6 +- ...OutboxMessageConfigurationMetadataTests.cs | 30 +- 18 files changed, 749 insertions(+), 305 deletions(-) create mode 100644 src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs create mode 100644 src/NetEvolve.Pulse.EntityFramework/Outbox/IOutboxOperationsExecutor.cs create mode 100644 src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs create mode 100644 src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs create mode 100644 src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Outbox/MySqlEntityFrameworkOutboxTests.cs create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/MySqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_2f3dd6e39de993fc.verified.txt diff --git a/Directory.Packages.props b/Directory.Packages.props index a09b63d3..ccf79609 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,6 +31,7 @@ + @@ -41,6 +42,7 @@ + diff --git a/src/NetEvolve.Pulse.EntityFramework/Configurations/MySqlOutboxMessageConfiguration.cs b/src/NetEvolve.Pulse.EntityFramework/Configurations/MySqlOutboxMessageConfiguration.cs index cd61866a..418df1fb 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Configurations/MySqlOutboxMessageConfiguration.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Configurations/MySqlOutboxMessageConfiguration.cs @@ -7,18 +7,26 @@ using NetEvolve.Pulse.Outbox; /// -/// Entity Framework Core configuration for targeting MySQL. -/// Supports both Pomelo (Pomelo.EntityFrameworkCore.MySql) and the Oracle -/// (MySql.EntityFrameworkCore) providers. +/// Entity Framework Core configuration for targeting MySQL +/// via the Oracle provider (MySql.EntityFrameworkCore). /// /// /// Column Types: /// -/// char(36) for (UUID string representation) +/// binary(16) for — raw 16-byte UUID with a byte[] value converter /// varchar(n) for bounded strings /// longtext for unbounded strings (Payload, Error) -/// datetime(6) for — Pomelo stores values as UTC +/// bigint for — stored as UTC ticks via a value converter /// +/// Why binary(16) and bigint: +/// The Oracle MySQL provider does not produce a valid type mapping for +/// char(36) SQL parameter binding (returns , +/// causing a in +/// TypeMappedRelationalParameter.AddDbParameter). +/// Using binary(16) with an explicit byte[] converter provides a working +/// binding. Similarly, the provider lacks a proper SQL type +/// mapping; converting to (UTC ticks) eliminates the broken +/// provider-specific type resolution and ensures correct ordering and comparison semantics. /// Filtered Indexes: /// MySQL does not support partial/filtered indexes with a WHERE clause. /// All filter properties inherit from the base class, @@ -60,17 +68,48 @@ public MySqlOutboxMessageConfiguration(IOptions options) /// protected override void ApplyColumnTypes(EntityTypeBuilder builder) { - // char(36) is the canonical UUID string format used by Pomelo and Oracle MySQL provider - _ = builder.Property(m => m.Id).HasColumnType("char(36)"); + // binary(16) stores the raw 16-byte UUID — half the storage of char(36), faster binary + // comparisons, and better index locality. Critically, the Oracle MySQL provider + // (MySql.EntityFrameworkCore) has a working byte[]→binary(16) type mapping for SQL + // parameter binding, whereas Guid→char(36) returns a null TypeMapping and throws + // NullReferenceException inside TypeMappedRelationalParameter.AddDbParameter. + _ = builder + .Property(m => m.Id) + .HasColumnType("binary(16)") + .HasConversion(v => v.ToByteArray(), v => new Guid(v)); _ = builder.Property(m => m.EventType).HasColumnType("varchar(500)"); // longtext covers MySQL's maximum row size for arbitrarily large JSON payloads _ = builder.Property(m => m.Payload).HasColumnType("longtext"); _ = builder.Property(m => m.CorrelationId).HasColumnType("varchar(100)"); - // datetime(6) stores microsecond precision; Pomelo converts DateTimeOffset to UTC - _ = builder.Property(m => m.CreatedAt).HasColumnType("datetime(6)"); - _ = builder.Property(m => m.UpdatedAt).HasColumnType("datetime(6)"); - _ = builder.Property(m => m.ProcessedAt).HasColumnType("datetime(6)"); - _ = builder.Property(m => m.NextRetryAt).HasColumnType("datetime(6)"); + + // DateTimeOffset columns are stored as BIGINT (UTC ticks), matching the SQLite + // approach (INTEGER / UTC ticks). The Oracle MySQL provider lacks a proper + // DateTimeOffset type mapping for parameterised operations (ExecuteUpdateAsync, + // IN clauses, etc.). Converting to long eliminates the broken provider-specific + // type resolution and ensures correct ordering and comparison semantics. + _ = builder + .Property(m => m.CreatedAt) + .HasColumnType("bigint") + .HasConversion(v => v.UtcTicks, v => new DateTimeOffset(v, TimeSpan.Zero)); + _ = builder + .Property(m => m.UpdatedAt) + .HasColumnType("bigint") + .HasConversion(v => v.UtcTicks, v => new DateTimeOffset(v, TimeSpan.Zero)); + _ = builder + .Property(m => m.ProcessedAt) + .HasColumnType("bigint") + .HasConversion( + v => v.HasValue ? (long?)v.Value.UtcTicks : null, + v => v.HasValue ? (DateTimeOffset?)new DateTimeOffset(v.Value, TimeSpan.Zero) : null + ); + _ = builder + .Property(m => m.NextRetryAt) + .HasColumnType("bigint") + .HasConversion( + v => v.HasValue ? (long?)v.Value.UtcTicks : null, + v => v.HasValue ? (DateTimeOffset?)new DateTimeOffset(v.Value, TimeSpan.Zero) : null + ); + _ = builder.Property(m => m.RetryCount).HasColumnType("int"); _ = builder.Property(m => m.Error).HasColumnType("longtext"); _ = builder.Property(m => m.Status).HasColumnType("int"); diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs new file mode 100644 index 00000000..bf4a6012 --- /dev/null +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs @@ -0,0 +1,159 @@ +namespace NetEvolve.Pulse.Outbox; + +using Microsoft.EntityFrameworkCore; +using NetEvolve.Pulse.Extensibility.Outbox; + +/// +/// implementation that issues a single bulk +/// ExecuteUpdateAsync / ExecuteDeleteAsync statement per operation. +/// +/// +/// Suitable for any EF Core provider that supports these operations and can correctly +/// translate a parameterised collection into a SQL IN clause +/// (SQL Server, PostgreSQL, SQLite, and others). +/// +/// The DbContext type that implements . +internal sealed class BulkOutboxOperationsExecutor(TContext context) : IOutboxOperationsExecutor + where TContext : DbContext, IOutboxDbContext +{ + private readonly SemaphoreSlim _semaphore = new(1, 1); + private bool _disposedValue; + + /// + public async Task FetchAndMarkAsync( + IQueryable baseQuery, + DateTimeOffset updatedAt, + OutboxMessageStatus newStatus, + CancellationToken cancellationToken + ) + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + var ids = await baseQuery + .AsNoTracking() + .Select(m => m.Id) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + + if (ids.Length == 0) + { + return []; + } + + _ = await baseQuery + .Where(m => ids.Contains(m.Id)) + .ExecuteUpdateAsync( + m => m.SetProperty(m => m.Status, newStatus).SetProperty(m => m.UpdatedAt, updatedAt), + cancellationToken + ) + .ConfigureAwait(false); + + return await context + .OutboxMessages.AsNoTracking() + .Where(m => ids.Contains(m.Id)) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public async Task UpdateByQueryAsync( + IQueryable query, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ) + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + _ = await query + .ExecuteUpdateAsync( + m => + m.SetProperty(p => p.UpdatedAt, updatedAt) + .SetProperty( + p => p.ProcessedAt, + p => +#pragma warning disable IDE0030, RCS1084 // Use coalesce expression instead of conditional expression + processedAt.HasValue ? processedAt.Value : p.ProcessedAt +#pragma warning restore IDE0030, RCS1084 // Use coalesce expression instead of conditional expression + ) + .SetProperty(p => p.NextRetryAt, nextRetryAt) + .SetProperty(p => p.Status, newStatus) + .SetProperty(p => p.RetryCount, p => p.RetryCount + retryIncrement) + .SetProperty(p => p.Error, errorMessage), + cancellationToken + ) + .ConfigureAwait(false); + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public Task UpdateByIdsAsync( + IReadOnlyCollection ids, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ) => + UpdateByQueryAsync( + context.OutboxMessages.Where(m => ids.Contains(m.Id)), + updatedAt, + processedAt, + nextRetryAt, + newStatus, + retryIncrement, + errorMessage, + cancellationToken + ); + + /// + public async Task DeleteByQueryAsync(IQueryable query, CancellationToken cancellationToken) + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + return await query.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _ = _semaphore.Release(); + } + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _semaphore.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxManagement.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxManagement.cs index 72cb70fd..6ef0b7ce 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxManagement.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxManagement.cs @@ -12,7 +12,7 @@ internal sealed class EntityFrameworkOutboxManagement : IOutboxManagem where TContext : DbContext, IOutboxDbContext { /// Pre-compiled paged dead-letter query; eliminates expression-tree overhead on every call. - private static readonly Func> _deadLetterPageQuery = + private static readonly Func> DeadLetterPageQuery = EF.CompileAsyncQuery( (TContext ctx, int skip, int take) => ctx @@ -24,7 +24,7 @@ internal sealed class EntityFrameworkOutboxManagement : IOutboxManagem ); /// Pre-compiled single dead-letter lookup; eliminates expression-tree overhead on every call. - private static readonly Func> _deadLetterByIdQuery = EF.CompileAsyncQuery( + private static readonly Func> DeadLetterByIdQuery = EF.CompileAsyncQuery( (TContext ctx, Guid id) => ctx .OutboxMessages.Where(m => m.Id == id && m.Status == OutboxMessageStatus.DeadLetter) @@ -33,7 +33,7 @@ internal sealed class EntityFrameworkOutboxManagement : IOutboxManagem ); /// Pre-compiled dead-letter count query; eliminates expression-tree overhead on every call. - private static readonly Func> _deadLetterCountQuery = EF.CompileAsyncQuery( + private static readonly Func> DeadLetterCountQuery = EF.CompileAsyncQuery( (TContext ctx) => ctx.OutboxMessages.LongCount(m => m.Status == OutboxMessageStatus.DeadLetter) ); @@ -69,7 +69,7 @@ public async Task> GetDeadLetterMessagesAsync( var result = new List(pageSize); await foreach ( - var message in _deadLetterPageQuery(_context, page * pageSize, pageSize) + var message in DeadLetterPageQuery(_context, page * pageSize, pageSize) .WithCancellation(cancellationToken) .ConfigureAwait(false) ) @@ -84,14 +84,14 @@ var message in _deadLetterPageQuery(_context, page * pageSize, pageSize) public Task GetDeadLetterMessageAsync(Guid messageId, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - return _deadLetterByIdQuery(_context, messageId); + return DeadLetterByIdQuery(_context, messageId); } /// public Task GetDeadLetterCountAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - return _deadLetterCountQuery(_context); + return DeadLetterCountQuery(_context); } /// diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs index 00c561bb..01f51829 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs @@ -20,17 +20,17 @@ internal sealed class EntityFrameworkOutboxRepository : IOutboxRepository where TContext : DbContext, IOutboxDbContext { - /// The DbContext used for all LINQ-to-SQL query and update operations. + /// The DbContext used to build LINQ queries passed to the executor. private readonly TContext _context; /// The time provider used to generate consistent update and cutoff timestamps. private readonly TimeProvider _timeProvider; /// - /// when the current EF Core provider is the in-memory provider, - /// which does not support ExecuteUpdate / ExecuteDelete. + /// Provider-specific strategy that handles how entities are persisted + /// (change-tracking + SaveChangesAsync vs. bulk ExecuteUpdate / ExecuteDelete). /// - private readonly bool _useTrackingOperations; + private readonly IOutboxOperationsExecutor _executor; /// /// Initializes a new instance of the class. @@ -44,8 +44,15 @@ public EntityFrameworkOutboxRepository(TContext context, TimeProvider timeProvid _context = context; _timeProvider = timeProvider; - var providerName = context.Database.ProviderName; - _useTrackingOperations = providerName == "Microsoft.EntityFrameworkCore.InMemory"; + _executor = context.Database.ProviderName switch + { + // InMemory does not support ExecuteUpdate/ExecuteDelete at all. + "Microsoft.EntityFrameworkCore.InMemory" => new InMemoryOutboxOperationsExecutor(context), + // Oracle MySQL cannot apply value converters in ExecuteUpdateAsync parameters and + // cannot translate a parameterised Guid collection into a SQL IN clause. + "MySql.EntityFrameworkCore" => new MySqlOutboxOperationsExecutor(context), + _ => new BulkOutboxOperationsExecutor(context), + }; } /// @@ -65,42 +72,15 @@ public async Task> GetPendingAsync( { var now = _timeProvider.GetUtcNow(); - var ids = await _context - .OutboxMessages.AsNoTracking() - .Where(m => m.Status == OutboxMessageStatus.Pending && (m.NextRetryAt == null || m.NextRetryAt <= now)) - .OrderBy(m => m.CreatedAt) - .Take(batchSize) - .Select(m => m.Id) - .ToArrayAsync(cancellationToken) - .ConfigureAwait(false); - - if (ids.Length == 0) - { - return []; - } - - await UpdateEntitiesAsync( - _context.OutboxMessages.Where(m => ids.Contains(m.Id) && m.Status == OutboxMessageStatus.Pending), - msg => - { - msg.Status = OutboxMessageStatus.Processing; - msg.UpdatedAt = now; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Processing) - .SetProperty(m => m.UpdatedAt, now), - ct - ), - cancellationToken + var baseQuery = _context + .OutboxMessages.Where(m => + m.Status == OutboxMessageStatus.Pending && (m.NextRetryAt == null || m.NextRetryAt <= now) ) - .ConfigureAwait(false); + .OrderBy(m => m.CreatedAt) + .Take(batchSize); - return await _context - .OutboxMessages.AsNoTracking() - .Where(m => ids.Contains(m.Id)) - .ToArrayAsync(cancellationToken) + return await _executor + .FetchAndMarkAsync(baseQuery, now, OutboxMessageStatus.Processing, cancellationToken) .ConfigureAwait(false); } @@ -123,46 +103,17 @@ public async Task> GetFailedForRetryAsync( { var now = _timeProvider.GetUtcNow(); - var ids = await _context - .OutboxMessages.AsNoTracking() - .Where(m => + var baseQuery = _context + .OutboxMessages.Where(m => m.Status == OutboxMessageStatus.Failed && m.RetryCount < maxRetryCount && (m.NextRetryAt == null || m.NextRetryAt <= now) ) .OrderBy(m => m.UpdatedAt) - .Take(batchSize) - .Select(m => m.Id) - .ToArrayAsync(cancellationToken) - .ConfigureAwait(false); + .Take(batchSize); - if (ids.Length == 0) - { - return []; - } - - await UpdateEntitiesAsync( - _context.OutboxMessages.Where(m => ids.Contains(m.Id) && m.Status == OutboxMessageStatus.Failed), - msg => - { - msg.Status = OutboxMessageStatus.Processing; - msg.UpdatedAt = now; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Processing) - .SetProperty(m => m.UpdatedAt, now), - ct - ), - cancellationToken - ) - .ConfigureAwait(false); - - return await _context - .OutboxMessages.AsNoTracking() - .Where(m => ids.Contains(m.Id)) - .ToArrayAsync(cancellationToken) + return await _executor + .FetchAndMarkAsync(baseQuery, now, OutboxMessageStatus.Processing, cancellationToken) .ConfigureAwait(false); } @@ -171,22 +122,15 @@ public async Task MarkAsCompletedAsync(Guid messageId, CancellationToken cancell { var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( + await _executor + .UpdateByQueryAsync( _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), - msg => - { - msg.Status = OutboxMessageStatus.Completed; - msg.ProcessedAt = now; - msg.UpdatedAt = now; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Completed) - .SetProperty(m => m.ProcessedAt, now) - .SetProperty(m => m.UpdatedAt, now), - ct - ), + now, + now, + null, + OutboxMessageStatus.Completed, + 0, + null, cancellationToken ) .ConfigureAwait(false); @@ -205,26 +149,8 @@ public async Task MarkAsCompletedAsync( var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( - _context.OutboxMessages.Where(m => - messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing - ), - msg => - { - msg.Status = OutboxMessageStatus.Completed; - msg.ProcessedAt = now; - msg.UpdatedAt = now; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Completed) - .SetProperty(m => m.ProcessedAt, now) - .SetProperty(m => m.UpdatedAt, now), - ct - ), - cancellationToken - ) + await _executor + .UpdateByIdsAsync(messageIds, now, now, null, OutboxMessageStatus.Completed, 0, null, cancellationToken) .ConfigureAwait(false); } @@ -237,24 +163,15 @@ public async Task MarkAsFailedAsync( { var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( + await _executor + .UpdateByQueryAsync( _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), - msg => - { - msg.Status = OutboxMessageStatus.Failed; - msg.Error = errorMessage; - msg.UpdatedAt = now; - msg.RetryCount++; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now) - .SetProperty(m => m.RetryCount, m => m.RetryCount + 1), - ct - ), + now, + null, + null, + OutboxMessageStatus.Failed, + 1, + errorMessage, cancellationToken ) .ConfigureAwait(false); @@ -270,26 +187,15 @@ public async Task MarkAsFailedAsync( { var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( + await _executor + .UpdateByQueryAsync( _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), - msg => - { - msg.Status = OutboxMessageStatus.Failed; - msg.Error = errorMessage; - msg.UpdatedAt = now; - msg.RetryCount++; - msg.NextRetryAt = nextRetryAt; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now) - .SetProperty(m => m.RetryCount, m => m.RetryCount + 1) - .SetProperty(m => m.NextRetryAt, nextRetryAt), - ct - ), + now, + null, + nextRetryAt, + OutboxMessageStatus.Failed, + 1, + errorMessage, cancellationToken ) .ConfigureAwait(false); @@ -309,26 +215,15 @@ public async Task MarkAsFailedAsync( var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( - _context.OutboxMessages.Where(m => - messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing - ), - msg => - { - msg.Status = OutboxMessageStatus.Failed; - msg.Error = errorMessage; - msg.UpdatedAt = now; - msg.RetryCount++; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.Failed) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now) - .SetProperty(m => m.RetryCount, m => m.RetryCount + 1), - ct - ), + await _executor + .UpdateByIdsAsync( + messageIds, + now, + null, + null, + OutboxMessageStatus.Failed, + 1, + errorMessage, cancellationToken ) .ConfigureAwait(false); @@ -343,22 +238,15 @@ public async Task MarkAsDeadLetterAsync( { var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( + await _executor + .UpdateByQueryAsync( _context.OutboxMessages.Where(m => m.Id == messageId && m.Status == OutboxMessageStatus.Processing), - msg => - { - msg.Status = OutboxMessageStatus.DeadLetter; - msg.Error = errorMessage; - msg.UpdatedAt = now; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.DeadLetter) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now), - ct - ), + now, + null, + null, + OutboxMessageStatus.DeadLetter, + 0, + errorMessage, cancellationToken ) .ConfigureAwait(false); @@ -378,24 +266,15 @@ public async Task MarkAsDeadLetterAsync( var now = _timeProvider.GetUtcNow(); - await UpdateEntitiesAsync( - _context.OutboxMessages.Where(m => - messageIds.Contains(m.Id) && m.Status == OutboxMessageStatus.Processing - ), - msg => - { - msg.Status = OutboxMessageStatus.DeadLetter; - msg.Error = errorMessage; - msg.UpdatedAt = now; - }, - (q, ct) => - q.ExecuteUpdateAsync( - m => - m.SetProperty(m => m.Status, OutboxMessageStatus.DeadLetter) - .SetProperty(m => m.Error, errorMessage) - .SetProperty(m => m.UpdatedAt, now), - ct - ), + await _executor + .UpdateByIdsAsync( + messageIds, + now, + null, + null, + OutboxMessageStatus.DeadLetter, + 1, + errorMessage, cancellationToken ) .ConfigureAwait(false); @@ -406,7 +285,8 @@ public async Task DeleteCompletedAsync(TimeSpan olderThan, CancellationToke { var cutoffTime = _timeProvider.GetUtcNow().Subtract(olderThan); - return await DeleteEntitiesAsync( + return await _executor + .DeleteByQueryAsync( _context.OutboxMessages.Where(m => m.Status == OutboxMessageStatus.Completed && m.ProcessedAt < cutoffTime ), @@ -414,62 +294,4 @@ public async Task DeleteCompletedAsync(TimeSpan olderThan, CancellationToke ) .ConfigureAwait(false); } - - /// - /// Updates a set of entities by either tracking-and-saving (InMemory provider) - /// or issuing a single bulk UPDATE statement via ExecuteUpdateAsync (all other providers). - /// - private async Task UpdateEntitiesAsync( - IQueryable query, - Action applyChanges, - Func, CancellationToken, Task> executeBulkUpdate, - CancellationToken cancellationToken - ) - { - if (_useTrackingOperations) - { - var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); - - if (entities.Length == 0) - { - return; - } - - foreach (var entity in entities) - { - applyChanges(entity); - } - - _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - else - { - _ = await executeBulkUpdate(query, cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Deletes a set of entities by either tracking-and-removing (InMemory provider) - /// or issuing a single bulk DELETE statement via ExecuteDeleteAsync (all other providers). - /// - /// The number of deleted rows. - private async Task DeleteEntitiesAsync(IQueryable query, CancellationToken cancellationToken) - { - if (_useTrackingOperations) - { - var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); - - if (entities.Length == 0) - { - return 0; - } - - _context.OutboxMessages.RemoveRange(entities); - _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - return entities.Length; - } - - return await query.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - } } diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/IOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/IOutboxOperationsExecutor.cs new file mode 100644 index 00000000..49720078 --- /dev/null +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/IOutboxOperationsExecutor.cs @@ -0,0 +1,46 @@ +namespace NetEvolve.Pulse.Outbox; + +using NetEvolve.Pulse.Extensibility.Outbox; + +/// +/// Defines the provider-specific strategy for executing outbox entity operations. +/// +internal interface IOutboxOperationsExecutor : IDisposable +{ + Task FetchAndMarkAsync( + IQueryable baseQuery, + DateTimeOffset updatedAt, + OutboxMessageStatus newStatus, + CancellationToken cancellationToken + ); + + Task UpdateByQueryAsync( + IQueryable query, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ); + + Task UpdateByIdsAsync( + IReadOnlyCollection ids, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ); + + /// + /// Deletes the entities matched by and returns the number of + /// rows removed. + /// + /// A pre-filtered query that targets only the rows to delete. + /// Propagates notification that operations should be cancelled. + Task DeleteByQueryAsync(IQueryable query, CancellationToken cancellationToken); +} diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs new file mode 100644 index 00000000..5f13c7e6 --- /dev/null +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs @@ -0,0 +1,42 @@ +namespace NetEvolve.Pulse.Outbox; + +using Microsoft.EntityFrameworkCore; +using NetEvolve.Pulse.Extensibility.Outbox; + +/// +/// implementation for the EF Core InMemory provider. +/// +/// +/// The InMemory provider evaluates all LINQ in process, so any query — including those with +/// Contains over a collection — works without type-mapping issues. +/// ExecuteUpdate / ExecuteDelete are not supported; all mutations go through +/// change tracking and SaveChangesAsync (inherited from +/// ). +/// +/// The DbContext type that implements . +internal sealed class InMemoryOutboxOperationsExecutor(TContext context) + : TrackingOutboxOperationsExecutorBase(context) + where TContext : DbContext, IOutboxDbContext +{ + /// + public override Task UpdateByIdsAsync( + IReadOnlyCollection ids, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ) => + UpdateByQueryAsync( + _context.OutboxMessages.Where(m => ids.Contains(m.Id)), + updatedAt, + processedAt, + nextRetryAt, + newStatus, + retryIncrement, + errorMessage, + cancellationToken + ); +} diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs new file mode 100644 index 00000000..b891cac6 --- /dev/null +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs @@ -0,0 +1,84 @@ +namespace NetEvolve.Pulse.Outbox; + +using Microsoft.EntityFrameworkCore; +using NetEvolve.Pulse.Extensibility.Outbox; + +/// +/// implementation for the Oracle MySQL EF Core provider +/// (MySql.EntityFrameworkCore). +/// +/// +/// The Oracle MySQL provider has two known limitations: +/// +/// +/// It does not apply value converters (e.g. bigint) +/// when building ExecuteUpdateAsync parameters, causing a +/// in TypeMappedRelationalParameter.AddDbParameter. +/// +/// +/// It cannot assign a SQL type mapping to a parameterised collection used +/// in a WHERE id IN (@ids) clause, causing an . +/// +/// +/// All mutations go through change tracking and SaveChangesAsync (inherited from +/// ). Only +/// is overridden: it uses FindAsync per ID to avoid the +/// broken Guid IN clause, while still benefiting from the Local +/// cache for entities already loaded by FetchAndMarkAsync. +/// +/// The DbContext type that implements . +internal sealed class MySqlOutboxOperationsExecutor(TContext context) + : TrackingOutboxOperationsExecutorBase(context) + where TContext : DbContext, IOutboxDbContext +{ + /// + public override async Task UpdateByIdsAsync( + IReadOnlyCollection ids, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ) + { + // bulkQuery contains a Guid IN clause that MySQL cannot type-map. + // Use FindAsync per ID instead: it resolves the column's own type mapping and + // checks the DbContext Local cache before hitting the database. + var entities = new List(ids.Count); + + foreach (var id in ids) + { + var entity = await _context.OutboxMessages.FindAsync([id], cancellationToken).ConfigureAwait(false); + + if (entity?.Status == OutboxMessageStatus.Processing) + { + entities.Add(entity); + } + } + + if (entities.Count == 0) + { + return; + } + + foreach (var entity in entities) + { + entity.Status = newStatus; + entity.UpdatedAt = updatedAt; + if (processedAt.HasValue) + { + entity.ProcessedAt = processedAt.Value; + } + entity.NextRetryAt = nextRetryAt; + entity.RetryCount += retryIncrement; + if (!string.IsNullOrWhiteSpace(errorMessage)) + { + entity.Error = errorMessage; + } + } + + _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs new file mode 100644 index 00000000..671ee45b --- /dev/null +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs @@ -0,0 +1,145 @@ +namespace NetEvolve.Pulse.Outbox; + +using Microsoft.EntityFrameworkCore; +using NetEvolve.Pulse.Extensibility.Outbox; + +/// +/// Base class for implementations that persist changes +/// through EF Core change tracking and SaveChangesAsync. +/// +/// +/// Provides shared implementations of , +/// , and that load entities +/// into the change tracker, apply in-memory mutations, and flush via SaveChangesAsync. +/// Derived classes only need to implement , which varies by provider. +/// +/// The DbContext type that implements . +internal abstract class TrackingOutboxOperationsExecutorBase(TContext context) : IOutboxOperationsExecutor + where TContext : DbContext, IOutboxDbContext +{ + /// The DbContext used for all tracking-based query and update operations. + protected readonly TContext _context = context; + + protected readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + private bool _disposedValue; + + /// + public async Task FetchAndMarkAsync( + IQueryable baseQuery, + DateTimeOffset updatedAt, + OutboxMessageStatus newStatus, + CancellationToken cancellationToken + ) + { + var entities = await baseQuery.ToArrayAsync(cancellationToken).ConfigureAwait(false); + + if (entities.Length == 0) + { + return entities; + } + + foreach (var entity in entities) + { + entity.Status = newStatus; + entity.UpdatedAt = updatedAt; + } + + _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return entities; + } + + /// + public async Task UpdateByQueryAsync( + IQueryable query, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ) + { + await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); + + if (entities.Length == 0) + { + return; + } + + foreach (var entity in entities) + { + entity.Status = newStatus; + entity.UpdatedAt = updatedAt; + if (processedAt.HasValue) + { + entity.ProcessedAt = processedAt.Value; + } + entity.NextRetryAt = nextRetryAt; + entity.RetryCount += retryIncrement; + if (!string.IsNullOrWhiteSpace(errorMessage)) + { + entity.Error = errorMessage; + } + } + + _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _ = _semaphore.Release(); + } + } + + /// + public abstract Task UpdateByIdsAsync( + IReadOnlyCollection ids, + DateTimeOffset updatedAt, + DateTimeOffset? processedAt, + DateTimeOffset? nextRetryAt, + OutboxMessageStatus newStatus, + int retryIncrement, + string? errorMessage, + CancellationToken cancellationToken + ); + + /// + public async Task DeleteByQueryAsync(IQueryable query, CancellationToken cancellationToken) + { + var entities = await query.ToArrayAsync(cancellationToken).ConfigureAwait(false); + + if (entities.Length == 0) + { + return 0; + } + + _context.OutboxMessages.RemoveRange(entities); + _ = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + return entities.Length; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _semaphore.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index e4b29664..c113cfbf 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NetEvolve.Pulse.Extensibility; using NetEvolve.Pulse.Extensibility.Outbox; using NetEvolve.Pulse.Outbox; @@ -67,7 +68,7 @@ public async ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider, Can } public void Initialize(IServiceCollection services, IDatabaseServiceFixture databaseService) => - _ = services.AddDbContext(options => + _ = services.AddDbContextFactory(options => { var connectionString = databaseService.ConnectionString; @@ -78,11 +79,12 @@ public void Initialize(IServiceCollection services, IDatabaseServiceFixture data // This factory makes the cache key unique per (DbContext type, TableName), so each // test gets its own model while still sharing the internal EF Core service provider // (critical for correct type-mapping initialisation on providers like Oracle MySQL). - options.ReplaceService(); + _ = options.ReplaceService(); _ = databaseService.DatabaseType switch { DatabaseType.InMemory => options.UseInMemoryDatabase(connectionString), + DatabaseType.MySql => options.UseMySQL(connectionString), DatabaseType.PostgreSQL => options.UseNpgsql(connectionString), // Add a busy-timeout interceptor so that concurrent SaveChangesAsync calls from // parallel PublishAsync tasks wait and retry instead of failing with SQLITE_BUSY. @@ -131,7 +133,7 @@ public override async Task ConnectionOpenedAsync( /// second test picks up the first test's table name and CreateTablesAsync fails with /// "Table '<first-test-name>' already exists". /// - private sealed class OutboxTableModelCacheKeyFactory : IModelCacheKeyFactory + private sealed class TestTableModelCacheKeyFactory : IModelCacheKeyFactory { /// public object Create(DbContext context, bool designTime) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs new file mode 100644 index 00000000..c09e179c --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs @@ -0,0 +1,71 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using Microsoft.Extensions.Logging.Abstractions; +using MySql.Data.MySqlClient; +using Testcontainers.MySql; +using TUnit.Core.Interfaces; + +/// +/// Manages the lifecycle of a MySQL Testcontainer shared across a test session. +/// +public sealed class MySqlContainerFixture : IAsyncDisposable, IAsyncInitializer +{ + private readonly MySqlContainer _container = new MySqlBuilder( + /*dockerimage*/"mysql:8.0" + ) + .WithLogger(NullLogger.Instance) + .WithUsername(UserName) + .WithPrivileged(true) + .Build(); + + public string ConnectionString => _container.GetConnectionString(); + + public static string UserName => "root"; + + public ValueTask DisposeAsync() => _container.DisposeAsync(); + + public async Task InitializeAsync() => await _container.StartAsync().ConfigureAwait(false); +} + +/// +/// Provides a per-test backed by a MySQL Testcontainer, +/// creating a unique database for each test to ensure isolation. +/// +public sealed class MySqlDatabaseServiceFixture : IDatabaseServiceFixture +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public MySqlContainerFixture Container { get; set; } = default!; + + public string ConnectionString => + Container.ConnectionString.Replace(";Database=test;", $";Database={DatabaseName};", StringComparison.Ordinal); + + internal string DatabaseName { get; } = $"{TestHelper.TargetFramework}{Guid.NewGuid():N}"; + + public DatabaseType DatabaseType => DatabaseType.MySql; + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + public async Task InitializeAsync() + { + try + { + // Create temporary database to ensure the container is fully initialized and ready to accept connections + await using var con = new MySqlConnection(Container.ConnectionString); + await con.OpenAsync(); + + await using var cmd = con.CreateCommand(); +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + cmd.CommandText = $"CREATE DATABASE {DatabaseName}"; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + + _ = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new InvalidOperationException( + "MySQL container failed to start within the expected time frame. Try restarting Rancher Desktop.", + ex + ); + } + } +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs index 74cfe441..465be5f3 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/SqlServerContainerFixture.cs @@ -12,7 +12,7 @@ public sealed class SqlServerContainerFixture : IAsyncDisposable, IAsyncInitiali .WithLogger(NullLogger.Instance) .Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => _container.GetConnectionString() + ";MultipleActiveResultSets=True;"; public ValueTask DisposeAsync() => _container.DisposeAsync(); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj index 830fa86d..df6999ba 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj +++ b/tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj @@ -19,6 +19,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/MySqlEntityFrameworkOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/MySqlEntityFrameworkOutboxTests.cs new file mode 100644 index 00000000..5c7e9b37 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/MySqlEntityFrameworkOutboxTests.cs @@ -0,0 +1,13 @@ +namespace NetEvolve.Pulse.Tests.Integration.Outbox; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.Pulse.Tests.Integration.Internals; + +[ClassDataSource(Shared = [SharedType.None, SharedType.None])] +[TestGroup("MySql")] +[TestGroup("EntityFramework")] +[InheritsTests] +public class MySqlEntityFrameworkOutboxTests( + IDatabaseServiceFixture databaseServiceFixture, + IDatabaseInitializer databaseInitializer +) : OutboxTestsBase(databaseServiceFixture, databaseInitializer); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs index 4f3d97b4..f6cd72d2 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs @@ -343,10 +343,11 @@ await RunAndVerify( _ = await Assert.That(pending.Count).IsEqualTo(2); - await outbox - .MarkAsFailedAsync(pending[0].Id, "Scheduled error", TestDateTime.AddHours(1), token) + await Task.WhenAll( + outbox.MarkAsFailedAsync(pending[0].Id, "Scheduled error", TestDateTime.AddHours(1), token), + outbox.MarkAsFailedAsync(pending[1].Id, "Immediate error", token) + ) .ConfigureAwait(false); - await outbox.MarkAsFailedAsync(pending[1].Id, "Immediate error", token).ConfigureAwait(false); var failedForRetry = await outbox.GetFailedForRetryAsync(10, 50, token).ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/MySqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_2f3dd6e39de993fc.verified.txt b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/MySqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_2f3dd6e39de993fc.verified.txt new file mode 100644 index 00000000..4ee756e4 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/_snapshots/Outbox/MySqlEntityFrameworkOutboxTests.Should_Persist_Expected_Messages_2f3dd6e39de993fc.verified.txt @@ -0,0 +1,26 @@ +[ + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_1, + Payload: {"CorrelationId":null,"Id":"Test000","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_2, + Payload: {"CorrelationId":null,"Id":"Test001","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + }, + { + CreatedAt: DateTimeOffset_1, + EventType: OutboxTestsBase.TestEvent, + Id: Guid_3, + Payload: {"CorrelationId":null,"Id":"Test002","PublishedAt":"2025-01-01T12:00:00+00:00"}, + Status: Processing, + UpdatedAt: DateTimeOffset_1 + } +] \ No newline at end of file diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs index 4483b9a6..691279a6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxRepositoryTests.cs @@ -11,13 +11,13 @@ namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; public sealed class EntityFrameworkOutboxRepositoryTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => _ = await Assert .That(() => new EntityFrameworkOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException)) @@ -30,7 +30,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance)) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs index 352fb957..60ff6665 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/OutboxMessageConfigurationMetadataTests.cs @@ -15,9 +15,7 @@ public sealed class OutboxMessageConfigurationMetadataTests { [Test] - public async Task Create_WithSqlServerProvider_AppliesSqlServerFiltersAndColumnTypes( - CancellationToken cancellationToken - ) + public async Task Create_WithSqlServerProvider_AppliesSqlServerFiltersAndColumnTypes() { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.SqlServer"); @@ -44,9 +42,7 @@ CancellationToken cancellationToken } [Test] - public async Task Create_WithPostgreSqlProvider_AppliesPostgreSqlFiltersAndColumnTypes( - CancellationToken cancellationToken - ) + public async Task Create_WithPostgreSqlProvider_AppliesPostgreSqlFiltersAndColumnTypes() { var entityType = GetConfiguredEntityType("Npgsql.EntityFrameworkCore.PostgreSQL"); @@ -71,7 +67,7 @@ CancellationToken cancellationToken } [Test] - public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes(CancellationToken cancellationToken) + public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes() { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.Sqlite"); @@ -96,9 +92,7 @@ public async Task Create_WithSqliteProvider_AppliesSqliteFiltersAndColumnTypes(C } [Test] - public async Task Create_WithMySqlProvider_AppliesMySqlColumnTypesAndNoFilteredIndexes( - CancellationToken cancellationToken - ) + public async Task Create_WithMySqlProvider_AppliesMySqlColumnTypesAndNoFilteredIndexes() { var entityType = GetConfiguredEntityType("Pomelo.EntityFrameworkCore.MySql"); @@ -114,10 +108,10 @@ CancellationToken cancellationToken _ = await Assert .That(entityType.FindProperty(nameof(OutboxMessage.Id))!.GetColumnType()) - .IsEqualTo("char(36)"); + .IsEqualTo("binary(16)"); _ = await Assert .That(entityType.FindProperty(nameof(OutboxMessage.CreatedAt))!.GetColumnType()) - .IsEqualTo("datetime(6)"); + .IsEqualTo("bigint"); _ = await Assert .That(entityType.FindProperty(nameof(OutboxMessage.Payload))!.GetColumnType()) .IsEqualTo("longtext"); @@ -125,9 +119,7 @@ CancellationToken cancellationToken } [Test] - public async Task Create_WithInMemoryProvider_UsesBaseDefaultsWithoutColumnTypeOverrides( - CancellationToken cancellationToken - ) + public async Task Create_WithInMemoryProvider_UsesBaseDefaultsWithoutColumnTypeOverrides() { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.InMemory"); @@ -147,9 +139,7 @@ CancellationToken cancellationToken } [Test] - public async Task Create_WithWhitespaceSchema_UsesDefaultSchemaAndConfiguredTableName( - CancellationToken cancellationToken - ) + public async Task Create_WithWhitespaceSchema_UsesDefaultSchemaAndConfiguredTableName() { var entityType = GetConfiguredEntityType( "Microsoft.EntityFrameworkCore.SqlServer", @@ -164,7 +154,7 @@ CancellationToken cancellationToken } [Test] - public async Task Create_WithSchemaContainingWhitespace_StoresTrimmedSchema(CancellationToken cancellationToken) + public async Task Create_WithSchemaContainingWhitespace_StoresTrimmedSchema() { var entityType = GetConfiguredEntityType( "Microsoft.EntityFrameworkCore.SqlServer", @@ -175,7 +165,7 @@ public async Task Create_WithSchemaContainingWhitespace_StoresTrimmedSchema(Canc } [Test] - public async Task Create_WithSqlServerProvider_AppliesOutboxBaseDefaultValues(CancellationToken cancellationToken) + public async Task Create_WithSqlServerProvider_AppliesOutboxBaseDefaultValues() { var entityType = GetConfiguredEntityType("Microsoft.EntityFrameworkCore.SqlServer"); From e517887830d8a831d0144693424ab26e6911ddb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 09:54:36 +0200 Subject: [PATCH 22/25] fix: Centralize EF Core provider names and improve executor config Introduce ProviderName class to centralize EF Core provider name constants, replacing scattered string literals and private constants. Update all provider selection logic to use these constants. Refactor OutboxOperationsExecutor constructors to accept maxDegreeOfParallelism for improved concurrency control. Enhance maintainability and reduce errors in provider-specific logic. --- .../ModelBuilderExtensions.cs | 43 +++---------------- .../Outbox/BulkOutboxOperationsExecutor.cs | 5 ++- .../Outbox/EntityFrameworkOutboxRepository.cs | 7 +-- .../InMemoryOutboxOperationsExecutor.cs | 4 +- .../Outbox/MySqlOutboxOperationsExecutor.cs | 4 +- .../TrackingOutboxOperationsExecutorBase.cs | 5 ++- .../ProviderName.cs | 38 ++++++++++++++++ 7 files changed, 59 insertions(+), 47 deletions(-) create mode 100644 src/NetEvolve.Pulse.EntityFramework/ProviderName.cs diff --git a/src/NetEvolve.Pulse.EntityFramework/ModelBuilderExtensions.cs b/src/NetEvolve.Pulse.EntityFramework/ModelBuilderExtensions.cs index 53e5ea0d..23990d79 100644 --- a/src/NetEvolve.Pulse.EntityFramework/ModelBuilderExtensions.cs +++ b/src/NetEvolve.Pulse.EntityFramework/ModelBuilderExtensions.cs @@ -12,37 +12,6 @@ /// public static class ModelBuilderExtensions { - /// - /// The provider name for the EF Core InMemory provider (Microsoft.EntityFrameworkCore.InMemory). - /// Intended for testing only. - /// - private const string InMemoryProviderName = "Microsoft.EntityFrameworkCore.InMemory"; - - /// - /// The provider name for Npgsql (PostgreSQL). - /// - private const string NpgsqlProviderName = "Npgsql.EntityFrameworkCore.PostgreSQL"; - - /// - /// The provider name for Microsoft.EntityFrameworkCore.Sqlite. - /// - private const string SqliteProviderName = "Microsoft.EntityFrameworkCore.Sqlite"; - - /// - /// The provider name for Microsoft.EntityFrameworkCore.SqlServer. - /// - private const string SqlServerProviderName = "Microsoft.EntityFrameworkCore.SqlServer"; - - /// - /// The provider name for Pomelo MySQL (Pomelo.EntityFrameworkCore.MySql). - /// - private const string PomeloMySqlProviderName = "Pomelo.EntityFrameworkCore.MySql"; - - /// - /// The provider name for the Oracle MySQL provider (MySql.EntityFrameworkCore). - /// - private const string OracleMySqlProviderName = "MySql.EntityFrameworkCore"; - /// /// Applies all Pulse-related entity configurations to the model builder. /// @@ -98,11 +67,13 @@ private static IEntityTypeConfiguration GetOutboxConfiguration new PostgreSqlOutboxMessageConfiguration(resolvedOptions), - SqliteProviderName => new SqliteOutboxMessageConfiguration(resolvedOptions), - SqlServerProviderName => new SqlServerOutboxMessageConfiguration(resolvedOptions), - PomeloMySqlProviderName or OracleMySqlProviderName => new MySqlOutboxMessageConfiguration(resolvedOptions), - InMemoryProviderName => new InMemoryOutboxMessageConfiguration(resolvedOptions), + ProviderName.Npgsql => new PostgreSqlOutboxMessageConfiguration(resolvedOptions), + ProviderName.Sqlite => new SqliteOutboxMessageConfiguration(resolvedOptions), + ProviderName.SqlServer => new SqlServerOutboxMessageConfiguration(resolvedOptions), + ProviderName.PomeloMySql or ProviderName.OracleMySql => new MySqlOutboxMessageConfiguration( + resolvedOptions + ), + ProviderName.InMemory => new InMemoryOutboxMessageConfiguration(resolvedOptions), _ => throw new NotSupportedException($"Unsupported EF Core provider: {providerName}"), }; } diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs index bf4a6012..9117afac 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/BulkOutboxOperationsExecutor.cs @@ -13,10 +13,11 @@ namespace NetEvolve.Pulse.Outbox; /// (SQL Server, PostgreSQL, SQLite, and others). /// /// The DbContext type that implements . -internal sealed class BulkOutboxOperationsExecutor(TContext context) : IOutboxOperationsExecutor +internal sealed class BulkOutboxOperationsExecutor(TContext context, int maxDegreeOfParallelism) + : IOutboxOperationsExecutor where TContext : DbContext, IOutboxDbContext { - private readonly SemaphoreSlim _semaphore = new(1, 1); + private readonly SemaphoreSlim _semaphore = new(maxDegreeOfParallelism, maxDegreeOfParallelism); private bool _disposedValue; /// diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs index 01f51829..4ed318d3 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/EntityFrameworkOutboxRepository.cs @@ -47,11 +47,12 @@ public EntityFrameworkOutboxRepository(TContext context, TimeProvider timeProvid _executor = context.Database.ProviderName switch { // InMemory does not support ExecuteUpdate/ExecuteDelete at all. - "Microsoft.EntityFrameworkCore.InMemory" => new InMemoryOutboxOperationsExecutor(context), + ProviderName.InMemory => new InMemoryOutboxOperationsExecutor(context, 1), // Oracle MySQL cannot apply value converters in ExecuteUpdateAsync parameters and // cannot translate a parameterised Guid collection into a SQL IN clause. - "MySql.EntityFrameworkCore" => new MySqlOutboxOperationsExecutor(context), - _ => new BulkOutboxOperationsExecutor(context), + ProviderName.OracleMySql => new MySqlOutboxOperationsExecutor(context, 1), + ProviderName.Npgsql => new BulkOutboxOperationsExecutor(context, 1), + _ => new BulkOutboxOperationsExecutor(context, Environment.ProcessorCount - 1), }; } diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs index 5f13c7e6..b4550327 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/InMemoryOutboxOperationsExecutor.cs @@ -14,8 +14,8 @@ namespace NetEvolve.Pulse.Outbox; /// ). /// /// The DbContext type that implements . -internal sealed class InMemoryOutboxOperationsExecutor(TContext context) - : TrackingOutboxOperationsExecutorBase(context) +internal sealed class InMemoryOutboxOperationsExecutor(TContext context, int maxDegreeOfParallelism) + : TrackingOutboxOperationsExecutorBase(context, maxDegreeOfParallelism) where TContext : DbContext, IOutboxDbContext { /// diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs index b891cac6..adecad23 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/MySqlOutboxOperationsExecutor.cs @@ -27,8 +27,8 @@ namespace NetEvolve.Pulse.Outbox; /// cache for entities already loaded by FetchAndMarkAsync. /// /// The DbContext type that implements . -internal sealed class MySqlOutboxOperationsExecutor(TContext context) - : TrackingOutboxOperationsExecutorBase(context) +internal sealed class MySqlOutboxOperationsExecutor(TContext context, int maxDegreeOfParallelism) + : TrackingOutboxOperationsExecutorBase(context, maxDegreeOfParallelism) where TContext : DbContext, IOutboxDbContext { /// diff --git a/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs b/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs index 671ee45b..0b8de80f 100644 --- a/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs +++ b/src/NetEvolve.Pulse.EntityFramework/Outbox/TrackingOutboxOperationsExecutorBase.cs @@ -14,13 +14,14 @@ namespace NetEvolve.Pulse.Outbox; /// Derived classes only need to implement , which varies by provider. /// /// The DbContext type that implements . -internal abstract class TrackingOutboxOperationsExecutorBase(TContext context) : IOutboxOperationsExecutor +internal abstract class TrackingOutboxOperationsExecutorBase(TContext context, int maxDegreeOfParallelism) + : IOutboxOperationsExecutor where TContext : DbContext, IOutboxDbContext { /// The DbContext used for all tracking-based query and update operations. protected readonly TContext _context = context; - protected readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + protected readonly SemaphoreSlim _semaphore = new SemaphoreSlim(maxDegreeOfParallelism, maxDegreeOfParallelism); private bool _disposedValue; /// diff --git a/src/NetEvolve.Pulse.EntityFramework/ProviderName.cs b/src/NetEvolve.Pulse.EntityFramework/ProviderName.cs new file mode 100644 index 00000000..d05dac17 --- /dev/null +++ b/src/NetEvolve.Pulse.EntityFramework/ProviderName.cs @@ -0,0 +1,38 @@ +namespace NetEvolve.Pulse; + +/// +/// Static class containing constants for supported EF Core provider names, used for provider-specific +/// +internal static class ProviderName +{ + /// + /// The provider name for the EF Core InMemory provider (Microsoft.EntityFrameworkCore.InMemory). + /// Intended for testing only. + /// + internal const string InMemory = "Microsoft.EntityFrameworkCore.InMemory"; + + /// + /// The provider name for Npgsql (PostgreSQL). + /// + internal const string Npgsql = "Npgsql.EntityFrameworkCore.PostgreSQL"; + + /// + /// The provider name for Microsoft.EntityFrameworkCore.Sqlite. + /// + internal const string Sqlite = "Microsoft.EntityFrameworkCore.Sqlite"; + + /// + /// The provider name for Microsoft.EntityFrameworkCore.SqlServer. + /// + internal const string SqlServer = "Microsoft.EntityFrameworkCore.SqlServer"; + + /// + /// The provider name for Pomelo MySQL (Pomelo.EntityFrameworkCore.MySql). + /// + internal const string PomeloMySql = "Pomelo.EntityFrameworkCore.MySql"; + + /// + /// The provider name for the Oracle MySQL provider (MySql.EntityFrameworkCore). + /// + internal const string OracleMySql = "MySql.EntityFrameworkCore"; +} From 53c3b65b21a910f81aa504a0b5d17fa49ef61e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 11:08:22 +0200 Subject: [PATCH 23/25] chore(deps): Upgraded `NetEvolve.Defaults` to `2.3.0` --- .editorconfig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.editorconfig b/.editorconfig index 83532c32..920b54c7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -270,6 +270,10 @@ dotnet_diagnostic.IDE0046.severity = sugges csharp_style_prefer_primary_constructors = false dotnet_diagnostic.IDE0290.severity = suggestion +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = warning +dotnet_code_quality_unused_parameters = all + # [CSharpier] Incompatible rules deactivated # https://csharpier.com/docs/IntegratingWithLinters#code-analysis-rules dotnet_diagnostic.IDE0055.severity = none From b60a3126fbabbcff2e58c9315ce70ab9df1dd286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 12:20:14 +0200 Subject: [PATCH 24/25] refactor: test method signatures to remove unused CancellationToken parameters - Updated multiple test methods across various test classes to remove the CancellationToken parameter as it was not being utilized. - Ensured consistency in method signatures for better readability and maintainability. --- .../ActivityMetricsExtensionsTests.cs | 14 +- .../EndpointRouteBuilderExtensionsTests.cs | 49 +++---- .../AssemblyScanningExtensionsTests.cs | 94 +++++-------- .../AzureServiceBusExtensionsTests.cs | 30 ++--- .../AzureServiceBusMessageTransportTests.cs | 8 +- .../Dapr/DaprExtensionsTests.cs | 17 +-- .../Dapr/DaprMessageTransportTests.cs | 14 +- .../PrioritizedEventDispatcherTests.cs | 2 +- .../RateLimitedEventDispatcherTests.cs | 14 +- .../SequentialEventDispatcherTests.cs | 2 +- .../EntityFrameworkEventOutboxTests.cs | 12 +- .../EntityFrameworkExtensionsTests.cs | 22 ++-- .../EntityFrameworkOutboxManagementTests.cs | 14 +- ...ityFrameworkOutboxTransactionScopeTests.cs | 6 +- .../ModelBuilderExtensionsTests.cs | 16 +-- .../TypeValueConverterTests.cs | 12 +- .../EventDispatcherExtensionsTests.cs | 48 +++---- .../FluentValidationExtensionsTests.cs | 14 +- .../HandlerRegistrationExtensionsTests.cs | 124 ++++++------------ .../HttpCorrelationExtensionsTests.cs | 28 ++-- .../HttpCorrelationEventInterceptorTests.cs | 10 +- .../HttpCorrelationRequestInterceptorTests.cs | 10 +- ...pCorrelationStreamQueryInterceptorTests.cs | 10 +- .../IdempotencyExtensionsTests.cs | 17 +-- .../IdempotencyCommandInterceptorTests.cs | 8 +- ...LoggingInterceptorOptionsValidatorTests.cs | 10 +- .../Internals/MediatorBuilderTests.cs | 6 +- .../Internals/PulseMediatorTests.cs | 10 +- .../Kafka/KafkaExtensionsTests.cs | 8 +- .../LoggingExtensionsTests.cs | 14 +- .../Outbox/ExponentialBackoffTests.cs | 24 ++-- .../Outbox/OutboxExtensionsTests.cs | 12 +- .../OutboxProcessorHostedServiceTests.cs | 10 +- .../Outbox/OutboxStatisticsTests.cs | 10 +- .../PollyEventInterceptorTests.cs | 14 +- .../PollyRequestInterceptorTests.cs | 14 +- .../Polly/PollyExtensionsTests.cs | 90 ++++--------- .../PostgreSql/PostgreSqlEventOutboxTests.cs | 14 +- .../PostgreSql/PostgreSqlExtensionsTests.cs | 58 +++----- .../PostgreSqlOutboxManagementTests.cs | 36 ++--- .../PostgreSqlOutboxOptionsExtensionsTests.cs | 14 +- .../PostgreSqlOutboxRepositoryTests.cs | 30 ++--- .../PostgreSqlOutboxTransactionScopeTests.cs | 8 +- .../QueryCachingExtensionsTests.cs | 12 +- .../RabbitMQ/RabbitMqExtensionsTests.cs | 14 +- .../RabbitMQ/RabbitMqMessageTransportTests.cs | 12 +- .../SQLite/SQLiteEventOutboxTests.cs | 14 +- .../SQLite/SQLiteExtensionsTests.cs | 44 ++----- .../SQLite/SQLiteOutboxManagementTests.cs | 30 ++--- .../SQLiteOutboxOptionsExtensionsTests.cs | 4 +- .../SQLite/SQLiteOutboxRepositoryTests.cs | 22 ++-- .../SQLiteOutboxTransactionScopeTests.cs | 8 +- .../ServiceCollectionExtensionsTests.cs | 10 +- .../SqlServer/SqlServerEventOutboxTests.cs | 14 +- .../SqlServer/SqlServerExtensionsTests.cs | 56 +++----- .../SqlServerOutboxManagementTests.cs | 36 ++--- .../SqlServerOutboxOptionsExtensionsTests.cs | 18 +-- .../SqlServerOutboxRepositoryTests.cs | 30 ++--- .../SqlServerOutboxTransactionScopeTests.cs | 8 +- 59 files changed, 466 insertions(+), 843 deletions(-) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs index c3a38306..f2e0040c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/ActivityMetricsExtensionsTests.cs @@ -16,15 +16,13 @@ public sealed class ActivityMetricsExtensionsTests { [Test] - public async Task AddActivityAndMetrics_NullBuilder_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddActivityAndMetrics_NullBuilder_ThrowsArgumentNullException() => _ = await Assert .That(() => ActivityMetricsExtensions.AddActivityAndMetrics(null!)) .Throws(); [Test] - public async Task AddActivityAndMetrics_RegistersEventInterceptor(CancellationToken cancellationToken) + public async Task AddActivityAndMetrics_RegistersEventInterceptor() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -46,7 +44,7 @@ public async Task AddActivityAndMetrics_RegistersEventInterceptor(CancellationTo } [Test] - public async Task AddActivityAndMetrics_RegistersRequestInterceptor(CancellationToken cancellationToken) + public async Task AddActivityAndMetrics_RegistersRequestInterceptor() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -68,9 +66,7 @@ public async Task AddActivityAndMetrics_RegistersRequestInterceptor(Cancellation } [Test] - public async Task AddActivityAndMetrics_CalledMultipleTimes_DoesNotDuplicateRegistrations( - CancellationToken cancellationToken - ) + public async Task AddActivityAndMetrics_CalledMultipleTimes_DoesNotDuplicateRegistrations() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -100,7 +96,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddActivityAndMetrics_ReturnsSameBuilder(CancellationToken cancellationToken) + public async Task AddActivityAndMetrics_ReturnsSameBuilder() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs index 28c79c67..814776e8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AspNetCore/EndpointRouteBuilderExtensionsTests.cs @@ -17,14 +17,11 @@ public sealed class EndpointRouteBuilderExtensionsTests // MapCommand — null-argument guards [Test] - public void MapCommand_WithResponseAndNullEndpoints_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => _ = Assert.Throws(() => PulseEndpoints.MapCommand(null!, "/test")); + public void MapCommand_WithResponseAndNullEndpoints_ThrowsArgumentNullException() => + _ = Assert.Throws(() => PulseEndpoints.MapCommand(null!, "/test")); [Test] - public async Task MapCommand_WithResponseAndNullPattern_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public async Task MapCommand_WithResponseAndNullPattern_ThrowsArgumentNullException() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -34,9 +31,7 @@ CancellationToken cancellationToken // MapCommand — httpMethod validation [Test] - public async Task MapCommand_WithResponse_WithUndefinedMethod_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task MapCommand_WithResponse_WithUndefinedMethod_ThrowsArgumentOutOfRangeException() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -48,9 +43,7 @@ CancellationToken cancellationToken // MapCommand — valid cases [Test] - public async Task MapCommand_WithResponse_DefaultPost_ReturnsRouteHandlerBuilder( - CancellationToken cancellationToken - ) + public async Task MapCommand_WithResponse_DefaultPost_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -60,9 +53,7 @@ CancellationToken cancellationToken } [Test] - public async Task MapCommand_WithResponse_WithPutMethod_ReturnsRouteHandlerBuilder( - CancellationToken cancellationToken - ) + public async Task MapCommand_WithResponse_WithPutMethod_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -72,9 +63,7 @@ CancellationToken cancellationToken } [Test] - public async Task MapCommand_WithResponse_WithPatchMethod_ReturnsRouteHandlerBuilder( - CancellationToken cancellationToken - ) + public async Task MapCommand_WithResponse_WithPatchMethod_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -84,9 +73,7 @@ CancellationToken cancellationToken } [Test] - public async Task MapCommand_WithResponse_WithDeleteMethod_ReturnsRouteHandlerBuilder( - CancellationToken cancellationToken - ) + public async Task MapCommand_WithResponse_WithDeleteMethod_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -98,11 +85,11 @@ CancellationToken cancellationToken // MapCommand (void) — null-argument guards [Test] - public void MapCommand_VoidAndNullEndpoints_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public void MapCommand_VoidAndNullEndpoints_ThrowsArgumentNullException() => _ = Assert.Throws(() => PulseEndpoints.MapCommand(null!, "/test")); [Test] - public async Task MapCommand_VoidAndNullPattern_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task MapCommand_VoidAndNullPattern_ThrowsArgumentNullException() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -112,9 +99,7 @@ public async Task MapCommand_VoidAndNullPattern_ThrowsArgumentNullException(Canc // MapCommand (void) — httpMethod validation [Test] - public async Task MapCommand_Void_WithUndefinedMethod_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task MapCommand_Void_WithUndefinedMethod_ThrowsArgumentOutOfRangeException() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -126,7 +111,7 @@ CancellationToken cancellationToken // MapCommand (void) — valid cases [Test] - public async Task MapCommand_Void_DefaultPost_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) + public async Task MapCommand_Void_DefaultPost_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -136,7 +121,7 @@ public async Task MapCommand_Void_DefaultPost_ReturnsRouteHandlerBuilder(Cancell } [Test] - public async Task MapCommand_Void_WithDeleteMethod_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) + public async Task MapCommand_Void_WithDeleteMethod_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -146,7 +131,7 @@ public async Task MapCommand_Void_WithDeleteMethod_ReturnsRouteHandlerBuilder(Ca } [Test] - public async Task MapCommand_Void_WithPutMethod_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) + public async Task MapCommand_Void_WithPutMethod_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -158,11 +143,11 @@ public async Task MapCommand_Void_WithPutMethod_ReturnsRouteHandlerBuilder(Cance // MapQuery — null-argument guards [Test] - public void MapQuery_WithNullEndpoints_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public void MapQuery_WithNullEndpoints_ThrowsArgumentNullException() => _ = Assert.Throws(() => PulseEndpoints.MapQuery(null!, "/test")); [Test] - public async Task MapQuery_WithNullPattern_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task MapQuery_WithNullPattern_ThrowsArgumentNullException() { await using var endpoints = WebApplication.CreateBuilder().Build(); @@ -170,7 +155,7 @@ public async Task MapQuery_WithNullPattern_ThrowsArgumentNullException(Cancellat } [Test] - public async Task MapQuery_ReturnsRouteHandlerBuilder(CancellationToken cancellationToken) + public async Task MapQuery_ReturnsRouteHandlerBuilder() { await using var endpoints = WebApplication.CreateBuilder().Build(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs index e5e4496c..f62740cd 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AssemblyScanningExtensionsTests.cs @@ -12,9 +12,7 @@ public class AssemblyScanningExtensionsTests { [Test] - public void AddHandlersFromAssembly_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddHandlersFromAssembly_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -23,9 +21,7 @@ CancellationToken cancellationToken } [Test] - public void AddHandlersFromAssembly_WithNullAssembly_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddHandlersFromAssembly_WithNullAssembly_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assembly? assembly = null; @@ -36,7 +32,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssembly_RegistersCommandHandlers(CancellationToken cancellationToken) + public async Task AddHandlersFromAssembly_RegistersCommandHandlers() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -56,7 +52,7 @@ public async Task AddHandlersFromAssembly_RegistersCommandHandlers(CancellationT } [Test] - public async Task AddHandlersFromAssembly_RegistersQueryHandlers(CancellationToken cancellationToken) + public async Task AddHandlersFromAssembly_RegistersQueryHandlers() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -74,7 +70,7 @@ public async Task AddHandlersFromAssembly_RegistersQueryHandlers(CancellationTok } [Test] - public async Task AddHandlersFromAssembly_RegistersEventHandlers(CancellationToken cancellationToken) + public async Task AddHandlersFromAssembly_RegistersEventHandlers() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -92,9 +88,7 @@ public async Task AddHandlersFromAssembly_RegistersEventHandlers(CancellationTok } [Test] - public async Task AddHandlersFromAssembly_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddHandlersFromAssembly_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -118,7 +112,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssembly_DoesNotRegisterAbstractClasses(CancellationToken cancellationToken) + public async Task AddHandlersFromAssembly_DoesNotRegisterAbstractClasses() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -131,7 +125,7 @@ public async Task AddHandlersFromAssembly_DoesNotRegisterAbstractClasses(Cancell } [Test] - public async Task AddHandlersFromAssembly_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddHandlersFromAssembly_ReturnsConfigurator() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -152,9 +146,7 @@ public async Task AddHandlersFromAssembly_ReturnsConfigurator(CancellationToken } [Test] - public void AddHandlersFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddHandlersFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -164,9 +156,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssemblyContaining_RegistersHandlersFromMarkerTypeAssembly( - CancellationToken cancellationToken - ) + public async Task AddHandlersFromAssemblyContaining_RegistersHandlersFromMarkerTypeAssembly() { var services = new ServiceCollection(); @@ -184,9 +174,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssemblyContaining_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddHandlersFromAssemblyContaining_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -206,7 +194,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssemblyContaining_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddHandlersFromAssemblyContaining_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -226,9 +214,7 @@ public async Task AddHandlersFromAssemblyContaining_ReturnsConfigurator(Cancella } [Test] - public void AddHandlersFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddHandlersFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -237,9 +223,7 @@ CancellationToken cancellationToken } [Test] - public void AddHandlersFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddHandlersFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assembly[]? assemblies = null; @@ -250,7 +234,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssemblies_RegistersHandlersFromAllAssemblies(CancellationToken cancellationToken) + public async Task AddHandlersFromAssemblies_RegistersHandlersFromAllAssemblies() { var services = new ServiceCollection(); var assemblies = new[] @@ -269,9 +253,7 @@ public async Task AddHandlersFromAssemblies_RegistersHandlersFromAllAssemblies(C } [Test] - public async Task AddHandlersFromAssemblies_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddHandlersFromAssemblies_WithCustomLifetime_RegistersHandlersWithSpecifiedLifetime() { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -290,7 +272,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHandlersFromAssemblies_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddHandlersFromAssemblies_ReturnsConfigurator() { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -362,9 +344,7 @@ public abstract Task HandleAsync( // Interceptor scanning tests [Test] - public void AddInterceptorsFromAssembly_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddInterceptorsFromAssembly_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -373,9 +353,7 @@ CancellationToken cancellationToken } [Test] - public void AddInterceptorsFromAssembly_WithNullAssembly_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddInterceptorsFromAssembly_WithNullAssembly_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assembly? assembly = null; @@ -386,7 +364,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddInterceptorsFromAssembly_RegistersRequestInterceptors(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssembly_RegistersRequestInterceptors() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -406,7 +384,7 @@ public async Task AddInterceptorsFromAssembly_RegistersRequestInterceptors(Cance } [Test] - public async Task AddInterceptorsFromAssembly_RegistersCommandInterceptors(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssembly_RegistersCommandInterceptors() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -426,7 +404,7 @@ public async Task AddInterceptorsFromAssembly_RegistersCommandInterceptors(Cance } [Test] - public async Task AddInterceptorsFromAssembly_RegistersQueryInterceptors(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssembly_RegistersQueryInterceptors() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -446,7 +424,7 @@ public async Task AddInterceptorsFromAssembly_RegistersQueryInterceptors(Cancell } [Test] - public async Task AddInterceptorsFromAssembly_RegistersEventInterceptors(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssembly_RegistersEventInterceptors() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -464,9 +442,7 @@ public async Task AddInterceptorsFromAssembly_RegistersEventInterceptors(Cancell } [Test] - public async Task AddInterceptorsFromAssembly_WithCustomLifetime_RegistersInterceptorsWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddInterceptorsFromAssembly_WithCustomLifetime_RegistersInterceptorsWithSpecifiedLifetime() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -498,7 +474,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddInterceptorsFromAssembly_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssembly_ReturnsConfigurator() { var services = new ServiceCollection(); var assembly = typeof(AssemblyScanningExtensionsTests).Assembly; @@ -519,9 +495,7 @@ public async Task AddInterceptorsFromAssembly_ReturnsConfigurator(CancellationTo } [Test] - public void AddInterceptorsFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddInterceptorsFromAssemblies_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -530,9 +504,7 @@ CancellationToken cancellationToken } [Test] - public void AddInterceptorsFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddInterceptorsFromAssemblies_WithNullAssemblies_ThrowsArgumentNullException() { var services = new ServiceCollection(); Assembly[]? assemblies = null; @@ -543,9 +515,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddInterceptorsFromAssemblies_RegistersInterceptorsFromAllAssemblies( - CancellationToken cancellationToken - ) + public async Task AddInterceptorsFromAssemblies_RegistersInterceptorsFromAllAssemblies() { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -570,7 +540,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddInterceptorsFromAssemblies_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssemblies_ReturnsConfigurator() { var services = new ServiceCollection(); var assemblies = new[] { typeof(AssemblyScanningExtensionsTests).Assembly }; @@ -591,7 +561,7 @@ public async Task AddInterceptorsFromAssemblies_ReturnsConfigurator(Cancellation } [Test] - public async Task AddInterceptorsFromAssemblyContaining_RegistersInterceptors(CancellationToken cancellationToken) + public async Task AddInterceptorsFromAssemblyContaining_RegistersInterceptors() { var services = new ServiceCollection(); @@ -615,9 +585,7 @@ public async Task AddInterceptorsFromAssemblyContaining_RegistersInterceptors(Ca } [Test] - public void AddInterceptorsFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddInterceptorsFromAssemblyContaining_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs index a5c11d2c..1bb2ea64 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusExtensionsTests.cs @@ -19,9 +19,7 @@ public sealed class AzureServiceBusExtensionsTests "Endpoint=sb://localhost/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=Fake="; [Test] - public void UseAzureServiceBusTransport_When_configurator_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) + public void UseAzureServiceBusTransport_When_configurator_is_null_throws_ArgumentNullException() { IMediatorBuilder? configurator = null; @@ -29,7 +27,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseAzureServiceBusTransport_registers_transport(CancellationToken cancellationToken) + public async Task UseAzureServiceBusTransport_registers_transport() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => @@ -41,9 +39,7 @@ public async Task UseAzureServiceBusTransport_registers_transport(CancellationTo } [Test] - public async Task UseAzureServiceBusTransport_registers_ServiceBusClient_as_singleton( - CancellationToken cancellationToken - ) + public async Task UseAzureServiceBusTransport_registers_ServiceBusClient_as_singleton() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => @@ -55,9 +51,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseAzureServiceBusTransport_registers_DefaultAzureCredential_as_TokenCredential( - CancellationToken cancellationToken - ) + public async Task UseAzureServiceBusTransport_registers_DefaultAzureCredential_as_TokenCredential() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseAzureServiceBusTransport()); @@ -67,7 +61,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseAzureServiceBusTransport_validates_required_options(CancellationToken cancellationToken) + public async Task UseAzureServiceBusTransport_validates_required_options() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseAzureServiceBusTransport()); @@ -78,9 +72,7 @@ public async Task UseAzureServiceBusTransport_validates_required_options(Cancell } [Test] - public async Task UseAzureServiceBusTransport_with_fully_qualified_namespace_creates_ServiceBusClient( - CancellationToken cancellationToken - ) + public async Task UseAzureServiceBusTransport_with_fully_qualified_namespace_creates_ServiceBusClient() { IServiceCollection services = new ServiceCollection(); @@ -101,9 +93,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseAzureServiceBusTransport_with_connection_string_creates_ServiceBusClient( - CancellationToken cancellationToken - ) + public async Task UseAzureServiceBusTransport_with_connection_string_creates_ServiceBusClient() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => @@ -117,7 +107,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseAzureServiceBusTransport_replaces_existing_transport(CancellationToken cancellationToken) + public async Task UseAzureServiceBusTransport_replaces_existing_transport() { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -130,9 +120,7 @@ public async Task UseAzureServiceBusTransport_replaces_existing_transport(Cancel } [Test] - public async Task UseAzureServiceBusTransport_does_not_replace_custom_TokenCredential( - CancellationToken cancellationToken - ) + public async Task UseAzureServiceBusTransport_does_not_replace_custom_TokenCredential() { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new FakeTokenCredential()); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs index 287d7bf1..a0382b48 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/AzureServiceBus/AzureServiceBusMessageTransportTests.cs @@ -22,7 +22,7 @@ public sealed class AzureServiceBusMessageTransportTests // ── Constructor guards ──────────────────────────────────────────────────── [Test] - public async Task Constructor_When_client_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_When_client_is_null_throws_ArgumentNullException() { var resolver = new FakeTopicNameResolver(); var options = Options.Create(new AzureServiceBusTransportOptions()); @@ -33,9 +33,7 @@ public async Task Constructor_When_client_is_null_throws_ArgumentNullException(C } [Test] - public async Task Constructor_When_resolver_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) + public async Task Constructor_When_resolver_is_null_throws_ArgumentNullException() { await using var client = new ServiceBusClient(FakeConnectionString); var options = Options.Create(new AzureServiceBusTransportOptions()); @@ -46,7 +44,7 @@ CancellationToken cancellationToken } [Test] - public async Task Constructor_When_options_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_When_options_is_null_throws_ArgumentNullException() { await using var client = new ServiceBusClient(FakeConnectionString); var resolver = new FakeTopicNameResolver(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs index 1a5386f4..559ee9b3 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprExtensionsTests.cs @@ -18,12 +18,11 @@ public sealed class DaprExtensionsTests { [Test] - public async Task UseDaprTransport_When_configurator_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) => _ = await Assert.That(() => DaprExtensions.UseDaprTransport(null!)).Throws(); + public async Task UseDaprTransport_When_configurator_is_null_throws_ArgumentNullException() => + _ = await Assert.That(() => DaprExtensions.UseDaprTransport(null!)).Throws(); [Test] - public async Task UseDaprTransport_Registers_transport_as_singleton(CancellationToken cancellationToken) + public async Task UseDaprTransport_Registers_transport_as_singleton() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseDaprTransport()); @@ -38,7 +37,7 @@ public async Task UseDaprTransport_Registers_transport_as_singleton(Cancellation } [Test] - public async Task UseDaprTransport_Replaces_existing_transport(CancellationToken cancellationToken) + public async Task UseDaprTransport_Replaces_existing_transport() { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -49,7 +48,7 @@ public async Task UseDaprTransport_Replaces_existing_transport(CancellationToken } [Test] - public async Task UseDaprTransport_Configures_options(CancellationToken cancellationToken) + public async Task UseDaprTransport_Configures_options() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseDaprTransport(o => o.PubSubName = "custom-pubsub")); @@ -61,9 +60,7 @@ public async Task UseDaprTransport_Configures_options(CancellationToken cancella } [Test] - public async Task UseDaprTransport_Without_configureOptions_uses_default_PubSubName( - CancellationToken cancellationToken - ) + public async Task UseDaprTransport_Without_configureOptions_uses_default_PubSubName() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseDaprTransport()); @@ -75,7 +72,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseDaprTransport_Returns_same_configurator_for_chaining(CancellationToken cancellationToken) + public async Task UseDaprTransport_Returns_same_configurator_for_chaining() { IServiceCollection services = new ServiceCollection(); IMediatorBuilder? captured = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs index 81f64eaf..f5f43150 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dapr/DaprMessageTransportTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Dapr; +namespace NetEvolve.Pulse.Tests.Unit.Dapr; using System; using System.Threading.Tasks; @@ -14,9 +14,7 @@ namespace NetEvolve.Pulse.Tests.Unit.Dapr; public sealed class DaprMessageTransportTests { [Test] - public async Task Constructor_When_daprClient_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_When_daprClient_is_null_throws_ArgumentNullException() => _ = await Assert .That(() => new DaprMessageTransport( @@ -28,9 +26,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_When_topicNameResolver_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) + public async Task Constructor_When_topicNameResolver_is_null_throws_ArgumentNullException() { using var daprClient = new DaprClientBuilder().Build(); @@ -40,7 +36,7 @@ CancellationToken cancellationToken } [Test] - public async Task Constructor_When_options_is_null_throws_ArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_When_options_is_null_throws_ArgumentNullException() { using var daprClient = new DaprClientBuilder().Build(); @@ -50,7 +46,7 @@ public async Task Constructor_When_options_is_null_throws_ArgumentNullException( } [Test] - public async Task Constructor_With_valid_arguments_creates_instance(CancellationToken cancellationToken) + public async Task Constructor_With_valid_arguments_creates_instance() { using var daprClient = new DaprClientBuilder().Build(); var transport = new DaprMessageTransport( diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs index c529b26b..b4a6640f 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/PrioritizedEventDispatcherTests.cs @@ -102,7 +102,7 @@ public async Task DispatchAsync_WithCancellation_StopsExecution(CancellationToke var dispatcher = new PrioritizedEventDispatcher(); var message = new TestEvent(); var executionOrder = new ConcurrentQueue(); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var handlers = new List> { new PrioritizedTestHandler(1, 0, executionOrder), diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs index 1a682c84..43282fca 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/RateLimitedEventDispatcherTests.cs @@ -14,7 +14,7 @@ public class RateLimitedEventDispatcherTests { [Test] - public async Task Constructor_WithDefaultConcurrency_CreatesWith5(CancellationToken cancellationToken) + public async Task Constructor_WithDefaultConcurrency_CreatesWith5() { var dispatcher = new RateLimitedEventDispatcher(); @@ -22,7 +22,7 @@ public async Task Constructor_WithDefaultConcurrency_CreatesWith5(CancellationTo } [Test] - public async Task Constructor_WithCustomConcurrency_CreatesWithSpecifiedValue(CancellationToken cancellationToken) + public async Task Constructor_WithCustomConcurrency_CreatesWithSpecifiedValue() { var dispatcher = new RateLimitedEventDispatcher(maxConcurrency: 10); @@ -30,14 +30,12 @@ public async Task Constructor_WithCustomConcurrency_CreatesWithSpecifiedValue(Ca } [Test] - public async Task Constructor_WithZeroConcurrency_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) => _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: 0)); + public async Task Constructor_WithZeroConcurrency_ThrowsArgumentOutOfRangeException() => + _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: 0)); [Test] - public async Task Constructor_WithNegativeConcurrency_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) => _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: -1)); + public async Task Constructor_WithNegativeConcurrency_ThrowsArgumentOutOfRangeException() => + _ = Assert.Throws(() => _ = new RateLimitedEventDispatcher(maxConcurrency: -1)); [Test] public async Task DispatchAsync_WithHandlers_InvokesAllHandlers(CancellationToken cancellationToken) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs index 009d2bd4..229ca784 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Dispatchers/SequentialEventDispatcherTests.cs @@ -82,7 +82,7 @@ public async Task DispatchAsync_WithCancellationBetweenHandlers_StopsExecution(C var dispatcher = new SequentialEventDispatcher(); var testEvent = new TestEvent(); var executionOrder = new List(); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var handlers = new List> { new CancellingEventHandler(1, executionOrder, cts), diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs index b877e666..0c81de36 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; +namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; using System; using System.Threading.Tasks; @@ -14,7 +14,7 @@ namespace NetEvolve.Pulse.Tests.Unit.EntityFramework; public sealed class EntityFrameworkEventOutboxTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => _ = await Assert .That(() => new EntityFrameworkOutbox( @@ -26,7 +26,7 @@ public async Task Constructor_WithNullContext_ThrowsArgumentNullException(Cancel .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullOptions_ThrowsArgumentNullException)) @@ -39,7 +39,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(Cancel } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException)) @@ -52,7 +52,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance)) @@ -69,7 +69,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(StoreAsync_WithNullMessage_ThrowsArgumentNullException)) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs index 2aef3b43..12056704 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkExtensionsTests.cs @@ -18,17 +18,13 @@ public sealed class EntityFrameworkExtensionsTests { [Test] - public async Task AddEntityFrameworkOutbox_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddEntityFrameworkOutbox_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => EntityFrameworkExtensions.AddEntityFrameworkOutbox(null!)) .Throws(); [Test] - public async Task AddEntityFrameworkOutbox_WithValidConfigurator_ReturnsConfiguratorForChaining( - CancellationToken cancellationToken - ) + public async Task AddEntityFrameworkOutbox_WithValidConfigurator_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -39,7 +35,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped() { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -57,7 +53,7 @@ public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped(Can } [Test] - public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped() { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -75,7 +71,7 @@ public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped(Cancella } [Test] - public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped() { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -93,7 +89,7 @@ public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped(Can } [Test] - public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddEntityFrameworkOutbox()); @@ -108,7 +104,7 @@ public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton(Canc } [Test] - public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -125,7 +121,7 @@ public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions(C } [Test] - public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddDbContext(o => @@ -142,7 +138,7 @@ public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions(Ca } [Test] - public async Task AddEntityFrameworkOutbox_RegistersOutboxManagementAsScoped(CancellationToken cancellationToken) + public async Task AddEntityFrameworkOutbox_RegistersOutboxManagementAsScoped() { var services = new ServiceCollection(); _ = services.AddDbContext(o => diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs index 366ad382..164254c8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxManagementTests.cs @@ -13,13 +13,13 @@ public sealed class EntityFrameworkOutboxManagementTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => _ = await Assert .That(() => new EntityFrameworkOutboxManagement(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException)) @@ -32,7 +32,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance)) @@ -45,9 +45,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase( @@ -63,9 +61,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException)) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs index efd7ddfd..7131a197 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/EntityFrameworkOutboxTransactionScopeTests.cs @@ -11,13 +11,13 @@ public sealed class EntityFrameworkOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithNullContext_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullContext_ThrowsArgumentNullException() => _ = await Assert .That(() => new EntityFrameworkOutboxTransactionScope(null!)) .Throws(); [Test] - public async Task Constructor_WithValidContext_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidContext_CreatesInstance() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(Constructor_WithValidContext_CreatesInstance)) @@ -30,7 +30,7 @@ public async Task Constructor_WithValidContext_CreatesInstance(CancellationToken } [Test] - public async Task GetCurrentTransaction_WithNoActiveTransaction_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithNoActiveTransaction_ReturnsNull() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(GetCurrentTransaction_WithNoActiveTransaction_ReturnsNull)) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs index e810ad7e..f60b383d 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/ModelBuilderExtensionsTests.cs @@ -12,9 +12,7 @@ public sealed class ModelBuilderExtensionsTests { [Test] - public async Task ApplyPulseConfiguration_When_modelBuilder_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) + public async Task ApplyPulseConfiguration_When_modelBuilder_is_null_throws_ArgumentNullException() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(nameof(ApplyPulseConfiguration_When_modelBuilder_is_null_throws_ArgumentNullException)) @@ -27,9 +25,7 @@ CancellationToken cancellationToken } [Test] - public async Task ApplyPulseConfiguration_When_context_is_null_throws_ArgumentNullException( - CancellationToken cancellationToken - ) + public async Task ApplyPulseConfiguration_When_context_is_null_throws_ArgumentNullException() { var modelBuilder = new ModelBuilder(); @@ -39,9 +35,7 @@ CancellationToken cancellationToken } [Test] - public async Task ApplyPulseConfiguration_When_context_is_not_IOutboxDbContext_returns_same_modelBuilder( - CancellationToken cancellationToken - ) + public async Task ApplyPulseConfiguration_When_context_is_not_IOutboxDbContext_returns_same_modelBuilder() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase( @@ -57,9 +51,7 @@ CancellationToken cancellationToken } [Test] - public async Task ApplyPulseConfiguration_When_context_implements_IOutboxDbContext_returns_same_modelBuilder( - CancellationToken cancellationToken - ) + public async Task ApplyPulseConfiguration_When_context_implements_IOutboxDbContext_returns_same_modelBuilder() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase( diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs index 6b932436..0768b4f8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EntityFramework/TypeValueConverterTests.cs @@ -11,7 +11,7 @@ public sealed class TypeValueConverterTests { [Test] - public async Task Constructor_Creates_instance(CancellationToken cancellationToken) + public async Task Constructor_Creates_instance() { var converter = new TypeValueConverter(); @@ -19,9 +19,7 @@ public async Task Constructor_Creates_instance(CancellationToken cancellationTok } [Test] - public async Task ConvertToProvider_With_valid_type_returns_assembly_qualified_name( - CancellationToken cancellationToken - ) + public async Task ConvertToProvider_With_valid_type_returns_assembly_qualified_name() { var converter = new TypeValueConverter(); var toProvider = converter.ConvertToProvider; @@ -36,7 +34,7 @@ CancellationToken cancellationToken } [Test] - public async Task ConvertFromProvider_With_valid_type_name_returns_type(CancellationToken cancellationToken) + public async Task ConvertFromProvider_With_valid_type_name_returns_type() { var converter = new TypeValueConverter(); var fromProvider = converter.ConvertFromProvider; @@ -47,9 +45,7 @@ public async Task ConvertFromProvider_With_valid_type_name_returns_type(Cancella } [Test] - public async Task ConvertFromProvider_With_invalid_type_name_throws_InvalidOperationException( - CancellationToken cancellationToken - ) + public async Task ConvertFromProvider_With_invalid_type_name_throws_InvalidOperationException() { var converter = new TypeValueConverter(); var fromProvider = converter.ConvertFromProvider; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs index ce720b98..575466e4 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/EventDispatcherExtensionsTests.cs @@ -16,15 +16,13 @@ public sealed class EventDispatcherExtensionsTests { [Test] - public async Task UseDefaultEventDispatcher_NullBuilder_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task UseDefaultEventDispatcher_NullBuilder_ThrowsArgumentNullException() => _ = await Assert .That(() => EventDispatcherExtensions.UseDefaultEventDispatcher(null!)) .Throws(); [Test] - public async Task UseDefaultEventDispatcher_RegistersDispatcherAsSingleton(CancellationToken cancellationToken) + public async Task UseDefaultEventDispatcher_RegistersDispatcherAsSingleton() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -45,9 +43,7 @@ public async Task UseDefaultEventDispatcher_RegistersDispatcherAsSingleton(Cance } [Test] - public async Task UseDefaultEventDispatcher_WithCustomLifetime_RegistersWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task UseDefaultEventDispatcher_WithCustomLifetime_RegistersWithSpecifiedLifetime() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -66,9 +62,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseDefaultEventDispatcher_CalledMultipleTimes_ReplacesRegistration( - CancellationToken cancellationToken - ) + public async Task UseDefaultEventDispatcher_CalledMultipleTimes_ReplacesRegistration() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -88,7 +82,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseDefaultEventDispatcher_DoesNotRemoveKeyedDispatchers(CancellationToken cancellationToken) + public async Task UseDefaultEventDispatcher_DoesNotRemoveKeyedDispatchers() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -115,7 +109,7 @@ public async Task UseDefaultEventDispatcher_DoesNotRemoveKeyedDispatchers(Cancel } [Test] - public async Task UseDefaultEventDispatcher_ReturnsSameBuilder(CancellationToken cancellationToken) + public async Task UseDefaultEventDispatcher_ReturnsSameBuilder() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -130,7 +124,7 @@ public async Task UseDefaultEventDispatcher_ReturnsSameBuilder(CancellationToken } [Test] - public async Task UseDefaultEventDispatcher_WithFactory_RegistersDispatcher(CancellationToken cancellationToken) + public async Task UseDefaultEventDispatcher_WithFactory_RegistersDispatcher() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -156,9 +150,7 @@ public async Task UseDefaultEventDispatcher_WithFactory_RegistersDispatcher(Canc } [Test] - public async Task UseDefaultEventDispatcher_WithFactory_WithNullFactory_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public async Task UseDefaultEventDispatcher_WithFactory_WithNullFactory_ThrowsArgumentNullException() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -168,15 +160,13 @@ CancellationToken cancellationToken } [Test] - public async Task UseEventDispatcherFor_NullBuilder_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task UseEventDispatcherFor_NullBuilder_ThrowsArgumentNullException() => _ = await Assert .That(() => EventDispatcherExtensions.UseEventDispatcherFor(null!)) .Throws(); [Test] - public async Task UseEventDispatcherFor_RegistersKeyedDispatcher(CancellationToken cancellationToken) + public async Task UseEventDispatcherFor_RegistersKeyedDispatcher() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -199,9 +189,7 @@ public async Task UseEventDispatcherFor_RegistersKeyedDispatcher(CancellationTok } [Test] - public async Task UseEventDispatcherFor_WithCustomLifetime_RegistersWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task UseEventDispatcherFor_WithCustomLifetime_RegistersWithSpecifiedLifetime() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -220,9 +208,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseEventDispatcherFor_CalledMultipleTimes_ReplacesRegistration( - CancellationToken cancellationToken - ) + public async Task UseEventDispatcherFor_CalledMultipleTimes_ReplacesRegistration() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -242,7 +228,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseEventDispatcherFor_DifferentEventTypes_RegistersSeparately(CancellationToken cancellationToken) + public async Task UseEventDispatcherFor_DifferentEventTypes_RegistersSeparately() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -271,7 +257,7 @@ public async Task UseEventDispatcherFor_DifferentEventTypes_RegistersSeparately( } [Test] - public async Task UseEventDispatcherFor_DoesNotAffectGlobalDispatcher(CancellationToken cancellationToken) + public async Task UseEventDispatcherFor_DoesNotAffectGlobalDispatcher() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -296,7 +282,7 @@ public async Task UseEventDispatcherFor_DoesNotAffectGlobalDispatcher(Cancellati } [Test] - public async Task UseEventDispatcherFor_WithFactory_RegistersKeyedDispatcher(CancellationToken cancellationToken) + public async Task UseEventDispatcherFor_WithFactory_RegistersKeyedDispatcher() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -324,9 +310,7 @@ public async Task UseEventDispatcherFor_WithFactory_RegistersKeyedDispatcher(Can } [Test] - public async Task UseEventDispatcherFor_WithFactory_WithNullFactory_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public async Task UseEventDispatcherFor_WithFactory_WithNullFactory_ThrowsArgumentNullException() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs index acd04849..bf8fc192 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/FluentValidation/FluentValidationExtensionsTests.cs @@ -15,15 +15,13 @@ public sealed class FluentValidationExtensionsTests { [Test] - public async Task AddFluentValidation_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddFluentValidation_NullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => FluentValidationExtensions.AddFluentValidation(null!)) .Throws(); [Test] - public async Task AddFluentValidation_RegistersRequestInterceptor(CancellationToken cancellationToken) + public async Task AddFluentValidation_RegistersRequestInterceptor() { // Arrange var services = new ServiceCollection(); @@ -37,9 +35,7 @@ public async Task AddFluentValidation_RegistersRequestInterceptor(CancellationTo } [Test] - public async Task AddFluentValidation_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor( - CancellationToken cancellationToken - ) + public async Task AddFluentValidation_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor() { // Arrange var services = new ServiceCollection(); @@ -57,7 +53,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddFluentValidation_ReturnsSameConfigurator(CancellationToken cancellationToken) + public async Task AddFluentValidation_ReturnsSameConfigurator() { // Arrange var services = new ServiceCollection(); @@ -69,7 +65,7 @@ public async Task AddFluentValidation_ReturnsSameConfigurator(CancellationToken } [Test] - public async Task AddFluentValidation_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddFluentValidation_RegistersInterceptorWithScopedLifetime() { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs index c6ff00f4..d831261c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HandlerRegistrationExtensionsTests.cs @@ -11,7 +11,7 @@ public class HandlerRegistrationExtensionsTests { [Test] - public void AddCommandHandler_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) + public void AddCommandHandler_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -21,7 +21,7 @@ public void AddCommandHandler_WithNullConfigurator_ThrowsArgumentNullException(C } [Test] - public async Task AddCommandHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddCommandHandler_RegistersHandlerWithScopedLifetime() { var services = new ServiceCollection(); @@ -38,9 +38,7 @@ public async Task AddCommandHandler_RegistersHandlerWithScopedLifetime(Cancellat } [Test] - public async Task AddCommandHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddCommandHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -59,7 +57,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddCommandHandler_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddCommandHandler_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -79,9 +77,7 @@ public async Task AddCommandHandler_ReturnsConfigurator(CancellationToken cancel } [Test] - public void AddCommandHandler_VoidCommand_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddCommandHandler_VoidCommand_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -91,9 +87,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddCommandHandler_VoidCommand_RegistersHandlerWithScopedLifetime( - CancellationToken cancellationToken - ) + public async Task AddCommandHandler_VoidCommand_RegistersHandlerWithScopedLifetime() { var services = new ServiceCollection(); @@ -110,9 +104,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddCommandHandler_VoidCommand_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddCommandHandler_VoidCommand_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -131,7 +123,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddCommandHandler_VoidCommand_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddCommandHandler_VoidCommand_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -151,7 +143,7 @@ public async Task AddCommandHandler_VoidCommand_ReturnsConfigurator(Cancellation } [Test] - public void AddQueryHandler_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) + public void AddQueryHandler_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -161,7 +153,7 @@ public void AddQueryHandler_WithNullConfigurator_ThrowsArgumentNullException(Can } [Test] - public async Task AddQueryHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddQueryHandler_RegistersHandlerWithScopedLifetime() { var services = new ServiceCollection(); @@ -178,9 +170,7 @@ public async Task AddQueryHandler_RegistersHandlerWithScopedLifetime(Cancellatio } [Test] - public async Task AddQueryHandler_WithTransientLifetime_RegistersHandlerWithTransientLifetime( - CancellationToken cancellationToken - ) + public async Task AddQueryHandler_WithTransientLifetime_RegistersHandlerWithTransientLifetime() { var services = new ServiceCollection(); @@ -199,7 +189,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddQueryHandler_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddQueryHandler_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -219,7 +209,7 @@ public async Task AddQueryHandler_ReturnsConfigurator(CancellationToken cancella } [Test] - public void AddEventHandler_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) + public void AddEventHandler_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -227,7 +217,7 @@ public void AddEventHandler_WithNullConfigurator_ThrowsArgumentNullException(Can } [Test] - public async Task AddEventHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddEventHandler_RegistersHandlerWithScopedLifetime() { var services = new ServiceCollection(); @@ -244,9 +234,7 @@ public async Task AddEventHandler_RegistersHandlerWithScopedLifetime(Cancellatio } [Test] - public async Task AddEventHandler_WithSingletonLifetime_RegistersHandlerWithSingletonLifetime( - CancellationToken cancellationToken - ) + public async Task AddEventHandler_WithSingletonLifetime_RegistersHandlerWithSingletonLifetime() { var services = new ServiceCollection(); @@ -263,7 +251,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddEventHandler_AllowsMultipleHandlersForSameEvent(CancellationToken cancellationToken) + public async Task AddEventHandler_AllowsMultipleHandlersForSameEvent() { var services = new ServiceCollection(); @@ -282,7 +270,7 @@ public async Task AddEventHandler_AllowsMultipleHandlersForSameEvent(Cancellatio } [Test] - public async Task AddEventHandler_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddEventHandler_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -302,7 +290,7 @@ public async Task AddEventHandler_ReturnsConfigurator(CancellationToken cancella } [Test] - public async Task HandlerRegistrationExtensions_SupportMethodChaining(CancellationToken cancellationToken) + public async Task HandlerRegistrationExtensions_SupportMethodChaining() { var services = new ServiceCollection(); @@ -378,9 +366,7 @@ private sealed partial class AnotherTestEventHandler : IEventHandler // Interceptor registration tests [Test] - public void AddRequestInterceptor_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddRequestInterceptor_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -390,7 +376,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddRequestInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddRequestInterceptor_RegistersInterceptorWithScopedLifetime() { var services = new ServiceCollection(); @@ -409,9 +395,7 @@ public async Task AddRequestInterceptor_RegistersInterceptorWithScopedLifetime(C } [Test] - public async Task AddRequestInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddRequestInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -432,7 +416,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddRequestInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddRequestInterceptor_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -452,9 +436,7 @@ public async Task AddRequestInterceptor_ReturnsConfigurator(CancellationToken ca } [Test] - public void AddCommandInterceptor_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddCommandInterceptor_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -464,7 +446,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddCommandInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddCommandInterceptor_RegistersInterceptorWithScopedLifetime() { var services = new ServiceCollection(); @@ -483,9 +465,7 @@ public async Task AddCommandInterceptor_RegistersInterceptorWithScopedLifetime(C } [Test] - public async Task AddCommandInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddCommandInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -506,7 +486,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddCommandInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddCommandInterceptor_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -526,9 +506,7 @@ public async Task AddCommandInterceptor_ReturnsConfigurator(CancellationToken ca } [Test] - public void AddQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -538,7 +516,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddQueryInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddQueryInterceptor_RegistersInterceptorWithScopedLifetime() { var services = new ServiceCollection(); @@ -555,9 +533,7 @@ public async Task AddQueryInterceptor_RegistersInterceptorWithScopedLifetime(Can } [Test] - public async Task AddQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -576,7 +552,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddQueryInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddQueryInterceptor_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -596,9 +572,7 @@ public async Task AddQueryInterceptor_ReturnsConfigurator(CancellationToken canc } [Test] - public void AddEventInterceptor_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddEventInterceptor_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -608,7 +582,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddEventInterceptor_RegistersInterceptorWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddEventInterceptor_RegistersInterceptorWithScopedLifetime() { var services = new ServiceCollection(); @@ -625,9 +599,7 @@ public async Task AddEventInterceptor_RegistersInterceptorWithScopedLifetime(Can } [Test] - public async Task AddEventInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddEventInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -646,7 +618,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddEventInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddEventInterceptor_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -666,7 +638,7 @@ public async Task AddEventInterceptor_ReturnsConfigurator(CancellationToken canc } [Test] - public async Task AddEventInterceptor_AllowsMultipleInterceptorsForSameEvent(CancellationToken cancellationToken) + public async Task AddEventInterceptor_AllowsMultipleInterceptorsForSameEvent() { var services = new ServiceCollection(); @@ -736,9 +708,7 @@ public Task HandleAsync( // Stream query handler registration tests [Test] - public void AddStreamQueryHandler_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddStreamQueryHandler_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -748,7 +718,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddStreamQueryHandler_RegistersHandlerWithScopedLifetime(CancellationToken cancellationToken) + public async Task AddStreamQueryHandler_RegistersHandlerWithScopedLifetime() { var services = new ServiceCollection(); @@ -769,9 +739,7 @@ public async Task AddStreamQueryHandler_RegistersHandlerWithScopedLifetime(Cance } [Test] - public async Task AddStreamQueryHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddStreamQueryHandler_WithExplicitLifetime_RegistersHandlerWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -792,7 +760,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddStreamQueryHandler_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddStreamQueryHandler_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; @@ -812,9 +780,7 @@ public async Task AddStreamQueryHandler_ReturnsConfigurator(CancellationToken ca } [Test] - public void AddStreamQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public void AddStreamQueryInterceptor_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -824,9 +790,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddStreamQueryInterceptor_RegistersInterceptorWithScopedLifetime( - CancellationToken cancellationToken - ) + public async Task AddStreamQueryInterceptor_RegistersInterceptorWithScopedLifetime() { var services = new ServiceCollection(); @@ -847,9 +811,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddStreamQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime( - CancellationToken cancellationToken - ) + public async Task AddStreamQueryInterceptor_WithExplicitLifetime_RegistersInterceptorWithSpecifiedLifetime() { var services = new ServiceCollection(); @@ -872,7 +834,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddStreamQueryInterceptor_ReturnsConfigurator(CancellationToken cancellationToken) + public async Task AddStreamQueryInterceptor_ReturnsConfigurator() { var services = new ServiceCollection(); IMediatorBuilder? capturedConfig = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs index 74fcc19a..8ad16746 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/HttpCorrelationExtensionsTests.cs @@ -14,15 +14,13 @@ public sealed class HttpCorrelationExtensionsTests { [Test] - public async Task AddHttpCorrelationEnrichment_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddHttpCorrelationEnrichment_NullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => HttpCorrelationExtensions.AddHttpCorrelationEnrichment(null!)) .Throws(); [Test] - public async Task AddHttpCorrelationEnrichment_RegistersRequestInterceptor(CancellationToken cancellationToken) + public async Task AddHttpCorrelationEnrichment_RegistersRequestInterceptor() { // Arrange var services = new ServiceCollection(); @@ -36,7 +34,7 @@ public async Task AddHttpCorrelationEnrichment_RegistersRequestInterceptor(Cance } [Test] - public async Task AddHttpCorrelationEnrichment_RegistersEventInterceptor(CancellationToken cancellationToken) + public async Task AddHttpCorrelationEnrichment_RegistersEventInterceptor() { // Arrange var services = new ServiceCollection(); @@ -50,9 +48,7 @@ public async Task AddHttpCorrelationEnrichment_RegistersEventInterceptor(Cancell } [Test] - public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor( - CancellationToken cancellationToken - ) + public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateRequestInterceptor() { // Arrange var services = new ServiceCollection(); @@ -73,9 +69,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateEventInterceptor( - CancellationToken cancellationToken - ) + public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateEventInterceptor() { // Arrange var services = new ServiceCollection(); @@ -96,7 +90,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHttpCorrelationEnrichment_ReturnsSameConfigurator(CancellationToken cancellationToken) + public async Task AddHttpCorrelationEnrichment_ReturnsSameConfigurator() { // Arrange var services = new ServiceCollection(); @@ -108,9 +102,7 @@ public async Task AddHttpCorrelationEnrichment_ReturnsSameConfigurator(Cancellat } [Test] - public async Task AddHttpCorrelationEnrichment_WithoutAccessorRegistered_InterceptorResolvesSuccessfully( - CancellationToken cancellationToken - ) + public async Task AddHttpCorrelationEnrichment_WithoutAccessorRegistered_InterceptorResolvesSuccessfully() { // Arrange — IHttpCorrelationAccessor is intentionally NOT registered var services = new ServiceCollection(); @@ -127,7 +119,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddHttpCorrelationEnrichment_RegistersStreamQueryInterceptor(CancellationToken cancellationToken) + public async Task AddHttpCorrelationEnrichment_RegistersStreamQueryInterceptor() { // Arrange var services = new ServiceCollection(); @@ -141,9 +133,7 @@ public async Task AddHttpCorrelationEnrichment_RegistersStreamQueryInterceptor(C } [Test] - public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateStreamQueryInterceptor( - CancellationToken cancellationToken - ) + public async Task AddHttpCorrelationEnrichment_CalledMultipleTimes_DoesNotDuplicateStreamQueryInterceptor() { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs index 85822546..1326b793 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationEventInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -23,15 +23,13 @@ namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; public sealed class HttpCorrelationEventInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new HttpCorrelationEventInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_NoAccessorRegistered_DoesNotThrow() { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -44,7 +42,7 @@ public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToke } [Test] - public async Task Constructor_WithAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_WithAccessorRegistered_DoesNotThrow() { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs index d2057f5d..0c91074a 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -23,15 +23,13 @@ namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; public sealed class HttpCorrelationRequestInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new HttpCorrelationRequestInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_NoAccessorRegistered_DoesNotThrow() { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -44,7 +42,7 @@ public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToke } [Test] - public async Task Constructor_WithAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_WithAccessorRegistered_DoesNotThrow() { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs index d436bbb1..71cd60d8 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/HttpCorrelation/Interceptors/HttpCorrelationStreamQueryInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; using System; using System.Collections.Generic; @@ -28,15 +28,13 @@ namespace NetEvolve.Pulse.Tests.Unit.HttpCorrelation.Interceptors; public sealed class HttpCorrelationStreamQueryInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new HttpCorrelationStreamQueryInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_NoAccessorRegistered_DoesNotThrow() { // Arrange var provider = new ServiceCollection().BuildServiceProvider(); @@ -49,7 +47,7 @@ public async Task Constructor_NoAccessorRegistered_DoesNotThrow(CancellationToke } [Test] - public async Task Constructor_WithAccessorRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_WithAccessorRegistered_DoesNotThrow() { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs index 64b3f7c9..627782eb 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/IdempotencyExtensionsTests.cs @@ -17,12 +17,11 @@ public sealed class IdempotencyExtensionsTests { [Test] - public async Task AddIdempotency_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => _ = await Assert.That(() => IdempotencyExtensions.AddIdempotency(null!)).Throws(); + public async Task AddIdempotency_NullConfigurator_ThrowsArgumentNullException() => + _ = await Assert.That(() => IdempotencyExtensions.AddIdempotency(null!)).Throws(); [Test] - public async Task AddIdempotency_RegistersRequestInterceptor(CancellationToken cancellationToken) + public async Task AddIdempotency_RegistersRequestInterceptor() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -44,9 +43,7 @@ public async Task AddIdempotency_RegistersRequestInterceptor(CancellationToken c } [Test] - public async Task AddIdempotency_CalledMultipleTimes_DoesNotDuplicateInterceptor( - CancellationToken cancellationToken - ) + public async Task AddIdempotency_CalledMultipleTimes_DoesNotDuplicateInterceptor() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -65,7 +62,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddIdempotency_ReturnsSameConfigurator(CancellationToken cancellationToken) + public async Task AddIdempotency_ReturnsSameConfigurator() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -76,9 +73,7 @@ public async Task AddIdempotency_ReturnsSameConfigurator(CancellationToken cance } [Test] - public async Task AddIdempotency_WithoutStoreRegistered_InterceptorResolvesSuccessfully( - CancellationToken cancellationToken - ) + public async Task AddIdempotency_WithoutStoreRegistered_InterceptorResolvesSuccessfully() { var services = new ServiceCollection(); _ = services.AddLogging().AddSingleton(TimeProvider.System); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs index 94feb3b8..739fd3cc 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/IdempotencyCommandInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -22,15 +22,13 @@ namespace NetEvolve.Pulse.Tests.Unit.Interceptors; public sealed class IdempotencyCommandInterceptorTests { [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new IdempotencyCommandInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoStoreRegistered_DoesNotThrow(CancellationToken cancellationToken) + public async Task Constructor_NoStoreRegistered_DoesNotThrow() { var provider = new ServiceCollection().BuildServiceProvider(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs index e716d0b4..10e88d59 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Interceptors/LoggingInterceptorOptionsValidatorTests.cs @@ -11,7 +11,7 @@ public class LoggingInterceptorOptionsValidatorTests private static readonly LoggingInterceptorOptionsValidator _validator = new(); [Test] - public async Task Validate_WithNullThreshold_Succeeds(CancellationToken cancellationToken) + public async Task Validate_WithNullThreshold_Succeeds() { var options = new LoggingInterceptorOptions { SlowRequestThreshold = null }; @@ -21,7 +21,7 @@ public async Task Validate_WithNullThreshold_Succeeds(CancellationToken cancella } [Test] - public async Task Validate_WithZeroThreshold_Succeeds(CancellationToken cancellationToken) + public async Task Validate_WithZeroThreshold_Succeeds() { var options = new LoggingInterceptorOptions { SlowRequestThreshold = TimeSpan.Zero }; @@ -31,7 +31,7 @@ public async Task Validate_WithZeroThreshold_Succeeds(CancellationToken cancella } [Test] - public async Task Validate_WithPositiveThreshold_Succeeds(CancellationToken cancellationToken) + public async Task Validate_WithPositiveThreshold_Succeeds() { var options = new LoggingInterceptorOptions { SlowRequestThreshold = TimeSpan.FromMilliseconds(500) }; @@ -41,7 +41,7 @@ public async Task Validate_WithPositiveThreshold_Succeeds(CancellationToken canc } [Test] - public async Task Validate_WithNegativeThreshold_Fails(CancellationToken cancellationToken) + public async Task Validate_WithNegativeThreshold_Fails() { var options = new LoggingInterceptorOptions { SlowRequestThreshold = TimeSpan.FromMilliseconds(-1) }; @@ -51,7 +51,7 @@ public async Task Validate_WithNegativeThreshold_Fails(CancellationToken cancell } [Test] - public async Task Validate_DefaultOptions_Succeeds(CancellationToken cancellationToken) + public async Task Validate_DefaultOptions_Succeeds() { var options = new LoggingInterceptorOptions(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs index dabb78f5..b1fddbfe 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Internals/MediatorBuilderTests.cs @@ -10,7 +10,7 @@ public class MediatorBuilderTests { [Test] - public async Task Constructor_WithNullServices_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullServices_ThrowsArgumentNullException() { IServiceCollection? services = null; @@ -18,7 +18,7 @@ public async Task Constructor_WithNullServices_ThrowsArgumentNullException(Cance } [Test] - public async Task Constructor_WithValidServices_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidServices_CreatesInstance() { var services = new ServiceCollection(); @@ -32,7 +32,7 @@ public async Task Constructor_WithValidServices_CreatesInstance(CancellationToke } [Test] - public async Task Services_ReturnsProvidedServiceCollection(CancellationToken cancellationToken) + public async Task Services_ReturnsProvidedServiceCollection() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs index 8b392889..27716a21 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Internals/PulseMediatorTests.cs @@ -13,7 +13,7 @@ public class PulseMediatorTests { [Test] - public async Task Constructor_WithNullLogger_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullLogger_ThrowsArgumentNullException() { ILogger? logger = null; var serviceProvider = new ServiceCollection().BuildServiceProvider(); @@ -26,9 +26,7 @@ public async Task Constructor_WithNullLogger_ThrowsArgumentNullException(Cancell } [Test] - public async Task Constructor_WithNullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) + public async Task Constructor_WithNullServiceProvider_ThrowsArgumentNullException() { var logger = new ServiceCollection() .AddLogging() @@ -44,7 +42,7 @@ CancellationToken cancellationToken } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { var logger = new ServiceCollection() .AddLogging() @@ -60,7 +58,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidParameters_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidParameters_CreatesInstance() { var logger = new ServiceCollection() .AddLogging() diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs index 6d0bb78d..c4143ed6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Kafka/KafkaExtensionsTests.cs @@ -13,7 +13,7 @@ public sealed class KafkaExtensionsTests { [Test] - public async Task UseKafkaTransport_Registers_transport(CancellationToken cancellationToken) + public async Task UseKafkaTransport_Registers_transport() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseKafkaTransport()); @@ -23,7 +23,7 @@ public async Task UseKafkaTransport_Registers_transport(CancellationToken cancel } [Test] - public async Task UseKafkaTransport_Replaces_existing_transport(CancellationToken cancellationToken) + public async Task UseKafkaTransport_Replaces_existing_transport() { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -34,7 +34,7 @@ public async Task UseKafkaTransport_Replaces_existing_transport(CancellationToke } [Test] - public async Task UseKafkaTransport_Returns_same_configurator_for_chaining(CancellationToken cancellationToken) + public async Task UseKafkaTransport_Returns_same_configurator_for_chaining() { IServiceCollection services = new ServiceCollection(); IMediatorBuilder? captured = null; @@ -49,7 +49,7 @@ public async Task UseKafkaTransport_Returns_same_configurator_for_chaining(Cance } [Test] - public async Task UseKafkaTransport_Does_not_register_adapters(CancellationToken cancellationToken) + public async Task UseKafkaTransport_Does_not_register_adapters() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseKafkaTransport()); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs index c088fd11..5d09e6b4 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/LoggingExtensionsTests.cs @@ -13,7 +13,7 @@ public class LoggingExtensionsTests { [Test] - public async Task AddLogging_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task AddLogging_WithNullConfigurator_ThrowsArgumentNullException() { IMediatorBuilder? configurator = null; @@ -21,7 +21,7 @@ public async Task AddLogging_WithNullConfigurator_ThrowsArgumentNullException(Ca } [Test] - public async Task AddLogging_RegistersEventInterceptor(CancellationToken cancellationToken) + public async Task AddLogging_RegistersEventInterceptor() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -42,7 +42,7 @@ public async Task AddLogging_RegistersEventInterceptor(CancellationToken cancell } [Test] - public async Task AddLogging_RegistersRequestInterceptor(CancellationToken cancellationToken) + public async Task AddLogging_RegistersRequestInterceptor() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -64,7 +64,7 @@ public async Task AddLogging_RegistersRequestInterceptor(CancellationToken cance } [Test] - public async Task AddLogging_CalledMultipleTimes_DoesNotDuplicateInterceptors(CancellationToken cancellationToken) + public async Task AddLogging_CalledMultipleTimes_DoesNotDuplicateInterceptors() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -94,7 +94,7 @@ public async Task AddLogging_CalledMultipleTimes_DoesNotDuplicateInterceptors(Ca } [Test] - public async Task AddLogging_WithConfigure_AppliesOptions(CancellationToken cancellationToken) + public async Task AddLogging_WithConfigure_AppliesOptions() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -116,7 +116,7 @@ public async Task AddLogging_WithConfigure_AppliesOptions(CancellationToken canc } [Test] - public async Task AddLogging_WithoutConfigure_UsesDefaultOptions(CancellationToken cancellationToken) + public async Task AddLogging_WithoutConfigure_UsesDefaultOptions() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); @@ -134,7 +134,7 @@ public async Task AddLogging_WithoutConfigure_UsesDefaultOptions(CancellationTok } [Test] - public async Task AddLogging_ReturnsConfiguratorForChaining(CancellationToken cancellationToken) + public async Task AddLogging_ReturnsConfiguratorForChaining() { var services = new ServiceCollection(); var configurator = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs index 199de0b7..31d53486 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/ExponentialBackoffTests.cs @@ -15,11 +15,7 @@ public sealed class ExponentialBackoffTests [Arguments(0, 5)] [Arguments(1, 10)] [Arguments(2, 20)] - public async Task ComputeNextRetryAt_WithNoJitter_ComputesCorrectBackoff( - int retryCount, - int expectedSeconds, - CancellationToken cancellationToken - ) + public async Task ComputeNextRetryAt_WithNoJitter_ComputesCorrectBackoff(int retryCount, int expectedSeconds) { var options = new OutboxProcessorOptions { @@ -41,7 +37,7 @@ CancellationToken cancellationToken } [Test] - public async Task ComputeNextRetryAt_WithMaxRetryDelayExceeded_ClampsToMax(CancellationToken cancellationToken) + public async Task ComputeNextRetryAt_WithMaxRetryDelayExceeded_ClampsToMax() { var options = new OutboxProcessorOptions { @@ -63,7 +59,7 @@ public async Task ComputeNextRetryAt_WithMaxRetryDelayExceeded_ClampsToMax(Cance } [Test] - public async Task ComputeNextRetryAt_WithJitter_AddsRandomizedDelay(CancellationToken cancellationToken) + public async Task ComputeNextRetryAt_WithJitter_AddsRandomizedDelay() { var options = new OutboxProcessorOptions { @@ -86,7 +82,7 @@ public async Task ComputeNextRetryAt_WithJitter_AddsRandomizedDelay(Cancellation } [Test] - public async Task ComputeNextRetryAt_WithMultipleSamples_JitterIsRandomized(CancellationToken cancellationToken) + public async Task ComputeNextRetryAt_WithMultipleSamples_JitterIsRandomized() { var options = new OutboxProcessorOptions { @@ -111,7 +107,7 @@ public async Task ComputeNextRetryAt_WithMultipleSamples_JitterIsRandomized(Canc } [Test] - public async Task ComputeNextRetryAt_WithZeroRetryCount_UsesBaseDelay(CancellationToken cancellationToken) + public async Task ComputeNextRetryAt_WithZeroRetryCount_UsesBaseDelay() { var options = new OutboxProcessorOptions { @@ -132,7 +128,7 @@ public async Task ComputeNextRetryAt_WithZeroRetryCount_UsesBaseDelay(Cancellati } [Test] - public async Task OutboxProcessorOptions_HasCorrectDefaults(CancellationToken cancellationToken) + public async Task OutboxProcessorOptions_HasCorrectDefaults() { var options = new OutboxProcessorOptions(); @@ -144,7 +140,7 @@ public async Task OutboxProcessorOptions_HasCorrectDefaults(CancellationToken ca } [Test] - public async Task OutboxProcessorOptions_CanConfigureCustomValues(CancellationToken cancellationToken) + public async Task OutboxProcessorOptions_CanConfigureCustomValues() { var options = new OutboxProcessorOptions { @@ -163,7 +159,7 @@ public async Task OutboxProcessorOptions_CanConfigureCustomValues(CancellationTo } [Test] - public async Task ComputeNextRetryAt_WithLargeRetryCount_DoesNotOverflow(CancellationToken cancellationToken) + public async Task ComputeNextRetryAt_WithLargeRetryCount_DoesNotOverflow() { var options = new OutboxProcessorOptions { @@ -186,9 +182,7 @@ public async Task ComputeNextRetryAt_WithLargeRetryCount_DoesNotOverflow(Cancell } [Test] - public async Task ComputeNextRetryAt_WithBaseDelayGreaterThanMax_ClampsCorrectly( - CancellationToken cancellationToken - ) + public async Task ComputeNextRetryAt_WithBaseDelayGreaterThanMax_ClampsCorrectly() { var options = new OutboxProcessorOptions { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs index b761a1c7..e713c62f 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxExtensionsTests.cs @@ -21,11 +21,11 @@ public sealed class OutboxExtensionsTests { [Test] - public async Task AddOutbox_WithNullConfigurator_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task AddOutbox_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert.That(() => OutboxExtensions.AddOutbox(null!)).Throws(); [Test] - public async Task AddOutbox_ReturnsSameBuilder(CancellationToken cancellationToken) + public async Task AddOutbox_ReturnsSameBuilder() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -36,7 +36,7 @@ public async Task AddOutbox_ReturnsSameBuilder(CancellationToken cancellationTok } [Test] - public async Task AddOutbox_RegistersOutboxEventHandlerAsOpenGenericScoped(CancellationToken cancellationToken) + public async Task AddOutbox_RegistersOutboxEventHandlerAsOpenGenericScoped() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -55,9 +55,7 @@ public async Task AddOutbox_RegistersOutboxEventHandlerAsOpenGenericScoped(Cance } [Test] - public async Task AddOutbox_CalledMultipleTimes_DoesNotDuplicateOutboxEventHandler( - CancellationToken cancellationToken - ) + public async Task AddOutbox_CalledMultipleTimes_DoesNotDuplicateOutboxEventHandler() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -75,7 +73,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddOutbox_RegistersEventOutboxAsScoped(CancellationToken cancellationToken) + public async Task AddOutbox_RegistersEventOutboxAsScoped() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs index 31c3de58..49e51059 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxProcessorHostedServiceTests.cs @@ -111,7 +111,7 @@ public async Task StartAsync_WithCancellationToken_StartsProcessing(Cancellation var logger = CreateLogger(); using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await service.StartAsync(cts.Token).ConfigureAwait(false); @@ -134,7 +134,7 @@ public async Task StopAsync_WhenRunning_StopsGracefully(CancellationToken cancel var logger = CreateLogger(); using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await service.StartAsync(cts.Token).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false); @@ -212,7 +212,7 @@ public async Task ExecuteAsync_WithNoMessages_WaitsForPollingInterval(Cancellati var logger = CreateLogger(); using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await service.StartAsync(cts.Token).ConfigureAwait(false); await Task.Delay(1000, cancellationToken).ConfigureAwait(false); @@ -741,7 +741,7 @@ public async Task ExecuteAsync_AfterProcessingCycle_RecordsProcessingDuration(Ca var logger = CreateLogger(); using var service = new OutboxProcessorHostedService(repository, transport, CreateLifetime(), options, logger); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await service.StartAsync(cts.Token).ConfigureAwait(false); await Task.Delay(200, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); @@ -1003,7 +1003,7 @@ public async Task ExecuteAsync_WhenDatabaseIsUnhealthy_SkipsProcessingCycle(Canc await repository.AddAsync(CreateMessage(), cancellationToken).ConfigureAwait(false); - using var cts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); await service.StartAsync(cts.Token).ConfigureAwait(false); await Task.Delay(300, cancellationToken).ConfigureAwait(false); await cts.CancelAsync().ConfigureAwait(false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs index c3259447..1f64f114 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Outbox/OutboxStatisticsTests.cs @@ -8,7 +8,7 @@ public sealed class OutboxStatisticsTests { [Test] - public async Task Total_WithDefaultValues_ReturnsZero(CancellationToken cancellationToken) + public async Task Total_WithDefaultValues_ReturnsZero() { var statistics = new OutboxStatistics(); @@ -16,7 +16,7 @@ public async Task Total_WithDefaultValues_ReturnsZero(CancellationToken cancella } [Test] - public async Task Total_WithOnlyPending_ReturnsPendingCount(CancellationToken cancellationToken) + public async Task Total_WithOnlyPending_ReturnsPendingCount() { var statistics = new OutboxStatistics { Pending = 5 }; @@ -24,7 +24,7 @@ public async Task Total_WithOnlyPending_ReturnsPendingCount(CancellationToken ca } [Test] - public async Task Total_WithAllStatusesSet_ReturnsSumOfAllStatuses(CancellationToken cancellationToken) + public async Task Total_WithAllStatusesSet_ReturnsSumOfAllStatuses() { var statistics = new OutboxStatistics { @@ -39,7 +39,7 @@ public async Task Total_WithAllStatusesSet_ReturnsSumOfAllStatuses(CancellationT } [Test] - public async Task Total_WithOnlyDeadLetter_ReturnsDeadLetterCount(CancellationToken cancellationToken) + public async Task Total_WithOnlyDeadLetter_ReturnsDeadLetterCount() { var statistics = new OutboxStatistics { DeadLetter = 7 }; @@ -47,7 +47,7 @@ public async Task Total_WithOnlyDeadLetter_ReturnsDeadLetterCount(CancellationTo } [Test] - public async Task AllProperties_WhenSet_TotalReflectsCorrectSum(CancellationToken cancellationToken) + public async Task AllProperties_WhenSet_TotalReflectsCorrectSum() { var statistics = new OutboxStatistics { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs index de547968..4f72e223 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyEventInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -45,16 +45,12 @@ private static ServiceProvider CreateServiceProvider( } [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => // Act & Assert _ = await Assert.That(() => new PollyEventInterceptor(null!)).Throws(); [Test] - public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException( - CancellationToken cancellationToken - ) + public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException() { // Arrange var services = new ServiceCollection(); @@ -67,7 +63,7 @@ CancellationToken cancellationToken } [Test] - public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) + public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully() { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: true); @@ -80,7 +76,7 @@ public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully(Cancellatio } [Test] - public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) + public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully() { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs index c68b0000..b9865b49 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Polly/Interceptors/PollyRequestInterceptorTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; +namespace NetEvolve.Pulse.Tests.Unit.Polly.Interceptors; using System; using System.Diagnostics.CodeAnalysis; @@ -45,18 +45,14 @@ private static ServiceProvider CreateServiceProvider( } [Test] - public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_NullServiceProvider_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => new PollyRequestInterceptor(null!)) .Throws(); [Test] - public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException( - CancellationToken cancellationToken - ) + public async Task Constructor_NoPipelineRegistered_ThrowsInvalidOperationException() { // Arrange var services = new ServiceCollection(); @@ -69,7 +65,7 @@ CancellationToken cancellationToken } [Test] - public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) + public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully() { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: true); @@ -82,7 +78,7 @@ public async Task Constructor_WithKeyedPipeline_ResolvesSuccessfully(Cancellatio } [Test] - public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully(CancellationToken cancellationToken) + public async Task Constructor_WithGlobalPipeline_ResolvesSuccessfully() { // Arrange var serviceProvider = CreateServiceProvider(useKeyedService: false); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs index 2b431fb3..70d7e0c1 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/Polly/PollyExtensionsTests.cs @@ -18,25 +18,21 @@ public sealed class PollyExtensionsTests { [Test] - public async Task AddPollyRequestPolicies_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyRequestPolicies_NullConfigurator_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyRequestPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyRequestPolicies_NullConfigure_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyRequestPolicies_NullConfigure_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyRequestPolicies(null!)) .Throws(); [Test] - public async Task AddPollyRequestPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) + public async Task AddPollyRequestPolicies_RegistersPipelineAndInterceptor() { // Arrange var services = new ServiceCollection(); @@ -59,9 +55,7 @@ public async Task AddPollyRequestPolicies_RegistersPipelineAndInterceptor(Cancel } [Test] - public async Task AddPollyRequestPolicies_VoidCommand_RegistersPipelineAndInterceptor( - CancellationToken cancellationToken - ) + public async Task AddPollyRequestPolicies_VoidCommand_RegistersPipelineAndInterceptor() { // Arrange var services = new ServiceCollection(); @@ -80,25 +74,21 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyEventPolicies_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyEventPolicies_NullConfigurator_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyEventPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyEventPolicies_NullConfigure_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyEventPolicies_NullConfigure_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyEventPolicies(null!)) .Throws(); [Test] - public async Task AddPollyEventPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) + public async Task AddPollyEventPolicies_RegistersPipelineAndInterceptor() { // Arrange var services = new ServiceCollection(); @@ -121,9 +111,7 @@ public async Task AddPollyEventPolicies_RegistersPipelineAndInterceptor(Cancella } [Test] - public async Task AddPollyRequestPolicies_WithDifferentLifetimes_RespectsLifetime( - CancellationToken cancellationToken - ) + public async Task AddPollyRequestPolicies_WithDifferentLifetimes_RespectsLifetime() { // Arrange var services = new ServiceCollection(); @@ -149,9 +137,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyRequestPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered( - CancellationToken cancellationToken - ) + public async Task AddPollyRequestPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered() { // Arrange var services = new ServiceCollection(); @@ -178,9 +164,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyRequestPolicies_CalledTwice_SecondConfigurationIsApplied( - CancellationToken cancellationToken - ) + public async Task AddPollyRequestPolicies_CalledTwice_SecondConfigurationIsApplied() { // Arrange var firstFactoryInvoked = false; @@ -212,9 +196,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyRequestPolicies_VoidCommand_CalledTwice_ReplacesExistingRegistration( - CancellationToken cancellationToken - ) + public async Task AddPollyRequestPolicies_VoidCommand_CalledTwice_ReplacesExistingRegistration() { // Arrange var services = new ServiceCollection(); @@ -235,9 +217,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyRequestPolicies_CalledTwice_ReplacesExistingRegistration( - CancellationToken cancellationToken - ) + public async Task AddPollyRequestPolicies_CalledTwice_ReplacesExistingRegistration() { // Arrange var services = new ServiceCollection(); @@ -262,9 +242,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyEventPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered( - CancellationToken cancellationToken - ) + public async Task AddPollyEventPolicies_CalledTwice_OnlyOnePipelineDescriptorRegistered() { // Arrange var services = new ServiceCollection(); @@ -289,9 +267,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyEventPolicies_CalledTwice_SecondConfigurationIsApplied( - CancellationToken cancellationToken - ) + public async Task AddPollyEventPolicies_CalledTwice_SecondConfigurationIsApplied() { // Arrange var firstFactoryInvoked = false; @@ -323,7 +299,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyEventPolicies_WithDifferentLifetimes_RespectsLifetime(CancellationToken cancellationToken) + public async Task AddPollyEventPolicies_WithDifferentLifetimes_RespectsLifetime() { // Arrange var services = new ServiceCollection(); @@ -349,9 +325,7 @@ public async Task AddPollyEventPolicies_WithDifferentLifetimes_RespectsLifetime( } [Test] - public async Task AddPollyEventPolicies_CalledTwice_ReplacesExistingRegistration( - CancellationToken cancellationToken - ) + public async Task AddPollyEventPolicies_CalledTwice_ReplacesExistingRegistration() { // Arrange var services = new ServiceCollection(); @@ -376,25 +350,21 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyQueryPolicies_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyQueryPolicies_NullConfigurator_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyQueryPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyQueryPolicies_NullConfigure_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyQueryPolicies_NullConfigure_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyQueryPolicies(null!)) .Throws(); [Test] - public async Task AddPollyQueryPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) + public async Task AddPollyQueryPolicies_RegistersPipelineAndInterceptor() { // Arrange var services = new ServiceCollection(); @@ -417,9 +387,7 @@ public async Task AddPollyQueryPolicies_RegistersPipelineAndInterceptor(Cancella } [Test] - public async Task AddPollyQueryPolicies_CalledTwice_ReplacesExistingRegistration( - CancellationToken cancellationToken - ) + public async Task AddPollyQueryPolicies_CalledTwice_ReplacesExistingRegistration() { // Arrange var services = new ServiceCollection(); @@ -444,25 +412,21 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyCommandPolicies_NullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyCommandPolicies_NullConfigurator_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => PollyExtensions.AddPollyCommandPolicies(null!, _ => { })) .Throws(); [Test] - public async Task AddPollyCommandPolicies_NullConfigure_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPollyCommandPolicies_NullConfigure_ThrowsArgumentNullException() => // Act & Assert _ = await Assert .That(() => Mock.Of().Object.AddPollyCommandPolicies(null!)) .Throws(); [Test] - public async Task AddPollyCommandPolicies_RegistersPipelineAndInterceptor(CancellationToken cancellationToken) + public async Task AddPollyCommandPolicies_RegistersPipelineAndInterceptor() { // Arrange var services = new ServiceCollection(); @@ -485,9 +449,7 @@ public async Task AddPollyCommandPolicies_RegistersPipelineAndInterceptor(Cancel } [Test] - public async Task AddPollyCommandPolicies_WithDifferentLifetimes_RespectsLifetime( - CancellationToken cancellationToken - ) + public async Task AddPollyCommandPolicies_WithDifferentLifetimes_RespectsLifetime() { // Arrange var services = new ServiceCollection(); @@ -513,9 +475,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPollyCommandPolicies_CalledTwice_ReplacesExistingRegistration( - CancellationToken cancellationToken - ) + public async Task AddPollyCommandPolicies_CalledTwice_ReplacesExistingRegistration() { // Arrange var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs index 605aa0b9..d16bfc9f 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; +namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; using System; using System.Threading.Tasks; @@ -14,13 +14,13 @@ namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; public sealed class PostgreSqlEventOutboxTests { [Test] - public async Task Constructor_WithNullConnection_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullConnection_ThrowsArgumentNullException() => _ = await Assert .That(() => new PostgreSqlEventOutbox(null!, Options.Create(new OutboxOptions()), TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -30,7 +30,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(Cancel } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -40,7 +40,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -50,7 +50,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithTransaction_CreatesInstance() { await using var connection = new NpgsqlConnection("Host=localhost;"); @@ -65,7 +65,7 @@ public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() { await using var connection = new NpgsqlConnection("Host=localhost;"); var outbox = new PostgreSqlEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs index 85eed625..929bf307 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlExtensionsTests.cs @@ -17,41 +17,31 @@ public sealed class PostgreSqlExtensionsTests { [Test] - public async Task AddPostgreSqlOutbox_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPostgreSqlOutbox_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => PostgreSqlExtensions.AddPostgreSqlOutbox(null!, "Host=localhost;Encrypt=true;")) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPostgreSqlOutbox_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox((string)null!)) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task AddPostgreSqlOutbox_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox(string.Empty)) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task AddPostgreSqlOutbox_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox(" ")) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -62,9 +52,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox("Host=localhost;Encrypt=true;")); @@ -79,9 +67,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddPostgreSqlOutbox("Host=localhost;Encrypt=true;")); @@ -96,7 +82,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithConfigureOptions_AppliesOptions(CancellationToken cancellationToken) + public async Task AddPostgreSqlOutbox_WithConfigureOptions_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -112,9 +98,7 @@ public async Task AddPostgreSqlOutbox_WithConfigureOptions_AppliesOptions(Cancel } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPostgreSqlOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => PostgreSqlExtensions.AddPostgreSqlOutbox( @@ -125,17 +109,13 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddPostgreSqlOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException() => _ = await Assert .That(() => Mock.Of().Object.AddPostgreSqlOutbox((Func)null!)) .Throws(); [Test] - public async Task AddPostgreSqlOutbox_WithFactory_ReturnsConfiguratorForChaining( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithFactory_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -146,9 +126,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxRepositoryAsScoped( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxRepositoryAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox(_ => "Host=localhost;Encrypt=true;")); @@ -163,9 +141,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_WithConfigureOptions_AppliesOptions( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithFactory_WithConfigureOptions_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -181,9 +157,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox("Host=localhost;Encrypt=true;")); @@ -198,9 +172,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxManagementAsScoped( - CancellationToken cancellationToken - ) + public async Task AddPostgreSqlOutbox_WithFactory_RegistersOutboxManagementAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddPostgreSqlOutbox(_ => "Host=localhost;Encrypt=true;")); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs index 5a28913e..8e00fbd6 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxManagementTests.cs @@ -13,17 +13,13 @@ public sealed class PostgreSqlOutboxManagementTests private const string ValidConnectionString = "Host=localhost;Database=Test;Username=postgres;Password=secret;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => new PostgreSqlOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = null }))) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new PostgreSqlOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = string.Empty })) @@ -31,19 +27,17 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new PostgreSqlOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = " " }))) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => _ = await Assert.That(() => new PostgreSqlOutboxManagement(null!)).Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -53,7 +47,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithNullSchema_UsesDefaultSchema(CancellationToken cancellationToken) + public async Task Constructor_WithNullSchema_UsesDefaultSchema() { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }); @@ -63,7 +57,7 @@ public async Task Constructor_WithNullSchema_UsesDefaultSchema(CancellationToken } [Test] - public async Task Constructor_WithEmptySchema_UsesDefaultSchema(CancellationToken cancellationToken) + public async Task Constructor_WithEmptySchema_UsesDefaultSchema() { var options = Options.Create( new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty } @@ -75,7 +69,7 @@ public async Task Constructor_WithEmptySchema_UsesDefaultSchema(CancellationToke } [Test] - public async Task Constructor_WithWhitespaceSchema_UsesDefaultSchema(CancellationToken cancellationToken) + public async Task Constructor_WithWhitespaceSchema_UsesDefaultSchema() { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = " " }); @@ -85,7 +79,7 @@ public async Task Constructor_WithWhitespaceSchema_UsesDefaultSchema(Cancellatio } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithCustomSchema_CreatesInstance() { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }); @@ -95,9 +89,7 @@ public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -109,9 +101,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -123,9 +113,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() { var management = new PostgreSqlOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs index e444060b..958e96f9 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxOptionsExtensionsTests.cs @@ -11,7 +11,7 @@ public sealed class PostgreSqlOutboxOptionsExtensionsTests { [Test] - public async Task FullTableName_WithDefaultOptions_Returns_correct_quoted_name(CancellationToken cancellationToken) + public async Task FullTableName_WithDefaultOptions_Returns_correct_quoted_name() { var options = new OutboxOptions(); @@ -19,7 +19,7 @@ public async Task FullTableName_WithDefaultOptions_Returns_correct_quoted_name(C } [Test] - public async Task FullTableName_WithCustomSchema_Returns_correct_quoted_name(CancellationToken cancellationToken) + public async Task FullTableName_WithCustomSchema_Returns_correct_quoted_name() { var options = new OutboxOptions { Schema = "myschema" }; @@ -27,7 +27,7 @@ public async Task FullTableName_WithCustomSchema_Returns_correct_quoted_name(Can } [Test] - public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema(CancellationToken cancellationToken) + public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema() { var options = new OutboxOptions { Schema = null! }; @@ -35,9 +35,7 @@ public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema(Canc } [Test] - public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema( - CancellationToken cancellationToken - ) + public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema() { var options = new OutboxOptions { Schema = " " }; @@ -45,7 +43,7 @@ CancellationToken cancellationToken } [Test] - public async Task FullTableName_WithCustomTableName_Returns_correct_quoted_name(CancellationToken cancellationToken) + public async Task FullTableName_WithCustomTableName_Returns_correct_quoted_name() { var options = new OutboxOptions { TableName = "MyTable" }; @@ -53,7 +51,7 @@ public async Task FullTableName_WithCustomTableName_Returns_correct_quoted_name( } [Test] - public async Task FullTableName_Trims_schema_whitespace(CancellationToken cancellationToken) + public async Task FullTableName_Trims_schema_whitespace() { var options = new OutboxOptions { Schema = " myschema " }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs index b8c2f718..a09f432b 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; +namespace NetEvolve.Pulse.Tests.Unit.PostgreSql; using System; using System.Threading.Tasks; @@ -13,9 +13,7 @@ public sealed class PostgreSqlOutboxRepositoryTests private const string ValidConnectionString = "Host=localhost;Database=Test;Username=postgres;Password=secret;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -26,9 +24,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -39,9 +35,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -52,15 +46,13 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => _ = await Assert .That(() => new PostgreSqlOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new PostgreSqlOutboxRepository( @@ -71,7 +63,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var repository = new PostgreSqlOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }), @@ -82,7 +74,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithTransactionScope_CreatesInstance() { var transactionScope = new PostgreSqlOutboxTransactionScope(null); @@ -96,7 +88,7 @@ public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationT } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithCustomSchema_CreatesInstance() { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }; @@ -106,7 +98,7 @@ public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken } [Test] - public async Task Constructor_WithNullSchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithNullSchema_CreatesInstance() { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }; @@ -116,7 +108,7 @@ public async Task Constructor_WithNullSchema_CreatesInstance(CancellationToken c } [Test] - public async Task Constructor_WithEmptySchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithEmptySchema_CreatesInstance() { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs index 93f08e5a..39d0ad1c 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/PostgreSql/PostgreSqlOutboxTransactionScopeTests.cs @@ -9,7 +9,7 @@ public sealed class PostgreSqlOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithDefaultParameter_CreatesInstance() { var scope = new PostgreSqlOutboxTransactionScope(); @@ -17,7 +17,7 @@ public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationT } [Test] - public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithNullTransaction_CreatesInstance() { var scope = new PostgreSqlOutboxTransactionScope(null); @@ -25,7 +25,7 @@ public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationTo } [Test] - public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() { var scope = new PostgreSqlOutboxTransactionScope(); @@ -35,7 +35,7 @@ public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(Cancell } [Test] - public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull() { var scope = new PostgreSqlOutboxTransactionScope(null); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs index bba531e9..4531aa94 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/QueryCachingExtensionsTests.cs @@ -18,11 +18,11 @@ public sealed class QueryCachingExtensionsTests { [Test] - public async Task AddQueryCaching_NullBuilder_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task AddQueryCaching_NullBuilder_ThrowsArgumentNullException() => _ = await Assert.That(() => QueryCachingExtensions.AddQueryCaching(null!)).Throws(); [Test] - public async Task AddQueryCaching_RegistersRequestInterceptor(CancellationToken cancellationToken) + public async Task AddQueryCaching_RegistersRequestInterceptor() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -44,7 +44,7 @@ public async Task AddQueryCaching_RegistersRequestInterceptor(CancellationToken } [Test] - public async Task AddQueryCaching_WithConfigure_AppliesOptions(CancellationToken cancellationToken) + public async Task AddQueryCaching_WithConfigure_AppliesOptions() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -58,9 +58,7 @@ public async Task AddQueryCaching_WithConfigure_AppliesOptions(CancellationToken } [Test] - public async Task AddQueryCaching_CalledMultipleTimes_DoesNotDuplicateInterceptor( - CancellationToken cancellationToken - ) + public async Task AddQueryCaching_CalledMultipleTimes_DoesNotDuplicateInterceptor() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); @@ -79,7 +77,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddQueryCaching_ReturnsSameBuilder(CancellationToken cancellationToken) + public async Task AddQueryCaching_ReturnsSameBuilder() { var services = new ServiceCollection(); var builder = new MediatorBuilder(services); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs index 76510844..50ed0d60 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqExtensionsTests.cs @@ -12,7 +12,7 @@ public sealed class RabbitMqExtensionsTests { [Test] - public async Task UseRabbitMqTransport_Registers_transport_service(CancellationToken cancellationToken) + public async Task UseRabbitMqTransport_Registers_transport_service() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseRabbitMqTransport()); @@ -23,7 +23,7 @@ public async Task UseRabbitMqTransport_Registers_transport_service(CancellationT } [Test] - public async Task UseRabbitMqTransport_Configures_options(CancellationToken cancellationToken) + public async Task UseRabbitMqTransport_Configures_options() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseRabbitMqTransport(options => options.ExchangeName = "test-exchange")); @@ -35,9 +35,7 @@ public async Task UseRabbitMqTransport_Configures_options(CancellationToken canc } [Test] - public async Task UseRabbitMqTransport_Without_configureOptions_registers_default_options( - CancellationToken cancellationToken - ) + public async Task UseRabbitMqTransport_Without_configureOptions_registers_default_options() { IServiceCollection services = new ServiceCollection(); _ = services.AddPulse(config => config.UseRabbitMqTransport()); @@ -51,7 +49,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseRabbitMqTransport_Replaces_existing_transport(CancellationToken cancellationToken) + public async Task UseRabbitMqTransport_Replaces_existing_transport() { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(new DummyTransport()); @@ -67,7 +65,7 @@ public async Task UseRabbitMqTransport_Replaces_existing_transport(CancellationT } [Test] - public async Task UseRabbitMqTransport_When_configurator_null_throws(CancellationToken cancellationToken) + public async Task UseRabbitMqTransport_When_configurator_null_throws() { IMediatorBuilder configurator = null!; @@ -78,7 +76,7 @@ public async Task UseRabbitMqTransport_When_configurator_null_throws(Cancellatio } [Test] - public async Task UseRabbitMqTransport_Returns_configurator_for_chaining(CancellationToken cancellationToken) + public async Task UseRabbitMqTransport_Returns_configurator_for_chaining() { IServiceCollection services = new ServiceCollection(); IMediatorBuilder? returnedConfigurator = null; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs index 3cb8c96a..125ec58a 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/RabbitMQ/RabbitMqMessageTransportTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.RabbitMQ; +namespace NetEvolve.Pulse.Tests.Unit.RabbitMQ; using System.Text; using global::RabbitMQ.Client; @@ -15,7 +15,7 @@ namespace NetEvolve.Pulse.Tests.Unit.RabbitMQ; public sealed class RabbitMqMessageTransportTests { [Test] - public async Task Constructor_When_connectionAdapter_null_throws(CancellationToken cancellationToken) + public async Task Constructor_When_connectionAdapter_null_throws() { IRabbitMqConnectionAdapter connectionAdapter = null!; var topicNameResolver = new FakeTopicNameResolver(); @@ -30,7 +30,7 @@ public async Task Constructor_When_connectionAdapter_null_throws(CancellationTok } [Test] - public async Task Constructor_When_topicNameResolver_null_throws(CancellationToken cancellationToken) + public async Task Constructor_When_topicNameResolver_null_throws() { var connectionAdapter = new FakeConnectionAdapter(); ITopicNameResolver topicNameResolver = null!; @@ -45,7 +45,7 @@ public async Task Constructor_When_topicNameResolver_null_throws(CancellationTok } [Test] - public async Task Constructor_When_options_null_throws(CancellationToken cancellationToken) + public async Task Constructor_When_options_null_throws() { var connectionAdapter = new FakeConnectionAdapter(); var topicNameResolver = new FakeTopicNameResolver(); @@ -261,7 +261,7 @@ public async Task Dispose_Is_idempotent(CancellationToken cancellationToken) } [Test] - public async Task Options_ExchangeName_can_be_configured(CancellationToken cancellationToken) + public async Task Options_ExchangeName_can_be_configured() { var options = new RabbitMqTransportOptions { ExchangeName = "test-exchange" }; @@ -269,7 +269,7 @@ public async Task Options_ExchangeName_can_be_configured(CancellationToken cance } [Test] - public async Task Options_Default_ExchangeName_is_empty_string(CancellationToken cancellationToken) + public async Task Options_Default_ExchangeName_is_empty_string() { var options = new RabbitMqTransportOptions(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs index 4938992c..93fa6fbe 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SQLite; +namespace NetEvolve.Pulse.Tests.Unit.SQLite; using System; using System.Reflection; @@ -16,13 +16,13 @@ namespace NetEvolve.Pulse.Tests.Unit.SQLite; public sealed class SQLiteEventOutboxTests { [Test] - public async Task Constructor_WithNullConnection_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullConnection_ThrowsArgumentNullException() => _ = await Assert .That(() => new SQLiteEventOutbox(null!, Options.Create(new OutboxOptions()), TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -32,7 +32,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(Cancel } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -42,7 +42,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -52,7 +52,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithTransaction_CreatesInstance() { await using var connection = new SqliteConnection("Data Source=:memory:"); @@ -67,7 +67,7 @@ public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() { await using var connection = new SqliteConnection("Data Source=:memory:"); var outbox = new SQLiteEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs index 326068eb..9f29d44b 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteExtensionsTests.cs @@ -17,49 +17,37 @@ public sealed class SQLiteExtensionsTests { [Test] - public async Task UseSQLiteOutbox_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task UseSQLiteOutbox_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => SQLiteExtensions.UseSQLiteOutbox(null!, "Data Source=:memory:")) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task UseSQLiteOutbox_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox((string)null!)) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task UseSQLiteOutbox_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox(string.Empty)) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task UseSQLiteOutbox_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox(" ")) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithNullConfigureOptions_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task UseSQLiteOutbox_WithNullConfigureOptions_ThrowsArgumentNullException() => _ = await Assert .That(() => Mock.Of().Object.UseSQLiteOutbox((Action)null!)) .Throws(); [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining( - CancellationToken cancellationToken - ) + public async Task UseSQLiteOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -70,9 +58,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped( - CancellationToken cancellationToken - ) + public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().UseSQLiteOutbox("Data Source=:memory:")); @@ -87,9 +73,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped( - CancellationToken cancellationToken - ) + public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().UseSQLiteOutbox("Data Source=:memory:")); @@ -104,9 +88,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton( - CancellationToken cancellationToken - ) + public async Task UseSQLiteOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.UseSQLiteOutbox("Data Source=:memory:")); @@ -121,7 +103,7 @@ CancellationToken cancellationToken } [Test] - public async Task UseSQLiteOutbox_WithConfigureOptions_AppliesTableName(CancellationToken cancellationToken) + public async Task UseSQLiteOutbox_WithConfigureOptions_AppliesTableName() { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -135,7 +117,7 @@ public async Task UseSQLiteOutbox_WithConfigureOptions_AppliesTableName(Cancella } [Test] - public async Task UseSQLiteOutbox_WithConfigureAction_AppliesOptions(CancellationToken cancellationToken) + public async Task UseSQLiteOutbox_WithConfigureAction_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -161,9 +143,7 @@ public async Task UseSQLiteOutbox_WithConfigureAction_AppliesOptions(Cancellatio } [Test] - public async Task UseSQLiteOutbox_WithConfigureAction_ReturnsConfiguratorForChaining( - CancellationToken cancellationToken - ) + public async Task UseSQLiteOutbox_WithConfigureAction_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs index 53ecac6a..26757952 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxManagementTests.cs @@ -11,23 +11,19 @@ public sealed class SQLiteOutboxManagementTests { [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => _ = await Assert .That(() => new SQLiteOutboxManagement(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new SQLiteOutboxManagement(Options.Create(new OutboxOptions()), null!)) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SQLiteOutboxManagement( @@ -38,9 +34,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SQLiteOutboxManagement( @@ -51,7 +45,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithValidOptions_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidOptions_CreatesInstance() { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -62,7 +56,7 @@ public async Task Constructor_WithValidOptions_CreatesInstance(CancellationToken } [Test] - public async Task Constructor_WithCustomTableName_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithCustomTableName_CreatesInstance() { var options = new OutboxOptions { @@ -77,9 +71,7 @@ public async Task Constructor_WithCustomTableName_CreatesInstance(CancellationTo } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -92,9 +84,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -107,9 +97,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() { var management = new SQLiteOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs index ddd91c60..9ea899c9 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxOptionsExtensionsTests.cs @@ -11,7 +11,7 @@ public sealed class OutboxOptionsExtensionsTests { [Test] - public async Task FullTableName_WithDefaultOptions_Returns_quoted_table_name(CancellationToken cancellationToken) + public async Task FullTableName_WithDefaultOptions_Returns_quoted_table_name() { var options = new OutboxOptions(); @@ -19,7 +19,7 @@ public async Task FullTableName_WithDefaultOptions_Returns_quoted_table_name(Can } [Test] - public async Task FullTableName_WithCustomTableName_Returns_quoted_table_name(CancellationToken cancellationToken) + public async Task FullTableName_WithCustomTableName_Returns_quoted_table_name() { var options = new OutboxOptions { TableName = "MyCustomTable" }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs index ac557330..8a9c28b3 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SQLite; +namespace NetEvolve.Pulse.Tests.Unit.SQLite; using System; using System.Threading.Tasks; @@ -11,23 +11,19 @@ namespace NetEvolve.Pulse.Tests.Unit.SQLite; public sealed class SQLiteOutboxRepositoryTests { [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => _ = await Assert .That(() => new SQLiteOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new SQLiteOutboxRepository(Options.Create(new OutboxOptions()), null!)) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SQLiteOutboxRepository( @@ -38,9 +34,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SQLiteOutboxRepository( @@ -51,7 +45,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithValidOptions_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidOptions_CreatesInstance() { var repository = new SQLiteOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = "Data Source=:memory:", EnableWalMode = false }), @@ -62,7 +56,7 @@ public async Task Constructor_WithValidOptions_CreatesInstance(CancellationToken } [Test] - public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithTransactionScope_CreatesInstance() { var transactionScope = new SQLiteOutboxTransactionScope(null); @@ -76,7 +70,7 @@ public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationT } [Test] - public async Task Constructor_WithCustomTableName_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithCustomTableName_CreatesInstance() { var options = new OutboxOptions { diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs index c5f7b1c6..4ca77221 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SQLite/SQLiteOutboxTransactionScopeTests.cs @@ -9,7 +9,7 @@ public sealed class SQLiteOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithDefaultParameter_CreatesInstance() { var scope = new SQLiteOutboxTransactionScope(); @@ -17,7 +17,7 @@ public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationT } [Test] - public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithNullTransaction_CreatesInstance() { var scope = new SQLiteOutboxTransactionScope(null); @@ -25,7 +25,7 @@ public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationTo } [Test] - public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() { var scope = new SQLiteOutboxTransactionScope(); @@ -35,7 +35,7 @@ public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(Cancell } [Test] - public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull() { var scope = new SQLiteOutboxTransactionScope(null); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs index 68e97e45..68d68cac 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/ServiceCollectionExtensionsTests.cs @@ -12,7 +12,7 @@ public class ServiceCollectionExtensionsTests { [Test] - public void AddPulse_WithNullServices_ThrowsArgumentNullException(CancellationToken cancellationToken) + public void AddPulse_WithNullServices_ThrowsArgumentNullException() { IServiceCollection? services = null; @@ -20,7 +20,7 @@ public void AddPulse_WithNullServices_ThrowsArgumentNullException(CancellationTo } [Test] - public async Task AddPulse_WithoutBuilder_RegistersMediator(CancellationToken cancellationToken) + public async Task AddPulse_WithoutBuilder_RegistersMediator() { var services = new ServiceCollection(); @@ -38,7 +38,7 @@ public async Task AddPulse_WithoutBuilder_RegistersMediator(CancellationToken ca } [Test] - public async Task AddPulse_WithBuilder_InvokesBuilderAction(CancellationToken cancellationToken) + public async Task AddPulse_WithBuilder_InvokesBuilderAction() { var services = new ServiceCollection(); var builderInvoked = false; @@ -53,7 +53,7 @@ public async Task AddPulse_WithBuilder_InvokesBuilderAction(CancellationToken ca } [Test] - public async Task AddPulse_WithActivityAndMetrics_RegistersInterceptors(CancellationToken cancellationToken) + public async Task AddPulse_WithActivityAndMetrics_RegistersInterceptors() { var services = new ServiceCollection(); @@ -80,7 +80,7 @@ public async Task AddPulse_WithActivityAndMetrics_RegistersInterceptors(Cancella } [Test] - public async Task AddPulse_CanBeInvokedMultipleTimes(CancellationToken cancellationToken) + public async Task AddPulse_CanBeInvokedMultipleTimes() { var services = new ServiceCollection(); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs index 690879eb..c47d1090 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerEventOutboxTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SqlServer; +namespace NetEvolve.Pulse.Tests.Unit.SqlServer; using System; using System.Threading.Tasks; @@ -14,13 +14,13 @@ namespace NetEvolve.Pulse.Tests.Unit.SqlServer; public sealed class SqlServerEventOutboxTests { [Test] - public async Task Constructor_WithNullConnection_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullConnection_ThrowsArgumentNullException() => _ = await Assert .That(() => new SqlServerEventOutbox(null!, Options.Create(new OutboxOptions()), TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -30,7 +30,7 @@ public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(Cancel } [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -40,7 +40,7 @@ public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException(C } [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -50,7 +50,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithTransaction_CreatesInstance() { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); @@ -65,7 +65,7 @@ public async Task Constructor_WithTransaction_CreatesInstance(CancellationToken } [Test] - public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException(CancellationToken cancellationToken) + public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException() { await using var connection = new SqlConnection("Server=.;Encrypt=true;"); var outbox = new SqlServerEventOutbox(connection, Options.Create(new OutboxOptions()), TimeProvider.System); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs index aa9f20ac..b8618d39 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerExtensionsTests.cs @@ -17,41 +17,31 @@ public sealed class SqlServerExtensionsTests { [Test] - public async Task AddSqlServerOutbox_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddSqlServerOutbox_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => SqlServerExtensions.AddSqlServerOutbox(null!, "Server=.;Encrypt=true;")) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddSqlServerOutbox_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox((string)null!)) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task AddSqlServerOutbox_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox(string.Empty)) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task AddSqlServerOutbox_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox(" ")) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithValidConnectionString_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -62,9 +52,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxRepositoryAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox("Server=.;Encrypt=true;")); @@ -79,9 +67,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersTimeProviderAsSingleton() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddSqlServerOutbox("Server=.;Encrypt=true;")); @@ -96,7 +82,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddSqlServerOutbox_WithConfigureOptions_AppliesOptions(CancellationToken cancellationToken) + public async Task AddSqlServerOutbox_WithConfigureOptions_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -110,23 +96,19 @@ public async Task AddSqlServerOutbox_WithConfigureOptions_AppliesOptions(Cancell } [Test] - public async Task AddSqlServerOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddSqlServerOutbox_WithFactory_WithNullConfigurator_ThrowsArgumentNullException() => _ = await Assert .That(() => SqlServerExtensions.AddSqlServerOutbox(null!, _ => "Server=.;Encrypt=true;")) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task AddSqlServerOutbox_WithFactory_WithNullFactory_ThrowsArgumentNullException() => _ = await Assert .That(() => Mock.Of().Object.AddSqlServerOutbox((Func)null!)) .Throws(); [Test] - public async Task AddSqlServerOutbox_WithFactory_ReturnsConfiguratorForChaining(CancellationToken cancellationToken) + public async Task AddSqlServerOutbox_WithFactory_ReturnsConfiguratorForChaining() { var mock = Mock.Of(); _ = mock.Services.Returns(new ServiceCollection()); @@ -137,9 +119,7 @@ public async Task AddSqlServerOutbox_WithFactory_ReturnsConfiguratorForChaining( } [Test] - public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxRepositoryAsScoped( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxRepositoryAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox(_ => "Server=.;Encrypt=true;")); @@ -154,9 +134,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddSqlServerOutbox_WithFactory_WithConfigureOptions_AppliesOptions( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithFactory_WithConfigureOptions_AppliesOptions() { var services = new ServiceCollection(); _ = services.AddPulse(config => @@ -172,9 +150,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithValidConnectionString_RegistersOutboxManagementAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox("Server=.;Encrypt=true;")); @@ -189,9 +165,7 @@ CancellationToken cancellationToken } [Test] - public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxManagementAsScoped( - CancellationToken cancellationToken - ) + public async Task AddSqlServerOutbox_WithFactory_RegistersOutboxManagementAsScoped() { var services = new ServiceCollection(); _ = services.AddPulse(config => config.AddOutbox().AddSqlServerOutbox(_ => "Server=.;Encrypt=true;")); diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs index 4bc2cb36..76360914 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxManagementTests.cs @@ -13,17 +13,13 @@ public sealed class SqlServerOutboxManagementTests private const string ValidConnectionString = "Server=.;Database=Test;Integrated Security=true;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => new SqlServerOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = null }))) .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SqlServerOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = string.Empty })) @@ -31,19 +27,17 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SqlServerOutboxManagement(Options.Create(new OutboxOptions { ConnectionString = " " }))) .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => _ = await Assert.That(() => new SqlServerOutboxManagement(null!)).Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -53,7 +47,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithNullSchema_UsesDefaultDboSchema(CancellationToken cancellationToken) + public async Task Constructor_WithNullSchema_UsesDefaultDboSchema() { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }); @@ -63,7 +57,7 @@ public async Task Constructor_WithNullSchema_UsesDefaultDboSchema(CancellationTo } [Test] - public async Task Constructor_WithEmptySchema_UsesDefaultDboSchema(CancellationToken cancellationToken) + public async Task Constructor_WithEmptySchema_UsesDefaultDboSchema() { var options = Options.Create( new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty } @@ -75,7 +69,7 @@ public async Task Constructor_WithEmptySchema_UsesDefaultDboSchema(CancellationT } [Test] - public async Task Constructor_WithWhitespaceSchema_UsesDefaultDboSchema(CancellationToken cancellationToken) + public async Task Constructor_WithWhitespaceSchema_UsesDefaultDboSchema() { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = " " }); @@ -85,7 +79,7 @@ public async Task Constructor_WithWhitespaceSchema_UsesDefaultDboSchema(Cancella } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithCustomSchema_CreatesInstance() { var options = Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }); @@ -95,9 +89,7 @@ public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePageSize_ThrowsArgumentOutOfRangeException() { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -109,9 +101,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithZeroPageSize_ThrowsArgumentOutOfRangeException() { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) @@ -123,9 +113,7 @@ CancellationToken cancellationToken } [Test] - public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException( - CancellationToken cancellationToken - ) + public async Task GetDeadLetterMessagesAsync_WithNegativePage_ThrowsArgumentOutOfRangeException() { var management = new SqlServerOutboxManagement( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }) diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs index faeb8cc3..e15da219 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxOptionsExtensionsTests.cs @@ -11,9 +11,7 @@ public sealed class SqlServerOutboxOptionsExtensionsTests { [Test] - public async Task FullTableName_WithDefaultOptions_Returns_correct_bracketed_name( - CancellationToken cancellationToken - ) + public async Task FullTableName_WithDefaultOptions_Returns_correct_bracketed_name() { var options = new OutboxOptions(); @@ -21,7 +19,7 @@ CancellationToken cancellationToken } [Test] - public async Task FullTableName_WithCustomSchema_Returns_correct_bracketed_name(CancellationToken cancellationToken) + public async Task FullTableName_WithCustomSchema_Returns_correct_bracketed_name() { var options = new OutboxOptions { Schema = "myschema" }; @@ -29,7 +27,7 @@ public async Task FullTableName_WithCustomSchema_Returns_correct_bracketed_name( } [Test] - public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema(CancellationToken cancellationToken) + public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema() { var options = new OutboxOptions { Schema = null! }; @@ -37,9 +35,7 @@ public async Task FullTableName_WithNullSchema_Falls_back_to_default_schema(Canc } [Test] - public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema( - CancellationToken cancellationToken - ) + public async Task FullTableName_WithWhitespaceSchema_Falls_back_to_default_schema() { var options = new OutboxOptions { Schema = " " }; @@ -47,9 +43,7 @@ CancellationToken cancellationToken } [Test] - public async Task FullTableName_WithCustomTableName_Returns_correct_bracketed_name( - CancellationToken cancellationToken - ) + public async Task FullTableName_WithCustomTableName_Returns_correct_bracketed_name() { var options = new OutboxOptions { TableName = "MyTable" }; @@ -57,7 +51,7 @@ CancellationToken cancellationToken } [Test] - public async Task FullTableName_Trims_schema_whitespace(CancellationToken cancellationToken) + public async Task FullTableName_Trims_schema_whitespace() { var options = new OutboxOptions { Schema = " myschema " }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs index 3f1da136..23bff679 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxRepositoryTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.Pulse.Tests.Unit.SqlServer; +namespace NetEvolve.Pulse.Tests.Unit.SqlServer; using System; using System.Threading.Tasks; @@ -13,9 +13,7 @@ public sealed class SqlServerOutboxRepositoryTests private const string ValidConnectionString = "Server=.;Database=Test;Integrated Security=true;"; [Test] - public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullConnectionString_ThrowsArgumentNullException() => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -26,9 +24,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithEmptyConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -39,9 +35,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithWhitespaceConnectionString_ThrowsArgumentException() => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -52,15 +46,13 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithNullOptions_ThrowsArgumentNullException(CancellationToken cancellationToken) => + public async Task Constructor_WithNullOptions_ThrowsArgumentNullException() => _ = await Assert .That(() => new SqlServerOutboxRepository(null!, TimeProvider.System)) .Throws(); [Test] - public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException( - CancellationToken cancellationToken - ) => + public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException() => _ = await Assert .That(() => new SqlServerOutboxRepository( @@ -71,7 +63,7 @@ CancellationToken cancellationToken .Throws(); [Test] - public async Task Constructor_WithValidArguments_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithValidArguments_CreatesInstance() { var repository = new SqlServerOutboxRepository( Options.Create(new OutboxOptions { ConnectionString = ValidConnectionString }), @@ -82,7 +74,7 @@ public async Task Constructor_WithValidArguments_CreatesInstance(CancellationTok } [Test] - public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithTransactionScope_CreatesInstance() { var transactionScope = new SqlServerOutboxTransactionScope(null); @@ -96,7 +88,7 @@ public async Task Constructor_WithTransactionScope_CreatesInstance(CancellationT } [Test] - public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithCustomSchema_CreatesInstance() { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = "custom" }; @@ -106,7 +98,7 @@ public async Task Constructor_WithCustomSchema_CreatesInstance(CancellationToken } [Test] - public async Task Constructor_WithNullSchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithNullSchema_CreatesInstance() { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = null }; @@ -116,7 +108,7 @@ public async Task Constructor_WithNullSchema_CreatesInstance(CancellationToken c } [Test] - public async Task Constructor_WithEmptySchema_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithEmptySchema_CreatesInstance() { var options = new OutboxOptions { ConnectionString = ValidConnectionString, Schema = string.Empty }; diff --git a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs index 1bc3888f..4ad47817 100644 --- a/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs +++ b/tests/NetEvolve.Pulse.Tests.Unit/SqlServer/SqlServerOutboxTransactionScopeTests.cs @@ -9,7 +9,7 @@ public sealed class SqlServerOutboxTransactionScopeTests { [Test] - public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithDefaultParameter_CreatesInstance() { var scope = new SqlServerOutboxTransactionScope(); @@ -17,7 +17,7 @@ public async Task Constructor_WithDefaultParameter_CreatesInstance(CancellationT } [Test] - public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationToken cancellationToken) + public async Task Constructor_WithNullTransaction_CreatesInstance() { var scope = new SqlServerOutboxTransactionScope(null); @@ -25,7 +25,7 @@ public async Task Constructor_WithNullTransaction_CreatesInstance(CancellationTo } [Test] - public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull() { var scope = new SqlServerOutboxTransactionScope(); @@ -35,7 +35,7 @@ public async Task GetCurrentTransaction_WithDefaultParameter_ReturnsNull(Cancell } [Test] - public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull(CancellationToken cancellationToken) + public async Task GetCurrentTransaction_WithNullTransaction_ReturnsNull() { var scope = new SqlServerOutboxTransactionScope(null); From 64b06aeb1ee5c8444fe66f66299517410131cff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Mon, 13 Apr 2026 12:56:55 +0200 Subject: [PATCH 25/25] chore: Improve test infra: MySQL options, cancellation, timeouts - Check cancellation token before DB connect in EntityFrameworkInitializer for better test responsiveness. - Add connection options to MySqlContainerFixture for improved MySQL compatibility. - Move MySqlDatabaseServiceFixture to its own file (no logic changes). - Increase OutboxTestsBase timeout to 5 minutes for slow container startups. --- .../Internals/EntityFrameworkInitializer.cs | 2 + .../Internals/MySqlContainerFixture.cs | 46 +------------------ .../Internals/MySqlDatabaseServiceFixture.cs | 46 +++++++++++++++++++ .../Outbox/OutboxTestsBase.cs | 2 +- 4 files changed, 51 insertions(+), 45 deletions(-) create mode 100644 tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlDatabaseServiceFixture.cs diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs index c113cfbf..3d069e4e 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/EntityFrameworkInitializer.cs @@ -25,6 +25,8 @@ public async ValueTask CreateDatabaseAsync(IServiceProvider serviceProvider, Can var context = scope.ServiceProvider.GetRequiredService(); var databaseCreator = context.GetService(); + cancellationToken.ThrowIfCancellationRequested(); + if (databaseCreator is IRelationalDatabaseCreator relationalDatabaseCreator) { if (!await relationalDatabaseCreator.CanConnectAsync(cancellationToken).ConfigureAwait(false)) diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs index c09e179c..18e8aa6f 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlContainerFixture.cs @@ -18,7 +18,8 @@ public sealed class MySqlContainerFixture : IAsyncDisposable, IAsyncInitializer .WithPrivileged(true) .Build(); - public string ConnectionString => _container.GetConnectionString(); + public string ConnectionString => + _container.GetConnectionString() + ";SslMode=Disabled;AllowPublicKeyRetrieval=True;ConnectionTimeout=30;"; public static string UserName => "root"; @@ -26,46 +27,3 @@ public sealed class MySqlContainerFixture : IAsyncDisposable, IAsyncInitializer public async Task InitializeAsync() => await _container.StartAsync().ConfigureAwait(false); } - -/// -/// Provides a per-test backed by a MySQL Testcontainer, -/// creating a unique database for each test to ensure isolation. -/// -public sealed class MySqlDatabaseServiceFixture : IDatabaseServiceFixture -{ - [ClassDataSource(Shared = SharedType.PerTestSession)] - public MySqlContainerFixture Container { get; set; } = default!; - - public string ConnectionString => - Container.ConnectionString.Replace(";Database=test;", $";Database={DatabaseName};", StringComparison.Ordinal); - - internal string DatabaseName { get; } = $"{TestHelper.TargetFramework}{Guid.NewGuid():N}"; - - public DatabaseType DatabaseType => DatabaseType.MySql; - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - - public async Task InitializeAsync() - { - try - { - // Create temporary database to ensure the container is fully initialized and ready to accept connections - await using var con = new MySqlConnection(Container.ConnectionString); - await con.OpenAsync(); - - await using var cmd = con.CreateCommand(); -#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - cmd.CommandText = $"CREATE DATABASE {DatabaseName}"; -#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities - - _ = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - throw new InvalidOperationException( - "MySQL container failed to start within the expected time frame. Try restarting Rancher Desktop.", - ex - ); - } - } -} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlDatabaseServiceFixture.cs b/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlDatabaseServiceFixture.cs new file mode 100644 index 00000000..902cc553 --- /dev/null +++ b/tests/NetEvolve.Pulse.Tests.Integration/Internals/MySqlDatabaseServiceFixture.cs @@ -0,0 +1,46 @@ +namespace NetEvolve.Pulse.Tests.Integration.Internals; + +using MySql.Data.MySqlClient; + +/// +/// Provides a per-test backed by a MySQL Testcontainer, +/// creating a unique database for each test to ensure isolation. +/// +public sealed class MySqlDatabaseServiceFixture : IDatabaseServiceFixture +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public MySqlContainerFixture Container { get; set; } = default!; + + public string ConnectionString => + Container.ConnectionString.Replace(";Database=test;", $";Database={DatabaseName};", StringComparison.Ordinal); + + internal string DatabaseName { get; } = $"{TestHelper.TargetFramework}{Guid.NewGuid():N}"; + + public DatabaseType DatabaseType => DatabaseType.MySql; + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + + public async Task InitializeAsync() + { + try + { + // Create temporary database to ensure the container is fully initialized and ready to accept connections + await using var con = new MySqlConnection(Container.ConnectionString); + await con.OpenAsync(); + + await using var cmd = con.CreateCommand(); +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + cmd.CommandText = $"CREATE DATABASE {DatabaseName}"; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + + _ = await cmd.ExecuteNonQueryAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new InvalidOperationException( + "MySQL container failed to start within the expected time frame. Try restarting Rancher Desktop.", + ex + ); + } + } +} diff --git a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs index f6cd72d2..23f173ae 100644 --- a/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs +++ b/tests/NetEvolve.Pulse.Tests.Integration/Outbox/OutboxTestsBase.cs @@ -9,7 +9,7 @@ using NetEvolve.Pulse.Tests.Integration.Internals; [TestGroup("Outbox")] -[Timeout(60_000)] // Increased timeout to accommodate potential delays in CI environments, especially when using SQL Server containers. +[Timeout(300_000)] // Increased timeout to accommodate potential delays in CI environments, especially when using SQL Server or MySQL containers that can take a long time to cold-start. public abstract class OutboxTestsBase( IDatabaseServiceFixture databaseServiceFixture, IDatabaseInitializer databaseInitializer