Extensions instead of partial classes
All checks were successful
Branch Build / build-test-deploy (push) Successful in 44s
All checks were successful
Branch Build / build-test-deploy (push) Successful in 44s
This commit is contained in:
@@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [v0.1.1] - 2026-03-13
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ namespace AMWD.Net.Api.LinkMobility
|
|||||||
public interface ILinkMobilityClient
|
public interface ILinkMobilityClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a text message to a list of recipients.
|
/// Performs a POST request to the LINK mobility API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">The type of the response.</typeparam>
|
||||||
|
/// <typeparam name="TRequest">The type of the request.</typeparam>
|
||||||
|
/// <param name="requestPath">The path of the API endpoint.</param>
|
||||||
/// <param name="request">The request data.</param>
|
/// <param name="request">The request data.</param>
|
||||||
|
/// <param name="queryParams">Optional query parameters.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||||
Task<SendMessageResponse> SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default);
|
Task<TResponse> PostAsync<TResponse, TRequest>(string requestPath, TRequest? request, IQueryParameter? queryParams = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends a binary message to a list of recipients.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">The request data.</param>
|
|
||||||
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
|
||||||
Task<SendMessageResponse> SendBinaryMessage(SendBinaryMessageRequest request, CancellationToken cancellationToken = default);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AMWD.Net.Api.LinkMobility
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Implementation of text messaging (SMS). <see href="https://developer.linkmobility.eu/sms-api/rest-api">API</see>
|
|
||||||
/// </summary>
|
|
||||||
public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
|
||||||
{
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public Task<SendMessageResponse> 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<SendMessageResponse, SendTextMessageRequest>("/smsmessaging/text", request, cancellationToken: cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public Task<SendMessageResponse> 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<SendMessageResponse, SendBinaryMessageRequest>("/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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ namespace AMWD.Net.Api.LinkMobility
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a client for interacting with the Link Mobility API.
|
/// Provides a client for interacting with the Link Mobility API.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
public class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
||||||
{
|
{
|
||||||
private readonly ClientOptions _clientOptions;
|
private readonly ClientOptions _clientOptions;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
@@ -78,6 +78,52 @@ namespace AMWD.Net.Api.LinkMobility
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<TResponse> PostAsync<TResponse, TRequest>(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<TResponse>(httpResponse, cancellationToken).ConfigureAwait(false);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null)
|
||||||
|
{
|
||||||
|
string path = requestPath.Trim().TrimStart('/');
|
||||||
|
var param = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
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()
|
private void ValidateClientOptions()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
|
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
|
||||||
@@ -148,51 +194,6 @@ namespace AMWD.Net.Api.LinkMobility
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TResponse> PostAsync<TResponse, TRequest>(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<TResponse>(httpResponse, cancellationToken).ConfigureAwait(false);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildRequestUrl(string requestPath, IQueryParameter? queryParams = null)
|
|
||||||
{
|
|
||||||
string path = requestPath.Trim().TrimStart('/');
|
|
||||||
var param = new Dictionary<string, string>();
|
|
||||||
|
|
||||||
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>(T request)
|
private static HttpContent? ConvertRequest<T>(T request)
|
||||||
{
|
{
|
||||||
if (request == null)
|
if (request == null)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace AMWD.Net.Api.LinkMobility
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="Type.Text"/>, <see cref="Type.Binary"/>:
|
/// <see cref="Type.Text"/>, <see cref="Type.Binary"/>:
|
||||||
/// <br/>
|
/// <br/>
|
||||||
/// <see cref="AddressType.International"/> – defines the number format of the mobile originated <see cref="SenderAddress"/>.
|
/// <see cref="AddressType.International"/> - defines the number format of the mobile originated <see cref="SenderAddress"/>.
|
||||||
/// International numbers always includes the country prefix.
|
/// International numbers always includes the country prefix.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("senderAddressType")]
|
[JsonProperty("senderAddressType")]
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SendBinaryMessageRequest"/> class.
|
/// Initializes a new instance of the <see cref="SendBinaryMessageRequest"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="recipientAddressList">The recipient list.</param>
|
/// <param name="messageContent">A binary message as base64 encoded lines.</param>
|
||||||
public SendBinaryMessageRequest(IReadOnlyCollection<string> recipientAddressList)
|
/// <param name="recipientAddressList">A list of recipient numbers.</param>
|
||||||
|
public SendBinaryMessageRequest(IReadOnlyCollection<string> messageContent, IReadOnlyCollection<string> recipientAddressList)
|
||||||
{
|
{
|
||||||
|
MessageContent = messageContent;
|
||||||
RecipientAddressList = recipientAddressList;
|
RecipientAddressList = recipientAddressList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
/// The binary data is transmitted without being changed (using 8 bit alphabet).
|
/// The binary data is transmitted without being changed (using 8 bit alphabet).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonProperty("messageContent")]
|
[JsonProperty("messageContent")]
|
||||||
public IReadOnlyCollection<string>? MessageContent { get; set; }
|
public IReadOnlyCollection<string> MessageContent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <em>Optional</em>.
|
/// <em>Optional</em>.
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SendTextMessageRequest"/> class.
|
/// Initializes a new instance of the <see cref="SendTextMessageRequest"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="messageContent">The message.</param>
|
/// <param name="messageContent">A text message.</param>
|
||||||
/// <param name="recipientAddressList">The recipient list.</param>
|
/// <param name="recipientAddressList">A list of recipient numbers.</param>
|
||||||
public SendTextMessageRequest(string messageContent, IReadOnlyCollection<string> recipientAddressList)
|
public SendTextMessageRequest(string messageContent, IReadOnlyCollection<string> recipientAddressList)
|
||||||
{
|
{
|
||||||
MessageContent = messageContent;
|
MessageContent = messageContent;
|
||||||
|
|||||||
@@ -11,12 +11,6 @@
|
|||||||
[JsonProperty("clientMessageId")]
|
[JsonProperty("clientMessageId")]
|
||||||
public string? ClientMessageId { get; set; }
|
public string? ClientMessageId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The actual number of generated SMS.
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("smsCount")]
|
|
||||||
public int? SmsCount { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Status code.
|
/// Status code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
14
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
14
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Response of a text message sent to a list of recipients.
|
||||||
|
/// </summary>
|
||||||
|
public class SendTextMessageResponse : SendMessageResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The actual number of generated SMS.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("smsCount")]
|
||||||
|
public int? SmsCount { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/LinkMobility/TextMessageExtensions.cs
Normal file
78
src/LinkMobility/TextMessageExtensions.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Net.Api.LinkMobility.Utils;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implementation of text messaging (SMS). <see href="https://developer.linkmobility.eu/sms-api/rest-api">API</see>
|
||||||
|
/// </summary>
|
||||||
|
public static class TextMessageExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a text message to a list of recipients.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The <see cref="ILinkMobilityClient"/> instance.</param>
|
||||||
|
/// <param name="request">The request data.</param>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||||
|
public static Task<SendTextMessageResponse> 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<SendTextMessageResponse, SendTextMessageRequest>("/smsmessaging/text", request, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a binary message to a list of recipients.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The <see cref="ILinkMobilityClient"/> instance.</param>
|
||||||
|
/// <param name="request">The request data.</param>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||||
|
public static Task<SendTextMessageResponse> 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<SendTextMessageResponse, SendBinaryMessageRequest>("/smsmessaging/binary", request, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateRecipientList(IReadOnlyCollection<string>? 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/LinkMobility/Utils/Validation.cs
Normal file
25
src/LinkMobility/Utils/Validation.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validation helper for LINK Mobility API requirements.
|
||||||
|
/// </summary>
|
||||||
|
public static class Validation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates whether the provided string is a valid MSISDN (E.164 formatted).
|
||||||
|
/// <br/>
|
||||||
|
/// See <see href="https://en.wikipedia.org/wiki/MSISDN">Wikipedia: MSISDN</see> for more information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="msisdn">The string to validate.</param>
|
||||||
|
/// <returns><see langword="true"/> for a valid MSISDN number, <see langword="false"/> otherwise.</returns>
|
||||||
|
public static bool IsValidMSISDN(string msisdn)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(msisdn))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ namespace LinkMobility.Tests.Helpers
|
|||||||
public Queue<HttpResponseMessage> Responses { get; } = new();
|
public Queue<HttpResponseMessage> Responses { get; } = new();
|
||||||
|
|
||||||
public Mock<HttpClientHandler> Mock { get; }
|
public Mock<HttpClientHandler> Mock { get; }
|
||||||
|
|
||||||
|
public IProtectedMock<HttpClientHandler> Protected => Mock.Protected();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class HttpRequestMessageCallback
|
internal class HttpRequestMessageCallback
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ namespace LinkMobility.Tests
|
|||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "test", _request, null, TestContext.CancellationToken);
|
var response = await client.PostAsync<TestClass, TestClass>("test", _request, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(response);
|
Assert.IsNotNull(response);
|
||||||
@@ -188,7 +188,7 @@ namespace LinkMobility.Tests
|
|||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "params/path", _request, queryParams, TestContext.CancellationToken);
|
var response = await client.PostAsync<TestClass, TestClass>("params/path", _request, queryParams, TestContext.CancellationToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(response);
|
Assert.IsNotNull(response);
|
||||||
@@ -256,7 +256,7 @@ namespace LinkMobility.Tests
|
|||||||
public void ShouldAssertClientOptions()
|
public void ShouldAssertClientOptions()
|
||||||
{
|
{
|
||||||
// Arrange + Act
|
// Arrange + Act
|
||||||
var client = GetClient();
|
_ = GetClient();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
@@ -320,7 +320,7 @@ namespace LinkMobility.Tests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsExactlyAsync<ObjectDisposedException>(async () =>
|
await Assert.ThrowsExactlyAsync<ObjectDisposedException>(async () =>
|
||||||
{
|
{
|
||||||
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "/request/path", _request, null, TestContext.CancellationToken);
|
await client.PostAsync<object, TestClass>("/request/path", _request, null, TestContext.CancellationToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ namespace LinkMobility.Tests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsExactlyAsync<ArgumentNullException>(async () =>
|
await Assert.ThrowsExactlyAsync<ArgumentNullException>(async () =>
|
||||||
{
|
{
|
||||||
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", path, _request, null, TestContext.CancellationToken);
|
await client.PostAsync<object, TestClass>(path, _request, null, TestContext.CancellationToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +349,7 @@ namespace LinkMobility.Tests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsExactlyAsync<ArgumentException>(async () =>
|
await Assert.ThrowsExactlyAsync<ArgumentException>(async () =>
|
||||||
{
|
{
|
||||||
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "foo?bar=baz", _request, null, TestContext.CancellationToken);
|
await client.PostAsync<object, TestClass>("foo?bar=baz", _request, null, TestContext.CancellationToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +366,7 @@ namespace LinkMobility.Tests
|
|||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "/request/path", _request, null, TestContext.CancellationToken);
|
var response = await client.PostAsync<TestClass, TestClass>("/request/path", _request, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(response);
|
Assert.IsNotNull(response);
|
||||||
@@ -411,7 +411,7 @@ namespace LinkMobility.Tests
|
|||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "/request/path", stringContent, null, TestContext.CancellationToken);
|
var response = await client.PostAsync<TestClass, HttpContent>("/request/path", stringContent, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(response);
|
Assert.IsNotNull(response);
|
||||||
@@ -455,7 +455,7 @@ namespace LinkMobility.Tests
|
|||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "posting", null, null, TestContext.CancellationToken);
|
var response = await client.PostAsync<TestClass, object>("posting", null, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(response);
|
Assert.IsNotNull(response);
|
||||||
@@ -501,7 +501,7 @@ namespace LinkMobility.Tests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsExactlyAsync<AuthenticationException>(async () =>
|
var ex = await Assert.ThrowsExactlyAsync<AuthenticationException>(async () =>
|
||||||
{
|
{
|
||||||
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "foo", _request, null, TestContext.CancellationToken);
|
await client.PostAsync<object, TestClass>("foo", _request, null, TestContext.CancellationToken);
|
||||||
});
|
});
|
||||||
Assert.IsNull(ex.InnerException);
|
Assert.IsNull(ex.InnerException);
|
||||||
Assert.AreEqual($"HTTP auth missing: {statusCode}", ex.Message);
|
Assert.AreEqual($"HTTP auth missing: {statusCode}", ex.Message);
|
||||||
@@ -524,7 +524,7 @@ namespace LinkMobility.Tests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = await Assert.ThrowsExactlyAsync<ApplicationException>(async () =>
|
var ex = await Assert.ThrowsExactlyAsync<ApplicationException>(async () =>
|
||||||
{
|
{
|
||||||
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "foo", _request, null, TestContext.CancellationToken);
|
await client.PostAsync<object, TestClass>("foo", _request, null, TestContext.CancellationToken);
|
||||||
});
|
});
|
||||||
Assert.IsNull(ex.InnerException);
|
Assert.IsNull(ex.InnerException);
|
||||||
Assert.AreEqual($"Unknown HTTP response: {statusCode}", ex.Message);
|
Assert.AreEqual($"Unknown HTTP response: {statusCode}", ex.Message);
|
||||||
@@ -545,7 +545,7 @@ namespace LinkMobility.Tests
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
await Assert.ThrowsExactlyAsync<JsonReaderException>(async () =>
|
await Assert.ThrowsExactlyAsync<JsonReaderException>(async () =>
|
||||||
{
|
{
|
||||||
await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "some-path", _request, null, TestContext.CancellationToken);
|
await client.PostAsync<TestClass, TestClass>("some-path", _request, null, TestContext.CancellationToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +563,7 @@ namespace LinkMobility.Tests
|
|||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
string response = await ReflectionHelper.InvokePrivateMethodAsync<string>(client, "PostAsync", "path", _request, null, TestContext.CancellationToken);
|
string response = await client.PostAsync<string, TestClass>("path", _request, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsNotNull(response);
|
Assert.IsNotNull(response);
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
_clientOptionsMock.Setup(c => c.AllowRedirects).Returns(true);
|
_clientOptionsMock.Setup(c => c.AllowRedirects).Returns(true);
|
||||||
_clientOptionsMock.Setup(c => c.UseProxy).Returns(false);
|
_clientOptionsMock.Setup(c => c.UseProxy).Returns(false);
|
||||||
|
|
||||||
_request = new SendBinaryMessageRequest(["436991234567"])
|
_request = new SendBinaryMessageRequest(["SGVsbG8gV29ybGQ="], ["436991234567"]); // "Hello World" in Base64
|
||||||
{
|
|
||||||
MessageContent = ["SGVsbG8gV29ybGQ="] // "Hello World" base64
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
@@ -87,14 +84,27 @@ namespace LinkMobility.Tests.Sms
|
|||||||
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
||||||
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
||||||
|
|
||||||
_httpMessageHandlerMock.Mock
|
_httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
.Protected()
|
|
||||||
.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
|
||||||
|
|
||||||
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowOnInvalidContentCategoryForBinary()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_request.ContentCategory = 0;
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendBinaryMessage(_request, TestContext.CancellationToken));
|
||||||
|
Assert.AreEqual("contentCategory", ex.ParamName);
|
||||||
|
Assert.StartsWith("Content category '0' is not valid.", ex.Message);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ShouldSendBinaryMessageFullDetails()
|
public async Task ShouldSendBinaryMessageFullDetails()
|
||||||
{
|
{
|
||||||
@@ -145,9 +155,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
||||||
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
||||||
|
|
||||||
_httpMessageHandlerMock.Mock
|
_httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
.Protected()
|
|
||||||
.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
|
||||||
|
|
||||||
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
@@ -167,6 +175,36 @@ namespace LinkMobility.Tests.Sms
|
|||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowOnNullMessageContentList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_request.MessageContent = null;
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => 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<ArgumentException>(() => client.SendBinaryMessage(_request, TestContext.CancellationToken));
|
||||||
|
|
||||||
|
Assert.AreEqual("MessageContent", ex.ParamName);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ShouldThrowOnInvalidMessageEncoding()
|
public void ShouldThrowOnInvalidMessageEncoding()
|
||||||
{
|
{
|
||||||
@@ -205,7 +243,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendBinaryMessage(_request, TestContext.CancellationToken));
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendBinaryMessage(_request, TestContext.CancellationToken));
|
||||||
|
|
||||||
Assert.AreEqual("RecipientAddressList", ex.ParamName);
|
Assert.AreEqual("recipientAddressList", ex.ParamName);
|
||||||
|
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
@@ -224,7 +262,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendBinaryMessage(_request, TestContext.CancellationToken));
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => 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);
|
Assert.StartsWith($"Recipient address '{recipient}' is not a valid MSISDN format.", ex.Message);
|
||||||
|
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
|
|||||||
@@ -85,14 +85,27 @@ namespace LinkMobility.Tests.Sms
|
|||||||
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
||||||
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
||||||
|
|
||||||
_httpMessageHandlerMock.Mock
|
_httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
.Protected()
|
|
||||||
.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
|
||||||
|
|
||||||
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowOnInvalidContentCategory()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_request.ContentCategory = 0;
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(_request, TestContext.CancellationToken));
|
||||||
|
Assert.AreEqual("contentCategory", ex.ParamName);
|
||||||
|
Assert.StartsWith("Content category '0' is not valid.", ex.Message);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public async Task ShouldSendTextMessageFullDetails()
|
public async Task ShouldSendTextMessageFullDetails()
|
||||||
{
|
{
|
||||||
@@ -145,9 +158,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
Assert.AreEqual("Scheme Parameter", callback.Headers["Authorization"]);
|
||||||
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
Assert.AreEqual("LinkMobilityClient/1.0.0", callback.Headers["User-Agent"]);
|
||||||
|
|
||||||
_httpMessageHandlerMock.Mock
|
_httpMessageHandlerMock.Protected.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
.Protected()
|
|
||||||
.Verify("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
|
||||||
|
|
||||||
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
@@ -192,7 +203,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(req, TestContext.CancellationToken));
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(req, TestContext.CancellationToken));
|
||||||
Assert.AreEqual("RecipientAddressList", ex.ParamName);
|
Assert.AreEqual("recipientAddressList", ex.ParamName);
|
||||||
|
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
@@ -211,7 +222,7 @@ namespace LinkMobility.Tests.Sms
|
|||||||
// Act & Assert
|
// Act & Assert
|
||||||
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(req, TestContext.CancellationToken));
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => 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);
|
Assert.StartsWith($"Recipient address '{recipient}' is not a valid MSISDN format.", ex.Message);
|
||||||
|
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
|
|||||||
33
test/LinkMobility.Tests/Utils/ValidationTest.cs
Normal file
33
test/LinkMobility.Tests/Utils/ValidationTest.cs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user