Send Text Message
This commit is contained in:
130
.gitlab-ci.yml
Normal file
130
.gitlab-ci.yml
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
image: mcr.microsoft.com/dotnet/sdk:10.0
|
||||||
|
|
||||||
|
variables:
|
||||||
|
TZ: "Europe/Berlin"
|
||||||
|
LANG: "de"
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
build-debug:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
- 64bit
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG == null
|
||||||
|
script:
|
||||||
|
- dotnet build -c Debug --nologo
|
||||||
|
- mkdir ./artifacts
|
||||||
|
- shopt -s globstar
|
||||||
|
- mv ./**/*.nupkg ./artifacts/ || true
|
||||||
|
- mv ./**/*.snupkg ./artifacts/ || true
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- artifacts/*.nupkg
|
||||||
|
- artifacts/*.snupkg
|
||||||
|
expire_in: 1 days
|
||||||
|
|
||||||
|
test-debug:
|
||||||
|
stage: test
|
||||||
|
dependencies:
|
||||||
|
- build-debug
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
- 64bit
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG == null
|
||||||
|
coverage: /Branch coverage[\s\S].+%/
|
||||||
|
before_script:
|
||||||
|
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
|
||||||
|
script:
|
||||||
|
- dotnet test -c Debug --nologo /p:CoverletOutputFormat=Cobertura
|
||||||
|
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" "-reportType:TextSummary"
|
||||||
|
- cat /reports/Summary.txt
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: ./**/coverage.cobertura.xml
|
||||||
|
|
||||||
|
deploy-debug:
|
||||||
|
stage: deploy
|
||||||
|
dependencies:
|
||||||
|
- build-debug
|
||||||
|
- test-debug
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
- server
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG == null
|
||||||
|
script:
|
||||||
|
- dotnet nuget push -k $BAGET_APIKEY -s https://nuget.home.am-wd.de/v3/index.json --skip-duplicate artifacts/*.nupkg || true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
build-release:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
- 64bit
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG != null
|
||||||
|
script:
|
||||||
|
- dotnet build -c Release --nologo
|
||||||
|
- mkdir ./artifacts
|
||||||
|
- shopt -s globstar
|
||||||
|
- mv ./**/*.nupkg ./artifacts/
|
||||||
|
- mv ./**/*.snupkg ./artifacts/
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- artifacts/*.nupkg
|
||||||
|
- artifacts/*.snupkg
|
||||||
|
expire_in: 7 days
|
||||||
|
|
||||||
|
test-release:
|
||||||
|
stage: test
|
||||||
|
dependencies:
|
||||||
|
- build-release
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
- 64bit
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG != null
|
||||||
|
before_script:
|
||||||
|
- dotnet tool install dotnet-reportgenerator-globaltool --tool-path /dotnet-tools
|
||||||
|
script:
|
||||||
|
- dotnet test -c Release --nologo /p:CoverletOutputFormat=Cobertura
|
||||||
|
- /dotnet-tools/reportgenerator "-reports:${CI_PROJECT_DIR}/**/coverage.cobertura.xml" "-targetdir:/reports" -reportType:TextSummary
|
||||||
|
- cat /reports/Summary.txt
|
||||||
|
artifacts:
|
||||||
|
when: always
|
||||||
|
reports:
|
||||||
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
|
path: ./**/coverage.cobertura.xml
|
||||||
|
|
||||||
|
deploy-release:
|
||||||
|
stage: deploy
|
||||||
|
dependencies:
|
||||||
|
- build-release
|
||||||
|
- test-release
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
- lnx
|
||||||
|
- server
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_TAG != null
|
||||||
|
script:
|
||||||
|
- dotnet nuget push -k $NUGET_APIKEY -s https://api.nuget.org/v3/index.json --skip-duplicate artifacts/*.nupkg
|
||||||
|
|
||||||
24
LinkMobility.slnx
Normal file
24
LinkMobility.slnx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/Solution Items/" />
|
||||||
|
<Folder Name="/Solution Items/build/">
|
||||||
|
<File Path=".gitlab-ci.yml" />
|
||||||
|
<File Path="Directory.Build.props" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Solution Items/config/">
|
||||||
|
<File Path=".editorconfig" />
|
||||||
|
<File Path=".gitignore" />
|
||||||
|
<File Path="CodeMaid.config" />
|
||||||
|
<File Path="nuget.config" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/Solution Items/docs/">
|
||||||
|
<File Path="CHANGELOG.md" />
|
||||||
|
<File Path="LICENSE.txt" />
|
||||||
|
<File Path="README.md" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/LinkMobility/LinkMobility.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/test/">
|
||||||
|
<Project Path="test/LinkMobility.Tests/LinkMobility.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
31
src/LinkMobility/Auth/AccessTokenAuthentication.cs
Normal file
31
src/LinkMobility/Auth/AccessTokenAuthentication.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the <see cref="IAuthentication"/> interface for BEARER authentication.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokenAuthentication : IAuthentication
|
||||||
|
{
|
||||||
|
private readonly string _token;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AccessTokenAuthentication"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The bearer token.</param>
|
||||||
|
public AccessTokenAuthentication(string token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
throw new ArgumentNullException(nameof(token), "The token cannot be null or whitespace.");
|
||||||
|
|
||||||
|
_token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void AddHeader(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/LinkMobility/Auth/BasicAuthentication.cs
Normal file
42
src/LinkMobility/Auth/BasicAuthentication.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the <see cref="IAuthentication"/> interface for BASIC authentication.
|
||||||
|
/// </summary>
|
||||||
|
public class BasicAuthentication : IAuthentication
|
||||||
|
{
|
||||||
|
private readonly string _username;
|
||||||
|
private readonly string _password;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="BasicAuthentication"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username.</param>
|
||||||
|
/// <param name="password">The password.</param>
|
||||||
|
public BasicAuthentication(string username, string password)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
throw new ArgumentNullException(nameof(username), "The username cannot be null or whitespace.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(password))
|
||||||
|
throw new ArgumentNullException(nameof(password), "The password cannot be null or whitespace.");
|
||||||
|
|
||||||
|
_username = username;
|
||||||
|
_password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void AddHeader(HttpClient httpClient)
|
||||||
|
{
|
||||||
|
string plainText = $"{_username}:{_password}";
|
||||||
|
byte[] plainBytes = Encoding.ASCII.GetBytes(plainText);
|
||||||
|
string base64 = Convert.ToBase64String(plainBytes);
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/LinkMobility/Auth/IAuthentication.cs
Normal file
16
src/LinkMobility/Auth/IAuthentication.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the interface to add authentication information.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the required authentication header to the provided <see cref="HttpClient"/> instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpClient"></param>
|
||||||
|
void AddHeader(HttpClient httpClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/LinkMobility/ClientOptions.cs
Normal file
45
src/LinkMobility/ClientOptions.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Options for the LinkMobility API.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default base url for the API.
|
||||||
|
/// </summary>
|
||||||
|
public virtual string BaseUrl { get; set; } = "https://api.linkmobility.eu/rest/";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the default timeout for the API.
|
||||||
|
/// </summary>
|
||||||
|
public virtual TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(100);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets additional default headers to every request.
|
||||||
|
/// </summary>
|
||||||
|
public virtual IDictionary<string, string> DefaultHeaders { get; set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets additional default query parameters to every request.
|
||||||
|
/// </summary>
|
||||||
|
public virtual IDictionary<string, string> DefaultQueryParams { get; set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to allow redirects.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool AllowRedirects { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to use a proxy.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool UseProxy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the proxy information.
|
||||||
|
/// </summary>
|
||||||
|
public virtual IWebProxy Proxy { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/LinkMobility/Enums/ContentCategory.cs
Normal file
24
src/LinkMobility/Enums/ContentCategory.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Content categories as defined by <see href="https://developer.linkmobility.eu/sms-api/rest-api#operation/sendUsingPOST">Link Mobility</see>.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public enum ContentCategory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents content that is classified as informational.
|
||||||
|
/// </summary>
|
||||||
|
[EnumMember(Value = "informational")]
|
||||||
|
Informational = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents content that is classified as an advertisement.
|
||||||
|
/// </summary>
|
||||||
|
[EnumMember(Value = "advertisement")]
|
||||||
|
Advertisement = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/LinkMobility/Enums/MessageType.cs
Normal file
18
src/LinkMobility/Enums/MessageType.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the message type.
|
||||||
|
/// </summary>
|
||||||
|
public enum MessageType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The message is sent as defined in the account settings.
|
||||||
|
/// </summary>
|
||||||
|
Default = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message is sent as voice call.
|
||||||
|
/// </summary>
|
||||||
|
Voice = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/LinkMobility/Enums/SenderAddressType.cs
Normal file
36
src/LinkMobility/Enums/SenderAddressType.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies the type of sender address.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public enum SenderAddressType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// National number.
|
||||||
|
/// </summary>
|
||||||
|
[EnumMember(Value = "national")]
|
||||||
|
National = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// International number.
|
||||||
|
/// </summary>
|
||||||
|
[EnumMember(Value = "international")]
|
||||||
|
International = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alphanumeric sender ID.
|
||||||
|
/// </summary>
|
||||||
|
[EnumMember(Value = "alphanumeric")]
|
||||||
|
Alphanumeric = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shortcode.
|
||||||
|
/// </summary>
|
||||||
|
[EnumMember(Value = "shortcode")]
|
||||||
|
Shortcode = 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/LinkMobility/Enums/StatusCodes.cs
Normal file
149
src/LinkMobility/Enums/StatusCodes.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom status codes as defined by <see href="https://developer.linkmobility.eu/sms-api/rest-api#section/Status-codes">Link Mobility</see>.
|
||||||
|
/// </summary>
|
||||||
|
public enum StatusCodes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request accepted, Message(s) sent.
|
||||||
|
/// </summary>
|
||||||
|
Ok = 2000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request accepted, Message(s) queued.
|
||||||
|
/// </summary>
|
||||||
|
OkQueued = 2001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid Credentials. Inactive account or customer.
|
||||||
|
/// </summary>
|
||||||
|
InvalidCredentials = 4001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One or more recipients are not in the correct format or are containing invalid MSISDNs.
|
||||||
|
/// </summary>
|
||||||
|
InvalidRecipient = 4002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid Sender. Sender address or type is invalid.
|
||||||
|
/// </summary>
|
||||||
|
InvalidSender = 4003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid messageType.
|
||||||
|
/// </summary>
|
||||||
|
InvalidMessageType = 4004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid clientMessageId.
|
||||||
|
/// </summary>
|
||||||
|
InvalidMessageId = 4008,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message text (messageContent) is invalid.
|
||||||
|
/// </summary>
|
||||||
|
InvalidText = 4009,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message limit is reached.
|
||||||
|
/// </summary>
|
||||||
|
MessageLimitExceeded = 4013,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sender IP address is not authorized.
|
||||||
|
/// </summary>
|
||||||
|
UnauthorizedIp = 4014,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid Message Priority.
|
||||||
|
/// </summary>
|
||||||
|
InvalidMessagePriority = 4015,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid notification callback url.
|
||||||
|
/// </summary>
|
||||||
|
InvalidCallbackAddress = 4016,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A required parameter was not given. The parameter name is shown in the status message.
|
||||||
|
/// </summary>
|
||||||
|
ParameterMissing = 4019,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Account is invalid.
|
||||||
|
/// </summary>
|
||||||
|
InvalidAccount = 4021,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access to the API was denied.
|
||||||
|
/// </summary>
|
||||||
|
AccessDenied = 4022,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request limit exceeded for this IP address.
|
||||||
|
/// </summary>
|
||||||
|
ThrottlingSpammingIp = 4023,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Transfer rate for immediate transmissions exceeded.
|
||||||
|
/// Too many recipients in this request (1000).
|
||||||
|
/// </summary>
|
||||||
|
ThrottlingTooManyRecipients = 4025,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The message content results in too many (automatically generated) sms segments.
|
||||||
|
/// </summary>
|
||||||
|
MaxSmsPerMessageExceeded = 4026,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A message content segment is invalid
|
||||||
|
/// </summary>
|
||||||
|
InvalidMessageSegment = 4027,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recipients not allowed.
|
||||||
|
/// </summary>
|
||||||
|
RecipientsNotAllowed = 4029,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All recipients blacklisted.
|
||||||
|
/// </summary>
|
||||||
|
RecipientBlacklisted = 4030,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not allowed to send sms messages.
|
||||||
|
/// </summary>
|
||||||
|
SmsDisabled = 4035,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid content category.
|
||||||
|
/// </summary>
|
||||||
|
InvalidContentCategory = 4040,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid validity periode.
|
||||||
|
/// </summary>
|
||||||
|
InvalidValidityPeriode = 4041,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All of the recipients are blocked by quality rating.
|
||||||
|
/// </summary>
|
||||||
|
RecipientsBlockedByQualityRating = 4042,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All of the recipients are blocked by spamcheck.
|
||||||
|
/// </summary>
|
||||||
|
RecipientsBlockedBySpamcheck = 4043,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal error.
|
||||||
|
/// </summary>
|
||||||
|
InternalError = 5000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service unavailable.
|
||||||
|
/// </summary>
|
||||||
|
ServiceUnavailable = 5003
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/LinkMobility/ILinkMobilityClient.cs
Normal file
19
src/LinkMobility/ILinkMobilityClient.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Net.Api.LinkMobility.Requests;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the interface for a Link Mobility API client.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILinkMobilityClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a text 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<SendTextMessageResponse> SendTextMessage(SendTextMessageRequest request, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/LinkMobility/LinkMobility.csproj
Normal file
33
src/LinkMobility/LinkMobility.csproj
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<PackageId>AMWD.Net.Api.LinkMobility</PackageId>
|
||||||
|
<PackageTags>link mobility api</PackageTags>
|
||||||
|
|
||||||
|
<AssemblyName>amwd-linkmobility</AssemblyName>
|
||||||
|
<RootNamespace>AMWD.Net.Api.LinkMobility</RootNamespace>
|
||||||
|
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<EmbedUntrackedSources>false</EmbedUntrackedSources>
|
||||||
|
|
||||||
|
<PackageIcon>package-icon.png</PackageIcon>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
|
||||||
|
<Product>LinkMobility API</Product>
|
||||||
|
<Description>Implementation of the Link Mobility REST API</Description>
|
||||||
|
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="../../package-icon.png" Pack="true" PackagePath="/" />
|
||||||
|
<None Include="../../LICENSE.txt" Pack="true" PackagePath="/" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
43
src/LinkMobility/LinkMobilityClient.Messaging.cs
Normal file
43
src/LinkMobility/LinkMobilityClient.Messaging.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Net.Api.LinkMobility.Requests;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
public partial class LinkMobilityClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a text message to a list of recipients.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request.</param>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to propagate notification that operations should be canceled.</param>
|
||||||
|
public Task<SendTextMessageResponse> 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<SendTextMessageResponse, SendTextMessageRequest>("/smsmessaging/text", request, cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidMSISDN(string msisdn)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(msisdn))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Regex.IsMatch(msisdn, @"^[1-9][0-9]{7,14}$", RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/LinkMobility/LinkMobilityClient.cs
Normal file
223
src/LinkMobility/LinkMobilityClient.cs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a client for interacting with the Link Mobility API.
|
||||||
|
/// </summary>
|
||||||
|
public partial class LinkMobilityClient : ILinkMobilityClient, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerSettings _jsonSerializerSettings = new()
|
||||||
|
{
|
||||||
|
Culture = CultureInfo.InvariantCulture,
|
||||||
|
Formatting = Formatting.None,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly ClientOptions _clientOptions;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LinkMobilityClient" /> class using basic authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The username used for basic authentication.</param>
|
||||||
|
/// <param name="password">The password used for basic authentication.</param>
|
||||||
|
/// <param name="clientOptions">Optional configuration settings for the client.</param>
|
||||||
|
public LinkMobilityClient(string username, string password, ClientOptions? clientOptions = null)
|
||||||
|
: this(new BasicAuthentication(username, password), clientOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LinkMobilityClient"/> class using access token authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">The bearer token used for authentication.</param>
|
||||||
|
/// <param name="clientOptions">Optional configuration settings for the client.</param>
|
||||||
|
public LinkMobilityClient(string token, ClientOptions? clientOptions = null)
|
||||||
|
: this(new AccessTokenAuthentication(token), clientOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LinkMobilityClient"/> class using authentication and optional client
|
||||||
|
/// configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authentication">The authentication mechanism used to authorize requests.</param>
|
||||||
|
/// <param name="clientOptions">Optional client configuration settings.</param>
|
||||||
|
public LinkMobilityClient(IAuthentication authentication, ClientOptions? clientOptions = null)
|
||||||
|
{
|
||||||
|
if (authentication == null)
|
||||||
|
throw new ArgumentNullException(nameof(authentication));
|
||||||
|
|
||||||
|
_clientOptions = clientOptions ?? new ClientOptions();
|
||||||
|
ValidateClientOptions();
|
||||||
|
|
||||||
|
_httpClient = CreateHttpClient();
|
||||||
|
authentication.AddHeader(_httpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes of the resources used by the <see cref="LinkMobilityClient"/> object.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
_httpClient.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateClientOptions()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_clientOptions.BaseUrl))
|
||||||
|
throw new ArgumentNullException(nameof(_clientOptions.BaseUrl), "BaseUrl cannot be null or whitespace.");
|
||||||
|
|
||||||
|
if (_clientOptions.Timeout <= TimeSpan.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(_clientOptions.Timeout), "Timeout must be greater than zero.");
|
||||||
|
|
||||||
|
if (_clientOptions.UseProxy && _clientOptions.Proxy == null)
|
||||||
|
throw new ArgumentNullException(nameof(_clientOptions.Proxy), "Proxy cannot be null when UseProxy is true.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateRequestPath(string requestPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(requestPath))
|
||||||
|
throw new ArgumentNullException(nameof(requestPath), "The request path is required");
|
||||||
|
|
||||||
|
if (requestPath.Contains('?'))
|
||||||
|
throw new ArgumentException("Query parameters are not allowed in the request path", nameof(requestPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient CreateHttpClient()
|
||||||
|
{
|
||||||
|
string version = GetType().Assembly
|
||||||
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||||
|
?.InformationalVersion ?? "unknown";
|
||||||
|
|
||||||
|
HttpMessageHandler handler;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = _clientOptions.AllowRedirects,
|
||||||
|
UseProxy = _clientOptions.UseProxy,
|
||||||
|
Proxy = _clientOptions.Proxy
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException)
|
||||||
|
{
|
||||||
|
handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = _clientOptions.AllowRedirects
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string baseUrl = _clientOptions.BaseUrl.Trim().TrimEnd('/');
|
||||||
|
|
||||||
|
var client = new HttpClient(handler, disposeHandler: true)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri($"{baseUrl}/"),
|
||||||
|
Timeout = _clientOptions.Timeout
|
||||||
|
};
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(nameof(LinkMobilityClient), version));
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
if (_clientOptions.DefaultHeaders.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var headerKvp in _clientOptions.DefaultHeaders)
|
||||||
|
client.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 response = await _httpClient.PostAsync(requestUrl, httpContent, cancellationToken).ConfigureAwait(false);
|
||||||
|
return await GetResponse<TResponse>(response, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (request is HttpContent httpContent)
|
||||||
|
return httpContent;
|
||||||
|
|
||||||
|
string json = JsonConvert.SerializeObject(request, _jsonSerializerSettings);
|
||||||
|
return new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<TResponse> GetResponse<TResponse>(HttpResponseMessage httpResponse, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
string content = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
#else
|
||||||
|
string content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return httpResponse.StatusCode switch
|
||||||
|
{
|
||||||
|
HttpStatusCode.Unauthorized => throw new AuthenticationException($"HTTP auth missing: {httpResponse.StatusCode}"),
|
||||||
|
HttpStatusCode.Forbidden => throw new AuthenticationException($"HTTP auth missing: {httpResponse.StatusCode}"),
|
||||||
|
HttpStatusCode.OK =>
|
||||||
|
JsonConvert.DeserializeObject<TResponse>(content, _jsonSerializerSettings)
|
||||||
|
?? throw new ApplicationException("Response could not be deserialized"),
|
||||||
|
_ => throw new ApplicationException($"Unknown HTTP response: {httpResponse.StatusCode}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
throw new ObjectDisposedException(GetType().FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/LinkMobility/LinkMobilityResponse.cs
Normal file
20
src/LinkMobility/LinkMobilityResponse.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
public class LinkMobilityResponse
|
||||||
|
{
|
||||||
|
[JsonProperty("clientMessageId")]
|
||||||
|
public string ClientMessageId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("smsCount")]
|
||||||
|
public int SmsCount { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("statusCode")]
|
||||||
|
public StatusCodes StatusCode { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("statusMessage")]
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("transferId")]
|
||||||
|
public string TransferId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/LinkMobility/QueryParameters/IQueryParameter.cs
Normal file
12
src/LinkMobility/QueryParameters/IQueryParameter.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents options defined via query parameters.
|
||||||
|
/// </summary>
|
||||||
|
public interface IQueryParameter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the query parameters.
|
||||||
|
IReadOnlyDictionary<string, string> GetQueryParameters();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/LinkMobility/README.md
Normal file
18
src/LinkMobility/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# LinkMobility API
|
||||||
|
|
||||||
|
This project aims to implement the [LinkMobility REST API].
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Link Mobility is a provider for communication with customers via SMS, RCS or WhatsApp.
|
||||||
|
|
||||||
|
With this project the REST API of Link Mobility will be implemented.
|
||||||
|
|
||||||
|
- [SMS API](https://developer.linkmobility.eu/sms-api/rest-api)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Published under MIT License (see [**tl;dr**Legal])
|
||||||
|
|
||||||
|
[LinkMobility REST API]: https://developer.linkmobility.eu/
|
||||||
|
[**tl;dr**Legal]: https://www.tldrlegal.com/license/mit-license
|
||||||
139
src/LinkMobility/Requests/SendTextMessageRequest.cs
Normal file
139
src/LinkMobility/Requests/SendTextMessageRequest.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility.Requests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request to send a text message to a list of recipients.
|
||||||
|
/// </summary>
|
||||||
|
public class SendTextMessageRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SendTextMessageRequest"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageContent">The message.</param>
|
||||||
|
/// <param name="recipientAddressList">The recipient list.</param>
|
||||||
|
public SendTextMessageRequest(string messageContent, IReadOnlyCollection<string> recipientAddressList)
|
||||||
|
{
|
||||||
|
MessageContent = messageContent;
|
||||||
|
RecipientAddressList = recipientAddressList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// May contain a freely definable message id.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("clientMessageId")]
|
||||||
|
public string? ClientMessageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// The content category that is used to categorize the message (used for blacklisting).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The following content categories are supported: <see cref="ContentCategory.Informational"/> or <see cref="ContentCategory.Advertisement"/>.
|
||||||
|
/// If no content category is provided, the default setting is used (may be changed inside the web interface).
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("contentCategory")]
|
||||||
|
public ContentCategory? ContentCategory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// Specifies the maximum number of SMS to be generated.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If the system generates more than this number of SMS, the status code <see cref="StatusCodes.MaxSmsPerMessageExceeded"/> is returned.
|
||||||
|
/// The default value of this parameter is <c>0</c>.
|
||||||
|
/// If set to <c>0</c>, no limitation is applied.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("maxSmsPerMessage")]
|
||||||
|
public int? MaxSmsPerMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>UTF-8</em> encoded message content.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("messageContent")]
|
||||||
|
public string MessageContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// Specifies the message type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Allowed values are <see cref="MessageType.Default"/> and <see cref="MessageType.Voice"/>.
|
||||||
|
/// When using the message type <see cref="MessageType.Default"/>, the outgoing message type is determined based on account settings.
|
||||||
|
/// Using the message type <see cref="MessageType.Voice"/> triggers a voice call.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("messageType")]
|
||||||
|
public MessageType? MessageType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// When setting a <c>NotificationCallbackUrl</c> all delivery reports are forwarded to this URL.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("notificationCallbackUrl")]
|
||||||
|
public string? NotificationCallbackUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// Priority of the message.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Must not exceed the value configured for the account used to send the message.
|
||||||
|
/// For more information please contact our customer service.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("priority")]
|
||||||
|
public int? Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of recipients (E.164 formatted <see href="https://en.wikipedia.org/wiki/MSISDN">MSISDN</see>s)
|
||||||
|
/// to whom the message should be sent.
|
||||||
|
/// <br/>
|
||||||
|
/// The list of recipients may contain a maximum of <em>1000</em> entries.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("recipientAddressList")]
|
||||||
|
public IReadOnlyCollection<string> RecipientAddressList { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// <br/>
|
||||||
|
/// <see langword="true"/>: The message is sent as flash SMS (displayed directly on the screen of the mobile phone).
|
||||||
|
/// <br/>
|
||||||
|
/// <see langword="false"/>: The message is sent as standard text SMS (default).
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("sendAsFlashSms")]
|
||||||
|
public bool? SendAsFlashSms { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// Address of the sender (assigned to the account) from which the message is sent.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("senderAddress")]
|
||||||
|
public string? SenderAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// The sender address type.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("senderAddressType")]
|
||||||
|
public SenderAddressType? SenderAddressType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// <br/>
|
||||||
|
/// <see langword="true"/>: The transmission is only simulated, no SMS is sent.
|
||||||
|
/// Depending on the number of recipients the status code <see cref="StatusCodes.Ok"/> or <see cref="StatusCodes.OkQueued"/> is returned.
|
||||||
|
/// <br/>
|
||||||
|
/// <see langword="false"/>: No simulation is done. The SMS is sent via the SMS Gateway. (default)
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("test")]
|
||||||
|
public bool? Test { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <em>Optional</em>.
|
||||||
|
/// Specifies the validity periode (in seconds) in which the message is tried to be delivered to the recipient.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A minimum of 1 minute (<c>60</c> seconds) and a maximum of 3 days (<c>259200</c> seconds) are allowed.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonProperty("validityPeriode")]
|
||||||
|
public int? ValidityPeriode { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
38
src/LinkMobility/Responses/SendTextMessageResponse.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
namespace AMWD.Net.Api.LinkMobility
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Response of a text message sent to a list of recipients.
|
||||||
|
/// </summary>
|
||||||
|
public class SendTextMessageResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the message id defined in the request.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("clientMessageId")]
|
||||||
|
public string? ClientMessageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The actual number of generated SMS.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("smsCount")]
|
||||||
|
public int? SmsCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status code.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("statusCode")]
|
||||||
|
public StatusCodes? StatusCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Description of the response status code.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("statusMessage")]
|
||||||
|
public string? StatusMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier that is set after successful processing of the request.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("transferId")]
|
||||||
|
public string? TransferId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using AMWD.Net.Api.LinkMobility;
|
||||||
|
|
||||||
|
namespace LinkMobility.Tests.Auth
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class AccessTokenAuthenticationTest
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldAddHeader()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string token = "test_token";
|
||||||
|
var auth = new AccessTokenAuthentication(token);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
auth.AddHeader(httpClient);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(httpClient.DefaultRequestHeaders.Contains("Authorization"));
|
||||||
|
|
||||||
|
Assert.AreEqual("Bearer", httpClient.DefaultRequestHeaders.Authorization.Scheme);
|
||||||
|
Assert.AreEqual(token, httpClient.DefaultRequestHeaders.Authorization.Parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public void ShouldThrowArgumentNullExceptionForToken(string token)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsExactly<ArgumentNullException>(() => new AccessTokenAuthentication(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
test/LinkMobility.Tests/Auth/BasicAuthenticationTest.cs
Normal file
60
test/LinkMobility.Tests/Auth/BasicAuthenticationTest.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using AMWD.Net.Api.LinkMobility;
|
||||||
|
|
||||||
|
namespace LinkMobility.Tests.Auth
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class BasicAuthenticationTest
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldAddHeader()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string username = "user";
|
||||||
|
string password = "pass";
|
||||||
|
var auth = new BasicAuthentication(username, password);
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
auth.AddHeader(httpClient);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(httpClient.DefaultRequestHeaders.Contains("Authorization"));
|
||||||
|
|
||||||
|
Assert.AreEqual("Basic", httpClient.DefaultRequestHeaders.Authorization.Scheme);
|
||||||
|
Assert.AreEqual(Base64(username, password), httpClient.DefaultRequestHeaders.Authorization.Parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public void ShouldThrowArgumentNullExceptionForUsername(string username)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsExactly<ArgumentNullException>(() => new BasicAuthentication(username, "pass"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public void ShouldThrowArgumentNullExceptionForPassword(string password)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsExactly<ArgumentNullException>(() => new BasicAuthentication("user", password));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Base64(string user, string pass)
|
||||||
|
{
|
||||||
|
string plainText = $"{user}:{pass}";
|
||||||
|
return Convert.ToBase64String(Encoding.ASCII.GetBytes(plainText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
test/LinkMobility.Tests/Helpers/HttpMessageHandlerMock.cs
Normal file
56
test/LinkMobility.Tests/Helpers/HttpMessageHandlerMock.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq.Protected;
|
||||||
|
|
||||||
|
namespace LinkMobility.Tests.Helpers
|
||||||
|
{
|
||||||
|
internal class HttpMessageHandlerMock
|
||||||
|
{
|
||||||
|
public HttpMessageHandlerMock()
|
||||||
|
{
|
||||||
|
Mock = new();
|
||||||
|
|
||||||
|
Mock.Protected()
|
||||||
|
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
|
||||||
|
.Callback<HttpRequestMessage, CancellationToken>(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var callback = new HttpRequestMessageCallback
|
||||||
|
{
|
||||||
|
HttpMethod = request.Method,
|
||||||
|
Url = request.RequestUri.ToString(),
|
||||||
|
Headers = request.Headers.ToDictionary(h => h.Key, h => string.Join(", ", h.Value))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.Content != null)
|
||||||
|
{
|
||||||
|
callback.ContentRaw = await request.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
callback.Content = await request.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestCallbacks.Add(callback);
|
||||||
|
})
|
||||||
|
.ReturnsAsync(Responses.Dequeue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<HttpRequestMessageCallback> RequestCallbacks { get; } = [];
|
||||||
|
|
||||||
|
public Queue<HttpResponseMessage> Responses { get; } = new();
|
||||||
|
|
||||||
|
public Mock<HttpClientHandler> Mock { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class HttpRequestMessageCallback
|
||||||
|
{
|
||||||
|
public HttpMethod HttpMethod { get; set; }
|
||||||
|
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, string> Headers { get; set; }
|
||||||
|
|
||||||
|
public byte[] ContentRaw { get; set; }
|
||||||
|
|
||||||
|
public string Content { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
64
test/LinkMobility.Tests/Helpers/ReflectionHelper.cs
Normal file
64
test/LinkMobility.Tests/Helpers/ReflectionHelper.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LinkMobility.Tests.Helpers
|
||||||
|
{
|
||||||
|
internal static class ReflectionHelper
|
||||||
|
{
|
||||||
|
public static T GetPrivateField<T>(object obj, string fieldName)
|
||||||
|
{
|
||||||
|
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (field == null)
|
||||||
|
throw new ArgumentException($"Field '{fieldName}' not found in type '{obj.GetType().FullName}'.");
|
||||||
|
|
||||||
|
return (T)field.GetValue(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetPrivateField<T>(object obj, string fieldName, T value)
|
||||||
|
{
|
||||||
|
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (field == null)
|
||||||
|
throw new ArgumentException($"Field '{fieldName}' not found in type '{obj.GetType().FullName}'.");
|
||||||
|
|
||||||
|
field.SetValue(obj, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<TResult> InvokePrivateMethodAsync<TResult>(object obj, string methodName, params object[] parameters)
|
||||||
|
{
|
||||||
|
var method = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
if (method == null)
|
||||||
|
throw new ArgumentException($"Method '{methodName}' not found in type '{obj.GetType().FullName}'.");
|
||||||
|
|
||||||
|
// If the method is a generic method definition, construct it with concrete type arguments.
|
||||||
|
if (method.IsGenericMethodDefinition)
|
||||||
|
{
|
||||||
|
var genericArgs = method.GetGenericArguments();
|
||||||
|
var typeArgs = new Type[genericArgs.Length];
|
||||||
|
|
||||||
|
// First generic argument is the return type (TResult)
|
||||||
|
if (typeArgs.Length > 0)
|
||||||
|
typeArgs[0] = typeof(TResult);
|
||||||
|
|
||||||
|
// For additional generic arguments (e.g., TRequest) try to infer from provided parameters
|
||||||
|
if (typeArgs.Length > 1)
|
||||||
|
{
|
||||||
|
// Common pattern: second generic parameter corresponds to the second method parameter (index 1)
|
||||||
|
Type inferred = typeof(object);
|
||||||
|
if (parameters.Length > 1 && parameters[1] != null)
|
||||||
|
inferred = parameters[1].GetType();
|
||||||
|
|
||||||
|
for (int i = 1; i < typeArgs.Length; i++)
|
||||||
|
typeArgs[i] = inferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
method = method.MakeGenericMethod(typeArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = (Task)method.Invoke(obj, parameters);
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
var resultProperty = task.GetType().GetProperty("Result");
|
||||||
|
return (TResult)resultProperty.GetValue(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
test/LinkMobility.Tests/LinkMobility.Tests.csproj
Normal file
32
test/LinkMobility.Tests/LinkMobility.Tests.csproj
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
<CollectCoverage>true</CollectCoverage>
|
||||||
|
<CoverletOutputFormat>Cobertura</CoverletOutputFormat>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="4.0.2" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\LinkMobility\LinkMobility.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||||
|
<Using Include="Moq" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
653
test/LinkMobility.Tests/LinkMobilityClientTest.cs
Normal file
653
test/LinkMobility.Tests/LinkMobilityClientTest.cs
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Authentication;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Net.Api.LinkMobility;
|
||||||
|
using LinkMobility.Tests.Helpers;
|
||||||
|
using Moq.Protected;
|
||||||
|
|
||||||
|
namespace LinkMobility.Tests
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class LinkMobilityClientTest
|
||||||
|
{
|
||||||
|
public TestContext TestContext { get; set; }
|
||||||
|
|
||||||
|
private const string BASE_URL = "https://localhost/rest/";
|
||||||
|
|
||||||
|
private Mock<IAuthentication> _authenticationMock;
|
||||||
|
private Mock<ClientOptions> _clientOptionsMock;
|
||||||
|
private HttpMessageHandlerMock _httpMessageHandlerMock;
|
||||||
|
|
||||||
|
private TestClass _request;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_authenticationMock = new Mock<IAuthentication>();
|
||||||
|
_clientOptionsMock = new Mock<ClientOptions>();
|
||||||
|
_httpMessageHandlerMock = new HttpMessageHandlerMock();
|
||||||
|
|
||||||
|
_authenticationMock
|
||||||
|
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
|
||||||
|
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Scheme", "Parameter"));
|
||||||
|
|
||||||
|
_clientOptionsMock.Setup(c => c.BaseUrl).Returns(BASE_URL);
|
||||||
|
_clientOptionsMock.Setup(c => c.Timeout).Returns(TimeSpan.FromSeconds(30));
|
||||||
|
_clientOptionsMock.Setup(c => c.DefaultHeaders).Returns(new Dictionary<string, string>());
|
||||||
|
_clientOptionsMock.Setup(c => c.DefaultQueryParams).Returns(new Dictionary<string, string>());
|
||||||
|
_clientOptionsMock.Setup(c => c.AllowRedirects).Returns(true);
|
||||||
|
_clientOptionsMock.Setup(c => c.UseProxy).Returns(false);
|
||||||
|
|
||||||
|
_request = new()
|
||||||
|
{
|
||||||
|
Str = "Happy Testing",
|
||||||
|
Int = 54321
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldInitializeWithBasicAuth()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string username = "user";
|
||||||
|
string password = "pass";
|
||||||
|
string expectedParameter = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using var client = new LinkMobilityClient(username, password);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpClient = ReflectionHelper.GetPrivateField<HttpClient>(client, "_httpClient");
|
||||||
|
|
||||||
|
Assert.IsNotNull(httpClient);
|
||||||
|
Assert.IsNotNull(httpClient.DefaultRequestHeaders.Authorization);
|
||||||
|
Assert.AreEqual("Basic", httpClient.DefaultRequestHeaders.Authorization.Scheme);
|
||||||
|
Assert.AreEqual(expectedParameter, httpClient.DefaultRequestHeaders.Authorization.Parameter);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldInitializeWithBearerAuth()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string token = "test_token";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using var client = new LinkMobilityClient(token);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpClient = ReflectionHelper.GetPrivateField<HttpClient>(client, "_httpClient");
|
||||||
|
|
||||||
|
Assert.IsNotNull(httpClient);
|
||||||
|
Assert.IsNotNull(httpClient.DefaultRequestHeaders.Authorization);
|
||||||
|
Assert.AreEqual("Bearer", httpClient.DefaultRequestHeaders.Authorization.Scheme);
|
||||||
|
Assert.AreEqual(token, httpClient.DefaultRequestHeaders.Authorization.Parameter);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowOnNullAuthentication()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentNullException>(() => new LinkMobilityClient((IAuthentication)null));
|
||||||
|
|
||||||
|
Assert.AreEqual("authentication", ex.ParamName);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldAddCustomDefaultHeaders()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var clientOptions = new ClientOptions();
|
||||||
|
clientOptions.DefaultHeaders.Add("SomeKey", "SomeValue");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
using var client = new LinkMobilityClient("token", clientOptions);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var httpClient = ReflectionHelper.GetPrivateField<HttpClient>(client, "_httpClient");
|
||||||
|
|
||||||
|
Assert.IsNotNull(httpClient);
|
||||||
|
Assert.IsTrue(httpClient.DefaultRequestHeaders.Contains("SomeKey"));
|
||||||
|
Assert.AreEqual("SomeValue", httpClient.DefaultRequestHeaders.GetValues("SomeKey").First());
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldAddDefaultQueryParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientOptionsMock
|
||||||
|
.Setup(o => o.DefaultQueryParams)
|
||||||
|
.Returns(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "SomeKey", "Some Value" },
|
||||||
|
{ "key2", "param2" }
|
||||||
|
});
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(@"{ ""string"": ""some-string"", ""integer"": 123 }", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "test", _request, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/test?SomeKey=Some+Value&key2=param2", callback.Url);
|
||||||
|
Assert.AreEqual(@"{""string"":""Happy Testing"",""integer"":54321}", callback.Content);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
|
||||||
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Exactly(2));
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldAddCustomQueryParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var queryParams = new TestParams();
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(@"{ ""string"": ""some-string"", ""integer"": 123 }", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "params/path", _request, queryParams, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/params/path?test=query+text", callback.Url);
|
||||||
|
Assert.AreEqual(@"{""string"":""Happy Testing"",""integer"":54321}", callback.Content);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
|
||||||
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldDisposeHttpClient()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_httpMessageHandlerMock.Mock
|
||||||
|
.Protected()
|
||||||
|
.Verify("Dispose", Times.Once(), exactParameterMatch: true, true);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldAllowMultipleDispose()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
client.Dispose();
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_httpMessageHandlerMock.Mock
|
||||||
|
.Protected()
|
||||||
|
.Verify("Dispose", Times.Once(), exactParameterMatch: true, true);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldAssertClientOptions()
|
||||||
|
{
|
||||||
|
// Arrange + Act
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public void ShouldThrowArgumentNullForBaseUrlOnAssertClientOptions(string baseUrl)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientOptionsMock
|
||||||
|
.Setup(o => o.BaseUrl)
|
||||||
|
.Returns(baseUrl);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsExactly<ArgumentNullException>(() =>
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowArgumentOutOfRangeForTimeoutOnAssertClientOptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientOptionsMock
|
||||||
|
.Setup(o => o.Timeout)
|
||||||
|
.Returns(TimeSpan.Zero);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowArgumentNullForUseProxyOnAssertClientOptions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_clientOptionsMock
|
||||||
|
.Setup(o => o.UseProxy)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.ThrowsExactly<ArgumentNullException>(() =>
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldThrowDisposed()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsExactlyAsync<ObjectDisposedException>(async () =>
|
||||||
|
{
|
||||||
|
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "/request/path", _request, null, TestContext.CancellationToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public async Task ShouldThrowArgumentNullOnRequestPath(string path)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsExactlyAsync<ArgumentNullException>(async () =>
|
||||||
|
{
|
||||||
|
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", path, _request, null, TestContext.CancellationToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldThrowArgumentOnRequestPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsExactlyAsync<ArgumentException>(async () =>
|
||||||
|
{
|
||||||
|
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "foo?bar=baz", _request, null, TestContext.CancellationToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldPost()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(@"{ ""string"": ""some-string"", ""integer"": 123 }", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "/request/path", _request, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
Assert.AreEqual("some-string", response.Str);
|
||||||
|
Assert.AreEqual(123, response.Int);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/request/path", callback.Url);
|
||||||
|
Assert.AreEqual(@"{""string"":""Happy Testing"",""integer"":54321}", callback.Content);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
|
||||||
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldPostHttpContentDirectly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stringContent = new StringContent(@"{""test"":""HERE ?""}", Encoding.UTF8, "application/json");
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(@"{ ""string"": ""some-string"", ""integer"": 123 }", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "/request/path", stringContent, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
Assert.AreEqual("some-string", response.Str);
|
||||||
|
Assert.AreEqual(123, response.Int);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/request/path", callback.Url);
|
||||||
|
Assert.AreEqual(@"{""test"":""HERE ?""}", callback.Content);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
|
||||||
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldPostWithoutContent()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(@"{ ""string"": ""some-string"", ""integer"": 123 }", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "posting", null, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
Assert.AreEqual("some-string", response.Str);
|
||||||
|
Assert.AreEqual(123, response.Int);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/posting", callback.Url);
|
||||||
|
Assert.IsNull(callback.Content);
|
||||||
|
Assert.IsNull(callback.ContentRaw);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(HttpStatusCode.Unauthorized)]
|
||||||
|
[DataRow(HttpStatusCode.Forbidden)]
|
||||||
|
public async Task ShouldThrowAuthenticationExceptionOnStatusCode(HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = statusCode,
|
||||||
|
Content = new StringContent(@"", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = await Assert.ThrowsExactlyAsync<AuthenticationException>(async () =>
|
||||||
|
{
|
||||||
|
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "foo", _request, null, TestContext.CancellationToken);
|
||||||
|
});
|
||||||
|
Assert.IsNull(ex.InnerException);
|
||||||
|
Assert.AreEqual($"HTTP auth missing: {statusCode}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(HttpStatusCode.NotFound)]
|
||||||
|
[DataRow(HttpStatusCode.InternalServerError)]
|
||||||
|
public async Task ShouldThrowApplicationExceptionOnStatusCode(HttpStatusCode statusCode)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = statusCode,
|
||||||
|
Content = new StringContent(@"", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = await Assert.ThrowsExactlyAsync<ApplicationException>(async () =>
|
||||||
|
{
|
||||||
|
await ReflectionHelper.InvokePrivateMethodAsync<object>(client, "PostAsync", "foo", _request, null, TestContext.CancellationToken);
|
||||||
|
});
|
||||||
|
Assert.IsNull(ex.InnerException);
|
||||||
|
Assert.AreEqual($"Unknown HTTP response: {statusCode}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldThrowExceptionOnInvalidResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent("This is a bad text :p", Encoding.UTF8, "text/plain"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await Assert.ThrowsExactlyAsync<JsonReaderException>(async () =>
|
||||||
|
{
|
||||||
|
await ReflectionHelper.InvokePrivateMethodAsync<TestClass>(client, "PostAsync", "some-path", _request, null, TestContext.CancellationToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldOnlySerializeNonNullValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_request.Str = null;
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent("\"This is an awesome text ;-)\"", Encoding.UTF8, "text/plain"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
string response = await ReflectionHelper.InvokePrivateMethodAsync<string>(client, "PostAsync", "path", _request, null, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
|
||||||
|
Assert.AreEqual("This is an awesome text ;-)", response);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/path", callback.Url);
|
||||||
|
Assert.AreEqual(@"{""integer"":54321}", callback.Content);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
|
||||||
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyNoOtherCalls()
|
||||||
|
{
|
||||||
|
_authenticationMock.VerifyNoOtherCalls();
|
||||||
|
_clientOptionsMock.VerifyNoOtherCalls();
|
||||||
|
_httpMessageHandlerMock.Mock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkMobilityClient GetClient()
|
||||||
|
{
|
||||||
|
var client = new LinkMobilityClient(_authenticationMock.Object, _clientOptionsMock.Object);
|
||||||
|
|
||||||
|
var httpClient = new HttpClient(_httpMessageHandlerMock.Mock.Object)
|
||||||
|
{
|
||||||
|
Timeout = _clientOptionsMock.Object.Timeout,
|
||||||
|
BaseAddress = new Uri(_clientOptionsMock.Object.BaseUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LinkMobilityClient", "1.0.0"));
|
||||||
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
|
||||||
|
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
|
||||||
|
}
|
||||||
|
_authenticationMock.Object.AddHeader(httpClient);
|
||||||
|
|
||||||
|
_authenticationMock.Invocations.Clear();
|
||||||
|
_clientOptionsMock.Invocations.Clear();
|
||||||
|
|
||||||
|
ReflectionHelper.GetPrivateField<HttpClient>(client, "_httpClient")?.Dispose();
|
||||||
|
ReflectionHelper.SetPrivateField(client, "_httpClient", httpClient);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestClass
|
||||||
|
{
|
||||||
|
[JsonProperty("string")]
|
||||||
|
public string Str { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("integer")]
|
||||||
|
public int Int { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestParams : IQueryParameter
|
||||||
|
{
|
||||||
|
public IReadOnlyDictionary<string, string> GetQueryParameters()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "test", "query text" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
test/LinkMobility.Tests/MSTestSettings.cs
Normal file
1
test/LinkMobility.Tests/MSTestSettings.cs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||||
191
test/LinkMobility.Tests/SendTextMessageTest.cs
Normal file
191
test/LinkMobility.Tests/SendTextMessageTest.cs
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AMWD.Net.Api.LinkMobility;
|
||||||
|
using AMWD.Net.Api.LinkMobility.Requests;
|
||||||
|
using LinkMobility.Tests.Helpers;
|
||||||
|
using Moq.Protected;
|
||||||
|
|
||||||
|
namespace LinkMobility.Tests
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class SendTextMessageTest
|
||||||
|
{
|
||||||
|
public TestContext TestContext { get; set; }
|
||||||
|
|
||||||
|
private const string BASE_URL = "https://localhost/rest/";
|
||||||
|
|
||||||
|
private Mock<IAuthentication> _authenticationMock;
|
||||||
|
private Mock<ClientOptions> _clientOptionsMock;
|
||||||
|
private HttpMessageHandlerMock _httpMessageHandlerMock;
|
||||||
|
|
||||||
|
private SendTextMessageRequest _request;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_authenticationMock = new Mock<IAuthentication>();
|
||||||
|
_clientOptionsMock = new Mock<ClientOptions>();
|
||||||
|
_httpMessageHandlerMock = new HttpMessageHandlerMock();
|
||||||
|
|
||||||
|
_authenticationMock
|
||||||
|
.Setup(a => a.AddHeader(It.IsAny<HttpClient>()))
|
||||||
|
.Callback<HttpClient>(c => c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Scheme", "Parameter"));
|
||||||
|
|
||||||
|
_clientOptionsMock.Setup(c => c.BaseUrl).Returns(BASE_URL);
|
||||||
|
_clientOptionsMock.Setup(c => c.Timeout).Returns(TimeSpan.FromSeconds(30));
|
||||||
|
_clientOptionsMock.Setup(c => c.DefaultHeaders).Returns(new Dictionary<string, string>());
|
||||||
|
_clientOptionsMock.Setup(c => c.DefaultQueryParams).Returns(new Dictionary<string, string>());
|
||||||
|
_clientOptionsMock.Setup(c => c.AllowRedirects).Returns(true);
|
||||||
|
_clientOptionsMock.Setup(c => c.UseProxy).Returns(false);
|
||||||
|
|
||||||
|
_request = new SendTextMessageRequest("Happy Testing", ["4791234567"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldSendTextMessage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_httpMessageHandlerMock.Responses.Enqueue(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent("{}", Encoding.UTF8, "application/json"),
|
||||||
|
});
|
||||||
|
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.SendTextMessage(_request, TestContext.CancellationToken);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsNotNull(response);
|
||||||
|
|
||||||
|
Assert.HasCount(1, _httpMessageHandlerMock.RequestCallbacks);
|
||||||
|
|
||||||
|
var callback = _httpMessageHandlerMock.RequestCallbacks.First();
|
||||||
|
Assert.AreEqual(HttpMethod.Post, callback.HttpMethod);
|
||||||
|
Assert.AreEqual("https://localhost/rest/smsmessaging/text", callback.Url);
|
||||||
|
Assert.AreEqual(@"{""messageContent"":""Happy Testing"",""recipientAddressList"":[""4791234567""]}", callback.Content);
|
||||||
|
|
||||||
|
Assert.HasCount(3, callback.Headers);
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Accept"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("Authorization"));
|
||||||
|
Assert.IsTrue(callback.Headers.ContainsKey("User-Agent"));
|
||||||
|
|
||||||
|
Assert.AreEqual("application/json", callback.Headers["Accept"]);
|
||||||
|
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<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
|
||||||
|
|
||||||
|
_clientOptionsMock.VerifyGet(o => o.DefaultQueryParams, Times.Once);
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowOnNullRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentNullException>(() => client.SendTextMessage(null, TestContext.CancellationToken));
|
||||||
|
Assert.AreEqual("request", ex.ParamName);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
public void ShouldThrowOnMissingMessage(string message)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var req = new SendTextMessageRequest(message, ["4791234567"]);
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(req, TestContext.CancellationToken));
|
||||||
|
Assert.AreEqual("MessageContent", ex.ParamName);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ShouldThrowOnNoRecipients()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var req = new SendTextMessageRequest("Hello", []);
|
||||||
|
var client = GetClient();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(req, TestContext.CancellationToken));
|
||||||
|
Assert.AreEqual("RecipientAddressList", ex.ParamName);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
[DataRow(null)]
|
||||||
|
[DataRow("")]
|
||||||
|
[DataRow(" ")]
|
||||||
|
[DataRow("invalid-recipient")]
|
||||||
|
public void ShouldThrowOnInvalidRecipient(string recipient)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
var req = new SendTextMessageRequest("Hello", ["4791234567", recipient]);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.ThrowsExactly<ArgumentException>(() => client.SendTextMessage(req, TestContext.CancellationToken));
|
||||||
|
|
||||||
|
Assert.AreEqual("RecipientAddressList", ex.ParamName);
|
||||||
|
Assert.StartsWith($"Recipient address '{recipient}' is not a valid MSISDN format.", ex.Message);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyNoOtherCalls()
|
||||||
|
{
|
||||||
|
_authenticationMock.VerifyNoOtherCalls();
|
||||||
|
_clientOptionsMock.VerifyNoOtherCalls();
|
||||||
|
_httpMessageHandlerMock.Mock.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ILinkMobilityClient GetClient()
|
||||||
|
{
|
||||||
|
var client = new LinkMobilityClient(_authenticationMock.Object, _clientOptionsMock.Object);
|
||||||
|
|
||||||
|
var httpClient = new HttpClient(_httpMessageHandlerMock.Mock.Object)
|
||||||
|
{
|
||||||
|
Timeout = _clientOptionsMock.Object.Timeout,
|
||||||
|
BaseAddress = new Uri(_clientOptionsMock.Object.BaseUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LinkMobilityClient", "1.0.0"));
|
||||||
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
if (_clientOptionsMock.Object.DefaultHeaders.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var headerKvp in _clientOptionsMock.Object.DefaultHeaders)
|
||||||
|
httpClient.DefaultRequestHeaders.Add(headerKvp.Key, headerKvp.Value);
|
||||||
|
}
|
||||||
|
_authenticationMock.Object.AddHeader(httpClient);
|
||||||
|
|
||||||
|
_authenticationMock.Invocations.Clear();
|
||||||
|
_clientOptionsMock.Invocations.Clear();
|
||||||
|
|
||||||
|
ReflectionHelper.GetPrivateField<HttpClient>(client, "_httpClient")?.Dispose();
|
||||||
|
ReflectionHelper.SetPrivateField(client, "_httpClient", httpClient);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user