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; /// /// Unit tests for the pure decision resolver behind the token-gated internal /// deployment-config fetch endpoint (). /// /// /// This endpoint is machine-to-machine: a site presents a per-deployment fetch /// token in the X-Deployment-Token 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: /// /// unknown / superseded (null row) → 404 NotFound /// expired row → 404 NotFound (does not leak existence to a wrong token) /// missing / wrong token → 401 Unauthorized /// valid row + correct token + not expired → 200 with the staged JSON /// /// /// 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; [Fact] public void Resolve_NullRow_ReturnsNotFound() { var result = DeploymentConfigEndpoints.Resolve(row: null, token: ValidToken, nowUtc: Now); Assert.IsType(result); Assert.Equal(StatusCodes.Status404NotFound, StatusOf(result)); } [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(result); Assert.Equal(StatusCodes.Status404NotFound, StatusOf(result)); } [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(result); Assert.Equal(StatusCodes.Status404NotFound, StatusOf(result)); } [Fact] public void Resolve_ValidRow_WrongToken_ReturnsUnauthorized() { var row = Row(); var result = DeploymentConfigEndpoints.Resolve(row, token: "wrong-token", nowUtc: Now); Assert.IsType(result); Assert.Equal(StatusCodes.Status401Unauthorized, StatusOf(result)); } [Fact] public void Resolve_ValidRow_EmptyToken_ReturnsUnauthorized() { var row = Row(); var result = DeploymentConfigEndpoints.Resolve(row, token: string.Empty, nowUtc: Now); Assert.IsType(result); Assert.Equal(StatusCodes.Status401Unauthorized, StatusOf(result)); } [Fact] public void Resolve_ValidRow_NullToken_ReturnsUnauthorized() { var row = Row(); var result = DeploymentConfigEndpoints.Resolve(row, token: null!, nowUtc: Now); Assert.IsType(result); Assert.Equal(StatusCodes.Status401Unauthorized, StatusOf(result)); } [Fact] public void Resolve_ValidRow_CorrectToken_NotExpired_ReturnsConfigJson() { var row = Row(); var result = DeploymentConfigEndpoints.Resolve(row, token: ValidToken, nowUtc: Now); var content = Assert.IsType(result); Assert.Equal(ConfigJson, content.ResponseContent); Assert.Equal("application/json", content.ContentType); } }