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); } }