Resolve all 46 low-severity architecture review findings
This commit is contained in:
@@ -125,6 +125,9 @@ public class TopologyPageTests : BunitContext
|
||||
.Returns(Task.FromResult<IReadOnlyList<Template>>(templates?.ToList() ?? new List<Template>()));
|
||||
_repo.GetAllInstancesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<Instance>>(instances?.ToList() ?? new List<Instance>()));
|
||||
_repo.GetTopologyInstancesAsync(Arg.Any<int>(), Arg.Any<int?>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(call => (IReadOnlyList<Instance>)(instances ?? []).Where(i => i.SiteId == call.ArgAt<int>(0) && i.AreaId == call.ArgAt<int?>(1))
|
||||
.Skip(call.ArgAt<int>(2)).Take(call.ArgAt<int>(3)).ToList());
|
||||
|
||||
areasBySite ??= new();
|
||||
_repo.GetAreasBySiteIdAsync(Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
@@ -137,6 +140,94 @@ public class TopologyPageTests : BunitContext
|
||||
});
|
||||
}
|
||||
|
||||
private IRenderedComponent<TopologyPage> RenderLoadedTopology()
|
||||
{
|
||||
var cut = Render<TopologyPage>();
|
||||
cut.InvokeAsync(async () =>
|
||||
{
|
||||
foreach (var site in await _siteRepo.GetAllSitesAsync())
|
||||
{
|
||||
await cut.Instance.LoadBranchAsync(site.Id, null);
|
||||
foreach (var area in await _repo.GetAreasBySiteIdAsync(site.Id))
|
||||
await cut.Instance.LoadBranchAsync(site.Id, area.Id);
|
||||
}
|
||||
cut.Render();
|
||||
}).GetAwaiter().GetResult();
|
||||
return cut;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task BranchAndRefresh_SerializeRepositoryReadsWithoutDroppingUserPage(bool branchFirst)
|
||||
{
|
||||
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }],
|
||||
instances: [new Instance("First") { Id = 1, SiteId = 1 }]);
|
||||
var cut = Render<TopologyPage>();
|
||||
await cut.InvokeAsync(() => cut.Instance.LoadBranchAsync(1, null));
|
||||
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var release = new TaskCompletionSource<IReadOnlyList<Instance>>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var calls = 0;
|
||||
_repo.GetTopologyInstancesAsync(1, null, Arg.Any<int>(), 50, Arg.Any<CancellationToken>())
|
||||
.Returns(call =>
|
||||
{
|
||||
if (Interlocked.Increment(ref calls) == 1)
|
||||
{
|
||||
entered.TrySetResult();
|
||||
return release.Task;
|
||||
}
|
||||
return Task.FromResult<IReadOnlyList<Instance>>([new Instance("Next") { Id = 51, SiteId = 1 }]);
|
||||
});
|
||||
var first = cut.InvokeAsync(() => branchFirst
|
||||
? cut.Instance.LoadBranchAsync(1, null, 50) : cut.Instance.LoadDeployedStateAsync());
|
||||
await entered.Task.WaitAsync(TimeSpan.FromSeconds(2));
|
||||
Task second = Task.CompletedTask;
|
||||
await cut.InvokeAsync(() =>
|
||||
{
|
||||
second = branchFirst ? cut.Instance.LoadDeployedStateAsync() : cut.Instance.LoadBranchAsync(1, null, 50);
|
||||
});
|
||||
try
|
||||
{
|
||||
Assert.False(second.IsCompleted);
|
||||
Assert.Equal(1, Volatile.Read(ref calls));
|
||||
}
|
||||
finally
|
||||
{
|
||||
release.TrySetResult([new Instance("Next") { Id = 51, SiteId = 1 }]);
|
||||
await Task.WhenAll(first, second).WaitAsync(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
Assert.Equal(2, calls);
|
||||
var field = typeof(TopologyPage).GetField("_allInstances", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
Assert.Equal(51, Assert.Single((List<Instance>)field.GetValue(cut.Instance)!).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BranchPages_AreLazyAndReplacePreviousPage()
|
||||
{
|
||||
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }],
|
||||
instances: Enumerable.Range(1, 101).Select(id => new Instance($"Pump-{id}") { Id = id, SiteId = 1 }));
|
||||
var cut = Render<TopologyPage>();
|
||||
await cut.InvokeAsync(() => cut.Instance.LoadBranchAsync(1, null));
|
||||
var field = typeof(TopologyPage).GetField("_allInstances", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
|
||||
Assert.Equal(50, ((List<Instance>)field.GetValue(cut.Instance)!).Count);
|
||||
await cut.InvokeAsync(() => cut.Instance.LoadBranchAsync(1, null, 50));
|
||||
var second = (List<Instance>)field.GetValue(cut.Instance)!;
|
||||
Assert.Equal(50, second.Count);
|
||||
Assert.Equal(51, second[0].Id);
|
||||
await _repo.DidNotReceive().GetAllInstancesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitialLoad_DoesNotReadInstancesOrTemplates()
|
||||
{
|
||||
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }]);
|
||||
_repo.ClearReceivedCalls();
|
||||
var cut = Render<TopologyPage>();
|
||||
cut.WaitForAssertion(() => Assert.Contains("Plant", cut.Markup));
|
||||
_repo.DidNotReceive().GetAllInstancesAsync(Arg.Any<CancellationToken>());
|
||||
_repo.DidNotReceive().GetAllTemplatesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
@@ -145,7 +236,7 @@ public class TopologyPageTests : BunitContext
|
||||
var instance = new Instance("Pump") { Id = 100, SiteId = 1, AreaId = 10 };
|
||||
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }], instances: [instance]);
|
||||
_repo.GetInstanceByIdAsync(100, Arg.Any<CancellationToken>()).Returns(instance);
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
var auth = Services.GetRequiredService<AuthenticationStateProvider>();
|
||||
var identity = (ClaimsIdentity)(await auth.GetAuthenticationStateAsync()).User.Identity!;
|
||||
if (revoked) identity.AddClaim(new Claim(JwtTokenService.SiteIdClaimType, "2"));
|
||||
@@ -164,7 +255,7 @@ public class TopologyPageTests : BunitContext
|
||||
public async Task UI13_ExistingTopologyCreateUsesCurrentSiteScope(bool revoked)
|
||||
{
|
||||
SeedRepos(sites: [new Site("Plant", "plant") { Id = 1 }]);
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
var auth = Services.GetRequiredService<AuthenticationStateProvider>();
|
||||
var identity = (ClaimsIdentity)(await auth.GetAuthenticationStateAsync()).User.Identity!;
|
||||
if (revoked) identity.AddClaim(new Claim(JwtTokenService.SiteIdClaimType, "2"));
|
||||
@@ -181,7 +272,7 @@ public class TopologyPageTests : BunitContext
|
||||
{
|
||||
SeedRepos();
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
|
||||
Assert.Contains("No sites configured", cut.Markup);
|
||||
}
|
||||
@@ -193,7 +284,7 @@ public class TopologyPageTests : BunitContext
|
||||
// under it must still appear so users can move/create into it.
|
||||
SeedRepos(sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } });
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
|
||||
Assert.Contains("Plant-A", cut.Markup);
|
||||
Assert.Contains("bi-building", cut.Markup);
|
||||
@@ -222,7 +313,7 @@ public class TopologyPageTests : BunitContext
|
||||
},
|
||||
areasBySite: areasBySite);
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
|
||||
// Expand the site, then the area, to render the instance leaf. The
|
||||
// helper scopes by the row's own label so we don't match outer rows
|
||||
@@ -251,7 +342,7 @@ public class TopologyPageTests : BunitContext
|
||||
sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } },
|
||||
areasBySite: areasBySite);
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
FindToggleForLabel(cut, "Plant-A")!.Click();
|
||||
|
||||
var search = cut.Find("input[type='text']");
|
||||
@@ -287,7 +378,7 @@ public class TopologyPageTests : BunitContext
|
||||
new Site("Plant-B", "plant-b") { Id = 2 },
|
||||
});
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
|
||||
// The permitted site is rendered; the non-permitted site is not.
|
||||
Assert.Contains("Plant-A", cut.Markup);
|
||||
@@ -304,7 +395,7 @@ public class TopologyPageTests : BunitContext
|
||||
new Site("Plant-B", "plant-b") { Id = 2 },
|
||||
});
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
|
||||
Assert.Contains("Plant-A", cut.Markup);
|
||||
Assert.Contains("Plant-B", cut.Markup);
|
||||
@@ -319,7 +410,7 @@ public class TopologyPageTests : BunitContext
|
||||
};
|
||||
SeedRepos(sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } }, areasBySite: areasBySite);
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
FindToggleForLabel(cut, "Plant-A")!.Click();
|
||||
|
||||
var areaLabel = cut.FindAll("span.tv-label").First(s => s.TextContent == "Line-1");
|
||||
@@ -342,7 +433,7 @@ public class TopologyPageTests : BunitContext
|
||||
new Instance("Pump-001") { Id = 100, SiteId = 1, State = InstanceState.NotDeployed }
|
||||
});
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
FindToggleForLabel(cut, "Plant-A")!.Click();
|
||||
|
||||
var instanceLabel = cut.FindAll("span.tv-label").First(s => s.TextContent == "Pump-001");
|
||||
@@ -410,7 +501,7 @@ public class TopologyPageTests : BunitContext
|
||||
.Returns(Task.FromResult(Result<FlatteningPipelineResult>.Success(
|
||||
new FlatteningPipelineResult(currentConfig, "hash-new", ValidationResult.Success()))));
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
FindToggleForLabel(cut, "Plant-A")!.Click();
|
||||
FindToggleForLabel(cut, "Line-1")!.Click();
|
||||
|
||||
@@ -472,7 +563,7 @@ public class TopologyPageTests : BunitContext
|
||||
// call count is the proxy for "did staleness recompute".
|
||||
SeedOneDeployedInstanceWithComparison();
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
|
||||
// Initial load computed staleness exactly once.
|
||||
await _deployRepo.Received(1)
|
||||
@@ -493,7 +584,7 @@ public class TopologyPageTests : BunitContext
|
||||
// cheap deployed state (hierarchy + State badge) must still render.
|
||||
SeedOneDeployedInstanceWithComparison();
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
FindToggleForLabel(cut, "Plant-A")!.Click();
|
||||
|
||||
Assert.Contains("Pump-001", cut.Markup);
|
||||
@@ -510,7 +601,7 @@ public class TopologyPageTests : BunitContext
|
||||
// again — a second snapshot-repository call.
|
||||
SeedOneDeployedInstanceWithComparison();
|
||||
|
||||
var cut = Render<TopologyPage>();
|
||||
var cut = RenderLoadedTopology();
|
||||
_deployRepo.Received(1)
|
||||
.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user