feat(m9/T26b): TemplateEdit full multi-level inherited set + read-only staleness banner
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user