My webapp relies on two strings for its api calls. I have tested that the api call works as expected when hard-coded. I have also verified that user-secrets holds the expected key-value pairs; my previous attempt using singleton ran into the issue of not being able to differentiate between the various strings coming from user-secrets.
My current attempt at implementation is to set the necessary headers in ConfigureServices:
services.AddHttpClient("OxfordDictionaryClient", _ =>
{
_.DefaultRequestHeaders.Add("app_id", Configuration["app_id"]);
_.DefaultRequestHeaders.Add("app_key", Configuration["app_key"]);
_.DefaultRequestHeaders.Add("Accept", "application/json");
});
However, it seems none of this is being carried into the class where the method is called:
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
private HttpClient _client;
public IndexModel(ILogger<IndexModel> logger, HttpClient client)
{
_logger = logger;
_client = client;
}
[BindProperty]
public WordsInput Delivery { get; set; }
public string synonyms { get; set; }
public async Task<IActionResult> OnPostAsync()
{
string word = "anger";
var URL = $"https://od-api.oxforddictionaries.com/api/v2/thesaurus/en/{word}?fields=synonyms&strictMatch=false";
//_client.DefaultRequestHeaders.Add("app_id", ABC);
//_client.DefaultRequestHeaders.Add("app_key", XYZ);
//_client.DefaultRequestHeaders.Add("Accept", "application/json");
var response = await _client.GetAsync(URL);
var result = await response.Content.ReadAsStringAsync();
JObject obj = JObject.Parse(result);
synonyms = obj.ToString();
return Page();
}
}
I can see _client has none of my expected headers with a breakpoint on the line where the URL is set. Setting the headers directly in this class (the commented out code) works as expected, so the issue seems to be in the AddHttpClient not holding onto the headers added in ConfigureServices.
For the best practice of setting headers in ConfigureServices using httpclient, you can refer to the following code:
Startup.cs:
services.AddHttpClient("OxfordDictionaryClient", c =>
{
c.DefaultRequestHeaders.Add("app_id", "testId");
c.DefaultRequestHeaders.Add("app_key", "testKey");
c.DefaultRequestHeaders.Add("Accept", "application/json");
});
PageModel:
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
private readonly IHttpClientFactory _clientFactory;
public PrivacyModel(ILogger<PrivacyModel> logger, IHttpClientFactory clientFactory)
{
_logger = logger;
_clientFactory = clientFactory;
}
public async Task OnGet()
{
string word = "anger";
var URL = $"https://od-api.oxforddictionaries.com/api/v2/thesaurus/en/{word}?fields=synonyms&strictMatch=false";
var request = new HttpRequestMessage(HttpMethod.Get,
URL);
//Consistent with the name in ConfigureServices
var client = _clientFactory.CreateClient("OxfordDictionaryClient");
Console.WriteLine(client.DefaultRequestHeaders);
var response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
}
}
Test Result:
For more details, please refer to this link.
Related
I have an API middle layer that captures requests, generates a token, and then passes that request on to the intended API endpoint. Only one token should be created at a time, so each request needs to check if a valid token exists and use it, or create a new one. If the middle layer API gets multiple requests at the same time, it should still only create one token.
To do this I am using SemaphoreSlim:
Controller that takes in a request to check for the current version of something:
[ApiController]
[Route("[controller]")]
[AuthorizationKey]
public class Controller : ControllerBase
{
private readonly ILogger<Controller> Logger;
private readonly Services Services;
public Controller(ILogger<Controller> logger, Services services)
{
Logger = logger;
Services = services;
}
[HttpGet]
[Route("version")]
[ResponseCache(NoStore = true, Duration = 0, Location = ResponseCacheLocation.None)]
public async Task<ActionResult> Version()
{
try
{
var result = await Services.GetVersionAsync();
var response = result.Content.ReadAsStringAsync();
if (result.StatusCode == HttpStatusCode.OK)
{
return Ok(response.Result);
}
else if (result.StatusCode == HttpStatusCode.Unauthorized)
{
return StatusCode(401, response.Result);
}
else
{
return StatusCode(500, response.Result);
}
}
catch (Exception ex)
{
return StatusCode(500);
}
}
}
Here is the class in my services:
public class Services
{
private readonly ILogger<Services> Logger;
private readonly IConfiguration Configuration;
private readonly IHttpClientFactory HttpClientFactory;
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
public Services(ILogger<Services> logger, IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
Logger = logger;
Configuration = configuration;
HttpClientFactory = httpClientFactory;
}
public async Task<HttpResponseMessage> GetVersionAsync()
{
var httpClient = await CreateHttpClient();
var response = await httpClient.GetAsync(Configuration["VERSION_ENDPOINT"]);
return response;
}
#region Helpers
private async Task<HttpClient> CreateHttpClient()
{
var token = await GetAccessTokenAsync();
var httpClient = HttpClientFactory.CreateClient();
httpClient.BaseAddress = new Uri(Configuration["API_URI"]);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return httpClient;
}
private async Task<string> GetAccessTokenAsync()
{
await _semaphore.WaitAsync();
try
{
//Checks for a token in a table and returns it if it is valid, or gets a new one if one does not exist
/if no token, then it calls RequestAccessTokenAsync()
}
finally
{
_semaphore.Release();
}
}
private async Task<TokenResponse> RequestAccessTokenAsync()
{
//Gets a new token from a service
}
#endregion
}
If I submit a multiple requests at the same time it is creating multiple tokens. It's not always a 1 to 1 with how many requests I submit. For example I have done 2 requests and it created 2 tokens, 3 requests and it created 2 tokens, and now I just did 10 requests and it created 7 tokens, but either way I only want 1 valid token to be created at a time.
Switching to static for the following code resolved the issue. Thank you again to user "Deleted" for the help.
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
I'm trying to set up an instance on IHttpClientFactory within .NET Core 3.1.
I have a route which is intended to make an GET request to an endpoint supplied by the user and return the result as a byte[].
In the first sample shown below I am getting an error in Visual Studio stating, "IHttpClientFactory is a type which is not valid in the current context".
But this is the type I am specifying in my constructor for DemoProcessor()
https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0119?f1url=%3FappId%3Droslyn%26k%3Dk(CS0119)
What am I doing wrong below and how should I be calling, or what argument should I be passing to DemoProcessor() in order for this to run correctly?
namespace POC_APP.Controllers
{
[ApiController]
[Route("api/")]
public class DemoController : ControllerBase
{
[HttpGet("Interact", Name ="Interact")]
public async Task<ActionResult<string>> GetStringFromEndpointAsync(string url)
{
var proc = new DemoProcessor(IHttpClientFactory); // HERE
var res = await proc.MakeGetRequest(url);
return res.ToString();
}
}
}
As seen above I am trying to call my DemoProcessor() class which contains the implementation of IHttpClientFactory as below.
namespace POC_APP
{
public class DemoProcessor
{
private readonly IHttpClientFactory _httpClientFactory;
public DemoProcessor(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public async Task<byte[]> MakeGetRequest(string url)
{
byte[] response = null;
try
{
using (var httpClient = _httpClientFactory.CreateClient())
{
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Add("foo", "bar");
var documentResponse = await httpClient.GetStringAsync(url.Replace(".exe", ""));
response = Convert.FromBase64String(documentResponse);
return response;
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
return response;
}
}
}
}
I am adding the HTTP client like so in Startup.cs which is called by Program.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHttpClient();
}
DemoProcessor has been setup to expect an instance of something which implements IHttpClientFactory, what is being passed is just the name 'IHttpClientFactory'. This wouldn't compile in any scenario. If you wanted to give the type even then you would need to do something like typeof(IHttpClientFactory).
As dependency injection comes out-of-the-box, you just need to inject IHttpClientFactory into your DemoController, and then pass it on.
namespace POC_APP.Controllers
{
[ApiController]
[Route("api/")]
public class DemoController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
public DemoController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpGet("Interact", Name ="Interact")]
public async Task<ActionResult<string>> GetStringFromEndpointAsync(string url)
{
var proc = new DemoProcessor(_httpClientFactory);
var res = await proc.MakeGetRequest(url);
return res.ToString();
}
}
}
Or, better still, you could register your DemoProcessor in the DI service and inject that into the controller, which will automatically get the IHttpClientFactory injected into it.
I've got some code that calls HttpClient's GetFromJsonAsync however I'm struggling to mock the method call and was wondering how can I do this?
C# code:
public class Client : IClient
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly HttpClient _httpClient;
public Client(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_httpClient = _httpClientFactory.CreateClient();
}
public async Task<List<ApiResponse>> GetData()
{
try
{
return await _httpClient.GetFromJsonAsync<ApiResponse>("endpointUrl"); // How to mock?
}
catch (Exception e)
{
throw;
}
return null;
}
}
I've seen previous posts that suggest I should mock HttpMessageHandler but how do I mock the response back from the GetFromJsonAsync method call?
As per one of the suggested answers, I've done the following:
var httpClientMock = new Mock<HttpClient>();
httpClientMock.Setup(x => x.GetFromJsonAsync<ApiResponse>(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ApiResponse());
_httpClientFactoryMock = new Mock<IHttpClientFactory>();
_httpClientFactoryMock.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(httpClientMock.Object);
However I receive the following error:
Message "Unsupported expression: x => x.GetFromJsonAsync<DataLookupResponse>(It.IsAny<string>(), It.IsAny<CancellationToken>())\nExtension methods (here: HttpClientJsonExtensions.GetFromJsonAsync) may not be used in setup / verification expressions."
If you create a mock of HttpClient you can then return this when calling _httpClientFactory.CreateClient();.
Something like this (haven't tested this code in my IDE so be aware of any typo's)
var httpClientMock = new Mock<HttpClient>();
httpClientMock.Setup(x => x.GetFromJsonAsync<ApiResponse>("endpointurl").Returns(...); httpClientFactoryMock.Setup(x => x.CreateClient()).Returns(httpClientMock.Object);
Recently I've been unit testing my HttpClients and I had to solve the same problem as you.
Modify where needed. I'm using the IConfiguration to retrieve some application settings. The code to mock this has also been included in the code you can find below.
The call in the test is a mocked call. You don't need an internet connection for this call to succeed. You can specify any endpoint and call it with any configured response.
This means can return anything you want in your mocked call and use fake endpoint in order to not expose any sensitive data in your code.
Install the following NuGet Packages in your test project in order for my solution to work:
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
My HttpClient:
public class MyHttpClient : IMyHttpClient
{
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
public MyHttpClient(IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_configuration = configuration;
_httpClientFactory = httpClientFactory;
}
public async Task<SomeType> GetSomeInformationAsync()
{
var token = await FetchAccessToken();
var client = CreateHttpClient(token);
var endpoint = _configuration.GetValue<string>("Endpoints:SomeEndpoint");
var response = client.GetAsync(endpoint);
var content = await response.Result.Content.ReadAsStringAsync();
return content;
}
private HttpClient CreateHttpClient(string accessToken)
{
var client = _httpClientFactory.CreateClient(nameof(MyHttpClient));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return client;
}
}
My TestClass:
public class MyHttpClientTests
{
private readonly MyHttpClient _sut;
private readonly Mock<IConfiguration> _configurationMock = new();
private readonly Mock<IHttpClientFactory> _httpClientFactoryMock = new();
private readonly MockHttpMessageHandler _httpMessageHandlerMock = new();
public MyHttpClientTests()
{
_sut = new MyHttpClient(_configurationMock.Object, _httpClientFactoryMock.Object);
}
[Fact]
public async void GetSomeInformationTest_ShouldReturnSomething()
{
// Since you are mocking you don't need the real endpoint.
var endpoint = "/someNotExistingEndpoint/";
var getSomeInformationValue = new Mock<IConfigurationSection>();
getSomeInformationValue.Setup(x => x.Value).Returns(endpoint);
// When I retrieve my configuration in my mocked HttpClient from 'Endpoints:SomeEndpoint' it will return the value '/someNotExistingEndpoint/'
_configurationMock.Setup(x => x.GetSection(It.Is<string>(x => x == "Endpoints:SomeEndpoint"))).Returns(getSomeInformationValue.Object);
// When the above endpoint is called I can respond with anything I want. In this case an StatusCode of OK and some JsonContent (application/json)).
_httpMessageHandlerMock.When(endpoint).Respond(HttpStatusCode.OK, JsonContent.Create(new { Message = "thisIsSomeJsonResponse" }));
_httpClientFactoryMock.Setup(x => x.CreateClient(nameof(MyHttpClient)))
.Returns(new HttpClient(_httpMessageHandlerMock)
{
BaseAddress = new Uri("someBaseAdress")
});
var result = await _sut.GetSomeInformationAsync();
// You can put your assertions here
}
}
I am trying to write a Blazor app that uses client secret credentials to get an access token for the API. I wanted to encapsulate it in such a way that it handles the token fetching and refreshing behind the scenes. To achieve this, I created the following inherited class which uses IdentityModel Nuget package:
public class MPSHttpClient : HttpClient
{
private readonly IConfiguration Configuration;
private readonly TokenProvider Tokens;
private readonly ILogger Logger;
public MPSHttpClient(IConfiguration configuration, TokenProvider tokens, ILogger logger)
{
Configuration = configuration;
Tokens = tokens;
Logger = logger;
}
public async Task<bool> RefreshTokens()
{
if (Tokens.RefreshToken == null)
return false;
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
RefreshToken = Tokens.RefreshToken
});
Logger.LogInformation("Refresh Token Result {0}", result.IsError);
if (result.IsError)
{
Logger.LogError("Error: {0)", result.ErrorDescription);
return false;
}
Tokens.RefreshToken = result.RefreshToken;
Tokens.AccessToken = result.AccessToken;
Logger.LogInformation("Access Token: {0}", result.AccessToken);
Logger.LogInformation("Refresh Token: {0}" , result.RefreshToken);
return true;
}
public async Task<bool> CheckTokens()
{
if (await RefreshTokens())
return true;
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
ClientSecret = Configuration["Settings:ClientSecret"]
});
if (result.IsError)
{
//Log("Error: " + result.Error);
return false;
}
Tokens.AccessToken = result.AccessToken;
Tokens.RefreshToken = result.RefreshToken;
return true;
}
public new async Task<HttpResponseMessage> GetAsync(string requestUri)
{
DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);
var response = await base.GetAsync(requestUri);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (await CheckTokens())
{
DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", Tokens.AccessToken);
response = await base.GetAsync(requestUri);
}
}
return response;
}
}
The idea is to keep from having to write a bunch of redundant code to try the API, then request/refresh the token if you are unauthorized. I tried it at first using extension methods to HttpClient, but there was no good way to inject the Configuration into a static class.
So my Service code is written as this:
public interface IEngineListService
{
Task<IEnumerable<EngineList>> GetEngineList();
}
public class EngineListService : IEngineListService
{
private readonly MPSHttpClient _httpClient;
public EngineListService(MPSHttpClient httpClient)
{
_httpClient = httpClient;
}
async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
{
return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
(await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
}
}
Everything compiles great. In my Startup, I have the following code:
services.AddScoped<TokenProvider>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
Just to be complete, Token Provider looks like this:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
When I run the App, it complains that it can't find a suitable constructor for EngineListService in the call to services.AddHttpClient. Is there a way to pass AddHttpClient an actual instance of the IEngineListService. Any other way I might be able to achieve this?
Thanks,
Jim
I think that EngineListService should not be registered as a HttpClient in services and instead you should register MPSHttpClient.
This follows the "Typed Client" example in the documentation and uses IHttpClientFactory behind the scenes.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#typed-clients
When you use services.AddHttpClient the constructor needs a HttpClient parameter. That is how the HttpClientFactory initializes the HttpClient and then passes it into your service ready to go.
You can change your MPSHttpClient to not inherit HttpClient and instead add a HttpClient parameter to the constructor. You could also have it implement an interface like IMPSHttpClient
public class MPSHttpClient
{
public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger logger)
{
HttpClient = httpClient;
Configuration = configuration;
Tokens = tokens;
Logger = logger;
}
}
You must remove these lines from MPSHttpClient and use the injected client.
// remove this
var client = new HttpClient();
In Startup add
services.AddHttpClient<MPSHttpClient>(client =>
{
// add any configuration
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
});
Change EngineListService to a normal service registration as it is not a HttpClient
services.AddScoped<IEngineListService, EngineListService>()
Special thanks to #pinkfloydx33 for helping me solve this. This link that he shared https://blog.joaograssi.com/typed-httpclient-with-messagehandler-getting-accesstokens-from-identityserver/ was everything I needed. The trick was that there exists a class called DelegatingHandler that you can inherit and override the OnSendAsync method and do all of your token-checking there before sending it to the final HttpHandler. So my new MPSHttpClient class is as so:
public class MPSHttpClient : DelegatingHandler
{
private readonly IConfiguration Configuration;
private readonly TokenProvider Tokens;
private readonly ILogger<MPSHttpClient> Logger;
private readonly HttpClient client;
public MPSHttpClient(HttpClient httpClient, IConfiguration configuration, TokenProvider tokens, ILogger<MPSHttpClient> logger)
{
Configuration = configuration;
Tokens = tokens;
Logger = logger;
client = httpClient;
}
public async Task<bool> CheckTokens()
{
var disco = await client.GetDiscoveryDocumentAsync(Configuration["Settings:Authority"]);
if (disco.IsError) throw new Exception(disco.Error);
var result = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = Configuration["Settings:ClientID"],
ClientSecret = Configuration["Settings:ClientSecret"]
});
if (result.IsError)
{
//Log("Error: " + result.Error);
return false;
}
Tokens.AccessToken = result.AccessToken;
Tokens.RefreshToken = result.RefreshToken;
return true;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.SetBearerToken(Tokens.AccessToken);
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (await CheckTokens())
{
request.SetBearerToken(Tokens.AccessToken);
response = await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
The big changes here are the inheritance and I used DI to obtain the HttpClient much like #Rosco mentioned. I had tried to override OnGetAsync in my original version. When inheriting from DelegatingHandler, all you have to override is OnSendAsync. This will handle all of your get, put, post, and deletes from your HttpContext all in one method.
My EngineList Service is written as if there were no tokens to be considered, which was my original goal:
public interface IEngineListService
{
Task<IEnumerable<EngineList>> GetEngineList();
}
public class EngineListService : IEngineListService
{
private readonly HttpClient _httpClient;
public EngineListService(HttpClient httpClient)
{
_httpClient = httpClient;
}
async Task<IEnumerable<EngineList>> IEngineListService.GetEngineList()
{
return await JsonSerializer.DeserializeAsync<IEnumerable<EngineList>>
(await _httpClient.GetStreamAsync($"api/EngineLists"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
}
}
The Token Provider stayed the same. I plan to add expirations and such to it, but it works as is:
public class TokenProvider
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
The ConfigureServices code changed just a bit:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<TokenProvider>();
services.AddTransient<MPSHttpClient>();
services.AddHttpClient<IEngineListService, EngineListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
}).AddHttpMessageHandler<MPSHttpClient>();
...
}
You instantiate MPSHttpClient as Transient, then reference it with the AddHttpMessageHandler call attached to the AddHttpClient call. I know this is different than how others implement HttpClients, but I learned this method of creating client services from a Pluralsight video and have been using it for everything. I create a separate Service for each entity in the database. If say I wanted to do tires, I would add the following to ConfigureServices:
services.AddHttpClient<ITireListService, TireListService>(client =>
{
client.BaseAddress = new Uri(Configuration["Settings:ApiAddress"]);
}).AddHttpMessageHandler<MPSHttpClient>();
It will use the same DelegatingHandler so I can just keep adding services for each entity type while no longer worrying about tokens. Thanks to everyone that responded.
Thanks,
Jim
I was trying to build a generic HTTP service in my project (c# with .net core 2.1), and I have done it as per the below snippet HttpService.
I also started using it by calling it from my business logic class which uses this generic PostAsync method to post an HTTP call to a 3rd party with a content in body. It works perfectly.
But, when I tried to test it, I failed!
Actually when I tried debugging (testing mode), I get null response when the debugger comes to this line var result = await _httpService.PostAsync("https://test.com/api", content); in business class Processor even with fake objects and mocks, although it works normally in debugging mode without testing/mocking.
HTTP service:
public interface IHttpService
{
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content);
}
public class HttpService : IHttpService
{
private readonly IHttpClientFactory _httpClientFactory;
public HttpService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
{
var httpClient = _httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(3);
var response = await httpClient.PostAsync(requestUri, content).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return response;
}
}
Business class:
public class Processor : IProcessor
{
private readonly IHttpService _httpService;
public Processor() { }
public Processor(IHttpService httpService, IAppSettings appSettings)
{
_httpService = httpService;
}
public async Task<HttpResponseMessage> PostToVendor(Order order)
{
// Building content
var json = JsonConvert.SerializeObject(order, Formatting.Indented);
var content = new StringContent(json, Encoding.UTF8, "application/json");
// HTTP POST
var result = await _httpService.PostAsync("https://test.com/api", content); // returns null during the test without stepping into the method PostAsyn itself
return result;
}
}
Test class:
public class MyTests
{
private readonly Mock<IHttpService> _fakeHttpMessageHandler;
private readonly IProcessor _processor; // contains business logic
private readonly Fixture _fixture = new Fixture();
public FunctionTest()
{
_fakeHttpMessageHandler = new Mock<IHttpService>();
_processor = new Processor(_fakeHttpMessageHandler.Object);
}
[Fact]
public async Task Post_To_Vendor_Should_Return_Valid_Response()
{
var fakeHttpResponseMessage = new Mock<HttpResponseMessage>(MockBehavior.Loose, new object[] { HttpStatusCode.OK });
var responseModel = new ResponseModel
{
success = true,
uuid = Guid.NewGuid().ToString()
};
fakeHttpResponseMessage.Object.Content = new StringContent(JsonConvert.SerializeObject(responseModel), Encoding.UTF8, "application/json");
var fakeContent = _fixture.Build<DTO>().Create(); // DTO is the body which gonna be sent to the API
var content = new StringContent(JsonConvert.SerializeObject(fakeContent), Encoding.UTF8, "application/json");
_fakeHttpMessageHandler.Setup(x => x.PostAsync(It.IsAny<string>(), content))
.Returns(Task.FromResult(fakeHttpResponseMessage.Object));
var res = _processor.PostToVendor(fakeContent).Result;
Assert.NotNull(res.Content);
var actual = JsonConvert.SerializeObject(responseModel);
var expected = await res.Content.ReadAsStringAsync();
Assert.Equal(expected, actual);
}
}
Your problem is in mock set up:
_fakeHttpMessageHandler.Setup(x => x.PostAsync(It.IsAny<string>(), content))
.Returns(Task.FromResult(fakeHttpResponseMessage.Object));
Second parameter for PostAsync method expected to be content, but since StringContent is a reference type, content you setup in mock is different from content you creating in processor. If you change it to next one, it should work as you expect:
_fakeHttpMessageHandler.Setup(x => x.PostAsync(It.IsAny<string>(), It.IsAny<StringContent>()))
.Returns(Task.FromResult(fakeHttpResponseMessage.Object));
P.S. null response to PostAsync means that method has default setup, which means that it will return default value