Skip to content

Commit d749fe0

Browse files
committed
test: add RegisterFromAssembly open generic handler discovery tests
- 6 new tests verifying RegisterFromAssembly discovers open generic handlers automatically - Tests cover: pre-handler discovery, post-handler discovery, pipeline ordering, multiple command types, constraint filtering, and full pipeline verification - Added [Collection("Sequential")] to CommandModuleTests for test isolation
1 parent 79180cd commit d749fe0

File tree

9 files changed

+299
-0
lines changed

9 files changed

+299
-0
lines changed

tests/LiteBus.CommandModule.UnitTests/CommandModuleTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace LiteBus.CommandModule.UnitTests;
1515

16+
[Collection("Sequential")]
1617
public sealed class CommandModuleTests : LiteBusTestBase
1718
{
1819
[Fact]
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
using LiteBus.CommandModule.UnitTests.UseCases;
2+
using LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
3+
using LiteBus.Commands;
4+
using LiteBus.Commands.Abstractions;
5+
using LiteBus.Extensions.Microsoft.DependencyInjection;
6+
using LiteBus.Testing;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace LiteBus.CommandModule.UnitTests;
10+
11+
/// <summary>
12+
/// Tests that verify <c>RegisterFromAssembly</c> automatically discovers open generic handlers
13+
/// without requiring an explicit <c>Register(typeof(...))</c> call.
14+
/// </summary>
15+
[Collection("Sequential")]
16+
public sealed class OpenGenericAssemblyScanTests : LiteBusTestBase
17+
{
18+
[Fact]
19+
public async Task RegisterFromAssembly_DiscoversOpenGenericPreHandler_AndExecutesIt()
20+
{
21+
// Arrange — only RegisterFromAssembly, no explicit Register(typeof(...))
22+
var serviceProvider = new ServiceCollection()
23+
.AddLiteBus(configuration =>
24+
{
25+
configuration.AddCommandModule(builder =>
26+
{
27+
builder.RegisterFromAssembly(typeof(ScanTestCommand).Assembly);
28+
});
29+
})
30+
.BuildServiceProvider();
31+
32+
var commandMediator = serviceProvider.GetRequiredService<ICommandMediator>();
33+
var command = new ScanTestCommand();
34+
35+
// Act
36+
await commandMediator.SendAsync(command);
37+
38+
// Assert — open generic pre-handler should have been discovered and executed
39+
command.ExecutedTypes.Should().Contain(typeof(ScanTestOpenGenericPreHandler<ScanTestCommand>));
40+
}
41+
42+
[Fact]
43+
public async Task RegisterFromAssembly_DiscoversOpenGenericPostHandler_AndExecutesIt()
44+
{
45+
// Arrange
46+
var serviceProvider = new ServiceCollection()
47+
.AddLiteBus(configuration =>
48+
{
49+
configuration.AddCommandModule(builder =>
50+
{
51+
builder.RegisterFromAssembly(typeof(ScanTestCommand).Assembly);
52+
});
53+
})
54+
.BuildServiceProvider();
55+
56+
var commandMediator = serviceProvider.GetRequiredService<ICommandMediator>();
57+
var command = new ScanTestCommand();
58+
59+
// Act
60+
await commandMediator.SendAsync(command);
61+
62+
// Assert — open generic post-handler should have been discovered and executed
63+
command.ExecutedTypes.Should().Contain(typeof(ScanTestOpenGenericPostHandler<ScanTestCommand>));
64+
}
65+
66+
[Fact]
67+
public async Task RegisterFromAssembly_OpenGenericHandlers_ExecuteInCorrectPipelineOrder()
68+
{
69+
// Arrange
70+
var serviceProvider = new ServiceCollection()
71+
.AddLiteBus(configuration =>
72+
{
73+
configuration.AddCommandModule(builder =>
74+
{
75+
builder.RegisterFromAssembly(typeof(ScanTestCommand).Assembly);
76+
});
77+
})
78+
.BuildServiceProvider();
79+
80+
var commandMediator = serviceProvider.GetRequiredService<ICommandMediator>();
81+
var command = new ScanTestCommand();
82+
83+
// Act
84+
await commandMediator.SendAsync(command);
85+
86+
// Assert — pre-handler runs before main handler, post-handler runs after
87+
var preIndex = command.ExecutedTypes.IndexOf(typeof(ScanTestOpenGenericPreHandler<ScanTestCommand>));
88+
var mainIndex = command.ExecutedTypes.IndexOf(typeof(ScanTestCommandHandler));
89+
var postIndex = command.ExecutedTypes.IndexOf(typeof(ScanTestOpenGenericPostHandler<ScanTestCommand>));
90+
91+
preIndex.Should().BeGreaterThanOrEqualTo(0, "pre-handler should have executed");
92+
mainIndex.Should().BeGreaterThanOrEqualTo(0, "main handler should have executed");
93+
postIndex.Should().BeGreaterThanOrEqualTo(0, "post-handler should have executed");
94+
95+
preIndex.Should().BeLessThan(mainIndex, "pre-handler should run before main handler");
96+
mainIndex.Should().BeLessThan(postIndex, "main handler should run before post-handler");
97+
}
98+
99+
[Fact]
100+
public async Task RegisterFromAssembly_OpenGenericHandlers_ApplyToMultipleCommandTypes()
101+
{
102+
// Arrange
103+
var serviceProvider = new ServiceCollection()
104+
.AddLiteBus(configuration =>
105+
{
106+
configuration.AddCommandModule(builder =>
107+
{
108+
builder.RegisterFromAssembly(typeof(ScanTestCommand).Assembly);
109+
});
110+
})
111+
.BuildServiceProvider();
112+
113+
var commandMediator = serviceProvider.GetRequiredService<ICommandMediator>();
114+
115+
var command1 = new ScanTestCommand();
116+
var command2 = new AnotherScanTestCommand();
117+
118+
// Act
119+
await commandMediator.SendAsync(command1);
120+
await commandMediator.SendAsync(command2);
121+
122+
// Assert — open generic handlers should be closed for both command types
123+
command1.ExecutedTypes.Should().Contain(typeof(ScanTestOpenGenericPreHandler<ScanTestCommand>));
124+
command1.ExecutedTypes.Should().Contain(typeof(ScanTestOpenGenericPostHandler<ScanTestCommand>));
125+
126+
command2.ExecutedTypes.Should().Contain(typeof(ScanTestOpenGenericPreHandler<AnotherScanTestCommand>));
127+
command2.ExecutedTypes.Should().Contain(typeof(ScanTestOpenGenericPostHandler<AnotherScanTestCommand>));
128+
}
129+
130+
[Fact]
131+
public async Task RegisterFromAssembly_OpenGenericHandlers_RespectConstraints_DoNotApplyToUnrelatedCommands()
132+
{
133+
// Arrange — CreateProductCommand does NOT implement IOpenGenericScanTestCommand
134+
var serviceProvider = new ServiceCollection()
135+
.AddLiteBus(configuration =>
136+
{
137+
configuration.AddCommandModule(builder =>
138+
{
139+
builder.RegisterFromAssembly(typeof(ScanTestCommand).Assembly);
140+
});
141+
})
142+
.BuildServiceProvider();
143+
144+
var commandMediator = serviceProvider.GetRequiredService<ICommandMediator>();
145+
146+
var command = new UseCases.CreateProduct.CreateProductCommand();
147+
148+
// Act
149+
var result = await commandMediator.SendAsync(command);
150+
151+
// Assert — CreateProductCommand should NOT have the constrained open generic handlers.
152+
// We verify by checking that none of the executed types are closed forms of the scan-test open generics.
153+
result.Should().NotBeNull();
154+
command.ExecutedTypes
155+
.Where(t => t.IsGenericType)
156+
.Select(t => t.GetGenericTypeDefinition())
157+
.Should().NotContain(typeof(ScanTestOpenGenericPreHandler<>));
158+
command.ExecutedTypes
159+
.Where(t => t.IsGenericType)
160+
.Select(t => t.GetGenericTypeDefinition())
161+
.Should().NotContain(typeof(ScanTestOpenGenericPostHandler<>));
162+
}
163+
164+
[Fact]
165+
public async Task RegisterFromAssembly_DiscoversOpenGenericHandlers_WithoutExplicitRegistration()
166+
{
167+
// Arrange — ONLY RegisterFromAssembly, no Register(typeof(...)) at all.
168+
// This proves that assembly scanning is sufficient to discover open generic handlers.
169+
var serviceProvider = new ServiceCollection()
170+
.AddLiteBus(configuration =>
171+
{
172+
configuration.AddCommandModule(builder =>
173+
{
174+
builder.RegisterFromAssembly(typeof(ScanTestCommand).Assembly);
175+
});
176+
})
177+
.BuildServiceProvider();
178+
179+
var commandMediator = serviceProvider.GetRequiredService<ICommandMediator>();
180+
181+
var command1 = new ScanTestCommand();
182+
var command2 = new AnotherScanTestCommand();
183+
184+
// Act
185+
await commandMediator.SendAsync(command1);
186+
await commandMediator.SendAsync(command2);
187+
188+
// Assert — both commands should have the full pipeline:
189+
// open generic pre-handler → main handler → open generic post-handler
190+
var command1Relevant = command1.ExecutedTypes
191+
.Where(t => t == typeof(ScanTestCommandHandler)
192+
|| (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(ScanTestOpenGenericPreHandler<>)
193+
|| t.GetGenericTypeDefinition() == typeof(ScanTestOpenGenericPostHandler<>))))
194+
.ToList();
195+
196+
command1Relevant.Should().HaveCount(3);
197+
command1Relevant[0].Should().Be(typeof(ScanTestOpenGenericPreHandler<ScanTestCommand>));
198+
command1Relevant[1].Should().Be(typeof(ScanTestCommandHandler));
199+
command1Relevant[2].Should().Be(typeof(ScanTestOpenGenericPostHandler<ScanTestCommand>));
200+
201+
var command2Relevant = command2.ExecutedTypes
202+
.Where(t => t == typeof(AnotherScanTestCommandHandler)
203+
|| (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(ScanTestOpenGenericPreHandler<>)
204+
|| t.GetGenericTypeDefinition() == typeof(ScanTestOpenGenericPostHandler<>))))
205+
.ToList();
206+
207+
command2Relevant.Should().HaveCount(3);
208+
command2Relevant[0].Should().Be(typeof(ScanTestOpenGenericPreHandler<AnotherScanTestCommand>));
209+
command2Relevant[1].Should().Be(typeof(AnotherScanTestCommandHandler));
210+
command2Relevant[2].Should().Be(typeof(ScanTestOpenGenericPostHandler<AnotherScanTestCommand>));
211+
}
212+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using LiteBus.Commands.Abstractions;
2+
3+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
4+
5+
public sealed class AnotherScanTestCommand : IOpenGenericScanTestCommand, ICommand
6+
{
7+
public List<Type> ExecutedTypes { get; } = new();
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using LiteBus.Commands.Abstractions;
2+
3+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
4+
5+
public sealed class AnotherScanTestCommandHandler : ICommandHandler<AnotherScanTestCommand>
6+
{
7+
public Task HandleAsync(AnotherScanTestCommand message, CancellationToken cancellationToken = default)
8+
{
9+
message.ExecutedTypes.Add(GetType());
10+
return Task.CompletedTask;
11+
}
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
2+
3+
/// <summary>
4+
/// Marker interface for commands used in open generic assembly scan tests.
5+
/// This constrains the open generic handlers so they only apply to these test commands
6+
/// and do not affect existing tests.
7+
/// </summary>
8+
public interface IOpenGenericScanTestCommand : IAuditableCommand;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using LiteBus.Commands.Abstractions;
2+
3+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
4+
5+
public sealed class ScanTestCommand : IOpenGenericScanTestCommand, ICommand
6+
{
7+
public List<Type> ExecutedTypes { get; } = new();
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using LiteBus.Commands.Abstractions;
2+
3+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
4+
5+
public sealed class ScanTestCommandHandler : ICommandHandler<ScanTestCommand>
6+
{
7+
public Task HandleAsync(ScanTestCommand message, CancellationToken cancellationToken = default)
8+
{
9+
message.ExecutedTypes.Add(GetType());
10+
return Task.CompletedTask;
11+
}
12+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using LiteBus.Commands.Abstractions;
2+
3+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
4+
5+
/// <summary>
6+
/// An open generic post-handler constrained to <see cref="IOpenGenericScanTestCommand" />.
7+
/// It lives in the same assembly as the tests, so <c>RegisterFromAssembly</c> should discover it automatically.
8+
/// </summary>
9+
public sealed class ScanTestOpenGenericPostHandler<TCommand> : ICommandPostHandler<TCommand>
10+
where TCommand : ICommand, IOpenGenericScanTestCommand
11+
{
12+
public Task PostHandleAsync(TCommand message, object? messageResult, CancellationToken cancellationToken = default)
13+
{
14+
if (message is IOpenGenericScanTestCommand auditable)
15+
{
16+
auditable.ExecutedTypes.Add(typeof(ScanTestOpenGenericPostHandler<TCommand>));
17+
}
18+
19+
return Task.CompletedTask;
20+
}
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using LiteBus.Commands.Abstractions;
2+
3+
namespace LiteBus.CommandModule.UnitTests.UseCases.OpenGenericAssemblyScan;
4+
5+
/// <summary>
6+
/// An open generic pre-handler constrained to <see cref="IOpenGenericScanTestCommand" />.
7+
/// It lives in the same assembly as the tests, so <c>RegisterFromAssembly</c> should discover it automatically.
8+
/// </summary>
9+
public sealed class ScanTestOpenGenericPreHandler<TCommand> : ICommandPreHandler<TCommand>
10+
where TCommand : ICommand, IOpenGenericScanTestCommand
11+
{
12+
public Task PreHandleAsync(TCommand message, CancellationToken cancellationToken = default)
13+
{
14+
message.ExecutedTypes.Add(typeof(ScanTestOpenGenericPreHandler<TCommand>));
15+
return Task.CompletedTask;
16+
}
17+
}

0 commit comments

Comments
 (0)