feat(ui): secured-write history paging + submission age in approve dialog (arch-review S2/P3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.RegularExpressions;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
@@ -48,10 +49,17 @@ public class SecuredWritesTests : BunitContext
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Empty list by default; individual tests seed the queue/history.
|
||||
_service.ListAsync(Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SecuredWriteDto>>(Array.Empty<SecuredWriteDto>()));
|
||||
_service.ListAsync(
|
||||
Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new SecuredWriteListResult(Array.Empty<SecuredWriteDto>(), 0)));
|
||||
}
|
||||
|
||||
/// <summary>Stubs <c>ListAsync</c> (any filter/paging args) to return <paramref name="rows"/>.</summary>
|
||||
private void SeedList(IReadOnlyList<SecuredWriteDto> rows, int? totalCount = null)
|
||||
=> _service.ListAsync(
|
||||
Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new SecuredWriteListResult(rows, totalCount ?? rows.Count)));
|
||||
|
||||
/// <summary>
|
||||
/// Registers the real authorization stack + a principal with the given username and
|
||||
/// roles so the page's <c>AuthorizeView Policy=...</c> regions evaluate genuinely.
|
||||
@@ -165,9 +173,7 @@ public class SecuredWritesTests : BunitContext
|
||||
{
|
||||
AddAuth("verifier-user", Roles.Verifier);
|
||||
SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
|
||||
_service.ListAsync(Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SecuredWriteDto>>(
|
||||
new[] { Dto(1, "Pending", "someone-else") }));
|
||||
SeedList(new[] { Dto(1, "Pending", "someone-else") });
|
||||
|
||||
var cut = RenderPage();
|
||||
|
||||
@@ -189,9 +195,7 @@ public class SecuredWritesTests : BunitContext
|
||||
// this too, but the UI surfaces it up front).
|
||||
AddAuth("self", Roles.Operator, Roles.Verifier);
|
||||
SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
|
||||
_service.ListAsync(Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SecuredWriteDto>>(
|
||||
new[] { Dto(1, "Pending", "self") }));
|
||||
SeedList(new[] { Dto(1, "Pending", "self") });
|
||||
|
||||
var cut = RenderPage();
|
||||
|
||||
@@ -210,9 +214,7 @@ public class SecuredWritesTests : BunitContext
|
||||
{
|
||||
AddAuth("verifier-user", Roles.Verifier);
|
||||
SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
|
||||
_service.ListAsync(Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SecuredWriteDto>>(
|
||||
new[] { Dto(7, "Pending", "someone-else") }));
|
||||
SeedList(new[] { Dto(7, "Pending", "someone-else") });
|
||||
_service.ApproveAsync(Arg.Any<long>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(SecuredWriteActionResult.Ok(Dto(7, "Executed", "someone-else", "verifier-user"))));
|
||||
|
||||
@@ -229,14 +231,13 @@ public class SecuredWritesTests : BunitContext
|
||||
{
|
||||
AddAuth("viewer", Roles.Viewer);
|
||||
SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
|
||||
_service.ListAsync(Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult<IReadOnlyList<SecuredWriteDto>>(new[]
|
||||
{
|
||||
Dto(1, "Executed", "op-a", "ver-a", tag: "Pump.Setpoint"),
|
||||
Dto(2, "Rejected", "op-b", "ver-b", tag: "Valve.Cmd"),
|
||||
Dto(3, "Failed", "op-c", "ver-c", executionError: "device timeout", tag: "Motor.Run"),
|
||||
Dto(4, "Pending", "op-d", tag: "ShouldNotAppear"),
|
||||
}));
|
||||
SeedList(new[]
|
||||
{
|
||||
Dto(1, "Executed", "op-a", "ver-a", tag: "Pump.Setpoint"),
|
||||
Dto(2, "Rejected", "op-b", "ver-b", tag: "Valve.Cmd"),
|
||||
Dto(3, "Failed", "op-c", "ver-c", executionError: "device timeout", tag: "Motor.Run"),
|
||||
Dto(4, "Pending", "op-d", tag: "ShouldNotAppear"),
|
||||
});
|
||||
|
||||
var cut = RenderPage();
|
||||
|
||||
@@ -253,6 +254,58 @@ public class SecuredWritesTests : BunitContext
|
||||
});
|
||||
}
|
||||
|
||||
// ── (d) History pager: page-size 50, driven by TotalCount ──
|
||||
|
||||
[Fact]
|
||||
public void History_Pager_NextRequestsSecondPage_UsingTotalCount()
|
||||
{
|
||||
// 60 terminal rows returned but TotalCount = 120 → a second page exists, so the
|
||||
// "Next" button is present + enabled and clicking it re-queries with skip: 50.
|
||||
AddAuth("viewer", Roles.Viewer);
|
||||
SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
|
||||
var terminal = Enumerable.Range(1, 60)
|
||||
.Select(i => Dto(i, "Executed", $"op-{i}", $"ver-{i}"))
|
||||
.ToArray();
|
||||
SeedList(terminal, totalCount: 120);
|
||||
|
||||
var cut = RenderPage();
|
||||
|
||||
cut.WaitForAssertion(() =>
|
||||
{
|
||||
var next = cut.Find("[data-test='secured-write-history-next']");
|
||||
Assert.False(next.HasAttribute("disabled"));
|
||||
});
|
||||
|
||||
cut.Find("[data-test='secured-write-history-next']").Click();
|
||||
|
||||
// The history page-2 fetch offsets by one page (skip: 50).
|
||||
_service.Received().ListAsync(
|
||||
Arg.Any<string?>(), Arg.Any<string?>(), 50, Arg.Any<int>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ── (e) Approve dialog surfaces submission age (stale-setpoint scenario) ──
|
||||
|
||||
[Fact]
|
||||
public void Approve_ConfirmDialog_ShowsSubmissionAge()
|
||||
{
|
||||
AddAuth("verifier-user", Roles.Verifier);
|
||||
SeedSites(new Site("Plant-A", "plant-a") { Id = 1 });
|
||||
var stale = Dto(9, "Pending", "someone-else") with { SubmittedAtUtc = DateTime.UtcNow.AddHours(-26) };
|
||||
SeedList(new[] { stale });
|
||||
_service.ApproveAsync(Arg.Any<long>(), Arg.Any<string?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(SecuredWriteActionResult.Ok(Dto(9, "Executed", "someone-else", "verifier-user"))));
|
||||
|
||||
var cut = RenderPage();
|
||||
cut.WaitForAssertion(() => Assert.NotNull(cut.Find("[data-test='secured-write-approve']")));
|
||||
|
||||
cut.Find("[data-test='secured-write-approve']").Click();
|
||||
|
||||
_dialog.Received(1).ConfirmAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<string>(m => Regex.IsMatch(m, "Submitted .* ago")),
|
||||
Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Viewer_DoesNotSeeOperatorOrVerifierRegions()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user