diff --git a/src/Altinn.Profile/Controllers/UserProfileInternalController.cs b/src/Altinn.Profile/Controllers/UserProfileInternalController.cs new file mode 100644 index 0000000..2cc92c7 --- /dev/null +++ b/src/Altinn.Profile/Controllers/UserProfileInternalController.cs @@ -0,0 +1,71 @@ +using System.Threading.Tasks; +using Altinn.Platform.Profile.Models; +using Altinn.Profile.Models; +using Altinn.Profile.Services.Interfaces; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Profile.Controllers +{ + /// + /// Controller for user profile API endpoints for internal consumption (e.g. Authorization) requiring neither authenticated user token nor access token authorization. + /// + [Route("profile/api/v1/internal/user")] + [ApiExplorerSettings(IgnoreApi = true)] + [Consumes("application/json")] + [Produces("application/json")] + public class UserProfileInternalController : Controller + { + private readonly IUserProfiles _userProfilesWrapper; + + /// + /// Initializes a new instance of the class + /// + /// The users wrapper + public UserProfileInternalController(IUserProfiles userProfilesWrapper) + { + _userProfilesWrapper = userProfilesWrapper; + } + + /// + /// Gets the user profile for a given user identified by one of the available types of user identifiers: + /// UserId (from Altinn 2 Authn UserProfile) + /// Username (from Altinn 2 Authn UserProfile) + /// SSN/Dnr (from Freg) + /// Uuid (from Altinn 2 Party/UserProfile implementation will be added later) + /// + /// Input model for providing one of the supported lookup parameters + /// User profile of the given user + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get([FromBody] UserProfileLookup userProfileLookup) + { + UserProfile result; + if (userProfileLookup != null && userProfileLookup.UserId != 0) + { + result = await _userProfilesWrapper.GetUser(userProfileLookup.UserId); + } + else if (!string.IsNullOrWhiteSpace(userProfileLookup?.Username)) + { + result = await _userProfilesWrapper.GetUserByUsername(userProfileLookup.Username); + } + else if (!string.IsNullOrWhiteSpace(userProfileLookup?.Ssn)) + { + result = await _userProfilesWrapper.GetUser(userProfileLookup.Ssn); + } + else + { + return BadRequest(); + } + + if (result == null) + { + return NotFound(); + } + + return Ok(result); + } + } +} diff --git a/src/Altinn.Profile/Models/UserProfileLookup.cs b/src/Altinn.Profile/Models/UserProfileLookup.cs new file mode 100644 index 0000000..75fab04 --- /dev/null +++ b/src/Altinn.Profile/Models/UserProfileLookup.cs @@ -0,0 +1,27 @@ +namespace Altinn.Profile.Models +{ + /// + /// Input model for internal UserProfile lookup requests, where one of the lookup identifiers available must be set for performing the lookup request: + /// UserId (from Altinn 2 Authn UserProfile) + /// Username (from Altinn 2 Authn UserProfile) + /// SSN/Dnr (from Freg) + /// Uuid (from Altinn 2 Party/UserProfile implementation will be added later) + /// + public class UserProfileLookup + { + /// + /// Gets or sets the users UserId if the lookup is to be performed based on this identifier + /// + public int UserId { get; set; } + + /// + /// Gets or sets the users Username if the lookup is to be performed based on this identifier + /// + public string Username { get; set; } + + /// + /// Gets or sets the users social security number or d-number from Folkeregisteret if the lookup is to be performed based on this identifier + /// + public string Ssn { get; set; } + } +} diff --git a/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs b/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs index 9a7d809..d900245 100644 --- a/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs +++ b/src/Altinn.Profile/Services/Decorators/UserProfileCachingDecorator.cs @@ -78,5 +78,25 @@ public async Task GetUser(string ssn) return user; } + + /// + public async Task GetUserByUsername(string username) + { + string uniqueCacheKey = "User_Username_" + username; + + if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile user)) + { + return user; + } + + user = await _decoratedService.GetUserByUsername(username); + + if (user != null) + { + _memoryCache.Set(uniqueCacheKey, user, _cacheOptions); + } + + return user; + } } } diff --git a/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs b/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs index e84af2d..9695b2b 100644 --- a/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs +++ b/src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs @@ -8,8 +8,6 @@ using Altinn.Platform.Profile.Models; using Altinn.Profile.Configuration; using Altinn.Profile.Services.Interfaces; - -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -89,5 +87,26 @@ public async Task GetUser(string ssn) return user; } + + /// + public async Task GetUserByUsername(string username) + { + UserProfile user; + + Uri endpointUrl = new Uri($"{_generalSettings.BridgeApiEndpoint}users/?username={username}"); + + HttpResponseMessage response = await _client.GetAsync(endpointUrl); + + if (!response.IsSuccessStatusCode) + { + _logger.LogError("Getting user {username} failed with {statusCode}", username, response.StatusCode); + return null; + } + + string content = await response.Content.ReadAsStringAsync(); + user = JsonSerializer.Deserialize(content, _serializerOptions); + + return user; + } } } diff --git a/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs b/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs index 5ce5502..420d811 100644 --- a/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs +++ b/src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs @@ -22,5 +22,12 @@ public interface IUserProfiles /// The user's ssn. /// User profile connected to given ssn. Task GetUser(string ssn); + + /// + /// Method that fetches a user based on username. + /// + /// The user's username. + /// User profile connected to given username. + Task GetUserByUsername(string username); } } diff --git a/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs new file mode 100644 index 0000000..90b4b26 --- /dev/null +++ b/test/Altinn.Profile.Tests/IntegrationTests/UserProfileInternalTests.cs @@ -0,0 +1,391 @@ +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Altinn.Platform.Profile.Models; +using Altinn.Profile.Configuration; +using Altinn.Profile.Controllers; +using Altinn.Profile.Models; +using Altinn.Profile.Tests.IntegrationTests.Utils; +using Altinn.Profile.Tests.Mocks; +using Altinn.Profile.Tests.Testdata; + +using Microsoft.AspNetCore.Mvc.Testing; + +using Xunit; + +namespace Altinn.Profile.Tests.IntegrationTests +{ + public class UserProfileInternalTests : IClassFixture> + { + private readonly WebApplicationFactorySetup _webApplicationFactorySetup; + + private readonly JsonSerializerOptions serializerOptionsCamelCase = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public UserProfileInternalTests(WebApplicationFactory factory) + { + _webApplicationFactorySetup = new WebApplicationFactorySetup(factory); + + GeneralSettings generalSettings = new() { BridgeApiEndpoint = "http://localhost/" }; + _webApplicationFactorySetup.GeneralSettingsOptions.Setup(s => s.Value).Returns(generalSettings); + } + + [Fact] + public async Task GetUserById_SblBridgeFindsProfile_ResponseOk_ReturnsUserProfile() + { + // Arrange + const int UserId = 2516356; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + UserProfile userProfile = await TestDataLoader.Load(UserId.ToString()); + return new HttpResponseMessage() { Content = JsonContent.Create(userProfile) }; + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { UserId = UserId }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Get, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/{UserId}", sblRequest.RequestUri.ToString()); + + string responseContent = await response.Content.ReadAsStringAsync(); + + UserProfile actualUser = JsonSerializer.Deserialize( + responseContent, serializerOptionsCamelCase); + + // These asserts check that deserializing with camel casing was successful. + Assert.Equal(UserId, actualUser.UserId); + Assert.Equal("sophie", actualUser.UserName); + Assert.Equal("Sophie Salt", actualUser.Party.Name); + Assert.Equal("Sophie", actualUser.Party.Person.FirstName); + Assert.Equal("nb", actualUser.ProfileSettingPreference.Language); + } + + [Fact] + public async Task GetUserById_SblBridgeReturnsNotFound_ResponseNotFound() + { + // Arrange + const int UserId = 2222222; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { UserId = UserId }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Get, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/{UserId}", sblRequest.RequestUri.ToString()); + } + + [Fact] + public async Task GetUserById_SblBridgeReturnsUnavailable_ResponseNotFound() + { + // Arrange + const int UserId = 2222222; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.ServiceUnavailable }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { UserId = UserId }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Get, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/{UserId}", sblRequest.RequestUri.ToString()); + } + + [Fact] + public async Task GetUserBySsn_SblBridgeFindsProfile_ReturnsUserProfile() + { + // Arrange + const string Ssn = "01017512345"; + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + UserProfile userProfile = await TestDataLoader.Load("2516356"); + return new HttpResponseMessage() { Content = JsonContent.Create(userProfile) }; + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { Ssn = Ssn }); + + 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", sblRequest.RequestUri.ToString()); + + string requestContent = await sblRequest.Content.ReadAsStringAsync(); + + Assert.Equal($"\"{Ssn}\"", requestContent); + + string responseContent = await response.Content.ReadAsStringAsync(); + + UserProfile actualUser = JsonSerializer.Deserialize( + responseContent, serializerOptionsCamelCase); + + // These asserts check that deserializing with camel casing was successful. + Assert.Equal(2516356, actualUser.UserId); + Assert.Equal("sophie", actualUser.UserName); + Assert.Equal("Sophie Salt", actualUser.Party.Name); + Assert.Equal("Sophie", actualUser.Party.Person.FirstName); + Assert.Equal("nb", actualUser.ProfileSettingPreference.Language); + } + + [Fact] + public async Task GetUserBySsn_SblBridgeReturnsNotFound_RespondsNotFound() + { + // Arrange + const string Ssn = "01017512345"; + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { Ssn = Ssn }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Post, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users", sblRequest.RequestUri.ToString()); + + string requestContent = await sblRequest.Content.ReadAsStringAsync(); + + Assert.Equal($"\"{Ssn}\"", requestContent); + } + + [Fact] + public async Task GetUserBySsn_SblBridgeReturnsUnavailable_RespondsNotFound() + { + // Arrange + const string Ssn = "01017512345"; + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.ServiceUnavailable }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { Ssn = Ssn }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Post, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users", sblRequest.RequestUri.ToString()); + + string requestContent = await sblRequest.Content.ReadAsStringAsync(); + + Assert.Equal($"\"{Ssn}\"", requestContent); + } + + [Fact] + public async Task GetUserByUsername_SblBridgeFindsProfile_ResponseOk_ReturnsUserProfile() + { + // Arrange + const string Username = "OrstaECUser"; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + UserProfile userProfile = await TestDataLoader.Load(Username); + return new HttpResponseMessage() { Content = JsonContent.Create(userProfile) }; + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { Username = Username }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Get, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/?username={Username}", sblRequest.RequestUri.ToString()); + + string responseContent = await response.Content.ReadAsStringAsync(); + + UserProfile actualUser = JsonSerializer.Deserialize( + responseContent, serializerOptionsCamelCase); + + // These asserts check that deserializing with camel casing was successful. + Assert.Equal(Username, actualUser.UserName); + Assert.Equal(50005545, actualUser.Party.PartyId); + Assert.Equal("ORSTA OG HEGGEDAL ", actualUser.Party.Name); + Assert.Equal("ORSTA OG HEGGEDAL", actualUser.Party.Organization.Name); + Assert.Equal("nb", actualUser.ProfileSettingPreference.Language); + } + + [Fact] + public async Task GetUserByUsername_SblBridgeReturnsNotFound_ResponseNotFound() + { + // Arrange + const string Username = "NonExistingUsername"; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { Username = Username }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Get, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/?username={Username}", sblRequest.RequestUri.ToString()); + } + + [Fact] + public async Task GetUserByUsername_SblBridgeReturnsUnavailable_ResponseNotFound() + { + // Arrange + const string Username = "OrstaECUser"; + + HttpRequestMessage sblRequest = null; + DelegatingHandlerStub messageHandler = new(async (request, token) => + { + sblRequest = request; + + return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.ServiceUnavailable }); + }); + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", new UserProfileLookup { Username = Username }); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + Assert.NotNull(sblRequest); + Assert.Equal(HttpMethod.Get, sblRequest.Method); + Assert.EndsWith($"sblbridge/profile/api/users/?username={Username}", sblRequest.RequestUri.ToString()); + } + + [Fact] + public async Task GetUserEmptyInputModel_UserProfileInternalController_ResponseBadRequest() + { + // Arrange + UserProfileLookup emptyInputModel = new UserProfileLookup(); + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", emptyInputModel); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task GetUserNullInputModel_UserProfileInternalController_ResponseBadRequest() + { + // Arrange + UserProfileLookup nullInputModel = null; + + HttpRequestMessage httpRequestMessage = CreatePostRequest($"/profile/api/v1/internal/user/", nullInputModel); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + private static HttpRequestMessage CreatePostRequest(string requestUri, UserProfileLookup lookupRequest) + { + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, requestUri); + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(lookupRequest), Encoding.UTF8, "application/json"); + return httpRequestMessage; + } + } +} diff --git a/test/Altinn.Profile.Tests/Testdata/UserProfile/OrstaECUser.json b/test/Altinn.Profile.Tests/Testdata/UserProfile/OrstaECUser.json new file mode 100644 index 0000000..bb5173a --- /dev/null +++ b/test/Altinn.Profile.Tests/Testdata/UserProfile/OrstaECUser.json @@ -0,0 +1,43 @@ +{ + "UserId": 2001072, + "UserType": 3, + "UserName": "OrstaECUser", + "ExternalIdentity": "", + "PhoneNumber": null, + "Email": null, + "PartyId": 50005545, + "Party": { + "PartyTypeName": 2, + "SSN": "", + "OrgNumber": "910459880", + "Person": null, + "Organization": { + "OrgNumber": "910459880", + "Name": "ORSTA OG HEGGEDAL", + "UnitType": "AS", + "TelephoneNumber": "12345678", + "MobileNumber": "99999999", + "FaxNumber": "12345679", + "EMailAddress": "test@test.test", + "InternetAddress": null, + "MailingAddress": null, + "MailingPostalCode": "", + "MailingPostalCity": "", + "BusinessAddress": null, + "BusinessPostalCode": "", + "BusinessPostalCity": "", + "UnitStatus": "N" + }, + "PartyId": 50005545, + "UnitType": "AS", + "Name": "ORSTA OG HEGGEDAL ", + "IsDeleted": false, + "OnlyHierarchyElementWithNoAccess": false, + "ChildParties": null + }, + "ProfileSettingPreference": { + "Language": "nb", + "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 52673e3..ef15cdb 100644 --- a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs +++ b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs @@ -166,5 +166,79 @@ public async Task GetUserUserSSN_NullFromDecoratedService_CacheNotPopulated() Assert.Null(actual); Assert.False(memoryCache.TryGetValue("User_UserId_2001607", out UserProfile _)); } + + /// + /// Tests that the userprofile available in the cache is returned to the caller without forwarding request to decorated service. + /// + [Fact] + public async Task GetUserByUsername_UserInCache_decoratedServiceNotCalled() + { + // Arrange + const string Username = "OrstaECUser"; + const int UserId = 2001072; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + var userProfile = await TestDataLoader.Load(Username); + memoryCache.Set("User_Username_OrstaECUser", userProfile); + var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + UserProfile actual = await target.GetUserByUsername(Username); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUser(It.IsAny()), Times.Never()); + Assert.NotNull(actual); + Assert.Equal(UserId, actual.UserId); + Assert.Equal(Username, actual.UserName); + } + + /// + /// Tests that when the userprofile is not available in the cache, the request is forwarded to the decorated service. + /// + [Fact] + public async Task GetUserByUsername_UserNotInCache_decoratedServiceCalledMockPopulated() + { + // Arrange + const string Username = "OrstaECUser"; + const int UserId = 2001072; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + var userProfile = await TestDataLoader.Load(Username); + _decoratedServiceMock.Setup(service => service.GetUserByUsername(Username)).ReturnsAsync(userProfile); + var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + UserProfile actual = await target.GetUserByUsername(Username); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUserByUsername(Username), Times.Once()); + + Assert.NotNull(actual); + Assert.Equal(UserId, actual.UserId); + Assert.Equal(Username, actual.UserName); + Assert.True(memoryCache.TryGetValue("User_Username_OrstaECUser", out UserProfile _)); + } + + /// + /// Tests that if the result from decorated service is null, nothing is stored in cache and the null object returned to caller. + /// + [Fact] + public async Task GetUserByUsername_NullFromDecoratedService_CacheNotPopulated() + { + // Arrange + const string Username = "NonExistingUsername"; + MemoryCache memoryCache = new(new MemoryCacheOptions()); + + _decoratedServiceMock.Setup(service => service.GetUserByUsername(Username)).ReturnsAsync((UserProfile)null); + var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); + + // Act + UserProfile actual = await target.GetUserByUsername(Username); + + // Assert + _decoratedServiceMock.Verify(service => service.GetUserByUsername(Username), Times.Once()); + Assert.Null(actual); + Assert.False(memoryCache.TryGetValue("User_Username_NonExistingUsername", out UserProfile _)); + } } }