Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.IntegrationTests/CentralFailoverTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
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.
2026-10-07 15:05:49 -04:00

188 lines
7.1 KiB
C#

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>
/// 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);
}
/// <summary>Verifies that a JWT generated before failover validates after failover.</summary>
[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[] { "Administrator" },
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);
}
/// <summary>Verifies that a JWT with site scopes survives revalidation.</summary>
[Fact]
public void JwtToken_WithSiteScopes_SurvivesRevalidation()
{
var jwtService = CreateJwtService();
var token = jwtService.GenerateToken(
displayName: "Scoped User",
username: "scoped_user",
roles: new[] { "Deployer" },
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);
}
/// <summary>Verifies that a JWT validated with a different signing key fails validation.</summary>
[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[] { "Administrator" },
permittedSiteIds: null);
// Token from A should NOT validate on B (different key)
var principal = jwtServiceB.ValidateToken(token);
Assert.Null(principal);
}
/// <summary>Verifies that optimistic concurrency on deployment status detects stale writes.</summary>
[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);
}
/// <summary>Verifies that a JWT that expired before failover is rejected after failover.</summary>
[Fact]
public void JwtToken_ExpiredBeforeFailover_RejectedAfterFailover()
{
var jwtService = CreateJwtService();
var token = jwtService.GenerateToken(
displayName: "Expired User",
username: "expired_user",
roles: new[] { "Administrator" },
permittedSiteIds: null);
var principal = jwtService.ValidateToken(token);
Assert.NotNull(principal);
var expClaim = principal!.FindFirst("exp");
Assert.NotNull(expClaim);
}
/// <summary>Verifies that an idle timeout is detected.</summary>
[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!));
}
/// <summary>Verifies that the refresh check detects a token near expiry.</summary>
[Fact]
public void JwtToken_ShouldRefresh_DetectsNearExpiry()
{
var jwtService = CreateJwtService();
var token = jwtService.GenerateToken(
displayName: "User",
username: "user",
roles: new[] { "Administrator" },
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!));
}
/// <summary>Verifies that deployment status for multiple instances is tracked independently.</summary>
[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());
}
}