feat(cli): notification smtp update --tls-mode / --credentials options
Expose the two previously-unreachable SmtpConfiguration fields on the
CLI. Both flags are optional — omitting them sends null so the server
preserves the existing value. --tls-mode is constrained to the canonical
{None, StartTLS, SSL} set via AcceptOnlyFromAmong for fast-fail.
This commit is contained in:
@@ -69,33 +69,72 @@ public static class NotificationCommands
|
||||
});
|
||||
group.Add(listCmd);
|
||||
|
||||
var idOption = new Option<int>("--id") { Description = "SMTP config ID", Required = true };
|
||||
var serverOption = new Option<string>("--server") { Description = "SMTP server", Required = true };
|
||||
var portOption = new Option<int>("--port") { Description = "SMTP port", Required = true };
|
||||
var authModeOption = new Option<string>("--auth-mode") { Description = "Auth mode", Required = true };
|
||||
var fromOption = new Option<string>("--from-address") { Description = "From email address", Required = true };
|
||||
var updateCmd = new Command("update") { Description = "Update SMTP configuration" };
|
||||
updateCmd.Add(idOption);
|
||||
updateCmd.Add(serverOption);
|
||||
updateCmd.Add(portOption);
|
||||
updateCmd.Add(authModeOption);
|
||||
updateCmd.Add(fromOption);
|
||||
updateCmd.Add(SmtpIdOption);
|
||||
updateCmd.Add(SmtpServerOption);
|
||||
updateCmd.Add(SmtpPortOption);
|
||||
updateCmd.Add(SmtpAuthModeOption);
|
||||
updateCmd.Add(SmtpFromOption);
|
||||
updateCmd.Add(SmtpTlsModeOption);
|
||||
updateCmd.Add(SmtpCredentialsOption);
|
||||
updateCmd.SetAction(async (ParseResult result) =>
|
||||
{
|
||||
var id = result.GetValue(idOption);
|
||||
var server = result.GetValue(serverOption)!;
|
||||
var port = result.GetValue(portOption);
|
||||
var authMode = result.GetValue(authModeOption)!;
|
||||
var from = result.GetValue(fromOption)!;
|
||||
return await CommandHelpers.ExecuteCommandAsync(
|
||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||
new UpdateSmtpConfigCommand(id, server, port, authMode, from));
|
||||
BuildUpdateSmtpConfigCommand(result));
|
||||
});
|
||||
group.Add(updateCmd);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
// SMTP update options are static so the parsed values can be read back both
|
||||
// from the SetAction and from BuildUpdateSmtpConfigCommand (used by tests).
|
||||
private static readonly Option<int> SmtpIdOption =
|
||||
new("--id") { Description = "SMTP config ID", Required = true };
|
||||
private static readonly Option<string> SmtpServerOption =
|
||||
new("--server") { Description = "SMTP server", Required = true };
|
||||
private static readonly Option<int> SmtpPortOption =
|
||||
new("--port") { Description = "SMTP port", Required = true };
|
||||
private static readonly Option<string> SmtpAuthModeOption =
|
||||
new("--auth-mode") { Description = "Auth mode", Required = true };
|
||||
private static readonly Option<string> SmtpFromOption =
|
||||
new("--from-address") { Description = "From email address", Required = true };
|
||||
private static readonly Option<string?> SmtpTlsModeOption = CreateTlsModeOption();
|
||||
private static readonly Option<string?> SmtpCredentialsOption =
|
||||
new("--credentials")
|
||||
{
|
||||
Description = "SMTP credentials — 'username:password' for Basic, or client secret " +
|
||||
"for OAuth2 (optional; preserves existing if omitted)",
|
||||
};
|
||||
|
||||
private static Option<string?> CreateTlsModeOption()
|
||||
{
|
||||
var option = new Option<string?>("--tls-mode")
|
||||
{
|
||||
Description = "TLS mode: None, StartTLS, or SSL (optional; preserves existing if omitted)",
|
||||
};
|
||||
option.AcceptOnlyFromAmong("None", "StartTLS", "SSL");
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the <see cref="UpdateSmtpConfigCommand"/> from a parsed <c>smtp update</c>
|
||||
/// invocation. The optional <c>--tls-mode</c> / <c>--credentials</c> flags map to
|
||||
/// null when omitted so the server-side handler preserves the existing values.
|
||||
/// </summary>
|
||||
internal static UpdateSmtpConfigCommand BuildUpdateSmtpConfigCommand(ParseResult result)
|
||||
{
|
||||
var id = result.GetValue(SmtpIdOption);
|
||||
var server = result.GetValue(SmtpServerOption)!;
|
||||
var port = result.GetValue(SmtpPortOption);
|
||||
var authMode = result.GetValue(SmtpAuthModeOption)!;
|
||||
var from = result.GetValue(SmtpFromOption)!;
|
||||
var tlsMode = result.GetValue(SmtpTlsModeOption);
|
||||
var credentials = result.GetValue(SmtpCredentialsOption);
|
||||
return new UpdateSmtpConfigCommand(id, server, port, authMode, from, tlsMode, credentials);
|
||||
}
|
||||
|
||||
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
||||
{
|
||||
var cmd = new Command("list") { Description = "List all notification lists" };
|
||||
|
||||
104
tests/ScadaLink.CLI.Tests/Commands/SmtpUpdateCommandTests.cs
Normal file
104
tests/ScadaLink.CLI.Tests/Commands/SmtpUpdateCommandTests.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.CommandLine;
|
||||
using ScadaLink.CLI.Commands;
|
||||
using ScadaLink.Commons.Messages.Management;
|
||||
|
||||
namespace ScadaLink.CLI.Tests.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the <c>scadalink notification smtp update</c> subcommand. The command
|
||||
/// gained two optional flags — <c>--tls-mode</c> and <c>--credentials</c> — that plumb
|
||||
/// through to <see cref="UpdateSmtpConfigCommand"/>. These tests pin that the flags
|
||||
/// parse, are genuinely optional (non-breaking), and that <c>--tls-mode</c> rejects
|
||||
/// values outside the canonical {None, StartTLS, SSL} set.
|
||||
/// </summary>
|
||||
public class SmtpUpdateCommandTests
|
||||
{
|
||||
private static readonly Option<string> Url = new("--url") { Recursive = true };
|
||||
private static readonly Option<string> Username = new("--username") { Recursive = true };
|
||||
private static readonly Option<string> Password = new("--password") { Recursive = true };
|
||||
private static readonly Option<string> Format = CliOptions.CreateFormatOption();
|
||||
|
||||
private static Command SmtpUpdateCommand()
|
||||
{
|
||||
var notification = NotificationCommands.Build(Url, Format, Username, Password);
|
||||
var smtp = notification.Subcommands.Single(c => c.Name == "smtp");
|
||||
return smtp.Subcommands.Single(c => c.Name == "update");
|
||||
}
|
||||
|
||||
private static ParseResult ParseUpdate(params string[] args)
|
||||
=> SmtpUpdateCommand().Parse(args);
|
||||
|
||||
[Fact]
|
||||
public void Update_WithTlsModeAndCredentials_ProducesCommandCarryingThem()
|
||||
{
|
||||
var parse = ParseUpdate(
|
||||
"--id", "1", "--server", "smtp.example.com", "--port", "587",
|
||||
"--auth-mode", "Basic", "--from-address", "noreply@example.com",
|
||||
"--tls-mode", "None", "--credentials", "user:pass");
|
||||
|
||||
Assert.Empty(parse.Errors);
|
||||
var cmd = NotificationCommands.BuildUpdateSmtpConfigCommand(parse);
|
||||
|
||||
Assert.Equal(1, cmd.SmtpConfigId);
|
||||
Assert.Equal("smtp.example.com", cmd.Server);
|
||||
Assert.Equal(587, cmd.Port);
|
||||
Assert.Equal("Basic", cmd.AuthMode);
|
||||
Assert.Equal("noreply@example.com", cmd.FromAddress);
|
||||
Assert.Equal("None", cmd.TlsMode);
|
||||
Assert.Equal("user:pass", cmd.Credentials);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithoutTlsModeAndCredentials_ProducesCommandWithNulls()
|
||||
{
|
||||
var parse = ParseUpdate(
|
||||
"--id", "2", "--server", "smtp.example.com", "--port", "25",
|
||||
"--auth-mode", "OAuth2", "--from-address", "noreply@example.com");
|
||||
|
||||
Assert.Empty(parse.Errors);
|
||||
var cmd = NotificationCommands.BuildUpdateSmtpConfigCommand(parse);
|
||||
|
||||
Assert.Equal(2, cmd.SmtpConfigId);
|
||||
Assert.Null(cmd.TlsMode);
|
||||
Assert.Null(cmd.Credentials);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("None")]
|
||||
[InlineData("StartTLS")]
|
||||
[InlineData("SSL")]
|
||||
public void Update_TlsModeOption_AcceptsCanonicalValues(string value)
|
||||
{
|
||||
var parse = ParseUpdate(
|
||||
"--id", "1", "--server", "smtp.example.com", "--port", "587",
|
||||
"--auth-mode", "Basic", "--from-address", "noreply@example.com",
|
||||
"--tls-mode", value);
|
||||
|
||||
Assert.Empty(parse.Errors);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Bogus")]
|
||||
[InlineData("tls")]
|
||||
[InlineData("none")] // AcceptOnlyFromAmong is case-sensitive: constrain to canonical spelling
|
||||
public void Update_TlsModeOption_RejectsValuesOutsideCanonicalSet(string value)
|
||||
{
|
||||
var parse = ParseUpdate(
|
||||
"--id", "1", "--server", "smtp.example.com", "--port", "587",
|
||||
"--auth-mode", "Basic", "--from-address", "noreply@example.com",
|
||||
"--tls-mode", value);
|
||||
|
||||
Assert.NotEmpty(parse.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TlsModeAndCredentials_AreNotRequired()
|
||||
{
|
||||
var update = SmtpUpdateCommand();
|
||||
var tls = update.Options.Single(o => o.Name == "--tls-mode");
|
||||
var creds = update.Options.Single(o => o.Name == "--credentials");
|
||||
|
||||
Assert.False(tls.Required, "--tls-mode must be optional (preserve-if-omitted).");
|
||||
Assert.False(creds.Required, "--credentials must be optional (preserve-if-omitted).");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user