From 113ee9fdd084d2e791818cb332928948614c87da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remi=20Andr=C3=A9=20L=C3=B8voll?= <56019927+lovoll@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:14:26 +0100 Subject: [PATCH] Added new internal api for fetching list of user profiles. (#130) * Adde bew internal api for fetching list of user profiles. * Fixed logging to not trigger on loging user input * Tries to fix a encoding problem in Assert comparer where strings in source code is encoded wrong on build server but correct on dev machine. * Removed double byte characters from utf8 file to remove encoding error in assert. * Fixed all entries of dual byte character * Fixed code issue from an ekstra blank line * Changed test name due to incorect description pointed out in review. --- .../UserProfileInternalController.cs | 24 ++++ .../Decorators/UserProfileCachingDecorator.cs | 34 +++++ .../Implementation/UserProfilesWrapper.cs | 21 +++ .../Services/Interfaces/IUserProfiles.cs | 10 +- .../UserProfileInternalTests.cs | 115 ++++++++++++++++- .../4c3b4909-eb17-45d5-bde1-256e065e196a.json | 48 +++++++ .../UserProfileCachingDecoratorTest.cs | 121 +++++++++++++++++- 7 files changed, 370 insertions(+), 3 deletions(-) create mode 100644 test/Altinn.Profile.Tests/Testdata/UserProfile/4c3b4909-eb17-45d5-bde1-256e065e196a.json diff --git a/src/Altinn.Profile/Controllers/UserProfileInternalController.cs b/src/Altinn.Profile/Controllers/UserProfileInternalController.cs index 27e3cb1..58b4b97 100644 --- a/src/Altinn.Profile/Controllers/UserProfileInternalController.cs +++ b/src/Altinn.Profile/Controllers/UserProfileInternalController.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Altinn.Platform.Profile.Models; using Altinn.Profile.Models; @@ -71,5 +73,27 @@ public async Task> Get([FromBody] UserProfileLookup us return Ok(result); } + + /// + /// Gets a list of user profiles for a list of of users identified by userUuid. + /// + /// List of uuid identifying the users profiles to return + /// List of user profiles + [HttpPost] + [Route("listbyuuid")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [Consumes("application/json")] + [Produces("application/json")] + public async Task>> GetList([FromBody] List userUuidList) + { + if (userUuidList == null || userUuidList.Count == 0) + { + return BadRequest(); + } + + List result = await _userProfilesWrapper.GetUserListByUuid(userUuidList); + return Ok(result); + } } } diff --git a/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs b/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs index ce0d165..9c79f20 100644 --- a/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs +++ b/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Altinn.Platform.Profile.Models; @@ -99,6 +100,39 @@ public async Task GetUserByUuid(Guid userUuid) return user; } + /// + public async Task> GetUserListByUuid(List userUuidList) + { + List userUuidListNotInCache = new List(); + List result = new List(); + + foreach (Guid userUuid in userUuidList) + { + string uniqueCacheKey = $"User:UserUuid:{userUuid}"; + if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile user)) + { + result.Add(user); + } + else + { + userUuidListNotInCache.Add(userUuid); + } + } + + if (userUuidListNotInCache.Count > 0) + { + List usersToCache = await _decoratedService.GetUserListByUuid(userUuidListNotInCache); + foreach (UserProfile user in usersToCache) + { + string uniqueCacheKey = $"User:UserUuid:{user.UserUuid}"; + _memoryCache.Set(uniqueCacheKey, user, _cacheOptions); + result.Add(user); + } + } + + return result; + } + /// public async Task GetUserByUsername(string username) { diff --git a/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs b/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs index fd886e4..c94f375 100644 --- a/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs +++ b/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; @@ -107,6 +108,26 @@ public async Task GetUserByUuid(Guid userUuid) return user; } + /// + public async Task> GetUserListByUuid(List userUuidList) + { + Uri endpointUrl = new Uri($"{_generalSettings.BridgeApiEndpoint}users/byuuid"); + StringContent requestBody = new StringContent(JsonSerializer.Serialize(userUuidList), Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await _client.PostAsync(endpointUrl, requestBody); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Getting users failed with {statusCode}", response.StatusCode); + return new List(); + } + + string content = await response.Content.ReadAsStringAsync(); + List users = JsonSerializer.Deserialize>(content, _serializerOptions); + + return users; + } + /// public async Task GetUserByUsername(string username) { diff --git a/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs b/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs index a925ac3..f2ba6db 100644 --- a/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs +++ b/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Altinn.Platform.Profile.Models; @@ -28,9 +29,16 @@ public interface IUserProfiles /// Method that fetches a user based on a user uuid /// /// The user uuid - /// User profile with given user id. + /// User profile with given user uuid. Task GetUserByUuid(Guid userUuid); + /// + /// Method that fetches a list of users based on a list of user uuid + /// + /// The list of user uuids + /// List of User profiles with given user uuids + Task> GetUserListByUuid(List userUuidList); + /// /// Method that fetches a user based on username. /// diff --git a/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs index 192d342..05fc677 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Json; @@ -12,7 +13,6 @@ using Altinn.Profile.Tests.IntegrationTests.Utils; using Altinn.Profile.Tests.Mocks; using Altinn.Profile.Tests.Testdata; - using Microsoft.AspNetCore.Mvc.Testing; using Xunit; @@ -117,6 +117,112 @@ public async Task GetUserByUuid_SblBridgeFindsProfile_ResponseOk_ReturnsUserProf Assert.Equal("nb", actualUser.ProfileSettingPreference.Language); } + [Fact] + public async Task GetUserListByUuid_SblBridgeFindsProfile_ResponseOk_ReturnsUserProfileList() + { + // Arrange + List userUuids = new List { new("cc86d2c7-1695-44b0-8e82-e633243fdf31"), new("4c3b4909-eb17-45d5-bde1-256e065e196a") }; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + List userProfiles = new() + { + await TestDataLoader.Load(userUuids[0].ToString()), + await TestDataLoader.Load(userUuids[1].ToString()) + }; + + return new HttpResponseMessage() { Content = JsonContent.Create(userProfiles) }; + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/listbyuuid", userUuids); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Post, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/byuuid", sblRequest.RequestUri.ToString()); + + string responseContent = await response.Content.ReadAsStringAsync(); + + List actualUsers = JsonSerializer.Deserialize>( + responseContent, serializerOptionsCamelCase); + + // These asserts check that deserializing with camel casing was successful. + Assert.Equal(userUuids[0], actualUsers[0].UserUuid); + Assert.Equal("LEO WILHELMSEN", actualUsers[0].Party.Name); + Assert.Equal("LEO", actualUsers[0].Party.Person.FirstName); + Assert.Equal("nb", actualUsers[0].ProfileSettingPreference.Language); + + Assert.Equal(userUuids[1], actualUsers[1].UserUuid); + Assert.Equal("ELENA FJAR", actualUsers[1].Party.Name); + Assert.Equal("ELENA", actualUsers[1].Party.Person.FirstName); + Assert.Equal("nn", actualUsers[1].ProfileSettingPreference.Language); + } + + [Fact] + public async Task GetUserListByUuid_SblBridgeFindsNoProfile_ResponseOk_ReturnsEmptyProfileList() + { + // Arrange + List userUuids = new List { new("cc86d2c7-1695-44b0-8e82-e633243fdf31"), new("4c3b4909-eb17-45d5-bde1-256e065e196a") }; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new((request, token) => + { + sblRequest = request; + + List userProfiles = new List(); + + return Task.FromResult(new HttpResponseMessage() { Content = JsonContent.Create(userProfiles) }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/listbyuuid", userUuids); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Post, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/byuuid", sblRequest.RequestUri.ToString()); + + string responseContent = await response.Content.ReadAsStringAsync(); + + List actualUsers = JsonSerializer.Deserialize>( + responseContent, serializerOptionsCamelCase); + + // These asserts check that deserializing with camel casing was successful. + Assert.NotNull(actualUsers); + Assert.Empty(actualUsers); + } + + [Fact] + public async Task GetUserListByUuid_EmptyInput_ResponseBadRequest_ReturnsBadRequest() + { + // Arrange + List userUuids = new List(); + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/listbyuuid", userUuids); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + [Fact] public async Task GetUserById_SblBridgeReturnsNotFound_ResponseNotFound() { @@ -488,5 +594,12 @@ private static HttpRequestMessage CreatePostRequest(string requestUri, UserProfi httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(lookupRequest), Encoding.UTF8, "application/json"); return httpRequestMessage; } + + private static HttpRequestMessage CreatePostRequest(string requestUri, List listRequest) + { + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, requestUri); + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(listRequest), Encoding.UTF8, "application/json"); + return httpRequestMessage; + } } } diff --git a/test/Altinn.Profile.Tests/Testdata/UserProfile/4c3b4909-eb17-45d5-bde1-256e065e196a.json b/test/Altinn.Profile.Tests/Testdata/UserProfile/4c3b4909-eb17-45d5-bde1-256e065e196a.json new file mode 100644 index 0000000..247398d --- /dev/null +++ b/test/Altinn.Profile.Tests/Testdata/UserProfile/4c3b4909-eb17-45d5-bde1-256e065e196a.json @@ -0,0 +1,48 @@ +{ + "UserId": 20000006, + "UserUUID": "4c3b4909-eb17-45d5-bde1-256e065e196a", + "UserType": 1, + "UserName": "", + "ExternalIdentity": "", + "PhoneNumber": null, + "Email": null, + "PartyId": 50002114, + "Party": { + "PartyTypeName": 1, + "SSN": "01025161013", + "OrgNumber": "", + "Person": { + "SSN": "01025161013", + "Name": "ELENA FJAR", + "FirstName": "ELENA", + "MiddleName": "", + "LastName": "FJAR", + "TelephoneNumber": "", + "MobileNumber": "", + "MailingAddress": " Søreidåsen 3 5252 SØREIDGREND", + "MailingPostalCode": "5252", + "MailingPostalCity": "SØREIDGREND", + "AddressMunicipalNumber": "", + "AddressMunicipalName": "", + "AddressStreetName": "", + "AddressHouseNumber": "", + "AddressHouseLetter": "", + "AddressPostalCode": "5252", + "AddressCity": "SØREIDGREND", + "DateOfDeath": null + }, + "Organization": null, + "PartyId": 50002114, + "PartyUUID": "4c3b4909-eb17-45d5-bde1-256e065e196a", + "UnitType": null, + "Name": "ELENA FJAR", + "IsDeleted": false, + "OnlyHierarchyElementWithNoAccess": false, + "ChildParties": null + }, + "ProfileSettingPreference": { + "Language": "nn", + "PreSelectedPartyId": 0, + "DoNotPromptForParty": false + } +} \ No newline at end of file diff --git a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs index de44e7b..7ec498e 100644 --- a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs +++ b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Altinn.Platform.Profile.Models; @@ -68,11 +70,75 @@ public async Task GetUserUserUuid_UserInCache_decoratedServiceNotCalled() UserProfile actual = await target.GetUserByUuid(userUuid); // Assert - _decoratedServiceMock.Verify(service => service.GetUser(It.IsAny()), Times.Never()); + _decoratedServiceMock.Verify(service => service.GetUserByUuid(It.IsAny()), Times.Never()); Assert.NotNull(actual); Assert.Equal(userUuid, actual.UserUuid); } + /// + /// Tests that one of the user profiles are available in the cache the other is fetched from the decorated service both is returned to caller. + /// + [Fact] + public async Task GetUserListUserUuid_UsersPartialInCache_decoratedServiceBothCalledAndNotCalled() + { + // Arrange + List userUuids = new List { new("cc86d2c7-1695-44b0-8e82-e633243fdf31"), new("4c3b4909-eb17-45d5-bde1-256e065e196a") }; + Guid userUuidNotInCache = new("4c3b4909-eb17-45d5-bde1-256e065e196a"); + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + UserProfile userProfile = await TestDataLoader.Load(userUuids[0].ToString()); + memoryCache.Set($"User:UserUuid:{userUuids[0]}", userProfile); + List userProfiles = new List(); + userProfiles.Add(await TestDataLoader.Load(userUuidNotInCache.ToString())); + _decoratedServiceMock.Setup(service => service.GetUserListByUuid(It.Is>(g => g.All(g2 => g2 == userUuidNotInCache)))).ReturnsAsync(userProfiles); + UserProfileCachingDecorator target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + List actual = await target.GetUserListByUuid(userUuids); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUserListByUuid(It.Is>(g => g.All(g2 => g2 == userUuidNotInCache))), Times.Once); + Assert.NotNull(actual); + foreach (var userUuid in userUuids) + { + UserProfile currentProfileFromResult = actual.Find(p => p.UserUuid == userUuid); + UserProfile currentProfileFromCache = memoryCache.Get($"User:UserUuid:{userUuid}"); + Assert.NotNull(currentProfileFromResult); + Assert.NotNull(currentProfileFromCache); + } + } + + /// + /// Tests that the user profiles are available in the cache and is returned to the caller without forwarding request to decorated service. + /// + [Fact] + public async Task GetUserListUserUuid_UsersInCache_decoratedServiceNotCalled() + { + // Arrange + List userUuids = new List { new("cc86d2c7-1695-44b0-8e82-e633243fdf31"), new("4c3b4909-eb17-45d5-bde1-256e065e196a") }; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + foreach (Guid userUuid in userUuids) + { + UserProfile userProfile = await TestDataLoader.Load(userUuid.ToString()); + memoryCache.Set($"User:UserUuid:{userUuid}", userProfile); + } + + UserProfileCachingDecorator target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + List actual = await target.GetUserListByUuid(userUuids); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUserListByUuid(It.IsAny>()), Times.Never()); + Assert.NotNull(actual); + foreach (var userUuid in userUuids) + { + UserProfile currentProfileFromResult = actual.Find(p => p.UserUuid == userUuid); + Assert.NotNull(currentProfileFromResult); + } + } + /// /// Tests that the userprofile is not available in the cache call is forwarded to decorated service and cache is populated result returned to caller. /// @@ -123,6 +189,36 @@ public async Task GetUserUserUuid_UserNotInCache_decoratedServiceCalledMockPopul Assert.True(memoryCache.TryGetValue($"User:UserUuid:{userUuid}", out UserProfile _)); } + /// + /// Tests that the user profiles is not available in the cache call is forwarded to decorated service and cache is populated result returned to caller. + /// + [Fact] + public async Task GetUserListUserUuid_UserNotInCache_decoratedServiceCalledMockPopulated() + { + // Arrange + List userUuids = new List { new("cc86d2c7-1695-44b0-8e82-e633243fdf31"), new("4c3b4909-eb17-45d5-bde1-256e065e196a") }; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + List userProfiles = new List(); + userProfiles.Add(await TestDataLoader.Load(userUuids[0].ToString())); + userProfiles.Add(await TestDataLoader.Load(userUuids[1].ToString())); + + _decoratedServiceMock.Setup(service => service.GetUserListByUuid(It.IsAny>())).ReturnsAsync(userProfiles); + var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + List actual = await target.GetUserListByUuid(userUuids); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUserListByUuid(It.IsAny>()), Times.Once()); + + Assert.Equal(2, actual.Count); + Assert.Equal(userUuids[0], actual[0].UserUuid); + Assert.Equal(userUuids[1], actual[1].UserUuid); + Assert.True(memoryCache.TryGetValue($"User:UserUuid:{userUuids[0]}", out UserProfile _)); + Assert.True(memoryCache.TryGetValue($"User:UserUuid:{userUuids[1]}", out UserProfile _)); + } + /// /// Tests that if the result from decorated service is null, nothing is stored in cache and the null object returned to caller. /// @@ -167,6 +263,29 @@ public async Task GetUserUserUserUuid_NullFromDecoratedService_CacheNotPopulated Assert.False(memoryCache.TryGetValue($"User:UserUuid:{userUuid}", out UserProfile _)); } + /// + /// Tests that if the result from decorated service is an empty list, nothing is stored in cache and the empty list returned to caller. + /// + [Fact] + public async Task GetUserListUserUserUuid_EmptyListFromDecoratedService_CacheNotPopulated() + { + // Arrange + List userUuids = new List { new("cc86d2c7-1695-44b0-8e82-e633243fdf31"), new("4c3b4909-eb17-45d5-bde1-256e065e196a") }; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + _decoratedServiceMock.Setup(service => service.GetUserListByUuid(It.IsAny>())).ReturnsAsync(new List()); + var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + List actual = await target.GetUserListByUuid(userUuids); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUserListByUuid(It.IsAny>()), Times.Once); + Assert.Empty(actual); + Assert.False(memoryCache.TryGetValue($"User:UserUuid:{userUuids[0]}", out UserProfile _)); + Assert.False(memoryCache.TryGetValue($"User:UserUuid:{userUuids[1]}", out UserProfile _)); + } + /// /// Tests that the userprofile available in the cache is returned to the caller without forwarding request to decorated service. ///