using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth; /// /// Regression tests for CentralUI-002. Site-scoped Deployment permissions are /// written as SiteId claims at login but were never read — Deployment /// pages listed and acted on every site. is the /// shared helper that reads those claims; these tests pin its behaviour. /// public class SiteScopeServiceTests { private sealed class StubAuthStateProvider : AuthenticationStateProvider { private readonly ClaimsPrincipal _user; public StubAuthStateProvider(ClaimsPrincipal user) => _user = user; public override Task GetAuthenticationStateAsync() => Task.FromResult(new AuthenticationState(_user)); } private static SiteScopeService ForUser(params Claim[] claims) { var identity = new ClaimsIdentity(claims, authenticationType: "TestCookie"); return new SiteScopeService(new StubAuthStateProvider(new ClaimsPrincipal(identity))); } private static Claim Role(string role) => new(JwtTokenService.RoleClaimType, role); private static Claim SiteClaim(int id) => new(JwtTokenService.SiteIdClaimType, id.ToString()); private static List Sites(params int[] ids) => ids.Select(id => new Site($"Site{id}", $"SITE-{id}") { Id = id }).ToList(); [Fact] public async Task DeploymentUserWithNoSiteClaims_IsSystemWide() { var svc = ForUser(Role("Deployer")); Assert.True(await svc.IsSystemWideAsync()); Assert.Empty(await svc.PermittedSiteIdsAsync()); } [Fact] public async Task SystemWideUser_FilterSites_ReturnsAllSites() { var svc = ForUser(Role("Deployer")); var filtered = await svc.FilterSitesAsync(Sites(1, 2, 3)); Assert.Equal(new[] { 1, 2, 3 }, filtered.Select(s => s.Id)); } [Fact] public async Task ScopedUser_FilterSites_ReturnsOnlyPermittedSites() { // Regression: a Deployment user scoped to sites 1 and 3 must NOT see site 2. var svc = ForUser(Role("Deployer"), SiteClaim(1), SiteClaim(3)); var filtered = await svc.FilterSitesAsync(Sites(1, 2, 3, 4)); Assert.Equal(new[] { 1, 3 }, filtered.Select(s => s.Id).OrderBy(x => x)); } [Fact] public async Task ScopedUser_IsSiteAllowed_OnlyForGrantedSites() { var svc = ForUser(Role("Deployer"), SiteClaim(5)); Assert.True(await svc.IsSiteAllowedAsync(5)); Assert.False(await svc.IsSiteAllowedAsync(6)); } [Fact] public async Task ScopedUser_IsNotSystemWide_AndReportsItsPermittedIds() { var svc = ForUser(Role("Deployer"), SiteClaim(7), SiteClaim(9)); Assert.False(await svc.IsSystemWideAsync()); Assert.Equal(new[] { 7, 9 }, (await svc.PermittedSiteIdsAsync()).OrderBy(x => x)); } [Fact] public async Task SystemWideUser_IsSiteAllowed_ForAnySite() { var svc = ForUser(Role("Deployer")); Assert.True(await svc.IsSiteAllowedAsync(1)); Assert.True(await svc.IsSiteAllowedAsync(999)); } // ── Administrator is system-wide regardless of SiteId claims (register 86) ── // // ManagementActor has always exempted Administrator from site scoping // (EnforceSiteScope and every handler that calls it). This service was // claims-only, so an Administrator who ALSO carried a scoped Deployer grant // could act fleet-wide through the CLI/HTTP surface while the UI hid the same // sites. The UI was the side that disagreed with the documented rule. [Fact] public async Task AdministratorWithSiteClaims_IsSystemWide() { var svc = ForUser(Role("Administrator"), Role("Deployer"), SiteClaim(1), SiteClaim(3)); Assert.True(await svc.IsSystemWideAsync()); } [Fact] public async Task AdministratorWithSiteClaims_FilterSites_ReturnsAllSites() { var svc = ForUser(Role("Administrator"), Role("Deployer"), SiteClaim(1)); var filtered = await svc.FilterSitesAsync(Sites(1, 2, 3)); Assert.Equal(new[] { 1, 2, 3 }, filtered.Select(s => s.Id)); } [Fact] public async Task AdministratorWithSiteClaims_IsSiteAllowed_ForSitesOutsideTheClaims() { var svc = ForUser(Role("Administrator"), SiteClaim(1)); Assert.True(await svc.IsSiteAllowedAsync(2)); } [Fact] public async Task AdministratorWithSiteClaims_ReportsNoPermittedIds() { // The invariant "system-wide => empty id set" must hold for every caller, so // a consumer that reads the ids without consulting IsSystemWide cannot // silently narrow an Administrator to a subset of sites. var svc = ForUser(Role("Administrator"), SiteClaim(1), SiteClaim(3)); Assert.Empty(await svc.PermittedSiteIdsAsync()); } [Theory] [InlineData("administrator")] [InlineData("ADMINISTRATOR")] public async Task AdministratorRoleMatch_IsCaseInsensitive(string role) { // ManagementActor compares roles with OrdinalIgnoreCase. ClaimsPrincipal // .IsInRole compares claim VALUES ordinally, so matching the actor requires // reading the role claims explicitly — otherwise the two sides would // disagree on a differently-cased role name. var svc = ForUser(Role(role), SiteClaim(1)); Assert.True(await svc.IsSystemWideAsync()); } [Fact] public async Task DesignerWithSiteClaims_IsStillScoped() { // The class doc used to claim "any Admin/Design user" is system-wide. Only // Administrator is — on either side of the rule — and the doc was corrected // rather than the bypass widened. var svc = ForUser(Role("Designer"), SiteClaim(1)); Assert.False(await svc.IsSystemWideAsync()); Assert.False(await svc.IsSiteAllowedAsync(2)); } }