513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
290 lines
14 KiB
C#
290 lines
14 KiB
C#
using Akka.Actor;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
|
using ZB.MOM.WW.ScadaBridge.Host;
|
|
using ZB.MOM.WW.ScadaBridge.Host.Actors;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
|
|
|
/// <summary>
|
|
/// Verifies that all expected Central-role actors are created at the correct paths
|
|
/// when AkkaHostedService starts.
|
|
/// </summary>
|
|
[Collection(HostBootCollection.Name)]
|
|
public class CentralActorPathTests : IAsyncLifetime
|
|
{
|
|
private WebApplicationFactory<Program>? _factory;
|
|
private ActorSystem? _actorSystem;
|
|
private string? _previousEnv;
|
|
private CentralDbTestEnvironment? _dbEnv;
|
|
|
|
/// <summary>Initializes the fixture before the tests run.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public Task InitializeAsync()
|
|
{
|
|
_previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
|
|
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central");
|
|
|
|
// Supply the pepper, MachineDataDb and the G-4 secret-backed keys
|
|
// (ConfigurationDb / LDAP service-account password / JWT signing key) as
|
|
// whole-key env overrides so the pre-host config builder — which calls
|
|
// AddEnvironmentVariables() before the ${secret:...} expander runs — sees
|
|
// concrete values and the Central-role StartupValidator preflight passes.
|
|
_dbEnv = new CentralDbTestEnvironment();
|
|
|
|
_factory = new WebApplicationFactory<Program>()
|
|
.WithWebHostBuilder(builder =>
|
|
{
|
|
builder.ConfigureAppConfiguration((_, config) =>
|
|
{
|
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaBridge:Node:NodeName"] = "central-a",
|
|
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
|
["ScadaBridge:Node:RemotingPort"] = "0",
|
|
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:25510",
|
|
["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:25520",
|
|
["ScadaBridge:Cluster:MinNrOfMembers"] = "1",
|
|
["ScadaBridge:Database:SkipMigrations"] = "true",
|
|
["ScadaBridge:Security:JwtSigningKey"] = "test-signing-key-must-be-at-least-32-chars-long!",
|
|
// LDAP settings nest under Security:Ldap (shared LdapOptions).
|
|
// ServiceAccountDn is now required by the library's LdapOptionsValidator
|
|
// (ValidateOnStart), so it must be present for the host to start.
|
|
["ScadaBridge:Security:Ldap:Server"] = "localhost",
|
|
["ScadaBridge:Security:Ldap:Port"] = "3893",
|
|
["ScadaBridge:Security:Ldap:Transport"] = "None",
|
|
["ScadaBridge:Security:Ldap:AllowInsecure"] = "true",
|
|
["ScadaBridge:Security:Ldap:SearchBase"] = "dc=zb,dc=local",
|
|
["ScadaBridge:Security:Ldap:ServiceAccountDn"] = "cn=admin,dc=zb,dc=local",
|
|
});
|
|
});
|
|
builder.UseSetting("ScadaBridge:Node:Role", "Central");
|
|
builder.UseSetting("ScadaBridge:Database:SkipMigrations", "true");
|
|
builder.ConfigureServices(services =>
|
|
{
|
|
// Replace SQL Server with in-memory database
|
|
var descriptorsToRemove = services
|
|
.Where(d =>
|
|
d.ServiceType == typeof(DbContextOptions<ScadaBridgeDbContext>) ||
|
|
d.ServiceType == typeof(DbContextOptions) ||
|
|
d.ServiceType == typeof(ScadaBridgeDbContext) ||
|
|
d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true)
|
|
.ToList();
|
|
foreach (var d in descriptorsToRemove)
|
|
services.Remove(d);
|
|
|
|
services.AddDbContext<ScadaBridgeDbContext>(options =>
|
|
options.UseInMemoryDatabase($"ActorPathTests_{Guid.NewGuid()}"));
|
|
});
|
|
});
|
|
|
|
// CreateClient triggers host startup including AkkaHostedService
|
|
_ = _factory.CreateClient();
|
|
|
|
var akkaService = _factory.Services.GetRequiredService<AkkaHostedService>();
|
|
_actorSystem = akkaService.ActorSystem;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>Releases the fixture after the tests run.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async Task DisposeAsync()
|
|
{
|
|
// try/finally: a throwing host teardown must not strand the process-wide
|
|
// environment variables. Without it, one failed _factory.Dispose() leaks
|
|
// DOTNET_ENVIRONMENT=Central and the CentralDbTestEnvironment vars into
|
|
// every later test in the run.
|
|
try
|
|
{
|
|
_factory?.Dispose();
|
|
}
|
|
finally
|
|
{
|
|
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv);
|
|
_dbEnv?.Dispose();
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>Verifies that the dead letter monitor actor exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_DeadLetterMonitor_Exists()
|
|
=> await AssertActorExists("/user/dead-letter-monitor");
|
|
|
|
/// <summary>Verifies that the central communication actor exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_CentralCommunication_Exists()
|
|
=> await AssertActorExists("/user/central-communication");
|
|
|
|
/// <summary>Verifies that the management actor exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_Management_Exists()
|
|
=> await AssertActorExists("/user/management");
|
|
|
|
/// <summary>Verifies that the notification outbox singleton exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_NotificationOutboxSingleton_Exists()
|
|
=> await AssertActorExists("/user/notification-outbox-singleton");
|
|
|
|
/// <summary>Verifies that the notification outbox proxy exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_NotificationOutboxProxy_Exists()
|
|
=> await AssertActorExists("/user/notification-outbox-proxy");
|
|
|
|
/// <summary>Verifies that the audit log purge singleton exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_AuditLogPurgeSingleton_Exists()
|
|
=> await AssertActorExists("/user/audit-log-purge-singleton");
|
|
|
|
/// <summary>Verifies that the audit log purge proxy exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_AuditLogPurgeProxy_Exists()
|
|
=> await AssertActorExists("/user/audit-log-purge-proxy");
|
|
|
|
/// <summary>Verifies that the site audit reconciliation singleton exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_SiteAuditReconciliationSingleton_Exists()
|
|
=> await AssertActorExists("/user/site-audit-reconciliation-singleton");
|
|
|
|
/// <summary>Verifies that the site audit reconciliation proxy exists on a central node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task CentralActors_SiteAuditReconciliationProxy_Exists()
|
|
=> await AssertActorExists("/user/site-audit-reconciliation-proxy");
|
|
|
|
private async Task AssertActorExists(string path)
|
|
{
|
|
Assert.NotNull(_actorSystem);
|
|
var selection = _actorSystem!.ActorSelection(path);
|
|
var identity = await selection.Ask<ActorIdentity>(
|
|
new Identify(path), TimeSpan.FromSeconds(5));
|
|
Assert.NotNull(identity.Subject);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that all expected Site-role actors are created at the correct paths
|
|
/// when AkkaHostedService starts.
|
|
/// </summary>
|
|
[Collection(HostBootCollection.Name)]
|
|
public class SiteActorPathTests : IAsyncLifetime
|
|
{
|
|
private IHost? _host;
|
|
private ActorSystem? _actorSystem;
|
|
private string _tempDbPath = null!;
|
|
private string _tempLocalDbPath = null!;
|
|
|
|
/// <summary>Initializes the fixture before the tests run.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async Task InitializeAsync()
|
|
{
|
|
_tempDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_actor_test_{Guid.NewGuid()}.db");
|
|
_tempLocalDbPath = Path.Combine(Path.GetTempPath(), $"scadabridge_actor_localdb_{Guid.NewGuid()}.db");
|
|
|
|
var builder = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder();
|
|
builder.ConfigureAppConfiguration(config =>
|
|
{
|
|
config.Sources.Clear();
|
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaBridge:Node:Role"] = "Site",
|
|
["ScadaBridge:Node:NodeName"] = "node-a",
|
|
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
|
["ScadaBridge:Node:SiteId"] = "TestSite",
|
|
["ScadaBridge:Node:RemotingPort"] = "0",
|
|
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:25510",
|
|
["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:25520",
|
|
["ScadaBridge:Cluster:MinNrOfMembers"] = "1",
|
|
["ScadaBridge:Database:SiteDbPath"] = _tempDbPath,
|
|
// Required: LocalDbOptions.Path is validated with ValidateOnStart, and this
|
|
// fixture actually starts the host — a site node with no configured
|
|
// consolidated-database path fails fast rather than booting half-working.
|
|
["LocalDb:Path"] = _tempLocalDbPath,
|
|
// A central gRPC endpoint so the Site branch builds its GrpcCentralTransport
|
|
// (the only site→central transport since Phase 4). Never dialled by this test.
|
|
["ScadaBridge:Communication:CentralGrpcEndpoints:0"] = "http://localhost:8083",
|
|
});
|
|
});
|
|
builder.ConfigureServices((context, services) =>
|
|
{
|
|
SiteServiceRegistration.Configure(services, context.Configuration);
|
|
});
|
|
|
|
_host = builder.Build();
|
|
await _host.StartAsync();
|
|
|
|
var akkaService = _host.Services.GetRequiredService<AkkaHostedService>();
|
|
_actorSystem = akkaService.ActorSystem;
|
|
}
|
|
|
|
/// <summary>Releases the fixture after the tests run.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async Task DisposeAsync()
|
|
{
|
|
if (_host != null)
|
|
{
|
|
await _host.StopAsync();
|
|
_host.Dispose();
|
|
}
|
|
try { File.Delete(_tempDbPath); } catch { /* best effort */ }
|
|
try { File.Delete(_tempLocalDbPath); } catch { /* best effort */ }
|
|
}
|
|
|
|
/// <summary>Verifies that the dead letter monitor actor exists on a site node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SiteActors_DeadLetterMonitor_Exists()
|
|
=> await AssertActorExists("/user/dead-letter-monitor");
|
|
|
|
/// <summary>Verifies that the data connection layer manager actor exists on a site node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SiteActors_DclManager_Exists()
|
|
=> await AssertActorExists("/user/dcl-manager");
|
|
|
|
/// <summary>Verifies that the deployment manager singleton exists on a site node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SiteActors_DeploymentManagerSingleton_Exists()
|
|
=> await AssertActorExists("/user/deployment-manager-singleton");
|
|
|
|
/// <summary>Verifies that the deployment manager proxy exists on a site node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SiteActors_DeploymentManagerProxy_Exists()
|
|
=> await AssertActorExists("/user/deployment-manager-proxy");
|
|
|
|
/// <summary>Verifies that the site communication actor exists on a site node.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SiteActors_SiteCommunication_Exists()
|
|
=> await AssertActorExists("/user/site-communication");
|
|
|
|
// SiteActors_CentralClusterClient_Exists was removed with the site→central Akka ClusterClient
|
|
// (the `central-cluster-client` actor) in the ClusterClient→gRPC migration's Phase 4. The site
|
|
// now reaches central over gRPC (GrpcCentralTransport dialling CentralControlService), which is
|
|
// not an actor in the local system, so there is no actor path to assert here.
|
|
|
|
private async Task AssertActorExists(string path)
|
|
{
|
|
Assert.NotNull(_actorSystem);
|
|
var selection = _actorSystem!.ActorSelection(path);
|
|
var identity = await selection.Ask<ActorIdentity>(
|
|
new Identify(path), TimeSpan.FromSeconds(5));
|
|
Assert.NotNull(identity.Subject);
|
|
}
|
|
}
|