using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Core.Authorization;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security;
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.OpcUa;
///
/// Guards the role-conditional data-plane authentication wiring (arch-review 03/S18). A decorator
/// registered in one branch of a role conditional is the "built but never wired" shape, so the branch
/// each node role lands in is asserted rather than assumed.
///
public sealed class ApiKeyStoreRegistrationTests
{
///
/// The authorizer's clock and the registered clock must be the SAME instance.
///
///
/// already documents that a divergence between two clocks denies
/// every request with a verdict indistinguishable from "no grant matched" — found once by a test
/// clock two hours ahead. One registered singleton threaded into every consumer makes the
/// divergence unrepresentable instead of merely unlikely, so the registration is what is pinned.
///
[Fact]
public void The_authorizer_and_the_key_store_share_one_TimeProvider()
{
using var sp = BuildNode(hasAdmin: true, hasDriver: true);
var registered = sp.GetRequiredService();
var authorizer = sp.GetRequiredService();
registered.ShouldBeSameAs(Probe.Instance);
authorizer.Clock.ShouldBeSameAs(registered);
}
/// Builds the data-plane authentication wiring for one node role combination.
/// Whether the node carries the admin role.
/// Whether the node carries the driver role.
/// The built provider; the caller disposes it.
internal static ServiceProvider BuildNode(bool hasAdmin, bool hasDriver)
{
var services = new ServiceCollection();
services.AddLogging(b => b.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton(Probe.Instance);
services.Configure(_ => { });
services.Configure(_ => { });
services.Configure(o => o.Roles = ["admin", "driver", "cluster-MAIN"]);
services.AddSingleton(EmptyDataPlaneAuthoritySource.Instance);
services.AddSingleton();
services.AddSingleton();
if (hasAdmin)
{
services.AddDbContextFactory(
o => o.UseInMemoryDatabase($"apikey-reg-{Guid.NewGuid():N}"));
}
services.AddOtOpcUaDataPlaneAuth(hasAdmin, hasDriver);
return services.BuildServiceProvider();
}
/// A clock distinguishable from by reference.
private sealed class Probe : TimeProvider
{
public static readonly Probe Instance = new();
///
public override DateTimeOffset GetUtcNow() => new(2026, 8, 20, 12, 0, 0, TimeSpan.Zero);
}
}