merge: small fixes (register rows 28, 35, 57) — confirm dialogs name their own action (9 mislabeled 'Delete' buttons fixed incl. secured-write Approve/Reject), DeploymentRecords composite (DeployedAt DESC, Id DESC) index, audit-config query CLI table projection
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the compact <c>audit-config query</c> table projection (deferred-work
|
||||
/// register row 57). The command passed no <c>tableProjector</c>, so the generic
|
||||
/// renderer saw an <see cref="AuditLogQueryResult"/> OBJECT, took its Property/Value
|
||||
/// branch, and printed the entire <c>entries</c> array as raw JSON inside one cell.
|
||||
/// The projection unwraps the envelope to a row array, drops the per-entry
|
||||
/// <c>afterStateJson</c> snapshot, and keeps <c>totalCount</c> visible as a trailing note.
|
||||
/// </summary>
|
||||
public class AuditConfigTableProjectionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// The real server shape, serialised from the REAL <see cref="AuditLogQueryResult"/>
|
||||
/// / <see cref="AuditLogEntry"/> types rather than hand-written JSON, so renaming one
|
||||
/// of their properties fails this test instead of silently blanking a column.
|
||||
/// </summary>
|
||||
private static string RealPayload(int totalCount) =>
|
||||
JsonSerializer.Serialize(
|
||||
new AuditLogQueryResult(
|
||||
new[]
|
||||
{
|
||||
new AuditLogEntry("multi-role", "Created", "Template", "7", "PumpTemplate")
|
||||
{
|
||||
Id = 412,
|
||||
AfterStateJson = "{\"id\":7,\"name\":\"PumpTemplate\",\"attributes\":[]}",
|
||||
Timestamp = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero),
|
||||
},
|
||||
new AuditLogEntry("designer", "Deleted", "ExternalSystem", "3", "MES")
|
||||
{
|
||||
Id = 413,
|
||||
AfterStateJson = null,
|
||||
Timestamp = new DateTimeOffset(2026, 8, 15, 12, 5, 0, TimeSpan.Zero),
|
||||
},
|
||||
},
|
||||
totalCount),
|
||||
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_UnwrapsTheEnvelopeIntoRows()
|
||||
{
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 2));
|
||||
|
||||
using var doc = JsonDocument.Parse(compact);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// An ARRAY is the whole point — the object envelope is what made the renderer
|
||||
// print one Property/Value row with the entries blob in a single cell.
|
||||
Assert.Equal(JsonValueKind.Array, root.ValueKind);
|
||||
Assert.Equal(2, root.GetArrayLength());
|
||||
|
||||
var first = root[0];
|
||||
Assert.Equal(412, first.GetProperty("id").GetInt32());
|
||||
Assert.Equal("multi-role", first.GetProperty("user").GetString());
|
||||
Assert.Equal("Created", first.GetProperty("action").GetString());
|
||||
Assert.Equal("Template", first.GetProperty("entityType").GetString());
|
||||
Assert.Equal("PumpTemplate", first.GetProperty("entityName").GetString());
|
||||
Assert.Equal("7", first.GetProperty("entityId").GetString());
|
||||
Assert.Contains("2026-08-15", first.GetProperty("timestamp").GetString());
|
||||
|
||||
var second = root[1];
|
||||
Assert.Equal("Deleted", second.GetProperty("action").GetString());
|
||||
Assert.Equal("MES", second.GetProperty("entityName").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_DropsAfterStateJson()
|
||||
{
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 2));
|
||||
|
||||
using var doc = JsonDocument.Parse(compact);
|
||||
// The fat field must NOT survive — one entity snapshot per row fills the terminal,
|
||||
// which is the audit analogue of template's child arrays.
|
||||
Assert.False(doc.RootElement[0].TryGetProperty("afterStateJson", out _));
|
||||
Assert.DoesNotContain("afterStateJson", compact, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_TruncatedPage_AppendsTotalCountNote()
|
||||
{
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 137));
|
||||
|
||||
using var doc = JsonDocument.Parse(compact);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Two entries plus the note row.
|
||||
Assert.Equal(3, root.GetArrayLength());
|
||||
var note = root[2].GetProperty("note").GetString();
|
||||
Assert.Contains("2 of 137", note);
|
||||
// The note must not masquerade as an entry.
|
||||
Assert.False(root[2].TryGetProperty("id", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_WholeResultSet_EmitsNoNoteColumn()
|
||||
{
|
||||
// When the page IS the result set the count adds nothing the operator cannot
|
||||
// already see, so the column must not appear and widen every small query.
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 2));
|
||||
|
||||
using var doc = JsonDocument.Parse(compact);
|
||||
Assert.Equal(2, doc.RootElement.GetArrayLength());
|
||||
Assert.DoesNotContain("note", compact, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_EmptyResult_StaysAnEmptyArray()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(
|
||||
new AuditLogQueryResult(Array.Empty<AuditLogEntry>(), 0),
|
||||
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(json);
|
||||
|
||||
using var doc = JsonDocument.Parse(compact);
|
||||
// The renderer prints "(no results)" for an empty array; an empty OBJECT would
|
||||
// have printed an empty Property/Value table instead.
|
||||
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
|
||||
Assert.Equal(0, doc.RootElement.GetArrayLength());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_BareArray_IsStillProjected()
|
||||
{
|
||||
// Tolerated so the projection does not regress if the payload is ever unwrapped
|
||||
// upstream, or when the deprecated `audit-log` alias hits an older server.
|
||||
const string bare = """
|
||||
[ { "id": 1, "user": "u", "action": "Updated", "entityType": "Site",
|
||||
"entityName": "SiteA", "entityId": "2", "timestamp": "2026-08-15T12:00:00+00:00",
|
||||
"afterStateJson": "{\"big\":true}" } ]
|
||||
""";
|
||||
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(bare);
|
||||
|
||||
using var doc = JsonDocument.Parse(compact);
|
||||
Assert.Equal(1, doc.RootElement.GetArrayLength());
|
||||
Assert.Equal("SiteA", doc.RootElement[0].GetProperty("entityName").GetString());
|
||||
Assert.False(doc.RootElement[0].TryGetProperty("afterStateJson", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_UnrecognisedObject_ReturnedVerbatim()
|
||||
{
|
||||
// e.g. an error envelope — leave it to the renderer's own fallbacks rather than
|
||||
// projecting it into an empty table that hides the message.
|
||||
const string other = """{ "error": "boom" }""";
|
||||
Assert.Equal(other, AuditConfigTableProjection.ProjectEntries(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_NonJson_ReturnedVerbatim()
|
||||
{
|
||||
const string notJson = "<html>proxy error</html>";
|
||||
Assert.Equal(notJson, AuditConfigTableProjection.ProjectEntries(notJson));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectEntries_IsSubstantiallySmallerThanTheRawPayload()
|
||||
{
|
||||
// The reported symptom was the whole entries array in one cell. A page of rows
|
||||
// carrying real afterStateJson snapshots must collapse hard.
|
||||
var entries = Enumerable.Range(0, 50).Select(i =>
|
||||
new AuditLogEntry("designer", "Updated", "Template", i.ToString(), $"Template{i}")
|
||||
{
|
||||
Id = i,
|
||||
AfterStateJson = new string('x', 2000),
|
||||
Timestamp = DateTimeOffset.UnixEpoch.AddMinutes(i),
|
||||
}).ToArray();
|
||||
var json = JsonSerializer.Serialize(new AuditLogQueryResult(entries, entries.Length),
|
||||
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
|
||||
var compact = AuditConfigTableProjection.ProjectEntries(json);
|
||||
|
||||
Assert.True(compact.Length * 4 < json.Length,
|
||||
$"Expected compact ({compact.Length}) to be far smaller than raw ({json.Length}).");
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public class SecuredWritesTests : BunitContext
|
||||
Services.AddSingleton(_browse);
|
||||
|
||||
// Confirm dialogs resolve to "yes" without a DialogHost in the render scope.
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>())
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>())
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Empty list by default; individual tests seed the queue/history.
|
||||
@@ -303,7 +303,8 @@ public class SecuredWritesTests : BunitContext
|
||||
_dialog.Received(1).ConfirmAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<string>(m => Regex.IsMatch(m, "Submitted .* ago")),
|
||||
Arg.Any<bool>());
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<string?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -210,7 +210,7 @@ public class SchemaLibraryPageTests : BunitContext
|
||||
/// <summary>A dialog service that auto-confirms, so the delete path runs end-to-end.</summary>
|
||||
private sealed class AlwaysConfirmDialogService : IDialogService
|
||||
{
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false, string? confirmLabel = null)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<string?> PromptAsync(
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.Security.Claims;
|
||||
using Bunit;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NSubstitute;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Health;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
|
||||
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Monitoring;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the verb on the "Trigger failover" confirmation button (deferred-work
|
||||
/// register row 28). The dialog copy was always right, but the red confirm button
|
||||
/// read <em>"Delete"</em>: <c>DialogHost</c> assumed every <c>danger: true</c>
|
||||
/// confirm was a delete, and no caller could say otherwise. These tests run the
|
||||
/// real <see cref="DialogService"/> behind a real <c>DialogHost</c> — not a
|
||||
/// substitute — so they cover both halves of the fix: the call site passing
|
||||
/// <c>confirmLabel</c>, and the host preferring it over its own default. A
|
||||
/// substitute-based test would pin the argument but not what the user reads.
|
||||
///
|
||||
/// bUnit cannot run real JS interop, so the host's <c>sbDialog.*</c> focus-trap
|
||||
/// calls are exercised in <see cref="JSRuntimeMode.Loose"/> mode as no-ops.
|
||||
/// </summary>
|
||||
public class FailoverConfirmLabelTests : BunitContext
|
||||
{
|
||||
private readonly IManualFailoverService _failover = Substitute.For<IManualFailoverService>();
|
||||
private readonly DialogService _dialog = new();
|
||||
|
||||
public FailoverConfirmLabelTests()
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtTokenService.UsernameClaimType, "tester"),
|
||||
new Claim(JwtTokenService.RoleClaimType, "Administrator")
|
||||
};
|
||||
var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
|
||||
Services.AddSingleton<AuthenticationStateProvider>(new TestAuthStateProvider(user));
|
||||
Services.AddAuthorizationCore();
|
||||
AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
|
||||
// BunitContext pre-registers a placeholder IAuthorizationService that throws when
|
||||
// AuthorizeView evaluates a policy; force the real one so the admin gate is real.
|
||||
Services.AddSingleton<IAuthorizationService, DefaultAuthorizationService>();
|
||||
Services.AddSingleton(_failover);
|
||||
// The concrete service registered as the interface, so the control and the
|
||||
// host share one instance (DialogHost injects IDialogService and casts).
|
||||
Services.AddSingleton<IDialogService>(_dialog);
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the failover control and a live <c>DialogHost</c> side by side under
|
||||
/// the cascading auth state <c>AuthorizeView</c> needs, mirroring how
|
||||
/// <c>MainLayout</c> hosts the single shared dialog.
|
||||
/// </summary>
|
||||
private IRenderedComponent<CascadingAuthenticationState> RenderControlWithHost(string? siteId)
|
||||
{
|
||||
return Render<CascadingAuthenticationState>(parameters => parameters
|
||||
.Add(p => p.ChildContent, (RenderFragment)(builder =>
|
||||
{
|
||||
builder.OpenComponent<CentralFailoverControl>(0);
|
||||
builder.AddAttribute(1, nameof(CentralFailoverControl.OnlineCentralNodeCount), 2);
|
||||
if (siteId is not null)
|
||||
{
|
||||
builder.AddAttribute(2, nameof(CentralFailoverControl.SiteId), siteId);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
builder.OpenComponent<DialogHost>(3);
|
||||
builder.CloseComponent();
|
||||
})));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the failover dialog and returns the footer buttons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The click is deliberately NOT awaited. With a real <see cref="DialogService"/>
|
||||
/// the control's handler parks on <c>await ConfirmAsync(...)</c> until someone
|
||||
/// answers the dialog, so awaiting the click would deadlock the test. The dialog
|
||||
/// state is published synchronously before <c>ConfirmAsync</c> returns its task, so
|
||||
/// the modal is rendered by the time the click dispatch returns.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<AngleSharp.Dom.IElement> OpenFailoverDialog(string? siteId)
|
||||
{
|
||||
var host = RenderControlWithHost(siteId);
|
||||
var trigger = host.FindAll("button")
|
||||
.First(b => b.TextContent.Contains("failover", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_ = trigger.ClickAsync(new());
|
||||
host.WaitForState(() => host.FindAll("div.modal-footer button").Count > 0);
|
||||
|
||||
return host.FindAll("div.modal-footer button").ToList();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)] // central pair
|
||||
[InlineData("SiteA")] // site pair
|
||||
public void Failover_confirm_button_says_fail_over_not_delete(string? siteId)
|
||||
{
|
||||
var footerButtons = OpenFailoverDialog(siteId).Select(b => b.TextContent.Trim()).ToList();
|
||||
|
||||
Assert.Contains("Fail over", footerButtons);
|
||||
// The whole point of the row: the destructive verb must not be "Delete".
|
||||
Assert.DoesNotContain("Delete", footerButtons);
|
||||
// Cancel is still the way out — the fix must not have eaten the footer.
|
||||
Assert.Contains("Cancel", footerButtons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Failover_confirm_button_keeps_destructive_styling()
|
||||
{
|
||||
// The label and the styling are separate concerns now (danger = red,
|
||||
// confirmLabel = the verb). Relabelling must not have downgraded the
|
||||
// button to a benign primary one.
|
||||
var confirm = OpenFailoverDialog(siteId: null).First(b => b.TextContent.Trim() == "Fail over");
|
||||
|
||||
Assert.Contains("btn-danger", confirm.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_unlabelled_danger_confirm_still_falls_back_to_delete()
|
||||
{
|
||||
// The fallback is retained deliberately: ~20 genuine delete call sites rely
|
||||
// on it (and the Playwright suites click the literal "Delete"). This pins that
|
||||
// the row-28 fix is additive and did not change the default.
|
||||
var cut = Render<DialogHost>();
|
||||
|
||||
await cut.InvokeAsync(() => { _ = _dialog.ConfirmAsync("Delete thing", "Sure?", danger: true); });
|
||||
cut.Render();
|
||||
|
||||
var footerButtons = cut.FindAll("div.modal-footer button").Select(b => b.TextContent.Trim()).ToList();
|
||||
Assert.Contains("Delete", footerButtons);
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -99,7 +99,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
public async Task Confirming_triggers_the_failover_exactly_once()
|
||||
{
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
_failover.FailOverCentralAsync(Arg.Any<string>())
|
||||
.Returns(Task.FromResult<string?>("akka.tcp://scadabridge@central-a:8081"));
|
||||
|
||||
@@ -113,7 +113,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
public async Task Cancelling_the_confirmation_does_not_trigger_a_failover()
|
||||
{
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(false);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(false);
|
||||
|
||||
var cut = Render(onlineNodes: 2);
|
||||
await cut.Find("button").ClickAsync(new());
|
||||
@@ -128,7 +128,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
// the admin's own circuit drops. If the dialog does not say so, a working failover
|
||||
// looks like a crash they caused.
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
|
||||
var cut = Render(onlineNodes: 2);
|
||||
await cut.Find("button").ClickAsync(new());
|
||||
@@ -136,7 +136,8 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
await _dialog.Received(1).ConfirmAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<string>(m => m.Contains("reconnect", StringComparison.OrdinalIgnoreCase)),
|
||||
Arg.Any<bool>());
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<string?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -145,7 +146,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
// The service returns null when the peer guard refuses. The UI must not report a
|
||||
// failover that never happened.
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
_failover.FailOverCentralAsync(Arg.Any<string>()).Returns(Task.FromResult<string?>(null));
|
||||
|
||||
var cut = Render(onlineNodes: 2);
|
||||
@@ -177,7 +178,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
public async Task Site_card_confirming_triggers_the_site_failover_path()
|
||||
{
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
_failover.FailOverSiteAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(Task.FromResult(new SiteFailoverOutcome(true, "akka.tcp://scadabridge@site-a-a:8082", null)));
|
||||
|
||||
@@ -194,7 +195,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
// Central failover drops the admin's circuit; a SITE failover does not, and saying
|
||||
// otherwise would train operators to distrust the warning that does matter.
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
_failover.FailOverSiteAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(Task.FromResult(new SiteFailoverOutcome(true, "addr", null)));
|
||||
|
||||
@@ -204,14 +205,15 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
await _dialog.Received(1).ConfirmAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<string>(m => !m.Contains("reconnect", StringComparison.OrdinalIgnoreCase)),
|
||||
Arg.Any<bool>());
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<string?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Site_card_surfaces_a_refusal_from_the_site()
|
||||
{
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
_failover.FailOverSiteAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(Task.FromResult(new SiteFailoverOutcome(
|
||||
false, null, "No standby available — failing over a lone node would be an outage.")));
|
||||
@@ -228,7 +230,7 @@ public class HealthFailoverButtonTests : BunitContext
|
||||
// A timeout is not a "no" from the site — the operator must be able to tell the
|
||||
// difference, because the failover may or may not have taken effect.
|
||||
Arrange("Administrator");
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>()).Returns(true);
|
||||
_dialog.ConfirmAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<string?>()).Returns(true);
|
||||
_failover.FailOverSiteAsync(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(Task.FromResult(new SiteFailoverOutcome(
|
||||
false, null, "Site did not respond: Ask timed out")));
|
||||
|
||||
@@ -191,7 +191,7 @@ public class NotificationListsPageTests : BunitContext
|
||||
/// <summary>A dialog service that auto-confirms, so action paths run end-to-end.</summary>
|
||||
private sealed class AlwaysConfirmDialogService : IDialogService
|
||||
{
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false, string? confirmLabel = null)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<string?> PromptAsync(
|
||||
|
||||
+1
-1
@@ -336,7 +336,7 @@ public class NotificationReportDetailModalTests : BunitContext
|
||||
|
||||
private sealed class AlwaysConfirmDialogService : IDialogService
|
||||
{
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false, string? confirmLabel = null)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<string?> PromptAsync(
|
||||
|
||||
@@ -271,7 +271,7 @@ public class NotificationReportPageTests : BunitContext
|
||||
/// <summary>A dialog service that auto-confirms, so action paths run end-to-end.</summary>
|
||||
private sealed class AlwaysConfirmDialogService : IDialogService
|
||||
{
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false, string? confirmLabel = null)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<string?> PromptAsync(
|
||||
|
||||
@@ -211,7 +211,7 @@ public sealed class QueryStringDrillInTests
|
||||
|
||||
private sealed class AlwaysConfirmDialogService : IDialogService
|
||||
{
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false, string? confirmLabel = null)
|
||||
=> Task.FromResult(true);
|
||||
public Task<string?> PromptAsync(string title, string label, string initialValue = "", string? placeholder = null)
|
||||
=> Task.FromResult<string?>(null);
|
||||
|
||||
@@ -679,7 +679,7 @@ public class SiteCallsReportPageTests : BunitContext
|
||||
/// <summary>A dialog service that auto-confirms, so action paths run end-to-end.</summary>
|
||||
private sealed class AlwaysConfirmDialogService : IDialogService
|
||||
{
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
||||
public Task<bool> ConfirmAsync(string title, string message, bool danger = false, string? confirmLabel = null)
|
||||
=> Task.FromResult(true);
|
||||
|
||||
public Task<string?> PromptAsync(
|
||||
|
||||
Reference in New Issue
Block a user