using System.Net; using System.Text; using ZB.MOM.WW.ScadaBridge.CLI; namespace ZB.MOM.WW.ScadaBridge.CLI.Tests; /// /// Regression tests for CLI-013 — /// (success, error-body parsing, connection-failure, and timeout paths) was untested. /// Uses a stub so no live server is required. /// public class ManagementHttpClientTests { private sealed class StubHandler : HttpMessageHandler { private readonly Func> _responder; public StubHandler(HttpStatusCode status, string body) : this((_, _) => Task.FromResult(new HttpResponseMessage(status) { Content = new StringContent(body, Encoding.UTF8, "application/json"), })) { } public StubHandler(Func> responder) { _responder = responder; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) => _responder(request, cancellationToken); } private static ManagementHttpClient ClientWith(StubHandler handler) => new(new HttpClient(handler), "http://localhost:9001", "user", "pass"); [Fact] public async Task SendCommandAsync_Success_ReturnsJsonData() { using var client = ClientWith(new StubHandler(HttpStatusCode.OK, "{\"id\":1}")); var response = await client.SendCommandAsync("ListSites", new { }, TimeSpan.FromSeconds(5)); Assert.Equal(200, response.StatusCode); Assert.Equal("{\"id\":1}", response.JsonData); Assert.Null(response.Error); Assert.Null(response.ErrorCode); } [Fact] public async Task SendCommandAsync_ErrorBody_ParsesErrorAndCode() { using var client = ClientWith(new StubHandler( HttpStatusCode.BadRequest, "{\"error\":\"Bad input\",\"code\":\"INVALID_ARGUMENT\"}")); var response = await client.SendCommandAsync("ListSites", new { }, TimeSpan.FromSeconds(5)); Assert.Equal(400, response.StatusCode); Assert.Null(response.JsonData); Assert.Equal("Bad input", response.Error); Assert.Equal("INVALID_ARGUMENT", response.ErrorCode); } [Fact] public async Task SendCommandAsync_NonJsonErrorBody_FallsBackToRawBody() { using var client = ClientWith(new StubHandler( HttpStatusCode.BadGateway, "Bad Gateway")); var response = await client.SendCommandAsync("ListSites", new { }, TimeSpan.FromSeconds(5)); Assert.Equal(502, response.StatusCode); Assert.Equal("Bad Gateway", response.Error); Assert.Null(response.ErrorCode); } [Fact] public async Task SendCommandAsync_ConnectionFailure_ReturnsStatusZero() { using var client = ClientWith(new StubHandler((_, _) => throw new HttpRequestException("connection refused"))); var response = await client.SendCommandAsync("ListSites", new { }, TimeSpan.FromSeconds(5)); Assert.Equal(0, response.StatusCode); Assert.Equal("CONNECTION_FAILED", response.ErrorCode); Assert.Contains("connection refused", response.Error); } [Fact] public async Task SendCommandAsync_Timeout_Returns504() { using var client = ClientWith(new StubHandler(async (_, ct) => { await Task.Delay(Timeout.Infinite, ct); return new HttpResponseMessage(HttpStatusCode.OK); })); var response = await client.SendCommandAsync("ListSites", new { }, TimeSpan.FromMilliseconds(50)); Assert.Equal(504, response.StatusCode); Assert.Equal("TIMEOUT", response.ErrorCode); } } /// /// WP2.6e (arch-review misc — CLI HttpClient timeout): the public /// constructor must bound its underlying /// explicitly (30 s default) rather than leaving the /// framework's 100 s default in place, and must honor the /// SCADABRIDGE_HTTP_TIMEOUT_SECONDS override — consistent with how every other /// CLI setting is environment-overridable (). Runs in the shared /// "Environment" collection (see ) so it never races another /// test mutating process-wide environment variables. /// [Collection("Environment")] public class ManagementHttpClientTimeoutTests { private const string EnvVar = "SCADABRIDGE_HTTP_TIMEOUT_SECONDS"; [Fact] public void DefaultConstructor_SetsThirtySecondTimeout_WhenEnvVarUnset() { var original = Environment.GetEnvironmentVariable(EnvVar); try { Environment.SetEnvironmentVariable(EnvVar, null); using var client = new ManagementHttpClient("http://localhost:9001", "user", "pass"); Assert.Equal(TimeSpan.FromSeconds(30), ManagementHttpClient.DefaultTimeout); Assert.Equal(TimeSpan.FromSeconds(30), client.EffectiveTimeout); } finally { Environment.SetEnvironmentVariable(EnvVar, original); } } [Theory] [InlineData("0")] [InlineData("-5")] [InlineData("not-a-number")] [InlineData("")] public void InvalidOrNonPositiveEnvValue_FallsBackToDefault(string value) { var original = Environment.GetEnvironmentVariable(EnvVar); try { Environment.SetEnvironmentVariable(EnvVar, value); using var client = new ManagementHttpClient("http://localhost:9001", "user", "pass"); Assert.Equal(TimeSpan.FromSeconds(30), client.EffectiveTimeout); } finally { Environment.SetEnvironmentVariable(EnvVar, original); } } [Fact] public void PositiveEnvValue_OverridesDefaultTimeout() { var original = Environment.GetEnvironmentVariable(EnvVar); try { Environment.SetEnvironmentVariable(EnvVar, "5"); using var client = new ManagementHttpClient("http://localhost:9001", "user", "pass"); Assert.Equal(TimeSpan.FromSeconds(5), client.EffectiveTimeout); } finally { Environment.SetEnvironmentVariable(EnvVar, original); } } }