From 3decabb383416f0766dac580623f348692463014 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 14:40:05 +0100 Subject: [PATCH 1/9] added endpoint and service supporting contactpoint lookup --- .../Controllers/UserContactPointController.cs | 56 +++++++++++++ .../Models/UserContactPointAvailability.cs | 46 +++++++++++ .../Models/UserContactPointLookup.cs | 15 ++++ .../Models/UserContactPoints.cs | 46 +++++++++++ .../Implementation/UserContactPointService.cs | 80 +++++++++++++++++++ .../Services/Interfaces/IUserContactPoints.cs | 27 +++++++ 6 files changed, 270 insertions(+) create mode 100644 src/Altinn.Profile/Controllers/UserContactPointController.cs create mode 100644 src/Altinn.Profile/Models/UserContactPointAvailability.cs create mode 100644 src/Altinn.Profile/Models/UserContactPointLookup.cs create mode 100644 src/Altinn.Profile/Models/UserContactPoints.cs create mode 100644 src/Altinn.Profile/Services/Implementation/UserContactPointService.cs create mode 100644 src/Altinn.Profile/Services/Interfaces/IUserContactPoints.cs diff --git a/src/Altinn.Profile/Controllers/UserContactPointController.cs b/src/Altinn.Profile/Controllers/UserContactPointController.cs new file mode 100644 index 0000000..a52facb --- /dev/null +++ b/src/Altinn.Profile/Controllers/UserContactPointController.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; + +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 contact point API endpoints for internal consumption (e.g. Notifications) requiring neither authenticated user token nor access token authorization. +/// +[Route("profile/api/v1/users/contactpoint")] +[ApiExplorerSettings(IgnoreApi = true)] +[Consumes("application/json")] +[Produces("application/json")] +public class UserContactPointController : Controller +{ + private readonly IUserContactPoints _contactPointService; + + /// + /// Initializes a new instance of the class. + /// + public UserContactPointController(IUserContactPoints contactPointService) + { + _contactPointService = contactPointService; + } + + /// + /// Endpoint looking up the availability of contact points for the provideded national identity number in the request body + /// + /// Returns an overview of the availability of various contact points for the user + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PostAvailabilityLookup([FromBody] UserContactPointLookup userContactPointLookup) + { + if (userContactPointLookup.NationalIdentityNumbers.Count == 0) + { + return new UserContactPointAvailabilityList(); + } + + return await _contactPointService.GetContactPointAvailability(userContactPointLookup.NationalIdentityNumbers); + } + + /// + /// Endpoint looking up the contact points for the user connected to the provideded national identity number in the request body + /// + /// Returns an overview of the contact points for the user + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> PostLookup([FromBody] UserContactPointLookup userContactPointLookup) + { + return await _contactPointService.GetContactPoints(userContactPointLookup.NationalIdentityNumbers); + } +} diff --git a/src/Altinn.Profile/Models/UserContactPointAvailability.cs b/src/Altinn.Profile/Models/UserContactPointAvailability.cs new file mode 100644 index 0000000..5a51cea --- /dev/null +++ b/src/Altinn.Profile/Models/UserContactPointAvailability.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +namespace Altinn.Profile.Models +{ + /// + /// Class describing the contact points of a user + /// + public class UserContactPointAvailability + { + /// + /// Gets or sets the ID of the user + /// + public int UserId { get; set; } + + /// + /// Gets or sets the national identityt number of the user + /// + public string NationalIdentityNumber { get; set; } + + /// + /// Gets or sets a boolean indicating whether the user has reserved themselves from electronic communication + /// + public bool IsReserved { get; set; } + + /// + /// Gets or sets a boolean indicating whether the user has registered a mobile number + /// + public bool MobileNumberRegistered { get; set; } + + /// + /// Gets or sets a boolean indicating whether the user has registered an email address + /// + public bool EmailRegistered { get; set; } + } + + /// + /// A list representation of + /// + public class UserContactPointAvailabilityList + { + /// + /// A list containing contact point availabiliy for users + /// + public List List { get; set; } = []; + } +} diff --git a/src/Altinn.Profile/Models/UserContactPointLookup.cs b/src/Altinn.Profile/Models/UserContactPointLookup.cs new file mode 100644 index 0000000..33400b6 --- /dev/null +++ b/src/Altinn.Profile/Models/UserContactPointLookup.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Altinn.Profile.Models +{ + /// + /// A class respresenting a user contact point lookup object + /// + public class UserContactPointLookup + { + /// + /// A list of national identity numbers to look up contact points or contact point availability for + /// + public List NationalIdentityNumbers { get; set; } + } +} diff --git a/src/Altinn.Profile/Models/UserContactPoints.cs b/src/Altinn.Profile/Models/UserContactPoints.cs new file mode 100644 index 0000000..47b0fda --- /dev/null +++ b/src/Altinn.Profile/Models/UserContactPoints.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +namespace Altinn.Profile.Models +{ + /// + /// Class describing the availability of contact points for a user + /// + public class UserContactPoints + { + /// + /// Gets or sets the ID of the user + /// + public int UserId { get; set; } + + /// + /// Gets or sets the national identityt number of the user + /// + public string NationalIdentityNumber { get; set; } + + /// + /// Gets or sets a boolean indicating whether the user has reserved themselves from electronic communication + /// + public bool IsReserved { get; set; } + + /// + /// Gets or sets the mobile number + /// + public string MobileNumber { get; set; } + + /// + /// Gets or sets the email address + /// + public string Email { get; set; } + } + + /// + /// A list representation of + /// + public class UserContactPointsList + { + /// + /// A list containing contact points for users + /// + public List List { get; set; } = []; + } +} diff --git a/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs b/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs new file mode 100644 index 0000000..f3aa54b --- /dev/null +++ b/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Platform.Profile.Models; +using Altinn.Profile.Models; +using Altinn.Profile.Services.Interfaces; + +namespace Altinn.Profile.Services.Implementation +{ + /// + /// An implementation of that uses the to obtain contact point information. + /// + public class UserContactPointService : IUserContactPoints + { + private readonly IUserProfiles _userProfiles; + + /// + /// Initializes a new instance of the class. + /// + public UserContactPointService(IUserProfiles userProfiles) + { + _userProfiles = userProfiles; + } + + /// + public async Task GetContactPointAvailability(List nationalIdentityNumbers) + { + UserContactPointAvailabilityList result = new(); + + foreach (var nationalIdentityNumber in nationalIdentityNumbers) + { + UserProfile profile = await _userProfiles.GetUser(nationalIdentityNumber); + + if (profile == null) + { + continue; + } + + result.List.Add(new UserContactPointAvailability() + { + UserId = profile.PartyId, + NationalIdentityNumber = profile.Party.SSN, + EmailRegistered = !string.IsNullOrEmpty(profile.Email), + MobileNumberRegistered = !string.IsNullOrEmpty(profile.PhoneNumber), + IsReserved = profile.IsReserved + }); + } + + return result; + } + + /// + public async Task GetContactPoints(List nationalIdentityNumbers) + { + UserContactPointsList result = new(); + + foreach (var nationalIdentityNumber in nationalIdentityNumbers) + { + var profile = await _userProfiles.GetUser(nationalIdentityNumber); + + if (profile == null) + { + continue; + } + + result.List.Add( + new UserContactPoints() + { + UserId = profile.PartyId, + NationalIdentityNumber = profile.Party.SSN, + Email = profile.Email, + MobileNumber = profile.PhoneNumber, + IsReserved = profile.IsReserved + }); + } + + return result; + } + } +} diff --git a/src/Altinn.Profile/Services/Interfaces/IUserContactPoints.cs b/src/Altinn.Profile/Services/Interfaces/IUserContactPoints.cs new file mode 100644 index 0000000..45e2f26 --- /dev/null +++ b/src/Altinn.Profile/Services/Interfaces/IUserContactPoints.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +using Altinn.Profile.Models; + +namespace Altinn.Profile.Services.Interfaces +{ + /// + /// Class describing the methods required for user contact point service + /// + public interface IUserContactPoints + { + /// + /// Method for retriveing contact points for a user + /// + /// A list of national identity numbers to lookup contact points for + /// The users' contact points and reservation status + Task GetContactPoints(List nationalIdentityNumbers); + + /// + /// Method for retriveing information about the availability of contact points for a user + /// + /// A list of national identity numbers to look up availability for + /// Information on the existense of the users' contact points and reservation status + Task GetContactPointAvailability(List nationalIdentityNumbers); + } +} From 64a6b1c56d6ecf4c7473c9bbb2e4505b41667ffe Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 14:42:40 +0100 Subject: [PATCH 2/9] Fixed warnings --- .../Services/Implementation/UserContactPointService.cs | 2 +- .../Mocks/Authentication/ConfigurationManagerStub.cs | 6 +++--- .../Authentication/JwtCookiePostConfigureOptionsStub.cs | 7 ++----- .../UnitTests/UserProfileCachingDecoratorTest.cs | 4 ++-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs b/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs index f3aa54b..8329d8c 100644 --- a/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs +++ b/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs @@ -8,7 +8,7 @@ namespace Altinn.Profile.Services.Implementation { /// - /// An implementation of that uses the to obtain contact point information. + /// An implementation of that uses the to obtain contact point information. /// public class UserContactPointService : IUserContactPoints { diff --git a/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/ConfigurationManagerStub.cs b/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/ConfigurationManagerStub.cs index af33d8d..4fd7062 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/ConfigurationManagerStub.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/ConfigurationManagerStub.cs @@ -17,9 +17,9 @@ namespace Altinn.Profile.Tests.IntegrationTests.Mocks.Authentication public class ConfigurationManagerStub : IConfigurationManager { /// - public async Task GetConfigurationAsync(CancellationToken _) + public async Task GetConfigurationAsync(CancellationToken cancel) { - ICollection signingKeys = await GetSigningKeys(string.Empty); + ICollection signingKeys = await GetSigningKeys(); OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(); foreach (var securityKey in signingKeys) @@ -36,7 +36,7 @@ public void RequestRefresh() throw new NotImplementedException(); } - private static async Task> GetSigningKeys(string _) + private static async Task> GetSigningKeys() { X509Certificate2 cert = new X509Certificate2("JWTValidationCert.cer"); SecurityKey key = new X509SecurityKey(cert); diff --git a/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs b/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs index 8188b35..1a323d8 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/Mocks/Authentication/JwtCookiePostConfigureOptionsStub.cs @@ -27,12 +27,9 @@ public void PostConfigure(string name, JwtCookieOptions options) options.CookieManager = new ChunkingCookieManager(); } - if (!string.IsNullOrEmpty(options.MetadataAddress)) + if (!string.IsNullOrEmpty(options.MetadataAddress) && !options.MetadataAddress.EndsWith('/')) { - if (!options.MetadataAddress.EndsWith("/", StringComparison.Ordinal)) - { - options.MetadataAddress += "/"; - } + options.MetadataAddress += "/"; } options.MetadataAddress += ".well-known/openid-configuration"; diff --git a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs index 7ec498e..222cc99 100644 --- a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs +++ b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs @@ -90,14 +90,14 @@ public async Task GetUserListUserUuid_UsersPartialInCache_decoratedServiceBothCa 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); + _decoratedServiceMock.Setup(service => service.GetUserListByUuid(It.Is>(g => g.TrueForAll(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); + _decoratedServiceMock.Verify(service => service.GetUserListByUuid(It.Is>(g => g.TrueForAll(g2 => g2 == userUuidNotInCache))), Times.Once); Assert.NotNull(actual); foreach (var userUuid in userUuids) { From f89109b68ebf84706654b5be4da9deaec6984717 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 14:46:27 +0100 Subject: [PATCH 3/9] registering new services --- src/Altinn.Profile/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Altinn.Profile/Program.cs b/src/Altinn.Profile/Program.cs index d3c2f88..fc65aa7 100644 --- a/src/Altinn.Profile/Program.cs +++ b/src/Altinn.Profile/Program.cs @@ -205,6 +205,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) }); services.AddHttpClient(); + services.AddSingleton(); services.Decorate(); if (!string.IsNullOrEmpty(applicationInsightsConnectionString)) From 1b61ae8fef52533c91e4340c58ba4eb25d375280 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 22:08:21 +0100 Subject: [PATCH 4/9] added unit tests --- .../Controllers/UserContactPointController.cs | 4 +- .../Models/UserContactPointAvailability.cs | 2 +- .../Models/UserContactPointLookup.cs | 2 +- .../Models/UserContactPoints.cs | 2 +- .../Implementation/UserContactPointService.cs | 4 +- .../UserContactPointControllerTests.cs | 208 ++++++++++++++++++ .../Testdata/TestDataLoader.cs | 6 + .../Testdata/UserProfile/2001606.json | 4 +- .../Testdata/UserProfile/2001607.json | 8 +- 9 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs diff --git a/src/Altinn.Profile/Controllers/UserContactPointController.cs b/src/Altinn.Profile/Controllers/UserContactPointController.cs index a52facb..000b677 100644 --- a/src/Altinn.Profile/Controllers/UserContactPointController.cs +++ b/src/Altinn.Profile/Controllers/UserContactPointController.cs @@ -31,7 +31,7 @@ public UserContactPointController(IUserContactPoints contactPointService) /// Endpoint looking up the availability of contact points for the provideded national identity number in the request body /// /// Returns an overview of the availability of various contact points for the user - [HttpPost] + [HttpPost("availability")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> PostAvailabilityLookup([FromBody] UserContactPointLookup userContactPointLookup) { @@ -47,7 +47,7 @@ public async Task> PostAvailabili /// Endpoint looking up the contact points for the user connected to the provideded national identity number in the request body /// /// Returns an overview of the contact points for the user - [HttpPost] + [HttpPost("lookup")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> PostLookup([FromBody] UserContactPointLookup userContactPointLookup) { diff --git a/src/Altinn.Profile/Models/UserContactPointAvailability.cs b/src/Altinn.Profile/Models/UserContactPointAvailability.cs index 5a51cea..78b02a9 100644 --- a/src/Altinn.Profile/Models/UserContactPointAvailability.cs +++ b/src/Altinn.Profile/Models/UserContactPointAvailability.cs @@ -41,6 +41,6 @@ public class UserContactPointAvailabilityList /// /// A list containing contact point availabiliy for users /// - public List List { get; set; } = []; + public List AvailabilityList { get; set; } = []; } } diff --git a/src/Altinn.Profile/Models/UserContactPointLookup.cs b/src/Altinn.Profile/Models/UserContactPointLookup.cs index 33400b6..128a588 100644 --- a/src/Altinn.Profile/Models/UserContactPointLookup.cs +++ b/src/Altinn.Profile/Models/UserContactPointLookup.cs @@ -10,6 +10,6 @@ public class UserContactPointLookup /// /// A list of national identity numbers to look up contact points or contact point availability for /// - public List NationalIdentityNumbers { get; set; } + public List NationalIdentityNumbers { get; set; } = []; } } diff --git a/src/Altinn.Profile/Models/UserContactPoints.cs b/src/Altinn.Profile/Models/UserContactPoints.cs index 47b0fda..1eb3d13 100644 --- a/src/Altinn.Profile/Models/UserContactPoints.cs +++ b/src/Altinn.Profile/Models/UserContactPoints.cs @@ -41,6 +41,6 @@ public class UserContactPointsList /// /// A list containing contact points for users /// - public List List { get; set; } = []; + public List ContactPointList { get; set; } = []; } } diff --git a/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs b/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs index 8329d8c..18aa12d 100644 --- a/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs +++ b/src/Altinn.Profile/Services/Implementation/UserContactPointService.cs @@ -36,7 +36,7 @@ public async Task GetContactPointAvailability( continue; } - result.List.Add(new UserContactPointAvailability() + result.AvailabilityList.Add(new UserContactPointAvailability() { UserId = profile.PartyId, NationalIdentityNumber = profile.Party.SSN, @@ -63,7 +63,7 @@ public async Task GetContactPoints(List nationalI continue; } - result.List.Add( + result.ContactPointList.Add( new UserContactPoints() { UserId = profile.PartyId, diff --git a/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs new file mode 100644 index 0000000..0339cc0 --- /dev/null +++ b/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +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 UserContactPointControllerTests : IClassFixture> + { + private readonly WebApplicationFactorySetup _webApplicationFactorySetup; + + private readonly JsonSerializerOptions _serializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public UserContactPointControllerTests(WebApplicationFactory factory) + { + _webApplicationFactorySetup = new WebApplicationFactorySetup(factory); + + _webApplicationFactorySetup.SblBridgeHttpMessageHandler = new DelegatingHandlerStub(async (request, token) => + { + string ssn = await request.Content.ReadAsStringAsync(); + return await GetSBlResponseForSsn(ssn); + }); + + GeneralSettings generalSettings = new() { BridgeApiEndpoint = "http://localhost/" }; + _webApplicationFactorySetup.GeneralSettingsOptions.Setup(s => s.Value).Returns(generalSettings); + } + + [Fact] + public async Task PostAvailabilityLookup_NoNationalIdentityNumbers_EmptyListReturned() + { + // Arrange + UserContactPointLookup input = new(); + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, "/profile/api/v1/users/contactpoint/availability"); + + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(input, _serializerOptions), System.Text.Encoding.UTF8, "application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + Assert.Empty(actual.AvailabilityList); + } + + [Fact] + public async Task PostAvailabilityLookup_SingleUser_DetailsReturned() + { + // Arrange + UserContactPointLookup input = new() + { + NationalIdentityNumbers = new List() { "01025101037" } + }; + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, "/profile/api/v1/users/contactpoint/availability"); + + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(input, _serializerOptions), System.Text.Encoding.UTF8, "application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + Assert.Single(actual.AvailabilityList); + Assert.NotEmpty(actual.AvailabilityList[0].Email); + } + + [Fact] + public async Task PostAvailabilityLookup_SingleProfileNotFoundInBridge_RemainingUsersReturned() + { + // Arrange + UserContactPointLookup input = new() + { + NationalIdentityNumbers = new List() { "01025101037", "99999999999" } + }; + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, "/profile/api/v1/users/contactpoint/availability"); + + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(input, _serializerOptions), System.Text.Encoding.UTF8, "application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + Assert.Single(actual.AvailabilityList); + Assert.NotEmpty(actual.AvailabilityList[0].Email); + } + + [Fact] + public async Task PostLookup_NoNationalIdentityNumbers_EmptyListReturned() + { + // Arrange + UserContactPointLookup input = new() + { + NationalIdentityNumbers = new List() { } + }; + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, "/profile/api/v1/users/contactpoint/lookup"); + + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(input, _serializerOptions), System.Text.Encoding.UTF8, "application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + Assert.Empty(actual.ContactPointList); + } + + [Fact] + public async Task PostLookup_SingleProfileNotFoundInBridge_RemainingUsersReturned() + { + // Arrange + UserContactPointLookup input = new() + { + NationalIdentityNumbers = new List() { "01025101037", "99999999999" } + }; + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, "/profile/api/v1/users/contactpoint/lookup"); + + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(input, _serializerOptions), System.Text.Encoding.UTF8, "application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + Assert.Single(actual.ContactPointList); + Assert.NotEmpty(actual.ContactPointList[0].Email); + } + + [Fact] + public async Task PostLookup_SingleUser_DetailsReturned() + { + // Arrange + UserContactPointLookup input = new() + { + NationalIdentityNumbers = new List() { "01025101037" } + }; + + HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, "/profile/api/v1/users/contactpoint/lookup"); + + httpRequestMessage.Content = new StringContent(JsonSerializer.Serialize(input, _serializerOptions), System.Text.Encoding.UTF8, "application/json"); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + string responseContent = await response.Content.ReadAsStringAsync(); + var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); + Assert.Single(actual.ContactPointList); + Assert.NotEmpty(actual.ContactPointList[0].Email); + } + + private async Task GetSBlResponseForSsn(string ssn) + { + UserProfile userProfile; + + switch (ssn) + { + case "\"01025101037\"": + userProfile = await TestDataLoader.Load("2001606"); + return new HttpResponseMessage() { Content = JsonContent.Create(userProfile, options: _serializerOptions), StatusCode = HttpStatusCode.OK }; + case "\"01025101038\"": + userProfile = await TestDataLoader.Load("2001607"); + return new HttpResponseMessage() { Content = JsonContent.Create(userProfile, options: _serializerOptions), StatusCode = HttpStatusCode.OK }; + default: + return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; + } + } + } +} diff --git a/test/Altinn.Profile.Tests/Testdata/TestDataLoader.cs b/test/Altinn.Profile.Tests/Testdata/TestDataLoader.cs index bf95ec4..be162e3 100644 --- a/test/Altinn.Profile.Tests/Testdata/TestDataLoader.cs +++ b/test/Altinn.Profile.Tests/Testdata/TestDataLoader.cs @@ -14,6 +14,12 @@ public static class TestDataLoader public static async Task Load(string id) { string path = $"../../../Testdata/{typeof(T).Name}/{id}.json"; + + if (!File.Exists(path)) + { + return default(T); + } + string fileContent = await File.ReadAllTextAsync(path); T data = JsonSerializer.Deserialize(fileContent, _options); diff --git a/test/Altinn.Profile.Tests/Testdata/UserProfile/2001606.json b/test/Altinn.Profile.Tests/Testdata/UserProfile/2001606.json index d4cb0a4..fb6de99 100644 --- a/test/Altinn.Profile.Tests/Testdata/UserProfile/2001606.json +++ b/test/Altinn.Profile.Tests/Testdata/UserProfile/2001606.json @@ -3,8 +3,8 @@ "UserUUID": "1a131a3b-c6c9-4572-86fd-dfe36c3de06a", "UserType": 1, "UserName": "", - "PhoneNumber": null, - "Email": null, + "PhoneNumber": "99319999", + "Email": "tuva@landro.no", "PartyId": 50002113, "Party": { "PartyTypeName": 1, diff --git a/test/Altinn.Profile.Tests/Testdata/UserProfile/2001607.json b/test/Altinn.Profile.Tests/Testdata/UserProfile/2001607.json index 2e4913f..6149ca6 100644 --- a/test/Altinn.Profile.Tests/Testdata/UserProfile/2001607.json +++ b/test/Altinn.Profile.Tests/Testdata/UserProfile/2001607.json @@ -3,15 +3,15 @@ "UserUUID": "db8eafc0-6056-43b5-b047-4adcd84f659c", "UserType": 1, "UserName": "", - "PhoneNumber": null, - "Email": null, + "PhoneNumber": "92019999", + "Email": "tuva@business.com", "PartyId": 50002113, "Party": { "PartyTypeName": 1, - "SSN": "01025101037", + "SSN": "01025101038", "OrgNumber": "", "Person": { - "SSN": "01025101037", + "SSN": "01025101038", "Name": "TUVA LANDRO", "FirstName": "TUVA", "MiddleName": "", From aec912d010674c4664f32ebbc14140292f5d9d30 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 22:29:56 +0100 Subject: [PATCH 5/9] fixed build errors --- .../IntegrationTests/UserContactPointControllerTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs index 0339cc0..31511b2 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs @@ -86,7 +86,7 @@ public async Task PostAvailabilityLookup_SingleUser_DetailsReturned() string responseContent = await response.Content.ReadAsStringAsync(); var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); Assert.Single(actual.AvailabilityList); - Assert.NotEmpty(actual.AvailabilityList[0].Email); + Assert.True(actual.AvailabilityList[0].EmailRegistered); } [Fact] @@ -111,7 +111,7 @@ public async Task PostAvailabilityLookup_SingleProfileNotFoundInBridge_Remaining string responseContent = await response.Content.ReadAsStringAsync(); var actual = JsonSerializer.Deserialize(responseContent, _serializerOptions); Assert.Single(actual.AvailabilityList); - Assert.NotEmpty(actual.AvailabilityList[0].Email); + Assert.True(actual.AvailabilityList[0].EmailRegistered); } [Fact] From 7f6b9cb80f8048526baf263b525a270388a09692 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 22:32:47 +0100 Subject: [PATCH 6/9] fixed tests to reflect changed testdata --- .../UnitTests/UserProfileCachingDecoratorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs index 222cc99..416c0aa 100644 --- a/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs +++ b/test/Altinn.Profile.Tests/UnitTests/UserProfileCachingDecoratorTest.cs @@ -293,11 +293,11 @@ public async Task GetUserListUserUserUuid_EmptyListFromDecoratedService_CacheNot public async Task GetUserUserSSN_UserInCache_decoratedServiceNotCalled() { // Arrange - const string Ssn = "01025101037"; + const string Ssn = "01025101038"; MemoryCache memoryCache = new(new MemoryCacheOptions()); var userProfile = await TestDataLoader.Load("2001607"); - memoryCache.Set("User_SSN_01025101037", userProfile); + memoryCache.Set("User_SSN_01025101038", userProfile); var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object); // Act @@ -316,7 +316,7 @@ public async Task GetUserUserSSN_UserInCache_decoratedServiceNotCalled() public async Task GetUserUserSSN_UserNotInCache_decoratedServiceCalledMockPopulated() { // Arrange - const string Ssn = "01025101037"; + const string Ssn = "01025101038"; MemoryCache memoryCache = new(new MemoryCacheOptions()); var userProfile = await TestDataLoader.Load("2001607"); @@ -331,7 +331,7 @@ public async Task GetUserUserSSN_UserNotInCache_decoratedServiceCalledMockPopula _decoratedServiceMock.Verify(service => service.GetUser(It.IsAny()), Times.Once()); Assert.NotNull(actual); Assert.Equal(Ssn, actual.Party.SSN); - Assert.True(memoryCache.TryGetValue("User_SSN_01025101037", out UserProfile _)); + Assert.True(memoryCache.TryGetValue("User_SSN_01025101038", out UserProfile _)); } /// From e20d1e0472d7cce23866344444a851c4e0b4eb0c Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 22:40:17 +0100 Subject: [PATCH 7/9] updated workflow --- .github/workflows/build-and-analyze.yml | 61 ++++++++----------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml index 4ac81eb..3aeff17 100644 --- a/.github/workflows/build-and-analyze.yml +++ b/.github/workflows/build-and-analyze.yml @@ -2,12 +2,15 @@ name: Code test and analysis on: push: branches: [ main ] + paths-ignore: + - "test/k6/**" + - ".github/**" pull_request: branches: [ main ] types: [opened, synchronize, reopened] workflow_dispatch: jobs: - build-and-test: + build-test-analyze: name: Build and Test if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest @@ -18,60 +21,34 @@ jobs: with: dotnet-version: | 8.0.x - - name: Build & Test - run: | - dotnet build Altinn.Profile.sln -v m - dotnet test Altinn.Profile.sln -v m - analyze: - if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' - name: Analyze - runs-on: windows-latest - steps: - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 8.0.x - - name: Set up JDK 17 + - name: Set up Java uses: actions/setup-java@v4 with: - distribution: 'microsoft' + distribution: 'temurin' java-version: 17 - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Cache SonarCloud packages uses: actions/cache@v4 with: path: ~\sonar\cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - - name: Cache SonarCloud scanner - id: cache-sonar-scanner - uses: actions/cache@v4 - with: - path: .\.sonar\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner - - name: Install SonarCloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell + - name: Install SonarCloud scanners run: | - New-Item -Path .\.sonar\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner - - name: Analyze + dotnet tool install --global dotnet-sonarscanner + - name: Build & Test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell - run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"Altinn_altinn-profile" /o:"altinn" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="src/**/Program.cs" + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + dotnet-sonarscanner begin /k:"Altinn_altinn-profile" /o:"altinn" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="src/**/Program.cs" + + dotnet build Altinn.Profile.sln -v q - dotnet build Altinn.Profile.sln - dotnet test Altinn.Profile.sln ` - --no-build ` - --results-directory TestResults/ ` - --collect:"XPlat Code Coverage" ` + dotnet test Altinn.Profile.sln \ + -v q \ + --results-directory TestResults/ \ + --collect:"XPlat Code Coverage" \ + --configuration release \ -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - name: Complete sonar analysis if: always() From 9fa9d0e1bede25e06dae11acbba5d9ba4f25c0c7 Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Wed, 28 Feb 2024 22:44:01 +0100 Subject: [PATCH 8/9] fixed code smells --- .../IntegrationTests/UserContactPointControllerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs index 31511b2..11b84af 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/UserContactPointControllerTests.cs @@ -35,7 +35,7 @@ public UserContactPointControllerTests(WebApplicationFactory { - string ssn = await request.Content.ReadAsStringAsync(); + string ssn = await request.Content.ReadAsStringAsync(token); return await GetSBlResponseForSsn(ssn); }); @@ -195,10 +195,10 @@ private async Task GetSBlResponseForSsn(string ssn) switch (ssn) { case "\"01025101037\"": - userProfile = await TestDataLoader.Load("2001606"); + userProfile = await TestDataLoader.Load("2001606"); return new HttpResponseMessage() { Content = JsonContent.Create(userProfile, options: _serializerOptions), StatusCode = HttpStatusCode.OK }; case "\"01025101038\"": - userProfile = await TestDataLoader.Load("2001607"); + userProfile = await TestDataLoader.Load("2001607"); return new HttpResponseMessage() { Content = JsonContent.Create(userProfile, options: _serializerOptions), StatusCode = HttpStatusCode.OK }; default: return new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound }; From 4e83ff8cc45f038bcd9974fee6033eff849d270c Mon Sep 17 00:00:00 2001 From: acn-sbuad Date: Thu, 29 Feb 2024 09:45:09 +0100 Subject: [PATCH 9/9] using dotnet-sonarscanner --- .github/workflows/build-and-analyze.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml index 3aeff17..1505ed2 100644 --- a/.github/workflows/build-and-analyze.yml +++ b/.github/workflows/build-and-analyze.yml @@ -11,11 +11,10 @@ on: workflow_dispatch: jobs: build-test-analyze: - name: Build and Test + name: Build, test & analyze if: ((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) || github.event_name == 'push') && github.repository_owner == 'Altinn' && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -26,6 +25,9 @@ jobs: with: distribution: 'temurin' java-version: 17 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Cache SonarCloud packages uses: actions/cache@v4 with: @@ -38,8 +40,8 @@ jobs: - name: Build & Test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | dotnet-sonarscanner begin /k:"Altinn_altinn-profile" /o:"altinn" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vstest.reportsPaths="**/*.trx" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="src/**/Program.cs" dotnet build Altinn.Profile.sln -v q @@ -56,7 +58,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" + dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" - name: Process .NET test result if: always() uses: NasAmin/trx-parser@v0.5.0