From 79567c730c49be24745644c7c6c4058be9424648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Mon, 16 Mar 2026 20:13:25 +0100 Subject: [PATCH] Extensions instead of partial classes --- CHANGELOG.md | 9 +- src/LinkMobility/ILinkMobilityClient.cs | 15 ++- src/LinkMobility/LinkMobilityClient.Sms.cs | 68 -------------- src/LinkMobility/LinkMobilityClient.cs | 93 ++++++++++--------- .../Models/IncomingMessageNotification.cs | 2 +- .../Requests/SendBinaryMessageRequest.cs | 8 +- .../Requests/SendTextMessageRequest.cs | 4 +- .../Responses/SendMessageResponse.cs | 6 -- .../Responses/SendTextMessageResponse.cs | 14 +++ src/LinkMobility/TextMessageExtensions.cs | 78 ++++++++++++++++ src/LinkMobility/Utils/Validation.cs | 25 +++++ .../Helpers/HttpMessageHandlerMock.cs | 2 + .../LinkMobilityClientTest.cs | 26 +++--- .../Sms/SendBinaryMessageTest.cs | 62 ++++++++++--- .../Sms/SendTextMessageTest.cs | 27 ++++-- .../Utils/ValidationTest.cs | 33 +++++++ 16 files changed, 303 insertions(+), 169 deletions(-) delete mode 100644 src/LinkMobility/LinkMobilityClient.Sms.cs create mode 100644 src/LinkMobility/Responses/SendTextMessageResponse.cs create mode 100644 src/LinkMobility/TextMessageExtensions.cs create mode 100644 src/LinkMobility/Utils/Validation.cs create mode 100644 test/LinkMobility.Tests/Utils/ValidationTest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 63abea0..eb0e0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -_No changes yet_ +### Added + +- `Validation` utility class for specifications as MSISDN + +### Changed + +- Channel implementations (SMS, WhatsApp, ...) are extensions to the `ILinkMobilityClient` interface. + ## [v0.1.1] - 2026-03-13 diff --git a/src/LinkMobility/ILinkMobilityClient.cs b/src/LinkMobility/ILinkMobilityClient.cs index 1d80245..22bc798 100644 --- a/src/LinkMobility/ILinkMobilityClient.cs +++ b/src/LinkMobility/ILinkMobilityClient.cs @@ -9,17 +9,14 @@ namespace AMWD.Net.Api.LinkMobility public interface ILinkMobilityClient { /// - /// Sends a text message to a list of recipients. + /// Performs a POST request to the LINK mobility API. /// + /// The type of the response. + /// The type of the request. + /// The path of the API endpoint. /// The request data. + /// Optional query parameters. /// A cancellation token to propagate notification that operations should be canceled. - Task SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default); - - /// - /// Sends a binary message to a list of recipients. - /// - /// The request data. - /// A cancellation token to propagate notification that operations should be canceled. - Task SendBinaryMessage(SendBinaryMessageRequest request, CancellationToken cancellationToken = default); + Task PostAsync(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default); } } diff --git a/src/LinkMobility/LinkMobilityClient.Sms.cs b/src/LinkMobility/LinkMobilityClient.Sms.cs deleted file mode 100644 index dc9c51b..0000000 --- a/src/LinkMobility/LinkMobilityClient.Sms.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; - -namespace AMWD.Net.Api.LinkMobility -{ - /// - /// Implementation of text messaging (SMS). API - /// - public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable - { - /// - public Task SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default) - { - if (request == null) - throw new ArgumentNullException(nameof(request)); - - if (string.IsNullOrWhiteSpace(request.MessageContent)) - throw new ArgumentException("A message must be provided.", nameof(request.MessageContent)); - - if (request.RecipientAddressList == null || request.RecipientAddressList.Count == 0) - throw new ArgumentException("At least one recipient must be provided.", nameof(request.RecipientAddressList)); - - foreach (string recipient in request.RecipientAddressList) - { - if (!IsValidMSISDN(recipient)) - throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(request.RecipientAddressList)); - } - - return PostAsync("/smsmessaging/text", request, cancellationToken: cancellationToken); - } - - /// - public Task SendBinaryMessage(SendBinaryMessageRequest request, CancellationToken cancellationToken = default) - { - if (request == null) - throw new ArgumentNullException(nameof(request)); - - if (request.MessageContent?.Count > 0) - { - // Validate that the string is a valid Base64 string - // Might throw a ArgumentNullException or FormatException - foreach (string str in request.MessageContent) - Convert.FromBase64String(str); - } - - if (request.RecipientAddressList == null || request.RecipientAddressList.Count == 0) - throw new ArgumentException("At least one recipient must be provided.", nameof(request.RecipientAddressList)); - - foreach (string recipient in request.RecipientAddressList) - { - if (!IsValidMSISDN(recipient)) - throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(request.RecipientAddressList)); - } - - return PostAsync("/smsmessaging/binary", request, cancellationToken: cancellationToken); - } - - // https://en.wikipedia.org/wiki/MSISDN - private static bool IsValidMSISDN(string msisdn) - { - if (string.IsNullOrWhiteSpace(msisdn)) - return false; - - return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled); - } - } -} diff --git a/src/LinkMobility/LinkMobilityClient.cs b/src/LinkMobility/LinkMobilityClient.cs index 25e7662..8a5f673 100644 --- a/src/LinkMobility/LinkMobilityClient.cs +++ b/src/LinkMobility/LinkMobilityClient.cs @@ -13,7 +13,7 @@ namespace AMWD.Net.Api.LinkMobility /// /// Provides a client for interacting with the Link Mobility API. /// - public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable + public class LinkMobilityClient : ILinkMobilityClient, IDisposable { private readonly ClientOptions _clientOptions; private readonly HttpClient _httpClient; @@ -78,6 +78,52 @@ namespace AMWD.Net.Api.LinkMobility GC.SuppressFinalize(this); } + /// + public async Task PostAsync(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + ValidateRequestPath(requestPath); + + string requestUrl = BuildRequestUrl(requestPath, queryParams); + var httpContent = ConvertRequest(request); + + var httpRequest = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(requestUrl, UriKind.Relative), + Content = httpContent, + }; + + var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); + var response = await GetResponse(httpResponse, cancellationToken).ConfigureAwait(false); + return response; + } + + private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null) + { + string path = requestPath.Trim().TrimStart('/'); + var param = new Dictionary(); + + if (_clientOptions.DefaultQueryParams.Count > 0) + { + foreach (var kvp in _clientOptions.DefaultQueryParams) + param[kvp.Key] = kvp.Value; + } + + var customQueryParams = queryParams?.GetQueryParameters(); + if (customQueryParams?.Count > 0) + { + foreach (var kvp in customQueryParams) + param[kvp.Key] = kvp.Value; + } + + if (param.Count == 0) + return path; + + string queryString = string.Join("&", param.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}")); + return $"{path}?{queryString}"; + } + private void ValidateClientOptions() { if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl)) @@ -148,51 +194,6 @@ namespace AMWD.Net.Api.LinkMobility } } - private async Task PostAsync(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default) - { - ThrowIfDisposed(); - ValidateRequestPath(requestPath); - - string requestUrl = BuildRequestUrl(requestPath, queryParams); - var httpContent = ConvertRequest(request); - - var httpRequest = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri(requestUrl, UriKind.Relative), - Content = httpContent, - }; - - var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); - var response = await GetResponse(httpResponse, cancellationToken).ConfigureAwait(false); - return response; - } - - private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null) - { - string path = requestPath.Trim().TrimStart('/'); - var param = new Dictionary(); - - if (_clientOptions.DefaultQueryParams.Count > 0) - { - foreach (var kvp in _clientOptions.DefaultQueryParams) - param[kvp.Key] = kvp.Value; - } - - var customQueryParams = queryParams?.GetQueryParameters(); - if (customQueryParams?.Count > 0) - { - foreach (var kvp in customQueryParams) - param[kvp.Key] = kvp.Value; - } - - if (param.Count == 0) - return path; - - string queryString = string.Join("&", param.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}")); - return $"{path}?{queryString}"; - } - private static HttpContent? ConvertRequest(T request) { if (request == null) diff --git a/src/LinkMobility/Models/IncomingMessageNotification.cs b/src/LinkMobility/Models/IncomingMessageNotification.cs index df13c52..552b527 100644 --- a/src/LinkMobility/Models/IncomingMessageNotification.cs +++ b/src/LinkMobility/Models/IncomingMessageNotification.cs @@ -56,7 +56,7 @@ namespace AMWD.Net.Api.LinkMobility /// /// , : ///
- /// – defines the number format of the mobile originated . + /// - defines the number format of the mobile originated . /// International numbers always includes the country prefix. ///
[JsonProperty("senderAddressType")] diff --git a/src/LinkMobility/Requests/SendBinaryMessageRequest.cs b/src/LinkMobility/Requests/SendBinaryMessageRequest.cs index b3276b3..f1939dd 100644 --- a/src/LinkMobility/Requests/SendBinaryMessageRequest.cs +++ b/src/LinkMobility/Requests/SendBinaryMessageRequest.cs @@ -8,9 +8,11 @@ /// /// Initializes a new instance of the class. /// - /// The recipient list. - public SendBinaryMessageRequest(IReadOnlyCollection recipientAddressList) + /// A binary message as base64 encoded lines. + /// A list of recipient numbers. + public SendBinaryMessageRequest(IReadOnlyCollection messageContent, IReadOnlyCollection recipientAddressList) { + MessageContent = messageContent; RecipientAddressList = recipientAddressList; } @@ -41,7 +43,7 @@ /// The binary data is transmitted without being changed (using 8 bit alphabet). /// [JsonProperty("messageContent")] - public IReadOnlyCollection? MessageContent { get; set; } + public IReadOnlyCollection MessageContent { get; set; } /// /// Optional. diff --git a/src/LinkMobility/Requests/SendTextMessageRequest.cs b/src/LinkMobility/Requests/SendTextMessageRequest.cs index c232052..c8d07e9 100644 --- a/src/LinkMobility/Requests/SendTextMessageRequest.cs +++ b/src/LinkMobility/Requests/SendTextMessageRequest.cs @@ -8,8 +8,8 @@ /// /// Initializes a new instance of the class. /// - /// The message. - /// The recipient list. + /// A text message. + /// A list of recipient numbers. public SendTextMessageRequest(string messageContent, IReadOnlyCollection recipientAddressList) { MessageContent = messageContent; diff --git a/src/LinkMobility/Responses/SendMessageResponse.cs b/src/LinkMobility/Responses/SendMessageResponse.cs index b5d5edd..f708ead 100644 --- a/src/LinkMobility/Responses/SendMessageResponse.cs +++ b/src/LinkMobility/Responses/SendMessageResponse.cs @@ -11,12 +11,6 @@ [JsonProperty("clientMessageId")] public string? ClientMessageId { get; set; } - /// - /// The actual number of generated SMS. - /// - [JsonProperty("smsCount")] - public int? SmsCount { get; set; } - /// /// Status code. /// diff --git a/src/LinkMobility/Responses/SendTextMessageResponse.cs b/src/LinkMobility/Responses/SendTextMessageResponse.cs new file mode 100644 index 0000000..a8e6d8e --- /dev/null +++ b/src/LinkMobility/Responses/SendTextMessageResponse.cs @@ -0,0 +1,14 @@ +namespace AMWD.Net.Api.LinkMobility +{ + /// + /// Response of a text message sent to a list of recipients. + /// + public class SendTextMessageResponse : SendMessageResponse + { + /// + /// The actual number of generated SMS. + /// + [JsonProperty("smsCount")] + public int? SmsCount { get; set; } + } +} diff --git a/src/LinkMobility/TextMessageExtensions.cs b/src/LinkMobility/TextMessageExtensions.cs new file mode 100644 index 0000000..45dd4c9 --- /dev/null +++ b/src/LinkMobility/TextMessageExtensions.cs @@ -0,0 +1,78 @@ +using System.Threading; +using System.Threading.Tasks; +using AMWD.Net.Api.LinkMobility.Utils; + +namespace AMWD.Net.Api.LinkMobility +{ + /// + /// Implementation of text messaging (SMS). API + /// + public static class TextMessageExtensions + { + /// + /// Sends a text message to a list of recipients. + /// + /// The instance. + /// The request data. + /// A cancellation token to propagate notification that operations should be canceled. + public static Task SendTextMessage(this ILinkMobilityClient client, SendTextMessageRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (string.IsNullOrWhiteSpace(request.MessageContent)) + throw new ArgumentException("A message must be provided.", nameof(request.MessageContent)); + + ValidateRecipientList(request.RecipientAddressList); + ValidateContentCategory(request.ContentCategory); + + return client.PostAsync("/smsmessaging/text", request, cancellationToken: cancellationToken); + } + + /// + /// Sends a binary message to a list of recipients. + /// + /// The instance. + /// The request data. + /// A cancellation token to propagate notification that operations should be canceled. + public static Task SendBinaryMessage(this ILinkMobilityClient client, SendBinaryMessageRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + throw new ArgumentNullException(nameof(request)); + + if (request.MessageContent == null || request.MessageContent.Count == 0) + throw new ArgumentException("A message must be provided.", nameof(request.MessageContent)); + + // Easiest way to validate that the string is a valid Base64 string. + // Might throw a ArgumentNullException or FormatException. + foreach (string str in request.MessageContent) + Convert.FromBase64String(str); + + ValidateRecipientList(request.RecipientAddressList); + ValidateContentCategory(request.ContentCategory); + + return client.PostAsync("/smsmessaging/binary", request, cancellationToken: cancellationToken); + } + + private static void ValidateRecipientList(IReadOnlyCollection? recipientAddressList) + { + if (recipientAddressList == null || recipientAddressList.Count == 0) + throw new ArgumentException("At least one recipient must be provided.", nameof(recipientAddressList)); + + foreach (string recipient in recipientAddressList) + { + if (!Validation.IsValidMSISDN(recipient)) + throw new ArgumentException($"Recipient address '{recipient}' is not a valid MSISDN format.", nameof(recipientAddressList)); + } + } + + private static void ValidateContentCategory(ContentCategory? contentCategory) + { + if (!contentCategory.HasValue) + return; + + if (contentCategory.Value != ContentCategory.Informational && contentCategory.Value != ContentCategory.Advertisement) + throw new ArgumentException($"Content category '{contentCategory.Value}' is not valid.", nameof(contentCategory)); + } + } +} diff --git a/src/LinkMobility/Utils/Validation.cs b/src/LinkMobility/Utils/Validation.cs new file mode 100644 index 0000000..6cd199f --- /dev/null +++ b/src/LinkMobility/Utils/Validation.cs @@ -0,0 +1,25 @@ +using System.Text.RegularExpressions; + +namespace AMWD.Net.Api.LinkMobility.Utils +{ + /// + /// Validation helper for LINK Mobility API requirements. + /// + public static class Validation + { + /// + /// Validates whether the provided string is a valid MSISDN (E.164 formatted). + ///
+ /// See Wikipedia: MSISDN for more information. + ///
+ /// The string to validate. + /// for a valid MSISDN number, otherwise. + public static bool IsValidMSISDN(string msisdn) + { + if (string.IsNullOrWhiteSpace(msisdn)) + return false; + + return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled); + } + } +} diff --git a/test/LinkMobility.Tests/Helpers/HttpMessageHandlerMock.cs b/test/LinkMobility.Tests/Helpers/HttpMessageHandlerMock.cs index 2060b67..28dc8d1 100644 --- a/test/LinkMobility.Tests/Helpers/HttpMessageHandlerMock.cs +++ b/test/LinkMobility.Tests/Helpers/HttpMessageHandlerMock.cs @@ -39,6 +39,8 @@ namespace LinkMobility.Tests.Helpers public Queue Responses { get; } = new(); public Mock Mock { get; } + + public IProtectedMock Protected => Mock.Protected(); } internal class HttpRequestMessageCallback diff --git a/test/LinkMobility.Tests/LinkMobilityClientTest.cs b/test/LinkMobility.Tests/LinkMobilityClientTest.cs index ed5a887..8e10922 100644 --- a/test/LinkMobility.Tests/LinkMobilityClientTest.cs +++ b/test/LinkMobility.Tests/LinkMobilityClientTest.cs @@ -145,7 +145,7 @@ namespace LinkMobility.Tests var client = GetClient(); // Act - var response = await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "test", _request, null, TestContext.CancellationToken); + var response = await client.PostAsync("test", _request, null, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); @@ -188,7 +188,7 @@ namespace LinkMobility.Tests var client = GetClient(); // Act - var response = await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "params/path", _request, queryParams, TestContext.CancellationToken); + var response = await client.PostAsync("params/path", _request, queryParams, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); @@ -256,7 +256,7 @@ namespace LinkMobility.Tests public void ShouldAssertClientOptions() { // Arrange + Act - var client = GetClient(); + _ = GetClient(); // Assert VerifyNoOtherCalls(); @@ -320,7 +320,7 @@ namespace LinkMobility.Tests // Act & Assert await Assert.ThrowsExactlyAsync(async () => { - await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "/request/path", _request, null, TestContext.CancellationToken); + await client.PostAsync("/request/path", _request, null, TestContext.CancellationToken); }); } @@ -336,7 +336,7 @@ namespace LinkMobility.Tests // Act & Assert await Assert.ThrowsExactlyAsync(async () => { - await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", path, _request, null, TestContext.CancellationToken); + await client.PostAsync(path, _request, null, TestContext.CancellationToken); }); } @@ -349,7 +349,7 @@ namespace LinkMobility.Tests // Act & Assert await Assert.ThrowsExactlyAsync(async () => { - await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "foo?bar=baz", _request, null, TestContext.CancellationToken); + await client.PostAsync("foo?bar=baz", _request, null, TestContext.CancellationToken); }); } @@ -366,7 +366,7 @@ namespace LinkMobility.Tests var client = GetClient(); // Act - var response = await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "/request/path", _request, null, TestContext.CancellationToken); + var response = await client.PostAsync("/request/path", _request, null, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); @@ -411,7 +411,7 @@ namespace LinkMobility.Tests var client = GetClient(); // Act - var response = await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "/request/path", stringContent, null, TestContext.CancellationToken); + var response = await client.PostAsync("/request/path", stringContent, null, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); @@ -455,7 +455,7 @@ namespace LinkMobility.Tests var client = GetClient(); // Act - var response = await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "posting", null, null, TestContext.CancellationToken); + var response = await client.PostAsync("posting", null, null, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); @@ -501,7 +501,7 @@ namespace LinkMobility.Tests // Act & Assert var ex = await Assert.ThrowsExactlyAsync(async () => { - await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "foo", _request, null, TestContext.CancellationToken); + await client.PostAsync("foo", _request, null, TestContext.CancellationToken); }); Assert.IsNull(ex.InnerException); Assert.AreEqual($"HTTP auth missing: {statusCode}", ex.Message); @@ -524,7 +524,7 @@ namespace LinkMobility.Tests // Act & Assert var ex = await Assert.ThrowsExactlyAsync(async () => { - await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "foo", _request, null, TestContext.CancellationToken); + await client.PostAsync("foo", _request, null, TestContext.CancellationToken); }); Assert.IsNull(ex.InnerException); Assert.AreEqual($"Unknown HTTP response: {statusCode}", ex.Message); @@ -545,7 +545,7 @@ namespace LinkMobility.Tests // Act & Assert await Assert.ThrowsExactlyAsync(async () => { - await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "some-path", _request, null, TestContext.CancellationToken); + await client.PostAsync("some-path", _request, null, TestContext.CancellationToken); }); } @@ -563,7 +563,7 @@ namespace LinkMobility.Tests var client = GetClient(); // Act - string response = await ReflectionHelper.InvokePrivateMethodAsync(client, "PostAsync", "path", _request, null, TestContext.CancellationToken); + string response = await client.PostAsync("path", _request, null, TestContext.CancellationToken); // Assert Assert.IsNotNull(response); diff --git a/test/LinkMobility.Tests/Sms/SendBinaryMessageTest.cs b/test/LinkMobility.Tests/Sms/SendBinaryMessageTest.cs index 80740de..8fc4fec 100644 --- a/test/LinkMobility.Tests/Sms/SendBinaryMessageTest.cs +++ b/test/LinkMobility.Tests/Sms/SendBinaryMessageTest.cs @@ -42,10 +42,7 @@ namespace LinkMobility.Tests.Sms _clientOptionsMock.Setup(c => c.AllowRedirects).Returns(true); _clientOptionsMock.Setup(c => c.UseProxy).Returns(false); - _request = new SendBinaryMessageRequest(["436991234567"]) - { - MessageContent = ["SGVsbG8gV29ybGQ="] // "Hello World" base64 - }; + _request = new SendBinaryMessageRequest(["SGVsbG8gV29ybGQ="], ["436991234567"]); // "Hello World" in Base64 } [TestMethod] @@ -87,14 +84,27 @@ namespace LinkMobility.Tests.Sms Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]); Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]); - _httpMessageHandlerMock.Mock - .Protected() - .Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); + _httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); _clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once); VerifyNoOtherCalls(); } + [TestMethod] + public void ShouldThrowOnInvalidContentCategoryForBinary() + { + // Arrange + _request.ContentCategory = 0; + var client = GetClient(); + + // Act & Assert + var ex = Assert.ThrowsExactly(() => client.SendBinaryMessage(_request, TestContext.CancellationToken)); + Assert.AreEqual("contentCategory", ex.ParamName); + Assert.StartsWith("Content category '0' is not valid.", ex.Message); + + VerifyNoOtherCalls(); + } + [TestMethod] public async Task ShouldSendBinaryMessageFullDetails() { @@ -145,9 +155,7 @@ namespace LinkMobility.Tests.Sms Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]); Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]); - _httpMessageHandlerMock.Mock - .Protected() - .Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); + _httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); _clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once); VerifyNoOtherCalls(); @@ -167,6 +175,36 @@ namespace LinkMobility.Tests.Sms VerifyNoOtherCalls(); } + [TestMethod] + public void ShouldThrowOnNullMessageContentList() + { + // Arrange + _request.MessageContent = null; + var client = GetClient(); + + // Act & Assert + var ex = Assert.ThrowsExactly(() => client.SendBinaryMessage(_request, TestContext.CancellationToken)); + + Assert.AreEqual("MessageContent", ex.ParamName); + + VerifyNoOtherCalls(); + } + + [TestMethod] + public void ShouldThrowOnEmptyMessageContentList() + { + // Arrange + _request.MessageContent = []; + var client = GetClient(); + + // Act & Assert + var ex = Assert.ThrowsExactly(() => client.SendBinaryMessage(_request, TestContext.CancellationToken)); + + Assert.AreEqual("MessageContent", ex.ParamName); + + VerifyNoOtherCalls(); + } + [TestMethod] public void ShouldThrowOnInvalidMessageEncoding() { @@ -205,7 +243,7 @@ namespace LinkMobility.Tests.Sms // Act & Assert var ex = Assert.ThrowsExactly(() => client.SendBinaryMessage(_request, TestContext.CancellationToken)); - Assert.AreEqual("RecipientAddressList", ex.ParamName); + Assert.AreEqual("recipientAddressList", ex.ParamName); VerifyNoOtherCalls(); } @@ -224,7 +262,7 @@ namespace LinkMobility.Tests.Sms // Act & Assert var ex = Assert.ThrowsExactly(() => client.SendBinaryMessage(_request, TestContext.CancellationToken)); - Assert.AreEqual("RecipientAddressList", ex.ParamName); + Assert.AreEqual("recipientAddressList", ex.ParamName); Assert.StartsWith($"Recipient address '{recipient}' is not a valid MSISDN format.", ex.Message); VerifyNoOtherCalls(); diff --git a/test/LinkMobility.Tests/Sms/SendTextMessageTest.cs b/test/LinkMobility.Tests/Sms/SendTextMessageTest.cs index 3ba925b..0229f6f 100644 --- a/test/LinkMobility.Tests/Sms/SendTextMessageTest.cs +++ b/test/LinkMobility.Tests/Sms/SendTextMessageTest.cs @@ -85,14 +85,27 @@ namespace LinkMobility.Tests.Sms Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]); Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]); - _httpMessageHandlerMock.Mock - .Protected() - .Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); + _httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); _clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once); VerifyNoOtherCalls(); } + [TestMethod] + public void ShouldThrowOnInvalidContentCategory() + { + // Arrange + _request.ContentCategory = 0; + var client = GetClient(); + + // Act & Assert + var ex = Assert.ThrowsExactly(() => client.SendTextMessage(_request, TestContext.CancellationToken)); + Assert.AreEqual("contentCategory", ex.ParamName); + Assert.StartsWith("Content category '0' is not valid.", ex.Message); + + VerifyNoOtherCalls(); + } + [TestMethod] public async Task ShouldSendTextMessageFullDetails() { @@ -145,9 +158,7 @@ namespace LinkMobility.Tests.Sms Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]); Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]); - _httpMessageHandlerMock.Mock - .Protected() - .Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); + _httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); _clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once); VerifyNoOtherCalls(); @@ -192,7 +203,7 @@ namespace LinkMobility.Tests.Sms // Act & Assert var ex = Assert.ThrowsExactly(() => client.SendTextMessage(req, TestContext.CancellationToken)); - Assert.AreEqual("RecipientAddressList", ex.ParamName); + Assert.AreEqual("recipientAddressList", ex.ParamName); VerifyNoOtherCalls(); } @@ -211,7 +222,7 @@ namespace LinkMobility.Tests.Sms // Act & Assert var ex = Assert.ThrowsExactly(() => client.SendTextMessage(req, TestContext.CancellationToken)); - Assert.AreEqual("RecipientAddressList", ex.ParamName); + Assert.AreEqual("recipientAddressList", ex.ParamName); Assert.StartsWith($"Recipient address '{recipient}' is not a valid MSISDN format.", ex.Message); VerifyNoOtherCalls(); diff --git a/test/LinkMobility.Tests/Utils/ValidationTest.cs b/test/LinkMobility.Tests/Utils/ValidationTest.cs new file mode 100644 index 0000000..d435dbe --- /dev/null +++ b/test/LinkMobility.Tests/Utils/ValidationTest.cs @@ -0,0 +1,33 @@ +using AMWD.Net.Api.LinkMobility.Utils; + +namespace LinkMobility.Tests.Utils +{ + [TestClass] + public class ValidationTest + { + [TestMethod] + [DataRow("10000000")] + [DataRow("12345678")] + [DataRow("123456789012345")] + [DataRow("14155552671")] + public void ShouldValidateMSISDNSuccessful(string msisdn) + { + Assert.IsTrue(Validation.IsValidMSISDN(msisdn)); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + [DataRow("012345678")] + [DataRow("+123456789")] + [DataRow("1234 5678")] + [DataRow("1234567")] + [DataRow("1234567890123456")] + [DataRow("abc1234567")] + public void ShouldValidateMSISDNNotSuccessful(string msisdn) + { + Assert.IsFalse(Validation.IsValidMSISDN(msisdn)); + } + } +}