test(managementservice): MgmtDeployArea in-scope success path relays Force to a real DeploymentService and returns its summary (Gitea #40 review)
This commit is contained in:
@@ -8,6 +8,7 @@ using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Schemas;
|
||||
@@ -90,6 +91,33 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
_services.AddScoped(_ => _artifactService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a REAL <see cref="DeploymentService"/> over substituted repositories
|
||||
/// (its methods are not virtual, so it cannot be substituted itself — the same
|
||||
/// approach the CentralUI bUnit tests take). Area lookups go through the
|
||||
/// fixture's shared <see cref="_templateRepo"/>.
|
||||
/// </summary>
|
||||
private (IDeploymentManagerRepository DeployRepo, IFlatteningPipeline Pipeline, ISiteRepository SiteRepo) RegisterDeploymentService()
|
||||
{
|
||||
var deployRepo = Substitute.For<IDeploymentManagerRepository>();
|
||||
var pipeline = Substitute.For<IFlatteningPipeline>();
|
||||
var siteRepo = Substitute.For<ISiteRepository>();
|
||||
pipeline.CreateSession().Returns(_ => new FlattenSession(new TemplateGraphWatermark()));
|
||||
|
||||
var service = new DeploymentService(
|
||||
deployRepo, siteRepo, _templateRepo, pipeline,
|
||||
new CommunicationService(Options.Create(new CommunicationOptions()), NullLogger<CommunicationService>.Instance),
|
||||
new OperationLockManager(), _auditService,
|
||||
new ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening.DiffService(),
|
||||
new ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening.RevisionHashService(),
|
||||
new DeploymentStatusNotifier(NullLogger<DeploymentStatusNotifier>.Instance),
|
||||
Options.Create(new DeploymentManagerOptions { OperationLockTimeout = TimeSpan.FromSeconds(5) }),
|
||||
Options.Create(new CommunicationOptions()),
|
||||
NullLogger<DeploymentService>.Instance);
|
||||
_services.AddScoped(_ => service);
|
||||
return (deployRepo, pipeline, siteRepo);
|
||||
}
|
||||
|
||||
private static Result<ArtifactDeploymentSummary> DeploySummary() =>
|
||||
Result<ArtifactDeploymentSummary>.Success(
|
||||
new ArtifactDeploymentSummary("deploy-1", Array.Empty<SiteArtifactResult>(), 1, 0));
|
||||
@@ -1449,6 +1477,82 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
ExpectMsg<ManagementUnauthorized>(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The in-scope success path of <c>MgmtDeployArea</c>: the handler resolves the
|
||||
/// REAL <see cref="DeploymentService"/> (its methods are not virtual, so it runs
|
||||
/// over substituted repositories), relays the command's <c>Force</c> verbatim to
|
||||
/// <c>DeployAreaAsync</c>, and returns the bulk summary as the success payload.
|
||||
///
|
||||
/// <para>
|
||||
/// <c>Force</c> is observable without a site: the one instance in the area is
|
||||
/// up-to-date, so unforced it is reported as an up-to-date skip; forced it is
|
||||
/// attempted, and the substituted flatten refuses it, so it comes back as a
|
||||
/// failed row — neither path ever reaches the site.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="force">The command's <c>Force</c> flag.</param>
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void DeployArea_InScopeForSiteScopedUser_RelaysForce_AndReturnsSummary(bool force)
|
||||
{
|
||||
var (deployRepo, pipeline, siteRepo) = RegisterDeploymentService();
|
||||
|
||||
var area = new Area("Line") { Id = 12, SiteId = 1 };
|
||||
_templateRepo.GetAreaByIdAsync(12, Arg.Any<CancellationToken>()).Returns(area);
|
||||
_templateRepo.GetAreasBySiteIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns((IReadOnlyList<Area>)new List<Area> { area });
|
||||
siteRepo.GetSiteByIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new Site("Site 1", "site-1") { Id = 1 });
|
||||
|
||||
var instance = new Instance("Pump5") { Id = 5, SiteId = 1, AreaId = 12, TemplateId = 10, State = InstanceState.Enabled };
|
||||
siteRepo.GetInstancesBySiteIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns((IReadOnlyList<Instance>)new List<Instance> { instance });
|
||||
deployRepo.GetInstanceByIdAsync(5, Arg.Any<CancellationToken>()).Returns(instance);
|
||||
|
||||
// Up-to-date: the deployed snapshot's hash equals the freshly recomputed one.
|
||||
var prior = new DeploymentRecord("prior-5", "admin")
|
||||
{
|
||||
InstanceId = 5, Status = DeploymentStatus.Success, RevisionHash = "sha256:5",
|
||||
};
|
||||
deployRepo.GetDeployedRevisionHashesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns((IReadOnlyDictionary<int, string>)new Dictionary<int, string> { [5] = "sha256:5" });
|
||||
deployRepo.GetCurrentDeploymentStatusesAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||
.Returns((IReadOnlyDictionary<int, DeploymentRecord>)new Dictionary<int, DeploymentRecord> { [5] = prior });
|
||||
deployRepo.GetCurrentDeploymentStatusAsync(5, Arg.Any<CancellationToken>()).Returns(prior);
|
||||
pipeline.ComputeRevisionHashAsync(5, Arg.Any<FlattenSession?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result<string>.Success("sha256:5"));
|
||||
pipeline.FlattenAndValidateAsync(5, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>())
|
||||
.Returns(Result<FlatteningPipelineResult>.Failure("flatten refused by test"));
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = ScopedEnvelope(new MgmtDeployAreaCommand(12, force), new[] { "1" }, "Deployer");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(10));
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
var summary = JsonSerializer.Deserialize<SiteDeploymentSummary>(
|
||||
response.JsonData, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
Assert.Equal("site-1", summary.SiteIdentifier);
|
||||
|
||||
if (force)
|
||||
{
|
||||
var row = Assert.Single(summary.InstanceResults);
|
||||
Assert.Equal("Pump5", row.UniqueName);
|
||||
Assert.False(row.Success);
|
||||
Assert.Equal(1, summary.FailureCount);
|
||||
Assert.Empty(summary.SkippedInstances ?? []);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Empty(summary.InstanceResults);
|
||||
var skipped = Assert.Single(summary.SkippedInstances!);
|
||||
Assert.Equal("Pump5", skipped.UniqueName);
|
||||
Assert.Contains("up-to-date", skipped.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that get instance, when in scope for site scoped user, returns success.</summary>
|
||||
[Fact]
|
||||
public void GetInstance_InScopeForSiteScopedUser_ReturnsSuccess()
|
||||
|
||||
Reference in New Issue
Block a user