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