feat(mesh-phase4): register ConfigDb only on admin-role nodes
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Per-cluster mesh Phase 4 — ConfigDb is registered iff the node has the <c>admin</c> role.
|
||||
/// A driver-only node (Akka role <c>driver</c>, NOT <c>admin</c>) boots with NO <c>ConfigDb</c>
|
||||
/// connection string at all; its configuration arrives from central via the Phase-3
|
||||
/// <c>ConfigSource:Mode=FetchAndCache</c> path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These tests replicate the exact ConfigDb gating decision Program.cs makes —
|
||||
/// <c>if (hasAdmin) services.AddOtOpcUaConfigDb(configuration)</c> — over the real
|
||||
/// <see cref="ServiceCollectionExtensions.AddOtOpcUaConfigDb"/> method, for both role shapes.
|
||||
/// They deliberately do NOT boot the full host: Program.cs reads roles from the
|
||||
/// <c>OTOPCUA_ROLES</c> process env var and starting the host triggers the Akka cluster join
|
||||
/// (see <c>TwoNodeClusterHarness</c> on why <c>WebApplicationFactory<Program></c> is not
|
||||
/// driven here, and <c>LocalDbWiringTests</c> for the same partial-graph harness model). The
|
||||
/// full driver-only boot is proven by the Phase-4 live gate + the later DI-graph sweep task.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class DriverOnlyNoConfigDbBootTests : IDisposable
|
||||
{
|
||||
private readonly List<WebApplication> _apps = [];
|
||||
|
||||
/// <summary>
|
||||
/// Builds the shared-services graph the way Program.cs gates ConfigDb: it is registered iff
|
||||
/// <paramref name="hasAdmin"/>. A driver-only node passes <c>hasAdmin: false</c> and supplies
|
||||
/// no <c>ConnectionStrings:ConfigDb</c> at all.
|
||||
/// </summary>
|
||||
private IServiceProvider BuildSharedServicesGraph(bool hasAdmin, string? configDbConnectionString)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [] });
|
||||
if (configDbConnectionString is not null)
|
||||
{
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:ConfigDb"] = configDbConnectionString,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasAdmin)
|
||||
builder.Services.AddOtOpcUaConfigDb(builder.Configuration);
|
||||
|
||||
var app = builder.Build();
|
||||
_apps.Add(app);
|
||||
return app.Services;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriverOnlyNode_NoConnectionString_BuildsWithoutThrowing_AndRegistersNoConfigDbFactory()
|
||||
{
|
||||
// The core Phase-4 guarantee: a driver-only node holds no ConfigDb connection string and must
|
||||
// still build its service graph without throwing — nothing in the shared registrations may
|
||||
// reach for the ConfigDb factory.
|
||||
IServiceProvider sp = null!;
|
||||
Should.NotThrow(() => sp = BuildSharedServicesGraph(hasAdmin: false, configDbConnectionString: null));
|
||||
|
||||
sp.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>().ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdminNode_WithConnectionString_RegistersTheConfigDbFactory()
|
||||
{
|
||||
// The fused/admin path is byte-for-byte unaffected: ConfigDb is registered exactly as before.
|
||||
var sp = BuildSharedServicesGraph(
|
||||
hasAdmin: true,
|
||||
configDbConnectionString: "Server=(local);Database=OtOpcUa;Trusted_Connection=True;TrustServerCertificate=True");
|
||||
|
||||
sp.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>().ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddOtOpcUaConfigDb_WithoutConnectionString_Throws()
|
||||
{
|
||||
// Why the gate exists: the unconditional call Program.cs made throws on a driver-only node,
|
||||
// which has no ConfigDb connection string. This pins the exact failure the hasAdmin gate avoids.
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [] });
|
||||
|
||||
Should.Throw<InvalidOperationException>(() => builder.Services.AddOtOpcUaConfigDb(builder.Configuration));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var app in _apps)
|
||||
((IDisposable)app).Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user