513fa1aa16
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.
149 lines
9.2 KiB
C#
149 lines
9.2 KiB
C#
using System.CommandLine;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
|
|
|
|
[Collection("Console")]
|
|
public class NotificationParityTests
|
|
{
|
|
private static RootCommand Root()
|
|
{
|
|
var root = new RootCommand();
|
|
var url = new Option<string>("--url") { Recursive = true };
|
|
var format = new Option<string>("--format") { Recursive = true };
|
|
var user = new Option<string>("--username") { Recursive = true };
|
|
var password = new Option<string>("--password") { Recursive = true };
|
|
root.Add(url); root.Add(format); root.Add(user); root.Add(password);
|
|
root.Add(NotificationCommands.Build(url, format, user, password));
|
|
return root;
|
|
}
|
|
|
|
/// <summary>Verifies that each invocation sends the dedicated command contract with the expected payload property.</summary>
|
|
/// <param name="arguments">The command-line arguments to invoke.</param>
|
|
/// <param name="command">The expected command type name.</param>
|
|
/// <param name="property">The payload property to check.</param>
|
|
/// <param name="expected">The expected value of that property.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Theory]
|
|
[InlineData("rename --id 7 --name Operators", "RenameNotificationListCommand", "notificationListId", "7")]
|
|
[InlineData("recipient add --list-id 7 --name Jane --email jane@example.com", "AddNotificationRecipientCommand", "name", "Jane")]
|
|
[InlineData("recipient add --list-id 7 --name Jane --phone +15551234567", "AddNotificationRecipientCommand", "phoneNumber", "+15551234567")]
|
|
[InlineData("recipient delete --id 42", "DeleteNotificationRecipientCommand", "recipientId", "42")]
|
|
[InlineData("smtp create --server mail.example.com --port 587 --auth-mode basic --from-address ops@example.com --tls-mode StartTLS --credentials test:secret", "CreateSmtpConfigCommand", "tlsMode", "StartTLS")]
|
|
[InlineData("sms create --account-sid AC00000000000000000000000000000000 --from-number +15551234567 --auth-token secret", "CreateSmsConfigCommand", "authToken", "secret")]
|
|
[InlineData("sms update --id 8 --account-sid AC00000000000000000000000000000000 --from-number +15551234567 --connection-timeout-seconds 15 --max-retries 4 --retry-delay-seconds 90", "UpdateSmsConfigCommand", "retryDelay", "00:01:30")]
|
|
public async Task InvocationSendsDedicatedContract(string arguments, string command, string property, string expected)
|
|
{
|
|
var body = await Capture(arguments);
|
|
Assert.Equal(command.Replace("Command", ""), body.GetProperty("command").GetString());
|
|
var payload = body.GetProperty("payload");
|
|
Assert.Equal(expected, payload.GetProperty(property).ToString());
|
|
if (command == "AddNotificationRecipientCommand")
|
|
{
|
|
Assert.Equal(7, payload.GetProperty("notificationListId").GetInt32());
|
|
Assert.Equal("Jane", payload.GetProperty("name").GetString());
|
|
Assert.Equal(arguments.Contains("--email") ? "jane@example.com" : null, payload.GetProperty("emailAddress").GetString());
|
|
Assert.Equal(arguments.Contains("--phone") ? "+15551234567" : null, payload.GetProperty("phoneNumber").GetString());
|
|
}
|
|
if (command == "RenameNotificationListCommand")
|
|
{
|
|
Assert.Equal("Operators", payload.GetProperty("name").GetString());
|
|
Assert.False(payload.TryGetProperty("recipientEmails", out _));
|
|
}
|
|
}
|
|
|
|
/// <summary>Verifies that explicitly supplied SMS policy values, including zero, are forwarded.</summary>
|
|
/// <param name="verb">The sms subcommand verb under test.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Theory]
|
|
[InlineData("create --auth-token secret")]
|
|
[InlineData("update --id 8")]
|
|
public async Task SmsPolicyExplicitValuesIncludingZeroAreForwarded(string verb)
|
|
{
|
|
var payload = (await Capture($"sms {verb} --account-sid AC00000000000000000000000000000000 --messaging-service-sid MG123 --connection-timeout-seconds 15 --max-retries 0 --retry-delay-seconds 0")).GetProperty("payload");
|
|
Assert.Equal(15, payload.GetProperty("connectionTimeoutSeconds").GetInt32());
|
|
Assert.Equal(0, payload.GetProperty("maxRetries").GetInt32());
|
|
Assert.Equal("00:00:00", payload.GetProperty("retryDelay").GetString());
|
|
Assert.Equal(JsonValueKind.Null, payload.GetProperty("fromNumber").ValueKind);
|
|
}
|
|
|
|
/// <summary>Verifies that SMTP create forwards all optional fields.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SmtpCreateForwardsAllOptionalFields()
|
|
{
|
|
var payload = (await Capture("smtp create --server mail.example.com --port 587 --auth-mode oauth2 --from-address ops@example.com --credentials secret --oauth2-authority https://login.example.com/token --oauth2-scope scope --transport Smtp --tls-mode StartTLS")).GetProperty("payload");
|
|
Assert.Equal("secret", payload.GetProperty("credentials").GetString());
|
|
Assert.Equal("https://login.example.com/token", payload.GetProperty("oAuth2Authority").GetString());
|
|
Assert.Equal("scope", payload.GetProperty("oAuth2Scope").GetString());
|
|
Assert.Equal("Smtp", payload.GetProperty("transport").GetString());
|
|
}
|
|
|
|
/// <summary>Verifies that omitted SMTP create options are sent as null so the server applies its defaults.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task SmtpCreateOmittedOptionsUseServerDefaults()
|
|
{
|
|
var payload = (await Capture("smtp create --server mail.example.com --port 25 --auth-mode none --from-address ops@example.com")).GetProperty("payload");
|
|
foreach (var name in new[] { "tlsMode", "credentials", "oAuth2Authority", "oAuth2Scope", "transport" })
|
|
Assert.Equal(JsonValueKind.Null, payload.GetProperty(name).ValueKind);
|
|
}
|
|
|
|
/// <summary>Verifies that an omitted SMS policy and omitted secrets remain null in the payload.</summary>
|
|
/// <param name="verb">The sms subcommand verb under test.</param>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Theory]
|
|
[InlineData("create --auth-token secret")]
|
|
[InlineData("update --id 8")]
|
|
public async Task OmittedSmsPolicyAndSecretsRemainNull(string verb)
|
|
{
|
|
var payload = (await Capture($"sms {verb} --account-sid AC00000000000000000000000000000000 --from-number +15551234567")).GetProperty("payload");
|
|
foreach (var name in new[] { "connectionTimeoutSeconds", "maxRetries", "retryDelay", "apiBaseUrl", "messagingServiceSid" })
|
|
Assert.Equal(JsonValueKind.Null, payload.GetProperty(name).ValueKind);
|
|
if (verb.StartsWith("update")) Assert.Equal(JsonValueKind.Null, payload.GetProperty("authToken").ValueKind);
|
|
}
|
|
|
|
/// <summary>Verifies that invalid input fails argument parsing.</summary>
|
|
/// <param name="arguments">The invalid command-line arguments.</param>
|
|
[Theory]
|
|
[InlineData("sms create --account-sid AC00000000000000000000000000000000 --from-number +15551234567")]
|
|
[InlineData("sms update --id 8 --account-sid AC00000000000000000000000000000000 --from-number +15551234567 --max-retries -1")]
|
|
[InlineData("sms update --id 8 --account-sid AC00000000000000000000000000000000 --from-number +15551234567 --connection-timeout-seconds 0")]
|
|
[InlineData("sms update --id 8 --account-sid AC00000000000000000000000000000000 --from-number +15551234567 --retry-delay-seconds -1")]
|
|
[InlineData("recipient add --list-id 7 --name Jane")]
|
|
[InlineData("recipient delete --id 0")]
|
|
public void InvalidInputFailsParsing(string arguments)
|
|
{
|
|
Assert.NotEmpty(Root().Parse(("notification " + arguments).Split(' ')).Errors);
|
|
}
|
|
|
|
private static async Task<JsonElement> Capture(string arguments)
|
|
{
|
|
using var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
listener.Start();
|
|
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
var parse = Root().Parse(("notification " + arguments + $" --url http://127.0.0.1:{port} --username test --password test --format json").Split(' '));
|
|
Assert.Empty(parse.Errors);
|
|
var server = Task.Run(async () =>
|
|
{
|
|
using var client = await listener.AcceptTcpClientAsync();
|
|
await using var stream = client.GetStream();
|
|
using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true);
|
|
var length = 0;
|
|
while (await reader.ReadLineAsync() is { Length: > 0 } line)
|
|
if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) length = int.Parse(line.Split(':')[1]);
|
|
var buffer = new char[length];
|
|
await reader.ReadBlockAsync(buffer);
|
|
var body = JsonDocument.Parse(new string(buffer)).RootElement.Clone();
|
|
await stream.WriteAsync(Encoding.ASCII.GetBytes("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"));
|
|
return body;
|
|
});
|
|
Assert.Equal(0, await parse.InvokeAsync());
|
|
return await server.WaitAsync(TimeSpan.FromSeconds(5));
|
|
}
|
|
}
|