Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ?

like image 258
vishwajeetrkale Avatar asked Jan 11 '18 13:01

vishwajeetrkale


2 Answers

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);
    }
}
like image 53
Nkosi Avatar answered Oct 23 '22 10:10

Nkosi


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;
        }
    }
like image 24
Ogglas Avatar answered Oct 23 '22 10:10

Ogglas