Testing controller with xUnit - c#

I have webapi where it needs to call some other endpoint and get data.
My current code as follows
//http client implementation
public interface IHttpClientFactory
{
HttpClient Create();
}
public class HttpClientFactory : IHttpClientFactory
{
private readonly ApplicationSettings _applicationSettings;
HttpClient _httpClient;
public HttpClientFactory(IOptions<ApplicationSettings> settings)
{
_applicationSettings = settings.Value;
}
public HttpClient Create()
{
if (_httpClient != null)
return _httpClient;
var client = new HttpClient()
{
BaseAddress = new Uri($"{_applicationSettings.BaseUrl}")
};
_httpClient = client;
return _httpClient;
}
}
public interface IGetItemsQuery
{
Task<IEnumerable<T>> Execute<T>(string url);
}
public class GetItemQuery: IGetItemsQuery
{
private readonly IHttpClientFactory _httpClientFactory;
public GetPhotosQuery(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<IEnumerable<T>> Execute<T>(string url)
{
using (var response = await _httpClientFactory.Create().GetAsync($"{url}").ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
var resp = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var items = JArray.Parse(resp);
return items.ToObject<T[]>();
}
}
In my controller part
private readonly IGetItemsQuery _getItemsQuery;
public HomeController(IGetItemsQuery getItemsQuery)
{
_getItemsQuery = getItemsQuery;
}
appsettings
"ApplicationSettings": {
"BaseUrl": "http://someendpoint.com/"
}
Startup
services.Configure<ApplicationSettings>(Configuration.GetSection("ApplicationSettings"));
services.AddScoped<IGetItemsQuery, GetPhotosQuery>();
services.AddScoped<IHttpClientFactory, HttpClientFactory>();
I want to try something like below in my test
[Fact]
public void Test_Index()
{
// Arrange
var itemsQuery = new Mock<IGetItemsQuery>();
var controller = new HomeController(itemsQuery.Object);
// Act
var result = controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Null(viewResult.ViewName);
}
This is creating mock IGetItemsQuery but this isn't mocking the actual IHttpClientFactory.
Is there a way to do this

Based on your design with the abstracted dependencies there would be no need to mock a client factory in order to unit test the controller.
As you have done in your test, you mock IGetItemsQuery, but you have not set it up to behave as expected when invoked in the test.
If, for example, the controller method under test look something like this
private readonly IGetItemsQuery getItemsQuery;
public HomeController(IGetItemsQuery getItemsQuery) {
this.getItemsQuery = getItemsQuery;
}
public async Task<IActionResult> Index() {
var url = "....";
var items = await getItemsQuery.Execute<MyItem>(url);
return View(items);
}
Then an isolated unit test for the Index action as the method under test could look something like
[Fact]
public async Task Index_Should_Return_View_With_Items() {
// Arrange
var itemsQuery = new Mock<IGetItemsQuery>();
var items = new MyItem[] {
new MyItem(),
new MyItem()
};
itemsQuery.Setup(_ => _.Execute<MyItem>(It.IsAny<string>()))
.ReturnsAsync(items);
var controller = new HomeController(itemsQuery.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Null(viewResult.ViewName);
}

Related

XUnit unit tests for MassTransit consumer

I am using MassTransit 5.5.5 version and xunit 2.4.1
My consumer looks like this
public class StorageInformationConsumer : IConsumer<CreateStorageInformationSummary>
{
private readonly IBus _serviceBus;
private readonly USIntegrationQueueServiceContext _USIntegrationQueueServiceContext;
public StorageInformationConsumer(IBus serviceBus, USIntegrationQueueServiceContext USIntegrationQueueServiceContext)
{
_serviceBus = serviceBus;
_USIntegrationQueueServiceContext = USIntegrationQueueServiceContext;
}
public async Task Consume(ConsumeContext<CreateStorageInformationSummary> createStorageInformationSummarycontext)
{
//....
}
}
And my Test like this
public class StorageInformationConsumerTest
{
private readonly USIntegrationQueueServiceContext _dbContext;
private readonly Mock<IBus> _serviceBusMock;
private readonly StorageInformationConsumer _storageInformationConsumer;
public StorageInformationConsumerTest()
{
var options = new DbContextOptionsBuilder<USIntegrationQueueServiceContext>()
.UseInMemoryDatabase(databaseName: "InMemoryArticleDatabase")
.Options;
_dbContext = new USIntegrationQueueServiceContext(options);
_serviceBusMock = new Mock<IBus>();
_storageInformationConsumer = new StorageInformationConsumer(_serviceBusMock.Object, _dbContext);
}
[Fact]
public async void ItShouldCreateStorageInformation()
{
var createStorageInformationSummary = new CreateStorageInformationSummary
{
ClaimNumber = "C-1234",
WorkQueueItemId = 1,
StorageInformation = CreateStorageInformation(),
};
//How to consume
}
}
How to consume the CreateStorageInformationSummary message in order to call consumer, following doesn't work
var mockMessage = new Mock<ConsumeContext<CreateStorageInformationSummary>>(createStorageInformationSummary);
await _storageInformationConsumer.Consume(mockMessage.Object);
Since you have not clarified what is actually not working, the most I can provide is how to create the mock context and pass it to the subject method under test.
This is simple enough since ConsumeContext<T> is already an interface
[Fact]
public async Task ItShouldCreateStorageInformation() {
//Arrange
var createStorageInformationSummary = new CreateStorageInformationSummary {
ClaimNumber = "C-1234",
WorkQueueItemId = 1,
StorageInformation = CreateStorageInformation(),
};
//Mock the context
var context = Mock.Of<ConsumeContext<CreateStorageInformationSummary>>(_ =>
_.Message == createStorageInformationSummary);
//Act
await _storageInformationConsumer.Consume(context);
//Assert
//...assert the expected behavior
}
Also take note that the test has been updated to return async Task and not async void
Reference Moq Quickstart

Refactoring code to allow for unit testing of HttpClient

I am dealing with a piece of code that looks like this:
public class Uploader : IUploader
{
public Uploader()
{
// assign member variables to dependency injected interface implementations
}
public async Task<string> Upload(string url, string data)
{
HttpResponseMessage result;
try
{
var handler = new HttpClientHandler();
var client = new HttpClient(handler);
result = await client.PostAsync(url, new FormUrlEncodedContent(data));
if (result.StatusCode != HttpStatusCode.OK)
{
return "Some Error Message";
}
else
{
return null; // Success!
}
}
catch (Exception ex)
{
// do some fancy stuff here
}
}
}
I am trying to unit test the Upload function. In particular, I need to mock the HttpClient. After reading the other answers on here and these two articles, I know that one of the better ways to solve this is to mock the HttpMessageHandler instead and pass that to HttpClient and have it return whatever I want.
So, I started along that path by first passing in HttpClient in the constructor as a dependency:
public class Uploader : IUploader
{
private readonly HttpClient m_httpClient; // made this a member variable
public Uploader(HttpClient httpClient) // dependency inject this
{
m_httpClient = httpClient;
}
public async Task<string> Upload(string url, string data)
{
HttpResponseMessage result;
try
{
var handler = new HttpClientHandler();
result = await m_httpClient.PostAsync(url, new FormUrlEncodedContent(data));
if (result.StatusCode != HttpStatusCode.OK)
{
return "Some Error Message";
}
else
{
return null; // Success!
}
}
catch (Exception ex)
{
// do some fancy stuff here
}
}
}
and adding: services.AddSingleton<HttpClient>(); to the ConfigureServices method of Startup.cs.
But now I face a slight issue where the original code specifically creates a HttpClientHandler to pass in. How then do I refactor that to take in a mockable handler?
I find the simplest way is to continue using HttpClient, but pass in a mocking HttpClientHandler such as https://github.com/richardszalay/mockhttp
Code sample from the link above:
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("http://localhost/api/user/*")
.Respond("application/json", "{'name' : 'Test McGee'}");
// Inject the handler or client into your application code
var client = mockHttp.ToHttpClient();
var response = await client.GetAsync("http://localhost/api/user/1234");
var json = await response.Content.ReadAsStringAsync();
Console.Write(json); // {'name' : 'Test McGee'}
The Dependency Injection framework built into .NET Core ignores internal constructors, so it will call the parameter-less constructor in this scenario.
public sealed class Uploader : IUploader
{
private readonly HttpClient m_httpClient;
public Uploader() : this(new HttpClientHandler())
{
}
internal Uploader(HttpClientHandler handler)
{
m_httpClient = new HttpClient(handler);
}
// regular methods
}
In your unit tests, you can use the constructor accepting the HttpClientHandler:
[Fact]
public async Task ShouldDoSomethingAsync()
{
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("http://myserver.com/upload")
.Respond("application/json", "{'status' : 'Success'}");
var uploader = new Uploader(mockHttp);
var result = await uploader.UploadAsync();
Assert.Equal("Success", result.Status);
}
Normally I'm not a big fan of having an internal constructor to facilitate testing, however, I find this more obvious and self-contained than registering a shared HttpClient.
HttpClientFactory might be another good option, but I haven't played around with that too much, so I'll just give info on what I've found useful myself.
One way would be to abstract your HTTP functionality into a service i.e. HttpService which implements an interface of IHttpService:
IHttpService
public interface IHttpService
{
Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null);
}
HttpService
public class HttpService : IHttpService
{
private static HttpClient _httpClient;
private const string MimeTypeApplicationJson = "application/json";
public HttpService()
{
_httpClient = new HttpClient();
}
private static async Task<HttpResponseMessage> HttpSendAsync(HttpMethod method, Uri url, string payload,
Dictionary<string, string> headers = null)
{
var request = new HttpRequestMessage(method, url);
request.Headers.Add("Accept", MimeTypeApplicationJson);
if (headers != null)
{
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
}
if (!string.IsNullOrWhiteSpace(payload))
request.Content = new StringContent(payload, Encoding.UTF8, MimeTypeApplicationJson);
return await _httpClient.SendAsync(request);
}
public async Task<HttpResponseMessage> Post(Uri url, string payload, Dictionary<string, string> headers = null)
{
return await HttpSendAsync(HttpMethod.Post, url, payload, headers);
}
}
Add to your services:
services.AddSingleton<IHttpService, HttpService>();
In your class you would then inject IHttpService as a dependency:
public class Uploader : IUploader
{
private readonly IHttpService _httpService; // made this a member variable
public Uploader(IHttpService httpService) // dependency inject this
{
_httpService = httpService;
}
public async Task<string> Upload(string url, string data)
{
HttpResponseMessage result;
try
{
result = await _httpService.PostAsync(new Uri(url), data);
if (result.StatusCode != HttpStatusCode.OK)
{
return "Some Error Message";
}
else
{
return null; // Success!
}
}
catch (Exception ex)
{
// do some fancy stuff here
}
}
}
You could then use Moq to mock HttpService in your unit test:
[TestClass]
public class UploaderTests
{
private Mock<IHttpService> _mockHttpService = new Mock<IHttpService>();
[TestMethod]
public async Task WhenStatusCodeIsNot200Ok_ThenErrorMessageReturned()
{
// arrange
var uploader = new Uploader(_mockHttpService.Object);
var url = "someurl.co.uk";
var data = "data";
// need to setup your mock to return the response you want to test
_mockHttpService
.Setup(s => s.PostAsync(url, data))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError));
// act
var result = await uploader.Upload(new Uri(url), data);
// assert
Assert.AreEqual("Some Error Message", result);
}
[TestMethod]
public async Task WhenStatusCodeIs200Ok_ThenNullReturned()
{
// arrange
var uploader = new Uploader(_mockHttpService.Object);
var url = "someurl.co.uk";
var data = "data";
// need to setup your mock to return the response you want to test
_mockHttpService
.Setup(s => s.PostAsync(new Uri(url), data))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
// act
var result = await uploader.Upload(url, data);
// assert
Assert.AreEqual(null, result);
}
}

XUnit mocking a method but doesn't return expected result

I am using moq in XUnit test but for some reason the mock is not working properly. Here's my unit test:
[Fact]
public async Task SampleUnitTest()
{
//Arrange
var httpClient = new HttpClient(new FakeHttpMessageHandler());
_mockConstructRequest.Setup(x => x.ConstructRequestString(searchRequestModel))
.Returns("a sample string");
var service = new LibraryService(_mockConstructRequest.Object);
//Act
var response = service.GetResponse(new Request(), httpClient);
//Assert
response.Should().BeNull();
}
private class FakeHttpMessageHandler : HttpMessageHandler
{
public Func<HttpRequestMessage, CancellationToken, HttpResponseMessage> HttpRequestHandler { get; set; } =
(r, c) =>
new HttpResponseMessage
{
ReasonPhrase = r.RequestUri.AbsoluteUri,
StatusCode = HttpStatusCode.OK
};
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(HttpRequestHandler(request, cancellationToken));
}
}
And here is the actual code and I am trying to test GetResponse method.
public class LibraryService : ILibraryService
{
private IConfiguration _configuration;
private IRequestConstructor _requestContructor;
public LibraryService (IRequestConstructor requestConstructor)
{
_requestConstructor = requestConstructor;
}
public async Task<Response> GetResponse(Request request, HttpClient client)
{
//construct request
string requestString = _constructRequestString.ConstructRequestString(request, client);
return null;
}
}
public class RequestContructor : IRequestContructor
{
public string ConstructRequestString(Request request)
{
return "a request string";
}
}
I was trying to step through the code from my unit test but when the break point is at this line, requestString variable is null while it is supposed to return "a sample string". Anyone knows why?
string requestString = _constructRequestString.ConstructRequestString(request, client);
As far as I can see you are mocking incorrectly:
Your mock:
_mockConstructRequest.Setup(x => x.ConstructRequestString(searchRequestModel))
.Returns("a sample string");
Method you are calling:
_constructRequestString.ConstructRequestString(request, client);
Should not it be something like this:
_mockConstructRequest.Setup(x => x.ConstructRequestString(It.IsAny<Request>(),It.IsAny<HttpClient>()))
.Returns("a sample string");
On top of that:
Try to initialize your mocks and your "classUnderTest" in constructor instead of in each test, it will run each time before the test and will do everything for you. For example:
public class UnitTestClass{
private readonly ClassUnderTest _classUnderTest;
private readonly Mock<ClassUnderTestDependecy> mockedInstance;
public UnitTestClass {
mockedInstance= new Mock<ClassUnderTestDependecy>();
_classUnderTest= new ClassUnderTest (ClassUnderTestDependecy.Object);
}
}

Unit test Controller with HttpClient in .NET MVC

So I have a controller that is using HttpClient to call a webservice like so:
public class DemoController : Controller
{
HttpClient client;
string baseUrl = "http://localhost:90/webservice";
public DemoController()
{
client = new HttpClient
{
BaseAddress = new Uri(baseUrl)
};
}
// GET: DemoInfo
public async Task<ActionResult> Index()
{
HttpResponseMessage response = await client.GetAsync(baseUrl + "vehicle/menu/year");
string content = "";
MenuItems result = null;
if (response.IsSuccessStatusCode)
{
content = await response.Content.ReadAsStringAsync();
result = (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(new StringReader(content));
}
return View("Index", result);
}
}
My unit test for this action is as follows:
[TestMethod]
public async Task Test_Index()
{
// Arrange
DemoController controller = new DemoController();
// Act
var result = await controller.Index();
ViewResult viewResult = (ViewResult) result;
// Assert
Assert.AreEqual("Index", viewResult.ViewName);
Assert.IsNotNull(viewResult.Model);
}
So obviously I would like to avoid making the web service call every time the test is run. Would I be on the right track in opting for an IoC container like Unity so that HttpClient would be injected into the controller? Is that overkill for what I'm trying to achieve? I'm aware that there is a lot of history with people struggling with properly mocking httpclient in there unit tests through this github issue. Any help would be greatly appreciated in giving some insight into how to write the controller to make a service call while still being testable.
All dependencies which makes tests slow should be abstracted.
Wrap HttpClient with an abstraction, which you can mock in your tests.
public interface IMyClient
{
Task<string> GetRawDataFrom(string url);
}
Then your controller will depend on that abstraction
public class DemoController : Controller
{
private readonly IMyClient _client;
private string _baseUrl = "http://localhost:90/webservice";
public DemoController(IMyClient client)
{
_client = client;
}
public async Task<ActionResult> Index()
{
var rawData = _client.GetRawDataFrom($"{_baseUrl}vehicle/menu/year");
using (var reader = new StringReader(rawData))
{
var result =
(MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(reader);
return View("Index", result);
}
}
}
Then in tests you can mock your abstraction to return expected data
public class FakeClient : IMyClient
{
public string RawData { get; set; }
public Task<string> GetRawDataFrom(string url)
{
return Task.FromResult(RawData);
}
}
[TestMethod]
public async Task Test_Index()
{
// Arrange
var fakeClient = new FakeClient
{
RawData = #"[
{ Name: "One", Path: "/one" },
{ Name: "Two", Path: "/two" }
]"
};
DemoController controller = new DemoController(fakeClient);
// Act
var result = await controller.Index();
ViewResult viewResult = (ViewResult)result;
// Assert
Assert.AreEqual("Index", viewResult.ViewName);
Assert.IsNotNull(viewResult.Model);
}
Actual implementation will use HttpClient
public class MyHttpClient : IMyClient
{
public Task<string> GetRawDataFrom(string url)
{
var response = await client.GetAsync(url);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
}
}
An alternative approach to testing HttpClient calls without service wrappers, mocks, or IoC containers is to use Flurl, a small wrapper library around HttpClient that provides (among other things) some robust testing features. [Disclaimer: I'm the author]
Here's what your controller would look like. There's a few ways to do this, but this approach uses string extension methods that abstract away the client entirely. (A single HttpClient instance per host is managed for you to prevent trouble.)
using Flurl.Http;
public class DemoController : Controller
{
string baseUrl = "http://localhost:90/webservice";
// GET: DemoInfo
public async Task<ActionResult> Index()
{
var content = await baseUrl
.AppendPathSegment("vehicle/menu/year")
.GetStringAsync();
var result = (MenuItems)new XmlSerializer(typeof(MenuItems)).Deserialize(new StringReader(content));
return View("Index", result);
}
}
And the test:
using Flurl.Http;
[TestMethod]
public async Task Test_Index()
{
// fake & record all HTTP calls in the test subject
using (var httpTest = new HttpTest())
{
// Arrange
httpTest.RespondWith(200, "<xml>some fake response xml...</xml>");
DemoController controller = new DemoController();
// Act
var result = await controller.Index();
ViewResult viewResult = (ViewResult) result;
// Assert
Assert.AreEqual("Index", viewResult.ViewName);
Assert.IsNotNull(viewResult.Model);
}
}
Flurl.Http is available on NuGet.

How to mock Microsoft Graph API SDK Client?

I have used Microsoft Graph SDK in my project to call graph API, for this I need to use GraphServiceClient.
To use GraphServiceClient, i have to add some helper classes, in which SDKHelper is a static class which has GetAuthenticatedClient() method.
Since method under test is tightly coupled to SDKHelper which is static, so I have created a service class and injected the dependency.
Below is the controller and method,
public class MyController
{
private IMyServices _iMyServices { get; set; }
public UserController(IMyServices iMyServices)
{
_iMyServices = iMyServices;
}
public async Task<HttpResponseMessage> GetGroupMembers([FromUri]string groupID)
{
GraphServiceClient graphClient = _iMyServices.GetAuthenticatedClient();
IGroupMembersCollectionWithReferencesPage groupMembers = await _iMyServices.GetGroupMembersCollectionWithReferencePage(graphClient, groupID);
return this.Request.CreateResponse(HttpStatusCode.OK, groupMembers, "application/json");
}
}
Service Class,
public class MyServices : IMyServices
{
public GraphServiceClient GetAuthenticatedClient()
{
GraphServiceClient graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await SampleAuthProvider.Instance.GetAccessTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
}));
return graphClient;
}
public async Task<IGraphServiceGroupsCollectionPage> GetGraphServiceGroupCollectionPage(GraphServiceClient graphClient)
{
return await graphClient.Groups.Request().GetAsync();
}
}
I am having challenge in writing Unit Test Case for the above service class methods, Below is my Unit Test Code:
public async Task GetGroupMembersCollectionWithReferencePage_Success()
{
GraphServiceClient graphClient = GetAuthenticatedClient();
IGraphServiceGroupsCollectionPage groupMembers = await graphClient.Groups.Request().GetAsync();
Mock<IUserServices> mockIUserService = new Mock<IUserServices>();
IGraphServiceGroupsCollectionPage expectedResult = await mockIUserService.Object.GetGraphServiceGroupCollectionPage(graphClient);
Assert.AreEqual(expectedResult, groupMembers);
}
In Above Test case line number 4 throws an exception -
Message: The type initializer for 'Connect3W.UserMgt.Api.Helpers.SampleAuthProvider' threw an exception.
Inner Exception Message: Value cannot be null. Parameter name: format
Can anyone suggest me how to use MOQ to mock above code or any other method to complete test case for this ?
Do not mock what you do not own. GraphServiceClient should be treated as a 3rd party dependency and should be encapsulated behind abstractions you control
You attempted to do that but are still leaking implementation concerns.
The service can be simplified to
public interface IUserServices {
Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID);
}
and the implementation
public class UserServices : IUserServices {
GraphServiceClient GetAuthenticatedClient() {
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await SampleAuthProvider.Instance.GetAccessTokenAsync();
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
}));
return graphClient;
}
public Task<IGroupMembersCollectionWithReferencesPage> GetGroupMembers(string groupID) {
var graphClient = GetAuthenticatedClient();
return graphClient.Groups[groupID].Members.Request().GetAsync();
}
}
Which would result in the controller being simplified as well
public class UserController : ApiController {
private readonly IUserServices service;
public UserController(IUserServices myServices) {
this.service = myServices;
}
public async Task<IHttpActionResult> GetGroupMembers([FromUri]string groupID) {
IGroupMembersCollectionWithReferencesPage groupMembers = await service.GetGroupMembers(groupID);
return Ok(groupMembers);
}
}
Now for testing of the controller you can easily mock the abstractions to behave as expected in order to exercise the test to completion because the controller is completely decoupled from the GraphServiceClient 3rd party dependency and the controller can be tested in isolation.
[TestClass]
public class UserControllerShould {
[TestMethod]
public async Task GetGroupMembersCollectionWithReferencePage_Success() {
//Arrange
var groupId = "12345";
var expectedResult = Mock.Of<IGroupMembersCollectionWithReferencesPage>();
var mockService = new Mock<IUserServices>();
mockService
.Setup(_ => _.GetGroupMembers(groupId))
.ReturnsAsync(expectedResult);
var controller = new UserController(mockService.Object);
//Act
var result = await controller.GetGroupMembers(groupId) as System.Web.Http.Results.OkNegotiatedContentResult<IGroupMembersCollectionWithReferencesPage>;
//Assert
Assert.IsNotNull(result);
var actualResult = result.Content;
Assert.AreEqual(expectedResult, actualResult);
}
}
An alternative solution to #Nkosi. Using the constructor public GraphServiceClient(IAuthenticationProvider authenticationProvider, IHttpProvider httpProvider = null); we can Mock the requests actually made.
Complete example below.
Our GraphApiService uses IMemoryCache, to cache both AccessToken and Users from ADB2C, IHttpClientFactory for HTTP requests and Settings is from appsettings.json.
https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-5.0
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-5.0
public class GraphApiService
{
private readonly IHttpClientFactory _clientFactory;
private readonly IMemoryCache _memoryCache;
private readonly Settings _settings;
private readonly string _accessToken;
public GraphApiService(IHttpClientFactory clientFactory, IMemoryCache memoryCache, Settings settings)
{
_clientFactory = clientFactory;
_memoryCache = memoryCache;
_settings = settings;
string graphApiAccessTokenCacheEntry;
// Look for cache key.
if (!_memoryCache.TryGetValue(CacheKeys.GraphApiAccessToken, out graphApiAccessTokenCacheEntry))
{
// Key not in cache, so get data.
var adb2cTokenResponse = GetAccessTokenAsync().GetAwaiter().GetResult();
graphApiAccessTokenCacheEntry = adb2cTokenResponse.access_token;
// Set cache options.
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromSeconds(adb2cTokenResponse.expires_in));
// Save data in cache.
_memoryCache.Set(CacheKeys.GraphApiAccessToken, graphApiAccessTokenCacheEntry, cacheEntryOptions);
}
_accessToken = graphApiAccessTokenCacheEntry;
}
public async Task<List<Adb2cUser>> GetAllUsersAsync(bool refreshCache = false)
{
if (refreshCache)
{
_memoryCache.Remove(CacheKeys.Adb2cUsers);
}
return await _memoryCache.GetOrCreateAsync(CacheKeys.Adb2cUsers, async (entry) =>
{
entry.SetAbsoluteExpiration(TimeSpan.FromHours(1));
var authProvider = new AuthenticationProvider(_accessToken);
GraphServiceClient graphClient = new GraphServiceClient(authProvider, new HttpClientHttpProvider(_clientFactory.CreateClient()));
var users = await graphClient.Users
.Request()
.GetAsync();
return users.Select(user => new Adb2cUser()
{
Id = Guid.Parse(user.Id),
GivenName = user.GivenName,
FamilyName = user.Surname,
}).ToList();
});
}
private async Task<Adb2cTokenResponse> GetAccessTokenAsync()
{
var client = _clientFactory.CreateClient();
var kvpList = new List<KeyValuePair<string, string>>();
kvpList.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
kvpList.Add(new KeyValuePair<string, string>("client_id", _settings.AzureAdB2C.ClientId));
kvpList.Add(new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"));
kvpList.Add(new KeyValuePair<string, string>("client_secret", _settings.AzureAdB2C.ClientSecret));
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
var req = new HttpRequestMessage(HttpMethod.Post, $"https://login.microsoftonline.com/{_settings.AzureAdB2C.Domain}/oauth2/v2.0/token")
{ Content = new FormUrlEncodedContent(kvpList) };
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
using var httpResponse = await client.SendAsync(req);
var response = await httpResponse.Content.ReadAsStringAsync();
httpResponse.EnsureSuccessStatusCode();
var adb2cTokenResponse = JsonSerializer.Deserialize<Adb2cTokenResponse>(response);
return adb2cTokenResponse;
}
}
public class AuthenticationProvider : IAuthenticationProvider
{
private readonly string _accessToken;
public AuthenticationProvider(string accessToken)
{
_accessToken = accessToken;
}
public Task AuthenticateRequestAsync(HttpRequestMessage request)
{
request.Headers.Add("Authorization", $"Bearer {_accessToken}");
return Task.CompletedTask;
}
}
public class HttpClientHttpProvider : IHttpProvider
{
private readonly HttpClient http;
public HttpClientHttpProvider(HttpClient http)
{
this.http = http;
}
public ISerializer Serializer { get; } = new Serializer();
public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);
public void Dispose()
{
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
{
return http.SendAsync(request);
}
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
HttpCompletionOption completionOption,
CancellationToken cancellationToken)
{
return http.SendAsync(request, completionOption, cancellationToken);
}
}
We then use the GraphApiService in various Controllers. Example from a simple CommentController below. CommentService not included but it is not needed for the example anyway.
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class CommentController : ControllerBase
{
private readonly CommentService _commentService;
private readonly GraphApiService _graphApiService;
public CommentController(CommentService commentService, GraphApiService graphApiService)
{
_commentService = commentService;
_graphApiService = graphApiService;
}
[HttpGet("{rootEntity}/{id}")]
public ActionResult<IEnumerable<CommentDto>> Get(RootEntity rootEntity, int id)
{
var comments = _commentService.Get(rootEntity, id);
var users = _graphApiService.GetAllUsersAsync().GetAwaiter().GetResult();
var commentDtos = new List<CommentDto>();
foreach (var comment in comments)
{
commentDtos.Add(CommonToDtoMapper.MapCommentToCommentDto(comment, users));
}
return Ok(commentDtos);
}
[HttpPost("{rootEntity}/{id}")]
public ActionResult Post(RootEntity rootEntity, int id, [FromBody] string message)
{
_commentService.Add(rootEntity, id, message);
_commentService.SaveChanges();
return Ok();
}
}
Since we use our own IAuthenticationProvider and IHttpProvider we can mock IHttpClientFactory based on what URI is called. Complete test example below, check mockMessageHandler.Protected() to see how the requests are mocked. To find the exact request made we look at the documentation. For example var users = await graphClient.Users.Request().GetAsync(); is equivalent to GET https://graph.microsoft.com/v1.0/users.
https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=http#request
public class CommentControllerTest : SeededDatabase
{
[Fact]
public void Get()
{
using (var context = new ApplicationDbContext(_dbContextOptions))
{
var controller = GeCommentController(context);
var result = controller.Get(RootEntity.Question, 1).Result;
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<CommentDto>>(okResult.Value);
Assert.Equal(2, returnValue.Count());
}
}
[Theory]
[MemberData(nameof(PostData))]
public void Post(RootEntity rootEntity, int id, string message)
{
using (var context = new ApplicationDbContext(_dbContextOptions))
{
var controller = GeCommentController(context);
var result = controller.Post(rootEntity, id, message);
var okResult = Assert.IsType<OkResult>(result);
var comment = context.Comments.First(x => x.Text == message);
if(rootEntity == RootEntity.Question)
{
Assert.Equal(comment.QuestionComments.First().QuestionId, id);
}
}
}
public static IEnumerable<object[]> PostData()
{
return new List<object[]>
{
new object[]
{ RootEntity.Question, 1, "Test comment from PostData" }
};
}
private CommentController GeCommentController(ApplicationDbContext dbContext)
{
var userService = new Mock<IUserResolverService>();
userService.Setup(x => x.GetNameIdentifier()).Returns(DbContextSeed.CurrentUser);
var settings = new Settings();
var commentService = new CommentService(new ExtendedApplicationDbContext(dbContext, userService.Object));
var expectedContentGetAccessTokenAsync = #"{
""token_type"": ""Bearer"",
""expires_in"": 3599,
""ext_expires_in"": 3599,
""access_token"": ""123""
}";
var expectedContentGetAllUsersAsync = #"{
""#odata.context"": ""https://graph.microsoft.com/v1.0/$metadata#users"",
""value"": [
{
""businessPhones"": [],
""displayName"": ""Oscar"",
""givenName"": ""Oscar"",
""jobTitle"": null,
""mail"": null,
""mobilePhone"": null,
""officeLocation"": null,
""preferredLanguage"": null,
""surname"": ""Andersson"",
""userPrincipalName"": """ + DbContextSeed.DummyUserExternalId + #"#contoso.onmicrosoft.com"",
""id"":""" + DbContextSeed.DummyUserExternalId + #"""
}
]
}";
var mockFactory = new Mock<IHttpClientFactory>();
var mockMessageHandler = new Mock<HttpMessageHandler>();
mockMessageHandler.Protected()
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains("https://login.microsoftonline.com/")), ItExpr.IsAny<CancellationToken>())
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(expectedContentGetAccessTokenAsync)
});
mockMessageHandler.Protected()
#pragma warning disable SecurityIntelliSenseCS // MS Security rules violation
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.Is<HttpRequestMessage>(x => x.RequestUri.AbsoluteUri.Contains("https://graph.microsoft.com/")), ItExpr.IsAny<CancellationToken>())
#pragma warning restore SecurityIntelliSenseCS // MS Security rules violation
.ReturnsAsync(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(expectedContentGetAllUsersAsync)
});
var httpClient = new HttpClient(mockMessageHandler.Object);
mockFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(httpClient);
var services = new ServiceCollection();
services.AddMemoryCache();
var serviceProvider = services.BuildServiceProvider();
var memoryCache = serviceProvider.GetService<IMemoryCache>();
var graphService = new GraphApiService(mockFactory.Object, memoryCache, settings);
var controller = new CommentController(commentService, graphService);
return controller;
}
}

Categories

Resources