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("--url") { Recursive = true }; var format = new Option("--format") { Recursive = true }; var user = new Option("--username") { Recursive = true }; var password = new Option("--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; } [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 _)); } } [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); } [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()); } [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); } [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); } [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 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)); } }