254 lines
11 KiB
C#
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);
|
|
}
|
|
}
|