fix(ui): preserve precise audit submissions and adopt delivered secrets
This commit is contained in:
+107
-26
@@ -57,6 +57,103 @@ public class AuditFilterBarTests : BunitContext
|
||||
Services.AddSingleton(_auditLogQueryService);
|
||||
}
|
||||
|
||||
/// <summary>Malformed exact identifiers never silently broaden a submitted query.</summary>
|
||||
[Theory]
|
||||
[InlineData("audit-execution-id")]
|
||||
[InlineData("audit-parent-execution-id")]
|
||||
public void MalformedExactId_PreservesInputAndLinkedErrorWithoutCallback(string id)
|
||||
{
|
||||
var calls = 0;
|
||||
var cut = Render<AuditFilterBar>(p => p.Add(c => c.OnFilterChanged, _ => calls++));
|
||||
cut.Find("#" + id).Change("not-a-guid");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.Equal(0, calls);
|
||||
var input = cut.Find("#" + id);
|
||||
Assert.Equal("not-a-guid", input.GetAttribute("value"));
|
||||
Assert.Equal("true", input.GetAttribute("aria-invalid"));
|
||||
var errorId = input.GetAttribute("aria-describedby");
|
||||
Assert.False(string.IsNullOrWhiteSpace(errorId));
|
||||
Assert.Contains("valid GUID", cut.Find("#" + errorId).TextContent);
|
||||
input.Change(Guid.NewGuid().ToString());
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.Equal(1, calls);
|
||||
Assert.Null(cut.Find("#" + id).GetAttribute("aria-invalid"));
|
||||
}
|
||||
|
||||
/// <summary>Submitted hidden constraints remain visible, and changed drafts are distinguished.</summary>
|
||||
[Fact]
|
||||
public void MoreFilters_SummaryRetainsSubmittedConstraintsAndClearOnlyResetsDraft()
|
||||
{
|
||||
var calls = 0;
|
||||
var cut = Render<AuditFilterBar>(p => p.Add(c => c.OnFilterChanged, _ => calls++));
|
||||
cut.Find("#audit-actor").Change("operator-a");
|
||||
cut.Find("#audit-target").Change("system-b");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.Equal(1, calls);
|
||||
Assert.Contains("Actor: operator-a", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
Assert.Contains("Target: system-b", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
Assert.Single(cut.FindAll("details[data-test=filter-more]"));
|
||||
Assert.Empty(cut.FindAll("[data-test=filter-pending]"));
|
||||
cut.Find("#audit-actor").Change("draft-only");
|
||||
Assert.Contains("Unsubmitted changes", cut.Find("[data-test=filter-pending]").TextContent);
|
||||
Assert.DoesNotContain("draft-only", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
cut.Find("[data-test=filter-clear]").Click();
|
||||
Assert.Equal(string.Empty, cut.Find("#audit-actor").GetAttribute("value"));
|
||||
Assert.Equal(string.Empty, cut.Find("#audit-target").GetAttribute("value"));
|
||||
Assert.Contains("Actor: operator-a", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
Assert.Equal(1, calls);
|
||||
}
|
||||
|
||||
/// <summary>A child selection change makes the parent draft visibly pending.</summary>
|
||||
[Fact]
|
||||
public void MoreFilters_NodeChangeUpdatesDraftCountAndPendingState()
|
||||
{
|
||||
var cut = Render<AuditFilterBar>();
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
cut.Find("[data-test=filter-node-ms-opt-central-a]").Change(true);
|
||||
Assert.Contains("Unsubmitted changes", cut.Find("[data-test=filter-pending]").TextContent);
|
||||
Assert.Contains("1 field set", cut.Find("details[data-test=filter-more] summary").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Clear removes invalid exact-id state as well as every advanced draft value.</summary>
|
||||
[Fact]
|
||||
public void Clear_RemovesAdvancedValuesAndValidationErrors()
|
||||
{
|
||||
var cut = Render<AuditFilterBar>();
|
||||
var fields = new[] { "audit-instance", "audit-script", "audit-target", "audit-actor", "audit-execution-id", "audit-parent-execution-id" };
|
||||
foreach (var id in fields) cut.Find("#" + id).Change("invalid-draft");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.Equal(2, cut.FindAll("[role=alert]").Count);
|
||||
cut.Find("[data-test=filter-clear]").Click();
|
||||
foreach (var id in fields) Assert.Equal(string.Empty, cut.Find("#" + id).GetAttribute("value"));
|
||||
Assert.Empty(cut.FindAll("[role=alert]"));
|
||||
Assert.Empty(cut.FindAll("input[aria-invalid=true]"));
|
||||
Assert.Contains("0 fields set", cut.Find("details[data-test=filter-more] summary").TextContent);
|
||||
}
|
||||
|
||||
/// <summary>Out-of-order callback completion cannot replace the most recently submitted draft.</summary>
|
||||
[Fact]
|
||||
public async Task Apply_OutOfOrderCallbacksRetainLatestSubmittedSummary()
|
||||
{
|
||||
var entered = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var pending = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var cut = Render<AuditFilterBar>(p => p.Add(c => c.OnFilterChanged, async filter =>
|
||||
{
|
||||
if (filter.Actor == "A") { entered.TrySetResult(true); await pending.Task; }
|
||||
}));
|
||||
cut.Find("#audit-actor").Change("A");
|
||||
var first = cut.Find("form[data-test=audit-filter-bar]").TriggerEventAsync("onsubmit", EventArgs.Empty);
|
||||
await entered.Task;
|
||||
cut.Find("#audit-actor").Change("B");
|
||||
await cut.Find("form[data-test=audit-filter-bar]").TriggerEventAsync("onsubmit", EventArgs.Empty);
|
||||
Assert.Contains("Actor: B", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
await cut.InvokeAsync(() => pending.TrySetResult(true));
|
||||
await first;
|
||||
Assert.Contains("Actor: B", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
Assert.DoesNotContain("Actor: A", cut.Find("[data-test=filter-submitted-summary]").TextContent);
|
||||
Assert.Empty(cut.FindAll("[data-test=filter-pending]"));
|
||||
}
|
||||
|
||||
/// <summary>UTC-labelled custom picks are identical in every process timezone.</summary>
|
||||
[Theory]
|
||||
[InlineData("2026-11-12T10:00", "2026-11-12T11:00")]
|
||||
@@ -302,24 +399,16 @@ public class AuditFilterBarTests : BunitContext
|
||||
Assert.Equal(executionId, captured!.ExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a blank or unparseable execution id leaves the filter execution id null.</summary>
|
||||
/// <summary>Whitespace remains an intentional unconstrained ExecutionId field.</summary>
|
||||
[Fact]
|
||||
public void Apply_WithBlankOrUnparseableExecutionId_LeavesFilterExecutionIdNull()
|
||||
public void Apply_WithBlankExecutionId_LeavesFilterExecutionIdNull()
|
||||
{
|
||||
// Lax parsing: a blank box yields no constraint; garbage text likewise.
|
||||
AuditLogQueryFilter? captured = null;
|
||||
var cut = Render<AuditFilterBar>(p => p
|
||||
.Add(c => c.OnFilterChanged, EventCallback.Factory.Create<AuditLogQueryFilter>(this, f => captured = f)));
|
||||
|
||||
// Blank — never typed into.
|
||||
var cut = Render<AuditFilterBar>(p => p.Add(c => c.OnFilterChanged, f => captured = f));
|
||||
cut.Find("#audit-execution-id").Change(" ");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.NotNull(captured);
|
||||
Assert.Null(captured!.ExecutionId);
|
||||
|
||||
// Unparseable paste — still dropped, no error.
|
||||
cut.Find("[data-test=\"filter-execution-id\"] input").Change("not-a-guid");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.Null(captured!.ExecutionId);
|
||||
Assert.Null(captured.ExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a pasted parent execution id is mapped through to the filter on apply.</summary>
|
||||
@@ -340,24 +429,16 @@ public class AuditFilterBarTests : BunitContext
|
||||
Assert.Equal(parentExecutionId, captured!.ParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a blank or unparseable parent execution id leaves the filter parent execution id null.</summary>
|
||||
/// <summary>Whitespace remains an intentional unconstrained ParentExecutionId field.</summary>
|
||||
[Fact]
|
||||
public void Apply_WithBlankOrUnparseableParentExecutionId_LeavesFilterParentExecutionIdNull()
|
||||
public void Apply_WithBlankParentExecutionId_LeavesFilterParentExecutionIdNull()
|
||||
{
|
||||
// Lax parsing: a blank box yields no constraint; garbage text likewise.
|
||||
AuditLogQueryFilter? captured = null;
|
||||
var cut = Render<AuditFilterBar>(p => p
|
||||
.Add(c => c.OnFilterChanged, EventCallback.Factory.Create<AuditLogQueryFilter>(this, f => captured = f)));
|
||||
|
||||
// Blank — never typed into.
|
||||
var cut = Render<AuditFilterBar>(p => p.Add(c => c.OnFilterChanged, f => captured = f));
|
||||
cut.Find("#audit-parent-execution-id").Change(" ");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.NotNull(captured);
|
||||
Assert.Null(captured!.ParentExecutionId);
|
||||
|
||||
// Unparseable paste — still dropped, no error.
|
||||
cut.Find("[data-test=\"filter-parent-execution-id\"] input").Change("not-a-guid");
|
||||
cut.Find("form[data-test=audit-filter-bar]").Submit();
|
||||
Assert.Null(captured!.ParentExecutionId);
|
||||
Assert.Null(captured.ParentExecutionId);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that choosing the last-hour time range sets the from-UTC value to approximately one hour ago.</summary>
|
||||
|
||||
Reference in New Issue
Block a user