513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
140 lines
5.9 KiB
C#
140 lines
5.9 KiB
C#
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();
|
|
}
|
|
|
|
/// <summary>Verifies that sites with a NodeA address are projected and sites with a blank address are skipped.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that a site with a null NodeA address is skipped.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that a site with distinct NodeA and NodeB addresses projects NodeB as the fallback endpoint.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task EnumerateAsync_BothAddresses_ProjectsNodeBAsFallback()
|
|
{
|
|
// A site with both NodeA and a DISTINCT NodeB yields
|
|
// SiteEntry(primary=NodeA, FallbackGrpcEndpoint=NodeB).
|
|
var repository = Substitute.For<ISiteRepository>();
|
|
repository.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<SiteEntity>
|
|
{
|
|
SiteWith("site-ab", grpcNodeA: "http://site-a:8083", grpcNodeB: "http://site-b:8083"),
|
|
});
|
|
|
|
var enumerator = new SiteEnumerator(BuildProvider(repository));
|
|
|
|
var entry = Assert.Single(await enumerator.EnumerateAsync());
|
|
Assert.Equal("site-ab", entry.SiteId);
|
|
Assert.Equal("http://site-a:8083", entry.GrpcEndpoint);
|
|
Assert.Equal("http://site-b:8083", entry.FallbackGrpcEndpoint);
|
|
}
|
|
|
|
/// <summary>Verifies that a site with only a NodeB address is not skipped and has no fallback endpoint.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task EnumerateAsync_OnlyNodeB_IsNotSkipped_AndHasNoFallback()
|
|
{
|
|
// A site with a blank NodeA but a valid NodeB is no longer
|
|
// skipped — NodeB becomes the primary and there is no separate fallback.
|
|
var repository = Substitute.For<ISiteRepository>();
|
|
repository.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(new List<SiteEntity>
|
|
{
|
|
SiteWith("site-bonly", grpcNodeA: " ", grpcNodeB: "http://site-b:8083"),
|
|
});
|
|
|
|
var enumerator = new SiteEnumerator(BuildProvider(repository));
|
|
|
|
var entry = Assert.Single(await enumerator.EnumerateAsync());
|
|
Assert.Equal("site-bonly", entry.SiteId);
|
|
Assert.Equal("http://site-b:8083", entry.GrpcEndpoint);
|
|
Assert.Null(entry.FallbackGrpcEndpoint);
|
|
}
|
|
|
|
/// <summary>Verifies that enumeration returns an empty list when there are no sites.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
}
|