using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
using ZB.MOM.WW.OtOpcUa.Host.OpcUa;
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.OpcUa;
///
/// The key source for a driver-only node (#520 stage 4) — the one with no ConfigDb, which until this
/// stage denied every UserName token.
///
public sealed class ArtifactApiKeyStoreTests
{
private static readonly DateTime Now = new(2026, 7, 31, 12, 0, 0, DateTimeKind.Utc);
///
/// No test here exercises a narrower-than-Cluster scope, so the resolved scope paths are empty.
///
private static readonly IReadOnlyDictionary> NoScopePaths =
new Dictionary>();
[Fact]
public async Task A_node_that_has_not_been_deployed_to_authenticates_nothing()
{
var sut = new ArtifactApiKeyStore(new DataPlaneAuthorityState());
(await sut.FindAsync("abc", Now, default)).ShouldBeNull();
}
[Fact]
public async Task A_deployed_key_resolves_with_its_principals_cluster_grants()
{
var state = new DataPlaneAuthorityState();
state.Apply("cluster-a", "rev-1",
[Key("abc", "scadabridge-dcl")],
[Acl("acl-1", "scadabridge-dcl", NodeAclScopeKind.Cluster, NodePermissions.WriteOperate)], NoScopePaths);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, default);
found.ShouldNotBeNull();
found!.Key.Principal.ShouldBe("scadabridge-dcl");
found.Grants.ShouldHaveSingleItem().PermissionFlags.ShouldBe(NodePermissions.WriteOperate);
}
///
/// The FULL grant set is returned, narrow scopes included (#520 stage 5). It used to narrow to
/// Cluster here, which was redundant — applies exactly
/// that filter itself — and became a trap once scope was enforced, because a caller reading
/// ApiKeyLookup.Grants as "this principal's grants" got a silently truncated set.
///
///
/// Cluster confinement still happens, just not here: ParseDataPlaneAuthority already kept
/// only this node's cluster's rows, which is where this store differs from
/// ConfigDbApiKeyStore (that one reads the fleet-wide database and confines it itself; see
/// ConfigDbApiKeyStoreTests).
///
[Fact]
public async Task Grants_narrower_than_cluster_are_returned_too()
{
var state = new DataPlaneAuthorityState();
state.Apply("cluster-a", "rev-1",
[Key("abc", "p")],
[
Acl("acl-cluster", "p", NodeAclScopeKind.Cluster, NodePermissions.Read),
Acl("acl-equipment", "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate),
], NoScopePaths);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, default);
found!.Grants.Select(g => g.NodeAclId).ShouldBe(["acl-cluster", "acl-equipment"], ignoreOrder: true);
}
///
/// Widening the store did not widen the ROLES. The narrow grant reaches the mapper and still
/// confers nothing, so a rule authored as "WriteOperate on one equipment" cannot become
/// server-wide write authority on a node with ACL enforcement switched off.
///
///
/// This assertion is the reason the widening is safe, and it lives here rather than only in the
/// mapper's own tests because the store is what changed: a test of the mapper alone would have
/// stayed green no matter what the store returned.
///
[Fact]
public async Task Widening_the_store_does_not_widen_the_interim_roles()
{
var state = new DataPlaneAuthorityState();
state.Apply("cluster-a", "rev-1",
[Key("abc", "p")],
[Acl("acl-equipment", "p", NodeAclScopeKind.Equipment, NodePermissions.WriteOperate)],
NoScopePaths);
var found = await new ArtifactApiKeyStore(state).FindAsync("abc", Now, default);
found!.Grants.ShouldHaveSingleItem();
ApiKeyInterimRoleMapper.Map(found.Grants, "p").ShouldBeEmpty();
}
[Fact]
public async Task A_revoked_key_is_denied_once_the_revoking_artifact_arrives()
{
// The whole revocation story for a driver-only node: it takes effect on the next deployment, and
// this is that deployment landing.
var state = new DataPlaneAuthorityState();
var sut = new ArtifactApiKeyStore(state);
state.Apply("cluster-a", "rev-1", [Key("abc", "p")], [], NoScopePaths);
(await sut.FindAsync("abc", Now, default)).ShouldNotBeNull();
var revoked = Key("abc", "p");
revoked.Enabled = false;
state.Apply("cluster-a", "rev-2", [revoked], [], NoScopePaths);
(await sut.FindAsync("abc", Now, default)).ShouldBeNull();
}
[Fact]
public async Task Anonymous_grants_resolve_without_a_key()
{
// The anonymous principal has grants but obviously no credential, so FindGrantsAsync is its path.
var state = new DataPlaneAuthorityState();
state.Apply("cluster-a", "rev-1", [],
[Acl("acl-anon", OpcUaPrincipals.Anonymous, NodeAclScopeKind.Cluster, NodePermissions.Read)],
NoScopePaths);
var grants = await new ArtifactApiKeyStore(state).FindGrantsAsync(OpcUaPrincipals.Anonymous, default);
grants.ShouldHaveSingleItem().PermissionFlags.ShouldBe(NodePermissions.Read);
}
private static ApiKey Key(string id, string principal) => new()
{
ApiKeyId = id,
Name = id,
SecretHash = "hash",
Principal = principal,
Enabled = true,
CreatedUtc = Now.AddDays(-1),
};
private static NodeAcl Acl(string id, string principal, NodeAclScopeKind scope, NodePermissions flags) => new()
{
NodeAclId = id,
ClusterId = "cluster-a",
Principal = principal,
ScopeKind = scope,
PermissionFlags = flags,
};
}