feat(audit): start purge + reconciliation singletons; production ISiteEnumerator
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using SiteEntity = ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites.Site;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the production <see cref="SiteEnumerator"/> — the central
|
||||
/// reconciliation-singleton collaborator that projects the config-DB
|
||||
/// <see cref="SiteEntity"/> rows into the <see cref="SiteEntry"/> targets the
|
||||
/// <see cref="SiteAuditReconciliationActor"/> polls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The enumerator opens a fresh DI scope per <see cref="SiteEnumerator.EnumerateAsync"/>
|
||||
/// call (mirroring the per-tick scope pattern in the reconciliation actor)
|
||||
/// because <see cref="ISiteRepository"/> is a SCOPED EF Core service. The tests
|
||||
/// register a substituted repository as a scoped service so the enumerator's
|
||||
/// <c>CreateAsyncScope</c> resolves it and the projection / blank-address
|
||||
/// filtering can be exercised without an MSSQL container.
|
||||
/// </remarks>
|
||||
public class SiteEnumeratorTests
|
||||
{
|
||||
private static SiteEntity SiteWith(string identifier, string? grpcNodeA, string? grpcNodeB = null)
|
||||
{
|
||||
var site = new SiteEntity($"Display {identifier}", identifier)
|
||||
{
|
||||
GrpcNodeAAddress = grpcNodeA,
|
||||
GrpcNodeBAddress = grpcNodeB,
|
||||
};
|
||||
return site;
|
||||
}
|
||||
|
||||
private static IServiceProvider BuildProvider(ISiteRepository repository)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
// Scoped to match the production lifetime (EF Core); the enumerator
|
||||
// must open a scope to resolve it.
|
||||
services.AddScoped(_ => repository);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnumerateAsync_ProjectsSitesWithNodeAAddress_AndSkipsBlankOnes()
|
||||
{
|
||||
var repository = Substitute.For<ISiteRepository>();
|
||||
repository.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<SiteEntity>
|
||||
{
|
||||
SiteWith("site-a", "http://site-a:8083"),
|
||||
SiteWith("site-b", grpcNodeA: " "), // blank NodeA -> skipped
|
||||
});
|
||||
|
||||
var enumerator = new SiteEnumerator(BuildProvider(repository));
|
||||
|
||||
var result = await enumerator.EnumerateAsync();
|
||||
|
||||
var entry = Assert.Single(result);
|
||||
Assert.Equal("site-a", entry.SiteId);
|
||||
Assert.Equal("http://site-a:8083", entry.GrpcEndpoint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnumerateAsync_SkipsNullNodeAAddress()
|
||||
{
|
||||
var repository = Substitute.For<ISiteRepository>();
|
||||
repository.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<SiteEntity>
|
||||
{
|
||||
SiteWith("site-null", grpcNodeA: null),
|
||||
});
|
||||
|
||||
var enumerator = new SiteEnumerator(BuildProvider(repository));
|
||||
|
||||
var result = await enumerator.EnumerateAsync();
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnumerateAsync_ReturnsEmpty_WhenNoSites()
|
||||
{
|
||||
var repository = Substitute.For<ISiteRepository>();
|
||||
repository.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<SiteEntity>());
|
||||
|
||||
var enumerator = new SiteEnumerator(BuildProvider(repository));
|
||||
|
||||
var result = await enumerator.EnumerateAsync();
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,22 @@ public class CentralActorPathTests : IAsyncLifetime
|
||||
public async Task CentralActors_NotificationOutboxProxy_Exists()
|
||||
=> await AssertActorExists("/user/notification-outbox-proxy");
|
||||
|
||||
[Fact]
|
||||
public async Task CentralActors_AuditLogPurgeSingleton_Exists()
|
||||
=> await AssertActorExists("/user/audit-log-purge-singleton");
|
||||
|
||||
[Fact]
|
||||
public async Task CentralActors_AuditLogPurgeProxy_Exists()
|
||||
=> await AssertActorExists("/user/audit-log-purge-proxy");
|
||||
|
||||
[Fact]
|
||||
public async Task CentralActors_SiteAuditReconciliationSingleton_Exists()
|
||||
=> await AssertActorExists("/user/site-audit-reconciliation-singleton");
|
||||
|
||||
[Fact]
|
||||
public async Task CentralActors_SiteAuditReconciliationProxy_Exists()
|
||||
=> await AssertActorExists("/user/site-audit-reconciliation-proxy");
|
||||
|
||||
private async Task AssertActorExists(string path)
|
||||
{
|
||||
Assert.NotNull(_actorSystem);
|
||||
|
||||
Reference in New Issue
Block a user