Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Design/TemplateEditInheritedSetTests.cs
T

254 lines
11 KiB
C#

using System.Security.Claims;
using Bunit;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Security;
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
using TemplateEditPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design.TemplateEdit;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Design;
/// <summary>
/// bUnit tests for the template editor's READ-ONLY inheritance preview (M9-T26b):
/// rendering the FULL transitively-resolved inherited member set (not just the
/// immediate base) annotated with origin, plus a read-only base-changed staleness
/// banner. The resolve query is dispatched through
/// <see cref="ITemplateInheritanceQueryService"/>, which is substituted here so the
/// tests feed a crafted <see cref="ResolvedTemplateMembers"/> without a real
/// ManagementActor. No mutation / deploy-path behavior is exercised.
/// </summary>
public class TemplateEditInheritedSetTests : BunitContext
{
private readonly ITemplateEngineRepository _repo = Substitute.For<ITemplateEngineRepository>();
private readonly ICentralUiRepository _centralUi = Substitute.For<ICentralUiRepository>();
private readonly IAuditService _audit = Substitute.For<IAuditService>();
private readonly ISharedScriptCatalog _sharedScripts = Substitute.For<ISharedScriptCatalog>();
private readonly ITemplateInheritanceQueryService _resolve =
Substitute.For<ITemplateInheritanceQueryService>();
// The derived template under edit (id 30) and its immediate base (id 20).
private const int DerivedId = 30;
private const int BaseId = 20;
public TemplateEditInheritedSetTests()
{
Services.AddSingleton(_repo);
Services.AddSingleton(_centralUi);
Services.AddSingleton(_audit);
Services.AddSingleton(_resolve);
Services.AddScoped<TemplateService>();
Services.AddScoped<IDialogService, DialogService>();
// ScriptAnalysisService is injected but never exercised on the initial
// render — supply its collaborators so DI can construct it.
Services.AddSingleton(_sharedScripts);
Services.AddSingleton<IMemoryCache>(new MemoryCache(new MemoryCacheOptions()));
Services.AddScoped<ScriptAnalysisService>();
AddTestAuth();
SeedDerivedTemplateRows();
}
private void AddTestAuth()
{
var claims = new[]
{
new Claim(JwtTokenService.UsernameClaimType, "tester"),
new Claim(JwtTokenService.RoleClaimType, "Designer"),
};
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
Services.AddAuthorizationCore();
}
/// <summary>
/// Seeds the stored-row repository view of a derived template. The stored rows
/// deliberately omit the transitively-inherited members — those only appear via
/// the resolved set the query service returns.
/// </summary>
private void SeedDerivedTemplateRows()
{
var grandparent = new Template("Root") { Id = 10 };
var baseTemplate = new Template("Base") { Id = BaseId, ParentTemplateId = 10 };
var derived = new Template("Derived")
{
Id = DerivedId,
ParentTemplateId = BaseId,
IsDerived = true,
};
_repo.GetAllTemplatesAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<Template>>(
new List<Template> { grandparent, baseTemplate, derived }));
_repo.GetTemplateWithChildrenAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<Template?>(derived));
_repo.GetTemplateByIdAsync(BaseId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<Template?>(baseTemplate));
// Stored child rows: empty (the inherited set comes from the resolver).
_repo.GetAttributesByTemplateIdAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<TemplateAttribute>>(new List<TemplateAttribute>()));
_repo.GetAlarmsByTemplateIdAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<TemplateAlarm>>(new List<TemplateAlarm>()));
_repo.GetScriptsByTemplateIdAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<TemplateScript>>(new List<TemplateScript>()));
_repo.GetCompositionsByTemplateIdAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<TemplateComposition>>(new List<TemplateComposition>()));
_repo.GetNativeAlarmSourcesByTemplateIdAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<TemplateNativeAlarmSource>>(
new List<TemplateNativeAlarmSource>()));
_centralUi.GetAllDataConnectionsAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<DataConnection>>(new List<DataConnection>()));
_centralUi.GetInstancesFilteredAsync(
Arg.Any<int?>(), Arg.Any<int?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IReadOnlyList<Instance>>(new List<Instance>()));
}
private void SeedResolved(ResolvedTemplateMembers resolved) =>
_resolve.ResolveAsync(DerivedId, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<ResolvedTemplateMembers?>(resolved));
[Fact]
public void Renders_TransitivelyInheritedMembers_WithOriginAnnotation()
{
// A grandparent-supplied attribute and a base attribute added AFTER the
// derived template was created — neither is in the stored rows; both must
// appear in the resolved inherited set with their origin template name.
SeedResolved(new ResolvedTemplateMembers
{
TemplateId = DerivedId,
TemplateName = "Derived",
ParentTemplateId = BaseId,
Attributes = new List<ResolvedTemplateMemberInfo>
{
new()
{
Name = "FromRoot",
IsInherited = true,
OriginTemplateId = 10,
OriginTemplateName = "Root",
EffectiveValue = "rootval",
},
new()
{
Name = "AddedLater",
IsInherited = true,
OriginTemplateId = BaseId,
OriginTemplateName = "Base",
EffectiveValue = "latebase",
},
},
Staleness = new ResolvedTemplateStaleness { IsStale = false, DifferingMemberCount = 0 },
});
var cut = Render<TemplateEditPage>(p => p.Add(c => c.Id, DerivedId));
cut.WaitForAssertion(() =>
{
// The transitively-inherited member names render...
Assert.Contains("FromRoot", cut.Markup);
Assert.Contains("AddedLater", cut.Markup);
// ...annotated with their origin template (multi-level + post-creation).
Assert.Contains("Root", cut.Markup);
Assert.Contains("Base", cut.Markup);
});
}
[Fact]
public void Renders_EffectiveAlarmTriggerConfiguration_FromResolvedSet()
{
// A HiLo alarm whose effective (merged) trigger config carries both
// setpoints — the editor must surface the merged EffectiveTriggerConfiguration.
SeedResolved(new ResolvedTemplateMembers
{
TemplateId = DerivedId,
TemplateName = "Derived",
ParentTemplateId = BaseId,
Alarms = new List<ResolvedTemplateMemberInfo>
{
new()
{
Name = "TempAlarm",
IsInherited = true,
OriginTemplateId = BaseId,
OriginTemplateName = "Base",
EffectiveValue = "500",
EffectiveTriggerConfiguration = "{\"hi\":90,\"lo\":10}",
},
},
Staleness = new ResolvedTemplateStaleness { IsStale = false, DifferingMemberCount = 0 },
});
var cut = Render<TemplateEditPage>(p => p.Add(c => c.Id, DerivedId));
cut.WaitForAssertion(() =>
{
Assert.Contains("TempAlarm", cut.Markup);
// The merged effective HiLo config (both setpoints) is shown.
Assert.Contains("hi", cut.Markup);
Assert.Contains("lo", cut.Markup);
});
}
[Fact]
public void Shows_StalenessBanner_WhenResolvedSetIsStale()
{
SeedResolved(new ResolvedTemplateMembers
{
TemplateId = DerivedId,
TemplateName = "Derived",
ParentTemplateId = BaseId,
Attributes = new List<ResolvedTemplateMemberInfo>
{
new()
{
Name = "AddedLater",
IsInherited = true,
OriginTemplateId = BaseId,
OriginTemplateName = "Base",
EffectiveValue = "latebase",
},
},
Staleness = new ResolvedTemplateStaleness { IsStale = true, DifferingMemberCount = 3 },
});
var cut = Render<TemplateEditPage>(p => p.Add(c => c.Id, DerivedId));
cut.WaitForAssertion(() =>
{
// Informational banner naming the base-changed condition + the count.
Assert.Contains("Base template changed", cut.Markup);
Assert.Contains("3", cut.Markup);
});
}
[Fact]
public void NoStalenessBanner_WhenResolvedSetIsNotStale()
{
SeedResolved(new ResolvedTemplateMembers
{
TemplateId = DerivedId,
TemplateName = "Derived",
ParentTemplateId = BaseId,
Staleness = new ResolvedTemplateStaleness { IsStale = false, DifferingMemberCount = 0 },
});
var cut = Render<TemplateEditPage>(p => p.Add(c => c.Id, DerivedId));
cut.WaitForAssertion(() => Assert.Contains("Derived", cut.Markup));
Assert.DoesNotContain("Base template changed", cut.Markup);
}
}