using System.Reflection; using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; 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.Entities.Deployment; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.DeploymentManager; using ZB.MOM.WW.ScadaBridge.Security; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services; using TopologyPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.Topology; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests; /// /// Regression tests for register row 103 (the P6-6 lesser instance in /// Topology). OnInitializedAsync awaits LoadDataAsync and only then /// calls StartLiveUpdatesTimer. If the circuit was torn down inside that await /// (user navigates away during the initial page load), the pre-fix Dispose would /// have already run and found _liveUpdatesTimer still null — a no-op — so the /// timer StartLiveUpdatesTimer creates once the await resumes is never disposed: /// it ticks against a torn-down component every 15s until process restart. The fix /// (mirroring DebugView's P6-6 fix and TransportImport's row-103 sibling /// fix) creates/replaces the timer and observes disposal atomically under /// _timerLock, so a (re)start that resumes after Dispose already ran /// refuses to create a new timer instead of leaking one. /// /// /// The race is reproduced deterministically by driving the exact call ORDER the async /// gap allows — Dispose() then a late-arriving StartLiveUpdatesTimer() — /// rather than by gating I/O and sleeping: StartLiveUpdatesTimer itself is /// synchronous, so once we control ordering there is nothing left to wait on. This is /// the "deterministic observation, never sleep-then-assert" style the shared plan /// constraints require: the assertion is a direct field-state check, not a timer tick. /// /// public class TopologyLiveUpdatesTimerDisposeRaceTests : BunitContext { private readonly ITemplateEngineRepository _repo = Substitute.For(); private readonly ISiteRepository _siteRepo = Substitute.For(); private readonly IDeploymentManagerRepository _deployRepo = Substitute.For(); private readonly IAuditService _audit = Substitute.For(); public TopologyLiveUpdatesTimerDisposeRaceTests() { Services.AddSingleton(_repo); Services.AddSingleton(_siteRepo); Services.AddSingleton(_deployRepo); Services.AddSingleton(Substitute.For()); // DeploymentService takes IStaleInstanceProbe since the Low-wave P7-6 pre-filter (T13). Services.AddSingleton(Substitute.For()); Services.AddSingleton(_audit); var comms = new CommunicationService( Options.Create(new CommunicationOptions()), NullLogger.Instance); Services.AddSingleton(comms); Services.AddSingleton(new OperationLockManager()); Services.AddSingleton(Options.Create(new DeploymentManagerOptions { OperationLockTimeout = TimeSpan.FromSeconds(5) })); Services.AddSingleton(Options.Create(new CommunicationOptions())); Services.AddSingleton>(NullLogger.Instance); Services.AddScoped(); Services.AddScoped(); Services.AddSingleton( new DeploymentStatusNotifier(NullLogger.Instance)); Services.AddScoped(); Services.AddScoped(); Services.AddScoped(); AddTestAuth(); Services.AddScoped(); Services.AddScoped(); JSInterop.Setup("treeviewStorage.load", _ => true).SetResult(null); JSInterop.SetupVoid("treeviewStorage.save", _ => true); SeedEmptyRepos(); } private void AddTestAuth() { var claims = new[] { new Claim(JwtTokenService.UsernameClaimType, "tester"), new Claim(JwtTokenService.RoleClaimType, "Deployer") }; var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); Services.AddSingleton(new TestAuthStateProvider(user)); Services.AddAuthorizationCore(); } private void SeedEmptyRepos() { _siteRepo.GetAllSitesAsync(Arg.Any()) .Returns(Task.FromResult>(new List())); _repo.GetAllTemplatesAsync(Arg.Any()) .Returns(Task.FromResult>(new List