fix(cli): resolve CLI-002..007 — robust response rendering, URL/JSON arg validation, credential env-vars, doc refresh
This commit is contained in:
@@ -2,6 +2,7 @@ using ScadaLink.CLI;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
[Collection("Environment")]
|
||||
public class CliConfigTests
|
||||
{
|
||||
[Fact]
|
||||
|
||||
@@ -3,6 +3,7 @@ using ScadaLink.CLI.Commands;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
[Collection("Console")]
|
||||
public class CommandHelpersTests
|
||||
{
|
||||
[Fact]
|
||||
|
||||
71
tests/ScadaLink.CLI.Tests/CredentialResolutionTests.cs
Normal file
71
tests/ScadaLink.CLI.Tests/CredentialResolutionTests.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using ScadaLink.CLI;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CLI-006 — credentials could only be supplied via the
|
||||
/// <c>--password</c> command-line option, which leaks into process listings and
|
||||
/// shell history. A <c>SCADALINK_PASSWORD</c> / <c>SCADALINK_USERNAME</c> environment
|
||||
/// fallback gives CI/CD a safer alternative.
|
||||
/// </summary>
|
||||
[Collection("Environment")]
|
||||
public class CredentialResolutionTests
|
||||
{
|
||||
[Fact]
|
||||
public void Load_Password_FromEnvironment()
|
||||
{
|
||||
var orig = Environment.GetEnvironmentVariable("SCADALINK_PASSWORD");
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCADALINK_PASSWORD", "s3cret");
|
||||
|
||||
var config = CliConfig.Load();
|
||||
|
||||
Assert.Equal("s3cret", config.Password);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCADALINK_PASSWORD", orig);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_Username_FromEnvironment()
|
||||
{
|
||||
var orig = Environment.GetEnvironmentVariable("SCADALINK_USERNAME");
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCADALINK_USERNAME", "ci-user");
|
||||
|
||||
var config = CliConfig.Load();
|
||||
|
||||
Assert.Equal("ci-user", config.Username);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCADALINK_USERNAME", orig);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_NoCredentialEnvVars_LeavesCredentialsNull()
|
||||
{
|
||||
var origUser = Environment.GetEnvironmentVariable("SCADALINK_USERNAME");
|
||||
var origPass = Environment.GetEnvironmentVariable("SCADALINK_PASSWORD");
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCADALINK_USERNAME", null);
|
||||
Environment.SetEnvironmentVariable("SCADALINK_PASSWORD", null);
|
||||
|
||||
var config = CliConfig.Load();
|
||||
|
||||
Assert.Null(config.Username);
|
||||
Assert.Null(config.Password);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCADALINK_USERNAME", origUser);
|
||||
Environment.SetEnvironmentVariable("SCADALINK_PASSWORD", origPass);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
tests/ScadaLink.CLI.Tests/InstanceArgumentParsingTests.cs
Normal file
98
tests/ScadaLink.CLI.Tests/InstanceArgumentParsingTests.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using ScadaLink.CLI.Commands;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CLI-005 — malformed <c>--bindings</c> / <c>--overrides</c> JSON
|
||||
/// previously threw unhandled exceptions instead of producing a clean validation error.
|
||||
/// </summary>
|
||||
public class InstanceArgumentParsingTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseBindings_ValidJson_ReturnsPairs()
|
||||
{
|
||||
var ok = InstanceCommands.TryParseBindings(
|
||||
"[[\"Speed\", 5], [\"Mode\", 7]]", out var bindings, out var error);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Null(error);
|
||||
Assert.Equal(2, bindings!.Count);
|
||||
Assert.Equal(("Speed", 5), bindings[0]);
|
||||
Assert.Equal(("Mode", 7), bindings[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseBindings_MalformedJson_ReturnsErrorNotException()
|
||||
{
|
||||
var ok = InstanceCommands.TryParseBindings("not json", out var bindings, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(bindings);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseBindings_ShortPair_ReturnsErrorNotException()
|
||||
{
|
||||
// A pair with fewer than two elements previously threw ArgumentOutOfRangeException.
|
||||
var ok = InstanceCommands.TryParseBindings("[[\"Speed\"]]", out var bindings, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(bindings);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseBindings_WrongElementTypes_ReturnsErrorNotException()
|
||||
{
|
||||
// A non-string name / non-int id previously threw InvalidOperationException.
|
||||
var ok = InstanceCommands.TryParseBindings("[[5, \"Speed\"]]", out var bindings, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(bindings);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseBindings_JsonNull_ReturnsErrorNotException()
|
||||
{
|
||||
var ok = InstanceCommands.TryParseBindings("null", out var bindings, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(bindings);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOverrides_ValidJson_ReturnsDictionary()
|
||||
{
|
||||
var ok = InstanceCommands.TryParseOverrides(
|
||||
"{\"Speed\": \"100\", \"Mode\": null}", out var overrides, out var error);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Null(error);
|
||||
Assert.Equal(2, overrides!.Count);
|
||||
Assert.Equal("100", overrides["Speed"]);
|
||||
Assert.Null(overrides["Mode"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOverrides_MalformedJson_ReturnsErrorNotException()
|
||||
{
|
||||
var ok = InstanceCommands.TryParseOverrides("{bad json", out var overrides, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(overrides);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseOverrides_JsonNull_ReturnsErrorNotException()
|
||||
{
|
||||
var ok = InstanceCommands.TryParseOverrides("null", out var overrides, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.Null(overrides);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using ScadaLink.CLI;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
[Collection("Console")]
|
||||
public class OutputFormatterTests
|
||||
{
|
||||
[Fact]
|
||||
|
||||
73
tests/ScadaLink.CLI.Tests/ResponseRenderingTests.cs
Normal file
73
tests/ScadaLink.CLI.Tests/ResponseRenderingTests.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using ScadaLink.CLI;
|
||||
using ScadaLink.CLI.Commands;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CLI-002 (empty success body) and CLI-003 (non-JSON success
|
||||
/// body) — both previously crashed table rendering with an unhandled exception.
|
||||
/// </summary>
|
||||
[Collection("Console")]
|
||||
public class ResponseRenderingTests
|
||||
{
|
||||
[Fact]
|
||||
public void HandleResponse_EmptyBody_TableFormat_DoesNotThrow_ReturnsZero()
|
||||
{
|
||||
// CLI-002: a 200/204 with an empty body must be treated as "succeeded, no output".
|
||||
var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
var response = new ManagementResponse(204, "", null, null);
|
||||
var exitCode = CommandHelpers.HandleResponse(response, "table");
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleResponse_EmptyBody_JsonFormat_DoesNotThrow_ReturnsZero()
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
var response = new ManagementResponse(200, " ", null, null);
|
||||
var exitCode = CommandHelpers.HandleResponse(response, "json");
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandleResponse_NonJsonBody_TableFormat_FallsBackToRaw_ReturnsZero()
|
||||
{
|
||||
// CLI-003: a success status with a non-JSON body (e.g. proxy HTML error page)
|
||||
// must not crash; it should print the raw body verbatim.
|
||||
var writer = new StringWriter();
|
||||
Console.SetOut(writer);
|
||||
|
||||
try
|
||||
{
|
||||
var response = new ManagementResponse(200, "<html>Service Unavailable</html>", null, null);
|
||||
var exitCode = CommandHelpers.HandleResponse(response, "table");
|
||||
|
||||
Assert.Equal(0, exitCode);
|
||||
Assert.Contains("<html>Service Unavailable</html>", writer.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/ScadaLink.CLI.Tests/TestCollections.cs
Normal file
16
tests/ScadaLink.CLI.Tests/TestCollections.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// xUnit runs test classes in parallel by default. Several CLI test classes redirect
|
||||
/// the process-global <see cref="System.Console.Out"/> / <see cref="System.Console.Error"/>,
|
||||
/// which races when they run concurrently. Tests in this collection run serially.
|
||||
/// </summary>
|
||||
[CollectionDefinition("Console")]
|
||||
public sealed class ConsoleCollection { }
|
||||
|
||||
/// <summary>
|
||||
/// Test classes that mutate process-global environment variables run serially so they
|
||||
/// do not observe each other's changes.
|
||||
/// </summary>
|
||||
[CollectionDefinition("Environment")]
|
||||
public sealed class EnvironmentCollection { }
|
||||
31
tests/ScadaLink.CLI.Tests/UrlValidationTests.cs
Normal file
31
tests/ScadaLink.CLI.Tests/UrlValidationTests.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using ScadaLink.CLI.Commands;
|
||||
|
||||
namespace ScadaLink.CLI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression tests for CLI-004 — a malformed <c>--url</c> previously reached
|
||||
/// <c>new Uri(...)</c> in the <see cref="ScadaLink.CLI.ManagementHttpClient"/> constructor
|
||||
/// and threw an unhandled <see cref="UriFormatException"/>.
|
||||
/// </summary>
|
||||
public class UrlValidationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("http://localhost:9001")]
|
||||
[InlineData("https://central-host:5000/")]
|
||||
[InlineData("http://central")]
|
||||
public void IsValidManagementUrl_AcceptsAbsoluteHttpUrls(string url)
|
||||
{
|
||||
Assert.True(CommandHelpers.IsValidManagementUrl(url));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("localhost:9001")] // no scheme
|
||||
[InlineData("")] // empty
|
||||
[InlineData(" ")] // whitespace
|
||||
[InlineData("not a url")]
|
||||
[InlineData("ftp://central:21")] // non-http scheme
|
||||
public void IsValidManagementUrl_RejectsMalformedOrNonHttpUrls(string url)
|
||||
{
|
||||
Assert.False(CommandHelpers.IsValidManagementUrl(url));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user