diff --git a/src/Storage/Altinn.Platform.Storage.csproj b/src/Storage/Altinn.Platform.Storage.csproj index 85fed13e..268aef0b 100644 --- a/src/Storage/Altinn.Platform.Storage.csproj +++ b/src/Storage/Altinn.Platform.Storage.csproj @@ -10,6 +10,8 @@ + + diff --git a/src/Storage/Configuration/GeneralSettings.cs b/src/Storage/Configuration/GeneralSettings.cs index 799ae824..49c01975 100644 --- a/src/Storage/Configuration/GeneralSettings.cs +++ b/src/Storage/Configuration/GeneralSettings.cs @@ -47,5 +47,10 @@ public class GeneralSettings /// Gets or sets the cache lifetime for application metadata document. /// public int AppMetadataCacheLifeTimeInSeconds { get; set; } + + /// + /// Name of the cookie for where JWT is stored + /// + public string JwtCookieName { get; set; } } } diff --git a/src/Storage/Configuration/PlatformSettings.cs b/src/Storage/Configuration/PlatformSettings.cs new file mode 100644 index 00000000..971c55b4 --- /dev/null +++ b/src/Storage/Configuration/PlatformSettings.cs @@ -0,0 +1,31 @@ +using System.Runtime.Serialization; + +namespace Altinn.Platform.Storage.Configuration +{ + /// + /// Represents a set of configuration options when communicating with the platform API. + /// Instances of this class is initialised with values from app settings. Some values can be overridden by environment variables. + /// + public class PlatformSettings + { + /// + /// Gets or sets the url for the Register API endpoint. + /// + public string ApiRegisterEndpoint { get; set; } + + /// + /// Gets or sets the url for the Profile API endpoint + /// + public string ApiProfileEndpoint { get; set; } + + /// + /// Gets or sets the apps domain used to match events source + /// + public string AppsDomain { get; set; } + + /// + /// The lifetime to cache subscriptions + /// + public int SubscriptionCachingLifetimeInSeconds { get; set; } + } +} diff --git a/src/Storage/Controllers/InstancesController.cs b/src/Storage/Controllers/InstancesController.cs index ced7099e..cb237900 100644 --- a/src/Storage/Controllers/InstancesController.cs +++ b/src/Storage/Controllers/InstancesController.cs @@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; @@ -46,6 +47,7 @@ public class InstancesController : ControllerBase private readonly IInstanceEventService _instanceEventService; private readonly string _storageBaseAndHost; private readonly GeneralSettings _generalSettings; + private readonly IRegisterService _registerService; /// /// Initializes a new instance of the class @@ -56,6 +58,7 @@ public class InstancesController : ControllerBase /// the logger /// the authorization service. /// the instance event service. + /// the instance register service. /// the general settings. public InstancesController( IInstanceRepository instanceRepository, @@ -64,6 +67,7 @@ public InstancesController( ILogger logger, IAuthorization authorizationService, IInstanceEventService instanceEventService, + IRegisterService registerService, IOptions settings) { _instanceRepository = instanceRepository; @@ -73,6 +77,7 @@ public InstancesController( _storageBaseAndHost = $"{settings.Value.Hostname}/storage/api/v1/"; _authorizationService = authorizationService; _instanceEventService = instanceEventService; + _registerService = registerService; _generalSettings = settings.Value; } @@ -86,6 +91,7 @@ public InstancesController( /// Process end state. /// Process ended value. /// Instance owner id. + /// Instance owner identifier, i.e. Person:PersonNumber, Organisation:OrganisationNumber, Username:Username. /// Last changed date. /// Created time. /// The visible after date time. @@ -111,6 +117,7 @@ public async Task>> GetInstances( [FromQuery(Name = "process.endEvent")] string processEndEvent, [FromQuery(Name = "process.ended")] string processEnded, [FromQuery(Name = "instanceOwner.partyId")] int? instanceOwnerPartyId, + [FromHeader(Name = "X-Ai-InstanceOwnerIdentifier")] string instanceOwnerIdentifier, [FromQuery] string lastChanged, [FromQuery] string created, [FromQuery(Name = "visibleAfter")] string visibleAfter, @@ -155,7 +162,22 @@ public async Task>> GetInstances( { if (instanceOwnerPartyId == null) { - return BadRequest("InstanceOwnerPartyId must be defined."); + if (string.IsNullOrEmpty(instanceOwnerIdentifier)) + { + return BadRequest("Either InstanceOwnerPartyId or InstanceOwnerIdentifier need to be defined."); + } + + (string instanceOwnerIdType, string instanceOwnerIdValue) = InstanceHelper.GetIdentifierFromInstanceOwnerIdentifier(instanceOwnerIdentifier); + + if (string.IsNullOrEmpty(instanceOwnerIdType) || string.IsNullOrEmpty(instanceOwnerIdValue)) + { + return BadRequest("Invalid InstanceOwnerIdentifier."); + } + + string orgNo = instanceOwnerIdType == "organization" ? instanceOwnerIdValue : string.Empty; + string person = instanceOwnerIdType == "person" ? instanceOwnerIdValue : string.Empty; + + instanceOwnerPartyId = await _registerService.PartyLookup(orgNo, person); } } else @@ -170,6 +192,10 @@ public async Task>> GetInstances( } Dictionary queryParams = QueryHelpers.ParseQuery(Request.QueryString.Value); + if (instanceOwnerPartyId > 0) + { + queryParams["instanceOwner.partyId"] = new StringValues(instanceOwnerPartyId.ToString()); + } // filter out hard deleted instances if it isn't appOwner requesting instances if (!appOwnerRequestingInstances) diff --git a/src/Storage/Exceptions/PlatformHttpException.cs b/src/Storage/Exceptions/PlatformHttpException.cs new file mode 100644 index 00000000..ffbdd696 --- /dev/null +++ b/src/Storage/Exceptions/PlatformHttpException.cs @@ -0,0 +1,40 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Altinn.Platform.Storage.Exceptions +{ + /// + /// Exception class to hold exceptions when talking to the platform REST services + /// + public class PlatformHttpException : Exception + { + /// + /// Responsible for holding an http request exception towards platform. + /// + public HttpResponseMessage Response { get; } + + /// + /// Creates a platform exception + /// + /// The http response + /// A PlatformHttpException + public static async Task CreateAsync(HttpResponseMessage response) + { + string content = await response.Content.ReadAsStringAsync(); + string message = $"{(int)response.StatusCode} - {response.ReasonPhrase} - {content}"; + + return new PlatformHttpException(response, message); + } + + /// + /// Copy the response for further investigations + /// + /// the response + /// the message + public PlatformHttpException(HttpResponseMessage response, string message) : base(message) + { + this.Response = response; + } + } +} diff --git a/src/Storage/Extensions/HttpClientExtension.cs b/src/Storage/Extensions/HttpClientExtension.cs new file mode 100644 index 00000000..bd5b18ba --- /dev/null +++ b/src/Storage/Extensions/HttpClientExtension.cs @@ -0,0 +1,56 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Altinn.Platform.Storage.Extensions +{ + /// + /// This extension is created to make it easy to add a bearer token to a HttpRequests. + /// + public static class HttpClientExtension + { + /// + /// Extension that add authorization header to request + /// + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The http content + /// The platformAccess tokens + /// A HttpResponseMessage + public static Task PostAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string platformAccessToken = null) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, new Uri(requestUri, UriKind.Relative)); + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + request.Content = content; + + if (!string.IsNullOrEmpty(platformAccessToken)) + { + request.Headers.Add("PlatformAccessToken", platformAccessToken); + } + + return httpClient.SendAsync(request, CancellationToken.None); + } + + /// + /// Extension that add authorization header to request + /// + /// The HttpClient + /// the authorization token (jwt) + /// The request Uri + /// The platformAccess tokens + /// A HttpResponseMessage + public static Task GetAsync(this HttpClient httpClient, string authorizationToken, string requestUri, string platformAccessToken = null) + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Add("Authorization", "Bearer " + authorizationToken); + if (!string.IsNullOrEmpty(platformAccessToken)) + { + request.Headers.Add("PlatformAccessToken", platformAccessToken); + } + + return httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, CancellationToken.None); + } + } +} diff --git a/src/Storage/Helpers/InstanceHelper.cs b/src/Storage/Helpers/InstanceHelper.cs index e620e239..53687f77 100644 --- a/src/Storage/Helpers/InstanceHelper.cs +++ b/src/Storage/Helpers/InstanceHelper.cs @@ -246,5 +246,38 @@ private static bool HideOnCurrentTask(HideSettings hideSettings, ProcessElementI return hideSettings.HideOnTask.Contains(currentTask.ElementId); } + + /// + /// Parsing instanceOwnerIdentifier string and find the number type and value. + /// + /// The list of applications + public static (string InstanceOwnerIdType, string InstanceOwnerIdValue) GetIdentifierFromInstanceOwnerIdentifier(string instanceOwnerIdentifier) + { + string partyType = null; + string partyNumber = null; + + if (string.IsNullOrEmpty(instanceOwnerIdentifier)) + { + return (string.Empty, string.Empty); + } + + string[] parts = instanceOwnerIdentifier.Replace(" ", string.Empty).ToLower().Split(':'); + if (parts.Length != 2) + { + return (string.Empty, string.Empty); + } + + partyType = parts[0]; + partyNumber = parts[1]; + + string[] partyTypeHayStack = ["person", "organization"]; + + if (Array.IndexOf(partyTypeHayStack, partyType) != -1) + { + return (partyType, partyNumber); + } + + return (string.Empty, string.Empty); + } } } diff --git a/src/Storage/Program.cs b/src/Storage/Program.cs index f8c36c1c..676a2ff8 100644 --- a/src/Storage/Program.cs +++ b/src/Storage/Program.cs @@ -6,6 +6,7 @@ using Altinn.Common.AccessToken; using Altinn.Common.AccessToken.Configuration; using Altinn.Common.AccessToken.Services; +using Altinn.Common.AccessTokenClient.Services; using Altinn.Common.PEP.Authorization; using Altinn.Common.PEP.Clients; using Altinn.Common.PEP.Configuration; @@ -189,12 +190,13 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.AddHealthChecks().AddCheck("storage_health_check"); services.AddHttpClient(); + services.AddHttpClient(); services.Configure(config.GetSection("AzureStorageConfiguration")); services.Configure(config.GetSection("GeneralSettings")); services.Configure(config.GetSection("kvSetting")); services.Configure(config.GetSection("PepSettings")); - services.Configure(config.GetSection("PlatformSettings")); + services.Configure(config.GetSection("PlatformSettings")); services.Configure(config.GetSection("QueueStorageSettings")); services.Configure(config.GetSection("AccessTokenSettings")); services.Configure(config.GetSection("PostgreSqlSettings")); @@ -250,6 +252,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Storage/Services/IRegisterService.cs b/src/Storage/Services/IRegisterService.cs new file mode 100644 index 00000000..047537e3 --- /dev/null +++ b/src/Storage/Services/IRegisterService.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Altinn.Platform.Register.Models; + +namespace Altinn.Platform.Storage.Services +{ + /// + /// Interface to handle services exposed in Platform Register + /// + public interface IRegisterService + { + /// + /// Returns party information + /// + /// The partyId + /// The party for the given partyId + Task GetParty(int partyId); + + /// + /// Party lookup + /// + /// organisation number + /// f or d number + /// + Task PartyLookup(string orgNo, string person); + } +} diff --git a/src/Storage/Services/RegisterService.cs b/src/Storage/Services/RegisterService.cs new file mode 100644 index 00000000..e9d574a6 --- /dev/null +++ b/src/Storage/Services/RegisterService.cs @@ -0,0 +1,115 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +using Altinn.Common.AccessTokenClient.Services; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Configuration; +using Altinn.Platform.Storage.Exceptions; +using Altinn.Platform.Storage.Extensions; + +using AltinnCore.Authentication.Utils; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Altinn.Platform.Storage.Services +{ + /// + /// Handles register service + /// + public class RegisterService : IRegisterService + { + private readonly HttpClient _client; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly GeneralSettings _generalSettings; + private readonly IAccessTokenGenerator _accessTokenGenerator; + private readonly ILogger _logger; + + private readonly JsonSerializerOptions _serializerOptions; + + /// + /// Initializes a new instance of the class. + /// + public RegisterService( + HttpClient httpClient, + IHttpContextAccessor httpContextAccessor, + IAccessTokenGenerator accessTokenGenerator, + IOptions generalSettings, + IOptions platformSettings, + ILogger logger) + { + httpClient.BaseAddress = new Uri(platformSettings.Value.ApiRegisterEndpoint); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + _client = httpClient; + _httpContextAccessor = httpContextAccessor; + _generalSettings = generalSettings.Value; + _accessTokenGenerator = accessTokenGenerator; + _logger = logger; + + _serializerOptions = new() + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }; + } + + /// + public async Task GetParty(int partyId) + { + Party party = null; + + string endpointUrl = $"parties/{partyId}"; + string token = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _generalSettings.JwtCookieName); + string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "events"); + + HttpResponseMessage response = await _client.GetAsync(token, endpointUrl, accessToken); + HttpStatusCode responseHttpStatusCode = response.StatusCode; + + if (responseHttpStatusCode == HttpStatusCode.OK) + { + party = await response.Content.ReadFromJsonAsync(_serializerOptions); + } + else + { + _logger.LogError("// Getting party with partyID {PartyId} failed with statuscode {ResponseHttpStatusCode}", partyId, responseHttpStatusCode); + } + + return party; + } + + /// + public async Task PartyLookup(string orgNo, string person) + { + string endpointUrl = "parties/lookup"; + + PartyLookup partyLookup = new PartyLookup() { Ssn = person, OrgNo = orgNo }; + + string bearerToken = JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _generalSettings.JwtCookieName); + string accessToken = _accessTokenGenerator.GenerateAccessToken("platform", "storage"); + + StringContent content = new StringContent(JsonSerializer.Serialize(partyLookup)); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + + HttpResponseMessage response = await _client.PostAsync(bearerToken, endpointUrl, content, accessToken); + if (response.StatusCode == HttpStatusCode.OK) + { + Party party = await response.Content.ReadFromJsonAsync(_serializerOptions); + return party.PartyId; + } + else + { + string reason = await response.Content.ReadAsStringAsync(); + _logger.LogError("// RegisterService // PartyLookup // Failed to lookup party in platform register. Response status code is {StatusCode}. \n Reason {Reason}.", response.StatusCode, reason); + + throw await PlatformHttpException.CreateAsync(response); + } + } + } +} diff --git a/src/Storage/appsettings.json b/src/Storage/appsettings.json index bf372f59..8d007fa8 100644 --- a/src/Storage/appsettings.json +++ b/src/Storage/appsettings.json @@ -26,9 +26,11 @@ "TextResourceCacheLifeTimeInSeconds": 3600, "AppTitleCacheLifeTimeInSeconds": 3600, "AppMetadataCacheLifeTimeInSeconds": 300, + "JwtCookieName": "AltinnStudioRuntime", "InstanceReadScope": [ "altinn:serviceowner/instances.read" ] }, "PlatformSettings": { + "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", "ApiAuthorizationEndpoint": "http://localhost:5050/authorization/api/v1/", "SubscriptionKey": "replace-with-apim-subscriptionkey" diff --git a/test/UnitTest/Extensions/HttpClientExtensionTests.cs b/test/UnitTest/Extensions/HttpClientExtensionTests.cs new file mode 100644 index 00000000..40d8d675 --- /dev/null +++ b/test/UnitTest/Extensions/HttpClientExtensionTests.cs @@ -0,0 +1,52 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Altinn.Platform.Storage.Extensions; +using Altinn.Platform.Storage.Tests.Stubs; + +using Xunit; + +namespace Altinn.Platform.Storage.Tests.Extensions +{ + public class HttpClientExtensionTests + { + private readonly HttpClient _httpClient; + private HttpRequestMessage _httpRequest; + + public HttpClientExtensionTests() + { + var httpMessageHandler = new DelegatingHandlerStub(async (request, token) => + { + _httpRequest = request; + return await Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + }); + + _httpClient = new HttpClient(httpMessageHandler); + _httpClient.BaseAddress = new Uri("http://localhost:5101/register/api/v1/"); + } + + [Fact] + public async Task PostAsync_ShouldAddAuthorizationHeaderAndReturnHttpResponseMessage() + { + // Arrange + HttpContent content = new StringContent("dummyContent"); + + // Act + _ = await _httpClient.PostAsync("dummyAuthorizationToken", "/api/resource", content, "dummyPlatformAccessToken"); + + // Assert + Assert.True(_httpRequest.Headers.Contains("PlatformAccessToken")); + } + + [Fact] + public async Task GetAsync_ShouldAddAuthorizationHeaderAndReturnHttpResponseMessage() + { + // Act + _ = await _httpClient.GetAsync("dummyAuthorizationToken", "/api/resource", "dummyPlatformAccessToken"); + + // Assert + Assert.True(_httpRequest.Headers.Contains("PlatformAccessToken")); + } + } +} diff --git a/test/UnitTest/HelperTests/InstanceHelperTest.cs b/test/UnitTest/HelperTests/InstanceHelperTest.cs index b6dfc2c9..9dd8f4f0 100644 --- a/test/UnitTest/HelperTests/InstanceHelperTest.cs +++ b/test/UnitTest/HelperTests/InstanceHelperTest.cs @@ -539,5 +539,27 @@ public void ConvertToSBLInstanceEvent_SingleEvent_AllPropertiesMapped() Assert.Equal(1337, actual.User.UserId); Assert.Equal("test.event", actual.EventType); } + + [Theory] + [InlineData(null, "", "")] + [InlineData("", "", "")] + [InlineData("person12345", "", "")] + [InlineData("invalid:12345", "", "")] + [InlineData("PERSON:12345", "person", "12345")] + [InlineData("organization: 12345 ", "organization", "12345")] + [InlineData(" person:12345", "person", "12345")] + [InlineData("Person:12345", "person", "12345")] + [InlineData("organization: 123 45", "organization", "12345")] + [InlineData("organization:12345", "organization", "12345")] + [InlineData("Organization:67890", "organization", "67890")] + [InlineData(" Organization : 456 78", "organization", "45678")] + public void GetIdentifierFromInstanceOwnerIdentifier_ValidInput_ReturnsCorrectTuple(string instanceOwnerIdentifier, string expectedType, string expectedValue) + { + // Act + var result = InstanceHelper.GetIdentifierFromInstanceOwnerIdentifier(instanceOwnerIdentifier); + + // Assert + Assert.Equal((expectedType, expectedValue), result); + } } } diff --git a/test/UnitTest/Stubs/DelegatingHandlerStub.cs b/test/UnitTest/Stubs/DelegatingHandlerStub.cs new file mode 100644 index 00000000..70ec3559 --- /dev/null +++ b/test/UnitTest/Stubs/DelegatingHandlerStub.cs @@ -0,0 +1,28 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Altinn.Platform.Storage.Tests.Stubs +{ + public class DelegatingHandlerStub : DelegatingHandler + { + private readonly Func> _handlerFunc; + + public DelegatingHandlerStub() + { + _handlerFunc = (request, cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); + } + + public DelegatingHandlerStub(Func> handlerFunc) + { + _handlerFunc = handlerFunc; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _handlerFunc(request, cancellationToken); + } + } +} diff --git a/test/UnitTest/TestingControllers/InstancesControllerTests.cs b/test/UnitTest/TestingControllers/InstancesControllerTests.cs index f77e81b1..4b16878d 100644 --- a/test/UnitTest/TestingControllers/InstancesControllerTests.cs +++ b/test/UnitTest/TestingControllers/InstancesControllerTests.cs @@ -478,7 +478,32 @@ public async Task GetMany_UserRequestsInstancesNoPartyIdDefined_ReturnsBadReques HttpClient client = GetTestClient(); string token = PrincipalUtil.GetToken(3, 1337); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - string expected = "InstanceOwnerPartyId must be defined."; + string expected = "Either InstanceOwnerPartyId or InstanceOwnerIdentifier need to be defined."; + + // Act + HttpResponseMessage response = await client.GetAsync(requestUri); + string responseMessage = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Contains(expected, responseMessage); + } + + /// + /// Test case: Get Multiple instances without specifying instance owner partyId. + /// Expected: Returns status bad request. + /// + [Fact] + public async Task GetMany_UserRequestsInstancesNoPartyIdButWithWrongInstanceOwnerIdDefined_ReturnsBadRequest() + { + // Arrange + string requestUri = $"{BasePath}"; + + HttpClient client = GetTestClient(); + string token = PrincipalUtil.GetToken(3, 1337); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + client.DefaultRequestHeaders.Add("X-Ai-InstanceOwnerIdentifier", "something:3312321321"); + string expected = "Invalid InstanceOwnerIdentifier."; // Act HttpResponseMessage response = await client.GetAsync(requestUri); diff --git a/test/UnitTest/TestingServices/RegisterServiceTest.cs b/test/UnitTest/TestingServices/RegisterServiceTest.cs new file mode 100644 index 00000000..1ac6f4df --- /dev/null +++ b/test/UnitTest/TestingServices/RegisterServiceTest.cs @@ -0,0 +1,225 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +using Altinn.Common.AccessTokenClient.Services; +using Altinn.Platform.Register.Enums; +using Altinn.Platform.Register.Models; +using Altinn.Platform.Storage.Configuration; +using Altinn.Platform.Storage.Exceptions; +using Altinn.Platform.Storage.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Moq; +using Moq.Protected; + +using Xunit; + +namespace Altinn.Platform.Storage.UnitTest.TestingServices +{ + public class RegisterServiceTest + { + private readonly Mock> _platformSettings; + private readonly Mock> _generalSettings; + private readonly Mock _handlerMock; + private readonly Mock _contextAccessor; + private readonly Mock _accessTokenGenerator; + private readonly Mock> _loggerRegisterService; + + public RegisterServiceTest() + { + _platformSettings = new Mock>(); + _generalSettings = new Mock>(); + _handlerMock = new Mock(MockBehavior.Strict); + _contextAccessor = new Mock(); + _accessTokenGenerator = new Mock(); + _loggerRegisterService = new Mock>(); + } + + [Fact] + public async Task PartyLookup_MatchFound_IdReturned() + { + // Arrange + Party party = new Party + { + PartyId = 500000, + OrgNumber = "897069650", + PartyTypeName = PartyType.Organisation + }; + int expected = 500000; + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(party), Encoding.UTF8, "application/json") + }; + + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); + + HttpClient httpClient = new HttpClient(_handlerMock.Object); + + RegisterService target = new RegisterService( + httpClient, + _contextAccessor.Object, + _accessTokenGenerator.Object, + _generalSettings.Object, + _platformSettings.Object, + new Mock>().Object); + + // Act + int actual = await target.PartyLookup("897069650", null); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public async Task GetParty_SuccessResponse_PartyTypeDeserializedSuccessfully() + { + // Arrange + PartyType expectedPartyType = PartyType.Organisation; + + string repsonseString = "{\"partyId\": 500000," + + "\"partyTypeName\": \"Organisation\"," + + "\"orgNumber\": \"897069650\"," + + "\"unitType\": \"AS\"," + + "\"name\": \"DDG Fitness\"," + + "\"isDeleted\": false," + + "\"onlyHierarchyElementWithNoAccess\": false," + + "\"organization\": {\"orgNumber\": \"897069650\",\"name\": \"DDG Fitness\",\"unitType\": \"AS\",\"telephoneNumber\": \"12345678\",\"mobileNumber\": \"92010000\",\"faxNumber\": \"92110000\",\"eMailAddress\": \"central@ddgfitness.no\",\"internetAddress\": \"http://ddgfitness.no\",\"mailingAddress\": \"Sofies Gate 1\",\"mailingPostalCode\": \"0170\",\"mailingPostalCity\": \"Oslo\",\"businessAddress\": \"Sofies Gate 1\",\"businessPostalCode\": \"0170\",\"businessPostalCity\": \"By\",\"unitStatus\": null},\"childParties\": null\r\n}"; + + HttpResponseMessage httpResponseMessage = new() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(repsonseString, Encoding.UTF8, "application/json") + }; + + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); + + HttpClient httpClient = new HttpClient(_handlerMock.Object); + + RegisterService target = new RegisterService( + httpClient, + _contextAccessor.Object, + _accessTokenGenerator.Object, + _generalSettings.Object, + _platformSettings.Object, + new Mock>().Object); + + // Act + Party actual = await target.GetParty(500000); + + // Assert + Assert.Equal(expectedPartyType, actual.PartyTypeName); + } + + [Fact] + public async Task GetParty_BadRequestResponse_PartyTypeDeserializationFailedWithHttpStatusCode() + { + // Arrange + int partyId = 500000; + string loggedMessasge = "// Getting party with partyID 500000 failed with statuscode BadRequest"; + + HttpResponseMessage httpResponseMessage = new() + { + StatusCode = HttpStatusCode.BadRequest + }; + + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); + + HttpClient httpClient = new HttpClient(_handlerMock.Object); + + RegisterService target = new RegisterService( + httpClient, + _contextAccessor.Object, + _accessTokenGenerator.Object, + _generalSettings.Object, + _platformSettings.Object, + _loggerRegisterService.Object); + + // Act + Party actual = await target.GetParty(partyId); + + // Assert + _loggerRegisterService.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((o, t) => o.ToString().Equals(loggedMessasge)), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task PartyLookup_ResponseIsNotSuccessful_PlatformExceptionThrown() + { + // Arrange + HttpResponseMessage httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound, + Content = new StringContent(string.Empty) + }; + + HttpRequestMessage actualRequest = null; + void SetRequest(HttpRequestMessage request) => actualRequest = request; + InitializeMocks(httpResponseMessage, SetRequest); + + HttpClient httpClient = new HttpClient(_handlerMock.Object); + + RegisterService target = new RegisterService( + httpClient, + _contextAccessor.Object, + _accessTokenGenerator.Object, + _generalSettings.Object, + _platformSettings.Object, + new Mock>().Object); + + // Act & Assert + await Assert.ThrowsAsync(async () => { await target.PartyLookup("16069412345", null); }); + } + + private void InitializeMocks(HttpResponseMessage httpResponseMessage, Action callback) + { + PlatformSettings platformSettings = new PlatformSettings + { + ApiRegisterEndpoint = "http://localhost:5101/register/api/v1/" + }; + + _platformSettings.Setup(s => s.Value).Returns(platformSettings); + + GeneralSettings generalSettings = new GeneralSettings + { + JwtCookieName = "AltinnStudioRuntime" + }; + + _generalSettings.Setup(s => s.Value).Returns(generalSettings); + + _contextAccessor.Setup(s => s.HttpContext).Returns(new DefaultHttpContext()); + + _accessTokenGenerator.Setup(s => s.GenerateAccessToken(It.IsAny(), It.IsAny())) + .Returns(string.Empty); + + _handlerMock.Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .Callback((request, _) => callback(request)) + .ReturnsAsync(httpResponseMessage) + .Verifiable(); + } + } +} diff --git a/test/UnitTest/appsettings.unittest.json b/test/UnitTest/appsettings.unittest.json index 2c9005ce..abbd646c 100644 --- a/test/UnitTest/appsettings.unittest.json +++ b/test/UnitTest/appsettings.unittest.json @@ -25,9 +25,11 @@ "AppTitleCacheLifeTimeInSeconds": 60, "AppMetadataCacheLifeTimeInSeconds": 60, "TextResourceCacheLifeTimeInSeconds": 60, - "UsePostgreSQL": "false" + "UsePostgreSQL": "false", + "JwtCookieName": "AltinnStudioRuntime" }, "PlatformSettings": { + "ApiRegisterEndpoint": "http://localhost:5101/register/api/v1/", "ApiAuthorizationEndpoint": "http://localhost:5050/authorization/api/v1/" }, "PepSettings": {