using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Services;
///
/// Inbound-API key re-arch (C3): unit tests for , which
/// inverts the API-method form's "Approved API Keys" selection into per-key method-scope edits.
/// Covers approve, revoke (preserving other scopes), the empty-last-scope guard, and the no-op case.
///
public sealed class ApiMethodKeyScopeReconcilerTests
{
private static IReadOnlyDictionary> Current(
params (string KeyId, string[] Methods)[] entries) =>
entries.ToDictionary(
e => e.KeyId,
e => (IReadOnlyList)e.Methods.ToList(),
StringComparer.Ordinal);
private static IReadOnlyDictionary Names(params (string KeyId, string Name)[] entries) =>
entries.ToDictionary(e => e.KeyId, e => e.Name, StringComparer.Ordinal);
[Fact]
public void Approve_AddsMethodToKey_PreservingExistingScopes()
{
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet { "k1" },
initialKeyIds: new HashSet(),
currentMethodsByKey: Current(("k1", new[] { "GetStatus" })),
keyNamesById: Names(("k1", "Key One")));
Assert.Empty(result.EmptyScopeKeyNames);
var update = Assert.Single(result.Updates);
Assert.Equal("k1", update.KeyId);
Assert.Equal(new[] { "GetStatus", "PlaceOrder" }, update.NewMethods);
}
[Fact]
public void Approve_KeyWithNoExistingScopes_GetsJustThisMethod()
{
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet { "k1" },
initialKeyIds: new HashSet(),
currentMethodsByKey: Current(("k1", Array.Empty())),
keyNamesById: Names(("k1", "Key One")));
var update = Assert.Single(result.Updates);
Assert.Equal(new[] { "PlaceOrder" }, update.NewMethods);
}
[Fact]
public void Revoke_RemovesMethod_LeavingOtherScopesIntact()
{
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet(),
initialKeyIds: new HashSet { "k1" },
currentMethodsByKey: Current(("k1", new[] { "PlaceOrder", "GetStatus" })),
keyNamesById: Names(("k1", "Key One")));
Assert.Empty(result.EmptyScopeKeyNames);
var update = Assert.Single(result.Updates);
Assert.Equal("k1", update.KeyId);
Assert.Equal(new[] { "GetStatus" }, update.NewMethods);
}
[Fact]
public void Revoke_LastScope_ReportedAsEmptyConflict_AndNotInUpdates()
{
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet(),
initialKeyIds: new HashSet { "k1" },
currentMethodsByKey: Current(("k1", new[] { "PlaceOrder" })),
keyNamesById: Names(("k1", "Key One")));
Assert.Empty(result.Updates);
var emptyName = Assert.Single(result.EmptyScopeKeyNames);
Assert.Equal("Key One", emptyName);
}
[Fact]
public void Mixed_ApproveOneRevokeAnother_ProducesBothUpdates()
{
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet { "k2" }, // approve k2
initialKeyIds: new HashSet { "k1" }, // revoke k1
currentMethodsByKey: Current(
("k1", new[] { "PlaceOrder", "GetStatus" }),
("k2", new[] { "Ping" })),
keyNamesById: Names(("k1", "Key One"), ("k2", "Key Two")));
Assert.Empty(result.EmptyScopeKeyNames);
Assert.Equal(2, result.Updates.Count);
var k1 = result.Updates.Single(u => u.KeyId == "k1");
Assert.Equal(new[] { "GetStatus" }, k1.NewMethods);
var k2 = result.Updates.Single(u => u.KeyId == "k2");
Assert.Equal(new[] { "Ping", "PlaceOrder" }, k2.NewMethods);
}
[Fact]
public void NoChange_ProducesNoUpdates()
{
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet { "k1" },
initialKeyIds: new HashSet { "k1" },
currentMethodsByKey: Current(("k1", new[] { "PlaceOrder" })),
keyNamesById: Names(("k1", "Key One")));
Assert.Empty(result.Updates);
Assert.Empty(result.EmptyScopeKeyNames);
}
///
/// Concurrent-edit guard: if selected == initial (no diff), the reconciler must produce
/// ZERO updates even when the live store shows different scopes on that key. The reconciler
/// only acts on keys that appear in the diff (added or removed relative to initialKeyIds)
/// — it must never touch keys that are not in the diff, regardless of what their current
/// live scopes look like.
///
[Fact]
public void NoDiff_ProducesNoUpdates_EvenWhenLiveScopesDiffer()
{
// k1 was approved at load time and is still approved — no diff.
// However, a concurrent edit changed k1's live scopes to include an extra method.
var result = ApiMethodKeyScopeReconciler.Reconcile(
methodName: "PlaceOrder",
selectedKeyIds: new HashSet { "k1" },
initialKeyIds: new HashSet { "k1" },
currentMethodsByKey: Current(("k1", new[] { "PlaceOrder", "OtherMethod" })),
keyNamesById: Names(("k1", "Key One")));
// k1 is not in the diff → reconciler must not touch it.
Assert.Empty(result.Updates);
Assert.Empty(result.EmptyScopeKeyNames);
}
}