Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/NotificationSmsCommandTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

318 lines
13 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 ----------------------------
/// <summary>Verifies that creating an SMS list with phones builds an SMS command carrying the recipient phones.</summary>
[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);
}
/// <summary>Verifies that the list type option is matched case-insensitively.</summary>
[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);
}
/// <summary>Verifies that a list created without a type defaults to email.</summary>
[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);
}
/// <summary>Verifies that an email list given phone numbers is rejected.</summary>
[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);
}
/// <summary>Verifies that an SMS list with no phone numbers is rejected.</summary>
[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);
}
/// <summary>Verifies that an email list with no email addresses is rejected.</summary>
[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);
}
/// <summary>Verifies that an SMS list given email addresses is rejected.</summary>
[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);
}
/// <summary>Verifies that an unknown list type is rejected.</summary>
[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));
}
/// <summary>Verifies that updating an SMS list with phones builds an SMS command carrying the recipient phones.</summary>
[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);
}
/// <summary>Verifies that updating an email list with phone numbers is rejected.</summary>
[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 -------------------------------------------
/// <summary>Verifies that the notification command has an SMS group with list and update subcommands.</summary>
[Fact]
public void Notification_HasSmsGroupWithListAndUpdate()
{
var subNames = SmsGroup().Subcommands.Select(c => c.Name).ToHashSet();
Assert.Contains("list", subNames);
Assert.Contains("update", subNames);
}
/// <summary>Verifies that an SMS update with every flag produces a command carrying all of them.</summary>
[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);
}
/// <summary>Verifies that omitting the optional flags produces a command with null values for them.</summary>
[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);
}
/// <summary>Verifies that the optional SMS update flags are not marked as required.</summary>
[Fact]
public void SmsUpdate_OptionalFlags_AreNotRequired()
{
var update = SmsUpdate();
Assert.False(update.Options.Single(o => o.Name == "--from-number").Required);
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);
}
/// <summary>Verifies that an SMS update with only a messaging service SID and no from number builds successfully.</summary>
[Fact]
public void SmsUpdate_MessagingServiceSidOnly_NoFromNumber_Builds()
{
// Twilio Messaging-Service-only config: From number omitted, Messaging Service SID
// supplied. The either-or validation accepts it and FromNumber maps to null.
var parse = Parse(SmsUpdate(),
"--id", "3", "--account-sid", "ACmsg", "--messaging-service-sid", "MGonly");
Assert.Empty(parse.Errors);
var cmd = NotificationCommands.BuildUpdateSmsConfigCommand(parse);
Assert.Null(cmd.FromNumber);
Assert.Equal("MGonly", cmd.MessagingServiceSid);
}
/// <summary>Verifies that an SMS update with neither a from number nor a messaging service SID is rejected.</summary>
[Fact]
public void SmsUpdate_NeitherFromNorMessagingService_Throws()
{
// Neither sender identity supplied => the build rejects it so an SMS config is
// never left with no way to send (mirrors the UI + delivery-adapter validation).
var parse = Parse(SmsUpdate(), "--id", "4", "--account-sid", "ACnone");
Assert.Throws<ArgumentException>(() => NotificationCommands.BuildUpdateSmsConfigCommand(parse));
}
/// <summary>Verifies that both SMS commands resolve through the management command registry.</summary>
[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
{
/// <summary>Verifies that a rendered SMS configuration list never contains the auth token value.</summary>
[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);
}
}