Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ManagementService.Tests/DeploymentConfigEndpointsTests.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

129 lines
5.1 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Deployment;
using ZB.MOM.WW.ScadaBridge.ManagementService;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
/// <summary>
/// Unit tests for the pure decision resolver behind the token-gated internal
/// deployment-config fetch endpoint (<see cref="DeploymentConfigEndpoints.Resolve"/>).
///
/// <para>
/// This endpoint is machine-to-machine: a site presents a per-deployment fetch
/// token in the <c>X-Deployment-Token</c> header and the token is the entire
/// security boundary. The resolver is extracted so the security-critical
/// decision logic is unit-testable without standing up a web host. The mapping
/// from the resolver result to the documented HTTP status codes is:
/// <list type="bullet">
/// <item>unknown / superseded (null row) → 404 NotFound</item>
/// <item>expired row → 404 NotFound (does not leak existence to a wrong token)</item>
/// <item>missing / wrong token → 401 Unauthorized</item>
/// <item>valid row + correct token + not expired → 200 with the staged JSON</item>
/// </list>
/// </para>
/// </summary>
public class DeploymentConfigEndpointsTests
{
private const string ValidToken = "test-deployment-token-abc123";
private const string ConfigJson = "{\"instanceId\":42,\"attributes\":[]}";
private static readonly DateTimeOffset Now = new(2026, 6, 26, 12, 0, 0, TimeSpan.Zero);
private static PendingDeployment Row(string token = ValidToken, DateTimeOffset? expiresAtUtc = null) =>
new(
deploymentId: "deploy-1",
instanceId: 42,
revisionHash: "hash-1",
configurationJson: ConfigJson,
token: token,
createdAtUtc: Now.AddMinutes(-1),
expiresAtUtc: expiresAtUtc ?? Now.AddMinutes(5));
private static int? StatusOf(IResult result) => (result as IStatusCodeHttpResult)?.StatusCode;
/// <summary>Verifies that a null row resolves to not found.</summary>
[Fact]
public void Resolve_NullRow_ReturnsNotFound()
{
var result = DeploymentConfigEndpoints.Resolve(row: null, token: ValidToken, nowUtc: Now);
Assert.IsType<NotFound>(result);
Assert.Equal(StatusCodes.Status404NotFound, StatusOf(result));
}
/// <summary>Verifies that an expired row with the correct token resolves to not found.</summary>
[Fact]
public void Resolve_ExpiredRow_WithCorrectToken_ReturnsNotFound()
{
// Expired exactly at now: ExpiresAtUtc <= now is the boundary that purges.
var row = Row(expiresAtUtc: Now);
var result = DeploymentConfigEndpoints.Resolve(row, token: ValidToken, nowUtc: Now);
Assert.IsType<NotFound>(result);
Assert.Equal(StatusCodes.Status404NotFound, StatusOf(result));
}
/// <summary>Verifies that a row that expired in the past with the correct token resolves to not found.</summary>
[Fact]
public void Resolve_ExpiredRowInThePast_WithCorrectToken_ReturnsNotFound()
{
var row = Row(expiresAtUtc: Now.AddSeconds(-1));
var result = DeploymentConfigEndpoints.Resolve(row, token: ValidToken, nowUtc: Now);
Assert.IsType<NotFound>(result);
Assert.Equal(StatusCodes.Status404NotFound, StatusOf(result));
}
/// <summary>Verifies that a valid row with the wrong token resolves to unauthorized.</summary>
[Fact]
public void Resolve_ValidRow_WrongToken_ReturnsUnauthorized()
{
var row = Row();
var result = DeploymentConfigEndpoints.Resolve(row, token: "wrong-token", nowUtc: Now);
Assert.IsType<UnauthorizedHttpResult>(result);
Assert.Equal(StatusCodes.Status401Unauthorized, StatusOf(result));
}
/// <summary>Verifies that a valid row with an empty token resolves to unauthorized.</summary>
[Fact]
public void Resolve_ValidRow_EmptyToken_ReturnsUnauthorized()
{
var row = Row();
var result = DeploymentConfigEndpoints.Resolve(row, token: string.Empty, nowUtc: Now);
Assert.IsType<UnauthorizedHttpResult>(result);
Assert.Equal(StatusCodes.Status401Unauthorized, StatusOf(result));
}
/// <summary>Verifies that a valid row with a null token resolves to unauthorized.</summary>
[Fact]
public void Resolve_ValidRow_NullToken_ReturnsUnauthorized()
{
var row = Row();
var result = DeploymentConfigEndpoints.Resolve(row, token: null!, nowUtc: Now);
Assert.IsType<UnauthorizedHttpResult>(result);
Assert.Equal(StatusCodes.Status401Unauthorized, StatusOf(result));
}
/// <summary>Verifies that a valid, unexpired row with the correct token resolves to the configuration JSON.</summary>
[Fact]
public void Resolve_ValidRow_CorrectToken_NotExpired_ReturnsConfigJson()
{
var row = Row();
var result = DeploymentConfigEndpoints.Resolve(row, token: ValidToken, nowUtc: Now);
var content = Assert.IsType<ContentHttpResult>(result);
Assert.Equal(ConfigJson, content.ResponseContent);
Assert.Equal("application/json", content.ContentType);
}
}