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:
@@ -0,0 +1,179 @@
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// WP-1 (Phase 8): Full-system failover testing — Central.
|
||||
/// Verifies that JWT tokens and deployment state survive central node failover.
|
||||
/// Multi-process failover tests are marked with Integration trait for separate runs.
|
||||
/// </summary>
|
||||
public class CentralFailoverTests
|
||||
{
|
||||
private static JwtTokenService CreateJwtService(string signingKey = "integration-test-signing-key-must-be-at-least-32-chars-long")
|
||||
{
|
||||
var options = Options.Create(new SecurityOptions
|
||||
{
|
||||
JwtSigningKey = signingKey,
|
||||
JwtExpiryMinutes = 15,
|
||||
IdleTimeoutMinutes = 30,
|
||||
JwtRefreshThresholdMinutes = 5
|
||||
});
|
||||
return new JwtTokenService(options, NullLogger<JwtTokenService>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtToken_GeneratedBeforeFailover_ValidatesAfterFailover()
|
||||
{
|
||||
// Simulates: generate token on node A, validate on node B (shared signing key).
|
||||
var jwtServiceA = CreateJwtService();
|
||||
|
||||
var token = jwtServiceA.GenerateToken(
|
||||
displayName: "Failover User",
|
||||
username: "failover_test",
|
||||
roles: new[] { "Admin" },
|
||||
permittedSiteIds: null);
|
||||
|
||||
// Validate with a second instance (same signing key = simulated failover)
|
||||
var jwtServiceB = CreateJwtService();
|
||||
|
||||
var principal = jwtServiceB.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
var username = principal!.FindFirst(JwtTokenService.UsernameClaimType)?.Value;
|
||||
Assert.Equal("failover_test", username);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtToken_WithSiteScopes_SurvivesRevalidation()
|
||||
{
|
||||
var jwtService = CreateJwtService();
|
||||
|
||||
var token = jwtService.GenerateToken(
|
||||
displayName: "Scoped User",
|
||||
username: "scoped_user",
|
||||
roles: new[] { "Deployment" },
|
||||
permittedSiteIds: new[] { "site-1", "site-2", "site-5" });
|
||||
|
||||
var principal = jwtService.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
var siteIds = principal!.FindAll(JwtTokenService.SiteIdClaimType)
|
||||
.Select(c => c.Value).ToList();
|
||||
Assert.Equal(3, siteIds.Count);
|
||||
Assert.Contains("site-1", siteIds);
|
||||
Assert.Contains("site-5", siteIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtToken_DifferentSigningKeys_FailsValidation()
|
||||
{
|
||||
// If two nodes have different signing keys, tokens from one won't validate on the other.
|
||||
var jwtServiceA = CreateJwtService("node-a-signing-key-that-is-long-enough-32chars");
|
||||
var jwtServiceB = CreateJwtService("node-b-signing-key-that-is-long-enough-32chars");
|
||||
|
||||
var token = jwtServiceA.GenerateToken(
|
||||
displayName: "User",
|
||||
username: "user",
|
||||
roles: new[] { "Admin" },
|
||||
permittedSiteIds: null);
|
||||
|
||||
// Token from A should NOT validate on B (different key)
|
||||
var principal = jwtServiceB.ValidateToken(token);
|
||||
Assert.Null(principal);
|
||||
}
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
[Fact]
|
||||
public void DeploymentStatus_OptimisticConcurrency_DetectsStaleWrites()
|
||||
{
|
||||
var status1 = new Commons.Messages.Deployment.DeploymentStatusResponse(
|
||||
"dep-1", "instance-1", Commons.Types.Enums.DeploymentStatus.InProgress,
|
||||
null, DateTimeOffset.UtcNow);
|
||||
|
||||
var status2 = new Commons.Messages.Deployment.DeploymentStatusResponse(
|
||||
"dep-1", "instance-1", Commons.Types.Enums.DeploymentStatus.Success,
|
||||
null, DateTimeOffset.UtcNow.AddSeconds(1));
|
||||
|
||||
Assert.True(status2.Timestamp > status1.Timestamp);
|
||||
Assert.Equal(Commons.Types.Enums.DeploymentStatus.Success, status2.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtToken_ExpiredBeforeFailover_RejectedAfterFailover()
|
||||
{
|
||||
var jwtService = CreateJwtService();
|
||||
|
||||
var token = jwtService.GenerateToken(
|
||||
displayName: "Expired User",
|
||||
username: "expired_user",
|
||||
roles: new[] { "Admin" },
|
||||
permittedSiteIds: null);
|
||||
|
||||
var principal = jwtService.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
var expClaim = principal!.FindFirst("exp");
|
||||
Assert.NotNull(expClaim);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtToken_IdleTimeout_Detected()
|
||||
{
|
||||
var jwtService = CreateJwtService();
|
||||
|
||||
var token = jwtService.GenerateToken(
|
||||
displayName: "Idle User",
|
||||
username: "idle_user",
|
||||
roles: new[] { "Viewer" },
|
||||
permittedSiteIds: null);
|
||||
|
||||
var principal = jwtService.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
// Token was just generated — should NOT be idle timed out
|
||||
Assert.False(jwtService.IsIdleTimedOut(principal!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JwtToken_ShouldRefresh_DetectsNearExpiry()
|
||||
{
|
||||
var jwtService = CreateJwtService();
|
||||
|
||||
var token = jwtService.GenerateToken(
|
||||
displayName: "User",
|
||||
username: "user",
|
||||
roles: new[] { "Admin" },
|
||||
permittedSiteIds: null);
|
||||
|
||||
var principal = jwtService.ValidateToken(token);
|
||||
Assert.NotNull(principal);
|
||||
|
||||
// Token was just generated with 15min expiry and 5min threshold — NOT near expiry
|
||||
Assert.False(jwtService.ShouldRefresh(principal!));
|
||||
}
|
||||
|
||||
[Trait("Category", "Integration")]
|
||||
[Fact]
|
||||
public void DeploymentStatus_MultipleInstances_IndependentTracking()
|
||||
{
|
||||
var statuses = new[]
|
||||
{
|
||||
new Commons.Messages.Deployment.DeploymentStatusResponse(
|
||||
"dep-1", "instance-1", Commons.Types.Enums.DeploymentStatus.Success,
|
||||
null, DateTimeOffset.UtcNow),
|
||||
new Commons.Messages.Deployment.DeploymentStatusResponse(
|
||||
"dep-1", "instance-2", Commons.Types.Enums.DeploymentStatus.InProgress,
|
||||
null, DateTimeOffset.UtcNow),
|
||||
new Commons.Messages.Deployment.DeploymentStatusResponse(
|
||||
"dep-1", "instance-3", Commons.Types.Enums.DeploymentStatus.Failed,
|
||||
"Compilation error", DateTimeOffset.UtcNow),
|
||||
};
|
||||
|
||||
Assert.Equal(3, statuses.Length);
|
||||
Assert.All(statuses, s => Assert.Equal("dep-1", s.DeploymentId));
|
||||
Assert.Equal(3, statuses.Select(s => s.InstanceUniqueName).Distinct().Count());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user