274 lines
10 KiB
C#
274 lines
10 KiB
C#
using System.CommandLine;
|
|
using ZB.MOM.WW.ScadaBridge.CLI;
|
|
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands;
|
|
|
|
/// <summary>
|
|
/// Tests for the SMS surface of the <c>notification</c> command group (SMS Notifications S6):
|
|
/// the channel-aware <c>--type</c> / <c>--phones</c> flags on <c>notification list create|update</c>,
|
|
/// and the new <c>notification sms list|update</c> group. Pins the validation rules, the
|
|
/// per-channel command construction, and that the Twilio Auth Token is never echoed back.
|
|
/// </summary>
|
|
public class NotificationSmsCommandTests
|
|
{
|
|
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 Notification() => NotificationCommands.Build(Url, Format, Username, Password);
|
|
|
|
private static Command ListCreate() =>
|
|
Notification().Subcommands.Single(c => c.Name == "create");
|
|
|
|
private static Command ListUpdate() =>
|
|
Notification().Subcommands.Single(c => c.Name == "update");
|
|
|
|
private static Command SmsGroup() =>
|
|
Notification().Subcommands.Single(c => c.Name == "sms");
|
|
|
|
private static Command SmsUpdate() =>
|
|
SmsGroup().Subcommands.Single(c => c.Name == "update");
|
|
|
|
// System.CommandLine's Parse takes a string[] (no params overload here), so route
|
|
// every invocation through a helper that wraps the variadic args into an array.
|
|
private static System.CommandLine.ParseResult Parse(Command command, params string[] args)
|
|
=> command.Parse(args);
|
|
|
|
// ---- list create/update: --type + --phones ----------------------------
|
|
|
|
[Fact]
|
|
public void ListCreate_SmsTypeWithPhones_BuildsSmsCommandWithRecipientPhones()
|
|
{
|
|
var parse = Parse(ListCreate(),
|
|
"--name", "Ops SMS", "--type", "sms", "--phones", "+15551230000,+15551230001");
|
|
|
|
Assert.Empty(parse.Errors);
|
|
var cmd = NotificationCommands.BuildCreateNotificationListCommand(parse);
|
|
|
|
Assert.Equal("Ops SMS", cmd.Name);
|
|
Assert.Equal(NotificationType.Sms, cmd.Type);
|
|
Assert.Equal(new[] { "+15551230000", "+15551230001" }, cmd.RecipientPhones);
|
|
Assert.Empty(cmd.RecipientEmails);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_TypeIsCaseInsensitive()
|
|
{
|
|
var parse = Parse(ListCreate(),
|
|
"--name", "Ops SMS", "--type", "SMS", "--phones", "+15551230000");
|
|
|
|
Assert.Empty(parse.Errors);
|
|
var cmd = NotificationCommands.BuildCreateNotificationListCommand(parse);
|
|
Assert.Equal(NotificationType.Sms, cmd.Type);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_DefaultType_IsEmail()
|
|
{
|
|
var parse = Parse(ListCreate(), "--name", "Ops", "--emails", "ops@example.com");
|
|
|
|
Assert.Empty(parse.Errors);
|
|
var cmd = NotificationCommands.BuildCreateNotificationListCommand(parse);
|
|
|
|
Assert.Equal(NotificationType.Email, cmd.Type);
|
|
Assert.Equal(new[] { "ops@example.com" }, cmd.RecipientEmails);
|
|
Assert.Null(cmd.RecipientPhones);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_EmailTypeWithPhones_Throws()
|
|
{
|
|
var parse = Parse(ListCreate(),
|
|
"--name", "Ops", "--type", "email", "--emails", "ops@example.com", "--phones", "+15551230000");
|
|
|
|
var ex = Assert.Throws<ArgumentException>(
|
|
() => NotificationCommands.BuildCreateNotificationListCommand(parse));
|
|
Assert.Contains("--phones", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_SmsTypeWithoutPhones_Throws()
|
|
{
|
|
var parse = Parse(ListCreate(), "--name", "Ops SMS", "--type", "sms");
|
|
|
|
var ex = Assert.Throws<ArgumentException>(
|
|
() => NotificationCommands.BuildCreateNotificationListCommand(parse));
|
|
Assert.Contains("--phones", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_EmailTypeWithoutEmails_Throws()
|
|
{
|
|
var parse = Parse(ListCreate(), "--name", "Ops", "--type", "email");
|
|
|
|
var ex = Assert.Throws<ArgumentException>(
|
|
() => NotificationCommands.BuildCreateNotificationListCommand(parse));
|
|
Assert.Contains("--emails", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_SmsTypeWithEmails_Throws()
|
|
{
|
|
var parse = Parse(ListCreate(),
|
|
"--name", "Ops SMS", "--type", "sms", "--emails", "ops@example.com");
|
|
|
|
var ex = Assert.Throws<ArgumentException>(
|
|
() => NotificationCommands.BuildCreateNotificationListCommand(parse));
|
|
Assert.Contains("--emails", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListCreate_UnknownType_Throws()
|
|
{
|
|
var parse = Parse(ListCreate(),
|
|
"--name", "Ops", "--type", "carrier-pigeon", "--emails", "ops@example.com");
|
|
|
|
Assert.Throws<ArgumentException>(
|
|
() => NotificationCommands.BuildCreateNotificationListCommand(parse));
|
|
}
|
|
|
|
[Fact]
|
|
public void ListUpdate_SmsTypeWithPhones_BuildsSmsCommandWithRecipientPhones()
|
|
{
|
|
var parse = Parse(ListUpdate(),
|
|
"--id", "7", "--name", "Ops SMS", "--type", "sms", "--phones", "+15551230000");
|
|
|
|
Assert.Empty(parse.Errors);
|
|
var cmd = NotificationCommands.BuildUpdateNotificationListCommand(parse);
|
|
|
|
Assert.Equal(7, cmd.NotificationListId);
|
|
Assert.Equal(NotificationType.Sms, cmd.Type);
|
|
Assert.Equal(new[] { "+15551230000" }, cmd.RecipientPhones);
|
|
Assert.Empty(cmd.RecipientEmails);
|
|
}
|
|
|
|
[Fact]
|
|
public void ListUpdate_EmailTypeWithPhones_Throws()
|
|
{
|
|
var parse = Parse(ListUpdate(),
|
|
"--id", "7", "--name", "Ops", "--type", "email",
|
|
"--emails", "ops@example.com", "--phones", "+15551230000");
|
|
|
|
var ex = Assert.Throws<ArgumentException>(
|
|
() => NotificationCommands.BuildUpdateNotificationListCommand(parse));
|
|
Assert.Contains("--phones", ex.Message);
|
|
}
|
|
|
|
// ---- notification sms group -------------------------------------------
|
|
|
|
[Fact]
|
|
public void Notification_HasSmsGroupWithListAndUpdate()
|
|
{
|
|
var subNames = SmsGroup().Subcommands.Select(c => c.Name).ToHashSet();
|
|
Assert.Contains("list", subNames);
|
|
Assert.Contains("update", subNames);
|
|
}
|
|
|
|
[Fact]
|
|
public void SmsUpdate_WithAllFlags_ProducesCommandCarryingThem()
|
|
{
|
|
var parse = Parse(SmsUpdate(),
|
|
"--id", "1", "--account-sid", "ACnew", "--from-number", "+15551110000",
|
|
"--messaging-service-sid", "MGnew", "--api-base-url", "https://new.example.com",
|
|
"--auth-token", "new-secret");
|
|
|
|
Assert.Empty(parse.Errors);
|
|
var cmd = NotificationCommands.BuildUpdateSmsConfigCommand(parse);
|
|
|
|
Assert.Equal(1, cmd.SmsConfigId);
|
|
Assert.Equal("ACnew", cmd.AccountSid);
|
|
Assert.Equal("+15551110000", cmd.FromNumber);
|
|
Assert.Equal("MGnew", cmd.MessagingServiceSid);
|
|
Assert.Equal("https://new.example.com", cmd.ApiBaseUrl);
|
|
Assert.Equal("new-secret", cmd.AuthToken);
|
|
}
|
|
|
|
[Fact]
|
|
public void SmsUpdate_WithoutOptionalFlags_ProducesCommandWithNulls()
|
|
{
|
|
var parse = Parse(SmsUpdate(),
|
|
"--id", "2", "--account-sid", "AConly", "--from-number", "+15552220000");
|
|
|
|
Assert.Empty(parse.Errors);
|
|
var cmd = NotificationCommands.BuildUpdateSmsConfigCommand(parse);
|
|
|
|
Assert.Equal(2, cmd.SmsConfigId);
|
|
// messaging-service-sid maps to null (handler CLEARS it); auth-token / api-base-url
|
|
// map to null (handler PRESERVES them) — all three optional here.
|
|
Assert.Null(cmd.MessagingServiceSid);
|
|
Assert.Null(cmd.ApiBaseUrl);
|
|
Assert.Null(cmd.AuthToken);
|
|
}
|
|
|
|
[Fact]
|
|
public void SmsUpdate_OptionalFlags_AreNotRequired()
|
|
{
|
|
var update = SmsUpdate();
|
|
Assert.False(update.Options.Single(o => o.Name == "--messaging-service-sid").Required);
|
|
Assert.False(update.Options.Single(o => o.Name == "--api-base-url").Required);
|
|
Assert.False(update.Options.Single(o => o.Name == "--auth-token").Required);
|
|
}
|
|
|
|
[Fact]
|
|
public void SmsCommands_ResolveViaRegistry()
|
|
{
|
|
// The CLI calls GetCommandName for every command it sends; both SMS commands
|
|
// must round-trip through the management command registry.
|
|
Assert.Equal(typeof(ListSmsConfigsCommand),
|
|
ManagementCommandRegistry.Resolve(ManagementCommandRegistry.GetCommandName(typeof(ListSmsConfigsCommand))));
|
|
Assert.Equal(typeof(UpdateSmsConfigCommand),
|
|
ManagementCommandRegistry.Resolve(ManagementCommandRegistry.GetCommandName(typeof(UpdateSmsConfigCommand))));
|
|
}
|
|
|
|
// ---- Auth Token is never rendered -------------------------------------
|
|
}
|
|
|
|
/// <summary>
|
|
/// Console-capturing companion to <see cref="NotificationSmsCommandTests"/> — pins that the
|
|
/// Twilio Auth Token is never echoed to the rendered output. Lives in the shared "Console"
|
|
/// collection so the <see cref="Console.SetOut"/> redirection does not race the other
|
|
/// console-capturing test classes.
|
|
/// </summary>
|
|
[Collection("Console")]
|
|
public class NotificationSmsAuthTokenRenderingTests
|
|
{
|
|
[Fact]
|
|
public void SmsConfigListResponse_RenderedAsJson_DoesNotContainAuthToken()
|
|
{
|
|
// The server projects the AuthToken away to a HasAuthToken presence flag
|
|
// (SmsConfigPublicShape). The CLI renders that projected response verbatim,
|
|
// so a rendered SMS-config list never surfaces the secret token value.
|
|
var projected = new[]
|
|
{
|
|
new
|
|
{
|
|
Id = 1,
|
|
AccountSid = "ACxxxx",
|
|
FromNumber = "+15550000000",
|
|
HasAuthToken = true,
|
|
},
|
|
};
|
|
|
|
var original = Console.Out;
|
|
using var sw = new StringWriter();
|
|
try
|
|
{
|
|
Console.SetOut(sw);
|
|
OutputFormatter.WriteJson(projected);
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(original);
|
|
}
|
|
|
|
var rendered = sw.ToString();
|
|
Assert.Contains("hasAuthToken", rendered);
|
|
Assert.DoesNotContain("authToken\"", rendered);
|
|
Assert.DoesNotContain("secret", rendered, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|