refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -0,0 +1,77 @@
namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests;
/// <summary>
/// Tests for ClusterOptions default values and property setters.
/// </summary>
public class ClusterOptionsTests
{
[Fact]
public void DefaultValues_AreCorrect()
{
var options = new ClusterOptions();
Assert.Equal("keep-oldest", options.SplitBrainResolverStrategy);
Assert.Equal(TimeSpan.FromSeconds(15), options.StableAfter);
Assert.Equal(TimeSpan.FromSeconds(2), options.HeartbeatInterval);
Assert.Equal(TimeSpan.FromSeconds(10), options.FailureDetectionThreshold);
Assert.Equal(1, options.MinNrOfMembers);
Assert.True(options.DownIfAlone);
}
[Fact]
public void DownIfAlone_CanBeSet()
{
var options = new ClusterOptions { DownIfAlone = false };
Assert.False(options.DownIfAlone);
}
[Fact]
public void SeedNodes_DefaultsToEmptyList()
{
var options = new ClusterOptions();
Assert.NotNull(options.SeedNodes);
Assert.Empty(options.SeedNodes);
}
// ClusterInfra-011: SectionName constant deleted — the previous test
// `SectionName_IsTheExpectedAppSettingsSection` is removed alongside it.
// The Host's SiteServiceRegistration / StartupValidator continue to
// reference the `"ScadaBridge:Cluster"` literal directly; reinstating the
// constant should happen when those Host binding sites can be updated in
// the same change.
[Fact]
public void Properties_CanBeSetToCustomValues()
{
// ClusterInfra-013: this test exercises the POCO property setters only —
// `SplitBrainResolverStrategy = "keep-majority"` and `MinNrOfMembers = 2`
// are values the design doc explicitly forbids in production
// (`keep-majority` causes total shutdown on a two-node partition;
// `MinNrOfMembers = 2` blocks the cluster singleton after failover).
// The POCO accepts any value by design; rejection lives in
// `ClusterOptionsValidator` and is covered by
// `ClusterOptionsValidatorTests.UnsupportedSplitBrainStrategy_FailsValidation`
// and `ClusterOptionsValidatorTests.MinNrOfMembers_NotOne_FailsValidation`.
// Do NOT read these values as endorsed runtime configuration.
var options = new ClusterOptions
{
SeedNodes = new List<string> { "akka.tcp://system@node1:2551", "akka.tcp://system@node2:2551" },
SplitBrainResolverStrategy = "keep-majority",
StableAfter = TimeSpan.FromSeconds(30),
HeartbeatInterval = TimeSpan.FromSeconds(5),
FailureDetectionThreshold = TimeSpan.FromSeconds(20),
MinNrOfMembers = 2
};
Assert.Equal(2, options.SeedNodes.Count);
Assert.Contains("akka.tcp://system@node1:2551", options.SeedNodes);
Assert.Contains("akka.tcp://system@node2:2551", options.SeedNodes);
Assert.Equal("keep-majority", options.SplitBrainResolverStrategy);
Assert.Equal(TimeSpan.FromSeconds(30), options.StableAfter);
Assert.Equal(TimeSpan.FromSeconds(5), options.HeartbeatInterval);
Assert.Equal(TimeSpan.FromSeconds(20), options.FailureDetectionThreshold);
Assert.Equal(2, options.MinNrOfMembers);
}
}
@@ -0,0 +1,148 @@
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests;
/// <summary>
/// CI-004: Tests that <see cref="ClusterOptionsValidator"/> rejects the
/// catastrophic misconfigurations the design doc warns against — a
/// <c>MinNrOfMembers</c> other than 1, an unsupported split-brain strategy,
/// empty seed nodes, and timings where the heartbeat is not below the
/// failure-detection threshold.
/// </summary>
public class ClusterOptionsValidatorTests
{
private static ClusterOptions ValidOptions() => new()
{
SeedNodes = new List<string> { "akka.tcp://scadabridge@node1:8081", "akka.tcp://scadabridge@node2:8081" },
SplitBrainResolverStrategy = "keep-oldest",
StableAfter = TimeSpan.FromSeconds(15),
HeartbeatInterval = TimeSpan.FromSeconds(2),
FailureDetectionThreshold = TimeSpan.FromSeconds(10),
MinNrOfMembers = 1,
DownIfAlone = true
};
[Fact]
public void DefaultOptions_AreValid()
{
var result = new ClusterOptionsValidator().Validate(null, ValidOptions());
Assert.True(result.Succeeded, result.FailureMessage);
}
[Fact]
public void MinNrOfMembers_NotOne_FailsValidation()
{
var options = ValidOptions();
options.MinNrOfMembers = 2;
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("MinNrOfMembers", result.FailureMessage);
}
[Theory]
[InlineData("keep-majority")]
[InlineData("static-quorum")]
[InlineData("nonsense")]
public void UnsupportedSplitBrainStrategy_FailsValidation(string strategy)
{
var options = ValidOptions();
options.SplitBrainResolverStrategy = strategy;
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("SplitBrainResolverStrategy", result.FailureMessage);
}
[Fact]
public void EmptySeedNodes_FailsValidation()
{
var options = ValidOptions();
options.SeedNodes = new List<string>();
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("SeedNodes", result.FailureMessage);
}
[Fact]
public void SingleSeedNode_FailsValidation()
{
// CI-012: design doc says "both nodes are seed nodes" — a single-seed
// configuration defeats the no-startup-ordering-dependency guarantee and
// must be rejected by the contract owner's validator, not just by the
// Host's startup validator.
var options = ValidOptions();
options.SeedNodes = new List<string> { "akka.tcp://scadabridge@node1:8081" };
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("SeedNodes", result.FailureMessage);
Assert.Contains("2", result.FailureMessage);
}
[Fact]
public void HeartbeatNotBelowFailureThreshold_FailsValidation()
{
var options = ValidOptions();
options.HeartbeatInterval = TimeSpan.FromSeconds(10);
options.FailureDetectionThreshold = TimeSpan.FromSeconds(10);
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("HeartbeatInterval", result.FailureMessage);
}
[Fact]
public void NonPositiveStableAfter_FailsValidation()
{
var options = ValidOptions();
options.StableAfter = TimeSpan.Zero;
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("StableAfter", result.FailureMessage);
}
[Fact]
public void DownIfAloneFalse_FailsValidation()
{
var options = ValidOptions();
options.DownIfAlone = false;
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("DownIfAlone", result.FailureMessage);
}
[Fact]
public void Validate_AccumulatesAllFailures()
{
var options = new ClusterOptions
{
SeedNodes = new List<string>(),
SplitBrainResolverStrategy = "keep-majority",
MinNrOfMembers = 2,
StableAfter = TimeSpan.Zero,
HeartbeatInterval = TimeSpan.FromSeconds(20),
FailureDetectionThreshold = TimeSpan.FromSeconds(10)
};
var result = new ClusterOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains("SeedNodes", result.FailureMessage);
Assert.Contains("SplitBrainResolverStrategy", result.FailureMessage);
Assert.Contains("MinNrOfMembers", result.FailureMessage);
Assert.Contains("StableAfter", result.FailureMessage);
Assert.Contains("HeartbeatInterval", result.FailureMessage);
}
}
@@ -0,0 +1,57 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests;
/// <summary>
/// CI-002: Tests that <see cref="ServiceCollectionExtensions.AddClusterInfrastructure"/>
/// does real work rather than silently returning success — it must register
/// the <see cref="ClusterOptionsValidator"/> so misconfiguration fails fast.
/// (The companion actor-registration test was removed alongside the deleted
/// `AddClusterInfrastructureActors` extension method — see ClusterInfra-014.)
/// </summary>
public class ServiceCollectionExtensionsTests
{
[Fact]
public void AddClusterInfrastructure_RegistersOptionsValidator()
{
var services = new ServiceCollection();
services.AddClusterInfrastructure();
var validators = services
.Where(d => d.ServiceType == typeof(IValidateOptions<ClusterOptions>))
.ToList();
Assert.NotEmpty(validators);
using var provider = services.BuildServiceProvider();
var validator = provider.GetService<IValidateOptions<ClusterOptions>>();
Assert.IsType<ClusterOptionsValidator>(validator);
}
[Fact]
public void AddClusterInfrastructure_ValidatorRejectsBadOptionsAtResolution()
{
var services = new ServiceCollection();
services.AddClusterInfrastructure();
// A MinNrOfMembers of 2 blocks the cluster singleton after failover.
services.Configure<ClusterOptions>(o =>
{
o.SeedNodes = new List<string> { "akka.tcp://scadabridge@node1:8081" };
o.MinNrOfMembers = 2;
});
using var provider = services.BuildServiceProvider();
var ex = Assert.Throws<OptionsValidationException>(
() => provider.GetRequiredService<IOptions<ClusterOptions>>().Value);
Assert.Contains("MinNrOfMembers", ex.Message);
}
// ClusterInfra-014: `AddClusterInfrastructureActors_ThrowsRatherThanSilentlySucceeding`
// was removed alongside the now-deleted `AddClusterInfrastructureActors`
// extension method. The Akka.NET actor wiring legitimately lives in
// `ZB.MOM.WW.ScadaBridge.Host` (AkkaHostedService) per the
// Component-ClusterInfrastructure.md "Implementation Note — Code Placement"
// section; this project no longer exposes an actor-registration extension.
}
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Options" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.csproj" />
</ItemGroup>
</Project>