diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml index c39cb10..c13a5c8 100644 --- a/.github/workflows/build-and-analyze.yml +++ b/.github/workflows/build-and-analyze.yml @@ -14,6 +14,20 @@ jobs: 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' runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: platform_profile_admin + POSTGRES_PASSWORD: Password + POSTGRES_DB: profiledb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -28,6 +42,13 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Setup PostgreSQL + run: | + chmod +x dbsetup.sh + ./dbsetup.sh + - name: Restart database to enable config changes + run: | + docker restart $(docker ps -q) - name: Cache SonarCloud packages uses: actions/cache@v4 with: diff --git a/dbsetup.sh b/dbsetup.sh new file mode 100644 index 0000000..f48a834 --- /dev/null +++ b/dbsetup.sh @@ -0,0 +1,13 @@ + #!/bin/bash +export PGPASSWORD=Password + +# alter max connections +psql -h localhost -p 5432 -U platform_profile_admin -d profiledb \ +-c "ALTER SYSTEM SET max_connections TO '200';" + +# set up platform_profile role +psql -h localhost -p 5432 -U platform_profile_admin -d profiledb \ +-c "DO \$\$ + BEGIN CREATE ROLE platform_profile WITH LOGIN PASSWORD 'Password'; + EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE; + END \$\$;" diff --git a/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj b/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj index abb629b..f778641 100644 --- a/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj +++ b/src/Altinn.Profile.Core/Altinn.Profile.Core.csproj @@ -10,12 +10,9 @@ + - - - - diff --git a/src/Altinn.Profile.Core/ContactRegister/ContactRegisterChangesLog.cs b/src/Altinn.Profile.Core/ContactRegister/ContactRegisterChangesLog.cs new file mode 100644 index 0000000..b50ed4a --- /dev/null +++ b/src/Altinn.Profile.Core/ContactRegister/ContactRegisterChangesLog.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +using Altinn.Profile.Core.Person.ContactPreferences; + +namespace Altinn.Profile.Core.ContactRegister; + +/// +/// Represents the changes to a person's contact preferences from the contact register. +/// +public record ContactRegisterChangesLog +{ + /// + /// Gets the collection of snapshots representing the changes to a person's contact preferences. + /// + [JsonPropertyName("list")] + public IImmutableList? ContactPreferencesSnapshots { get; init; } + + /// + /// Gets the ending change identifier, which indicates the point at which the system should stop retrieving changes. + /// + [JsonPropertyName("tilEndringsId")] + public long? EndingIdentifier { get; init; } + + /// + /// Gets the most recent change identifier, which represents the last change that was processed by the system. + /// + [JsonPropertyName("sisteEndringsId")] + public long? LatestChangeIdentifier { get; init; } + + /// + /// Gets the starting change identifier indicating the point from which the system begins retrieving changes. + /// + [JsonPropertyName("fraEndringsId")] + public long? StartingIdentifier { get; init; } +} diff --git a/src/Altinn.Profile.Core/ContactRegister/IContactRegisterHttpClient.cs b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterHttpClient.cs new file mode 100644 index 0000000..9383d3a --- /dev/null +++ b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterHttpClient.cs @@ -0,0 +1,19 @@ +using Altinn.Profile.Core.ContactRegister; + +namespace Altinn.Profile.Integrations.ContactRegister; + +/// +/// An HTTP client to interact with the contact register. +/// +public interface IContactRegisterHttpClient +{ + /// + /// Retrieves the changes in persons' contact details from the specified endpoint. + /// + /// The URL of the endpoint to retrieve contact details changes from. + /// The starting identifier for retrieving contact details changes. + /// + /// A task that represents the asynchronous operation. + /// + Task GetContactDetailsChangesAsync(string endpointUrl, long startingIdentifier); +} diff --git a/src/Altinn.Profile.Core/ContactRegister/IContactRegisterService.cs b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterService.cs new file mode 100644 index 0000000..fb299ef --- /dev/null +++ b/src/Altinn.Profile.Core/ContactRegister/IContactRegisterService.cs @@ -0,0 +1,16 @@ +namespace Altinn.Profile.Core.ContactRegister; + +/// +/// Interface for handling changes in a person's contact preferences. +/// +public interface IContactRegisterService +{ + /// + /// Asynchronously retrieves the changes in contact preferences for all persons starting from a given number. + /// + /// The identifier from which to start retrieving the data. + /// + /// A task that represents the asynchronous operation. + /// + Task RetrieveContactDetailsChangesAsync(long startingIdentifier); +} diff --git a/src/Altinn.Profile.Core/Domain/IRepository.cs b/src/Altinn.Profile.Core/Domain/IRepository.cs deleted file mode 100644 index 6a76054..0000000 --- a/src/Altinn.Profile.Core/Domain/IRepository.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable enable - -namespace Altinn.Profile.Core.Domain; - -/// -/// Defines generic methods for handling entities in a repository. -/// -/// The type of the entity. -public interface IRepository - where T : class -{ - /// - /// Asynchronously retrieves all entities. - /// - /// A task that represents the asynchronous operation. The task result contains a collection of entities. - Task> GetAllAsync(); - - /// - /// Asynchronously retrieves entities with optional filtering, sorting, and pagination. - /// - /// Optional filter criteria. - /// Optional ordering criteria. - /// The number of entities to skip. - /// The number of entities to take. - /// A task that represents the asynchronous operation. The task result contains a collection of filtered, sorted, and paginated entities. - Task> GetAsync( - Func? filter = null, - Func, IOrderedEnumerable>? orderBy = null, - int? skip = null, - int? take = null); - - /// - /// Asynchronously retrieves an entity by its identifier. - /// - /// The identifier of the entity. - /// A task that represents the asynchronous operation. The task result contains the entity that matches the identifier. - Task GetByIdAsync(string id); - - /// - /// Asynchronously checks if an entity exists by its identifier. - /// - /// The identifier of the entity. - /// A task that represents the asynchronous operation. The task result contains a boolean indicating the existence of the entity. - Task ExistsAsync(string id); - - /// - /// Asynchronously adds a new entity. - /// - /// The entity to add. - /// A task that represents the asynchronous operation. The task result contains the added entity. - Task AddAsync(T entity); - - /// - /// Asynchronously adds multiple entities. - /// - /// The entities to add. - /// A task that represents the asynchronous operation. - Task AddRangeAsync(IEnumerable entities); - - /// - /// Asynchronously updates an existing entity. - /// - /// The entity to update. - /// A task that represents the asynchronous operation. - Task UpdateAsync(T entity); - - /// - /// Asynchronously updates multiple entities. - /// - /// The entities to update. - /// A task that represents the asynchronous operation. - Task UpdateRangeAsync(IEnumerable entities); - - /// - /// Asynchronously deletes an entity by its identifier. - /// - /// The identifier of the entity to delete. - /// A task that represents the asynchronous operation. - Task DeleteAsync(string id); - - /// - /// Asynchronously deletes multiple entities by their identifiers. - /// - /// The identifiers of the entities to delete. - /// A task that represents the asynchronous operation. - Task DeleteRangeAsync(IEnumerable ids); - - /// - /// Saves changes to the data source asynchronously. - /// - /// A task that represents the asynchronous save operation. The task result contains the number of state entries written to the data source. - Task SaveChangesAsync(); -} diff --git a/src/Altinn.Profile.Core/Extensions/StringExtensions.cs b/src/Altinn.Profile.Core/Extensions/StringExtensions.cs index b59627c..256ea17 100644 --- a/src/Altinn.Profile.Core/Extensions/StringExtensions.cs +++ b/src/Altinn.Profile.Core/Extensions/StringExtensions.cs @@ -9,6 +9,28 @@ namespace Altinn.Profile.Core.Extensions; /// public static partial class StringExtensions { + /// + /// Validates the given URL. + /// + /// The URL to validate. + /// True if the URL is valid; otherwise, false. + public static bool IsValidUrl(this string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return false; + } + + try + { + return Uri.TryCreate(url, UriKind.Absolute, out Uri? uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + } + catch (UriFormatException) + { + return false; + } + } + /// /// Determines whether a given string consists of only digits. /// @@ -49,48 +71,48 @@ public static bool IsDigitsOnly(this string input) } /// - /// Determines whether a given string represents a valid format for a Norwegian Social Security Number (SSN). + /// Determines whether a given string represents a valid format for a Norwegian national identity mumber. /// - /// The Norwegian Social Security Number (SSN) to validate. + /// The Norwegian national identity number to validate. /// Indicates whether to validate the control digits. /// - /// true if the given string represents a valid format for a Norwegian Social Security Number (SSN) and, if specified, the control digits are valid; otherwise, false. + /// true if the given string represents a valid format for a Norwegian national identity number and, if specified, the control digits are valid; otherwise, false. /// /// - /// A valid Norwegian Social Security Number (SSN) is an 11-digit number where: + /// A valid Norwegian national identity number is an 11-digit number where: /// - The first six digits represent the date of birth in the format DDMMYY. /// - The next three digits are an individual number where the first digit indicates the century of birth. /// - The last two digits are control digits. /// - /// Thrown when the individual number part of the SSN cannot be parsed into an integer. + /// Thrown when the individual number part of the national identity number cannot be parsed into an integer. /// Thrown when the parsed date is outside the range of DateTime. - public static bool IsValidSocialSecurityNumber(this string socialSecurityNumber, bool controlDigits = true) + public static bool IsValidNationalIdentityNumber(this string nationalIdentityNumber, bool controlDigits = true) { - if (string.IsNullOrWhiteSpace(socialSecurityNumber) || socialSecurityNumber.Length != 11) + if (string.IsNullOrWhiteSpace(nationalIdentityNumber) || nationalIdentityNumber.Length != 11) { return false; } // Return the cached result if the given string has been checked once. - if (CachedSocialSecurityNumber.TryGetValue(socialSecurityNumber, out var cachedResult)) + if (CachedNationalIdentityNumber.TryGetValue(nationalIdentityNumber, out var cachedResult)) { return cachedResult; } - ReadOnlySpan socialSecurityNumberSpan = socialSecurityNumber.AsSpan(); + ReadOnlySpan nationalIdentityNumberSpan = nationalIdentityNumber.AsSpan(); - for (int i = 0; i < socialSecurityNumberSpan.Length; i++) + for (int i = 0; i < nationalIdentityNumberSpan.Length; i++) { - if (!char.IsDigit(socialSecurityNumberSpan[i])) + if (!char.IsDigit(nationalIdentityNumberSpan[i])) { return false; } } - // Extract parts of the Social Security Number (SSN) using slicing. - ReadOnlySpan datePart = socialSecurityNumberSpan[..6]; - ReadOnlySpan controlDigitsPart = socialSecurityNumberSpan[9..11]; - ReadOnlySpan individualNumberPart = socialSecurityNumberSpan[6..9]; + // Extract parts of the national identity number using slicing. + ReadOnlySpan datePart = nationalIdentityNumberSpan[..6]; + ReadOnlySpan individualNumberPart = nationalIdentityNumberSpan[6..9]; + ReadOnlySpan controlDigitsPart = nationalIdentityNumberSpan[9..11]; // If parsing the individual number part fails, return false. if (!int.TryParse(individualNumberPart, out _)) @@ -104,17 +126,17 @@ public static bool IsValidSocialSecurityNumber(this string socialSecurityNumber, return false; } - var isValidSocialSecurityNumber = !controlDigits || CalculateControlDigits(socialSecurityNumberSpan[..9].ToString()) == controlDigitsPart.ToString(); + var isValidNationalIdentityNumber = !controlDigits || CalculateControlDigits(nationalIdentityNumberSpan[..9].ToString()) == controlDigitsPart.ToString(); - CachedSocialSecurityNumber.TryAdd(socialSecurityNumber, isValidSocialSecurityNumber); + CachedNationalIdentityNumber.TryAdd(nationalIdentityNumber, isValidNationalIdentityNumber); - return isValidSocialSecurityNumber; + return isValidNationalIdentityNumber; } /// - /// Calculates the control digits used to validate a Norwegian Social Security Number. + /// Calculates the control digits used to validate a Norwegian national identity number. /// - /// The first nine digits of the Social Security Number. + /// The first nine digits of the national identity number. /// A represents the two control digits. private static string CalculateControlDigits(string firstNineDigits) { @@ -167,12 +189,12 @@ private static int CalculateControlDigit(string digits, int[] weights) private static partial Regex DigitsOnlyRegex(); /// - /// A cache for storing the validation results of Norwegian Social Security Numbers (SSNs). + /// A cache for storing the validation results of Norwegian national identity numbers. /// /// - /// This cache helps to avoid redundant validation checks for SSNs that have already been processed. - /// It maps the SSN as a string to a boolean indicating whether the SSN is valid (true) or not (false). - /// Utilizing this cache can significantly improve performance for applications that frequently validate the same SSNs. + /// This cache helps to avoid redundant validation checks for national identity numbers (nin) that have already been processed. + /// It maps the identity numbers as a string to a boolean indicating whether the number is valid (true) or not (false). + /// Utilizing this cache can significantly improve performance for applications that frequently validate the same numbers. /// - private static ConcurrentDictionary CachedSocialSecurityNumber => new(); + private static ConcurrentDictionary CachedNationalIdentityNumber => new(); } diff --git a/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs b/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs index 077a5f2..218d90c 100644 --- a/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs +++ b/src/Altinn.Profile.Core/Integrations/IUnitProfileRepository.cs @@ -1,17 +1,16 @@ using Altinn.Profile.Core.Unit.ContactPoints; -namespace Altinn.Profile.Core.Integrations +namespace Altinn.Profile.Core.Integrations; + +/// +/// Interface for accessing user profile services related to unit contact points. +/// +public interface IUnitProfileRepository { /// - /// Interface describing a client for the user profile service + /// Retrieves a list of user-registered contact points based on the specified lookup criteria. /// - public interface IUnitProfileRepository - { - /// - /// Provides a list of user registered contact points based on the lookup criteria - /// - /// Lookup object containing a list of organizations and a resource - /// A list of unit contact points - Task> GetUserRegisteredContactPoints(UnitContactPointLookup lookup); - } + /// An object containing a list of organization numbers and a resource ID to filter the contact points. + /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure. + Task> GetUserRegisteredContactPoints(UnitContactPointLookup lookup); } diff --git a/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs b/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs index ff673b7..9790058 100644 --- a/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs +++ b/src/Altinn.Profile.Core/Integrations/IUserProfileRepository.cs @@ -3,42 +3,42 @@ namespace Altinn.Profile.Core.Integrations; /// -/// Interface describing a client for the user profile service +/// Interface for accessing user profile services. /// public interface IUserProfileRepository { /// - /// Method that fetches a user based on a user id + /// Retrieves a user profile based on the user's social security number (SSN). /// - /// The user id - /// User profile with given user id or a boolean if failure. - Task> GetUser(int userId); + /// The user's social security number. + /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure. + Task> GetUser(string ssn); /// - /// Method that fetches a user based on ssn. + /// Retrieves a user profile based on the user's ID. /// - /// The user's ssn. - /// User profile connected to given ssn or a boolean if failure. - Task> GetUser(string ssn); + /// The user ID. + /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure. + Task> GetUser(int userId); /// - /// Method that fetches a user based on a user uuid + /// Retrieves a user profile based on the user's username. /// - /// The user uuid - /// User profile with given user uuid or a boolean if failure. - Task> GetUserByUuid(Guid userUuid); + /// The user's username. + /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure. + Task> GetUserByUsername(string username); /// - /// Method that fetches a list of users based on a list of user uuid + /// Retrieves a user profile based on the user's UUID. /// - /// The list of user uuids - /// List of User profiles with given user uuids or a boolean if failure. - Task, bool>> GetUserListByUuid(List userUuidList); + /// The user UUID. + /// A task that represents the asynchronous operation. The task result contains a object with a on success, or a boolean indicating failure. + Task> GetUserByUuid(Guid userUuid); /// - /// Method that fetches a user based on username. + /// Retrieves a list of user profiles based on a list of user UUIDs. /// - /// The user's username. - /// User profile connected to given username or a boolean if failure. - Task> GetUserByUsername(string username); + /// The list of user UUIDs. + /// A task that represents the asynchronous operation. The task result contains a object with a list of on success, or a boolean indicating failure. + Task, bool>> GetUserListByUuid(List userUuidList); } diff --git a/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactDetailsSnapshot.cs b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactDetailsSnapshot.cs new file mode 100644 index 0000000..525abbc --- /dev/null +++ b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactDetailsSnapshot.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Profile.Core.Person.ContactPreferences; + +/// +/// Represents a snapshot of the contact details retrieved from the changes log. +/// +public record PersonContactDetailsSnapshot +{ + /// + /// Gets the email address of the person. + /// + [JsonPropertyName("epostadresse")] + public string? Email { get; init; } + + /// + /// Gets the date and time when the email address was last verified. + /// + [JsonPropertyName("epostadresse_sist_verifisert")] + public DateTime? EmailLastVerified { get; init; } + + /// + /// Gets the date and time when the email address was last updated. + /// + [JsonPropertyName("epostadresse_oppdatert")] + public DateTime? EmailLastUpdated { get; init; } + + /// + /// Gets a value indicating whether the email address is duplicated. + /// + [JsonPropertyName("epostadresse_duplisert")] + public string? IsEmailDuplicated { get; init; } + + /// + /// Gets a value indicating whether the mobile phone number is duplicated. + /// + [JsonPropertyName("mobiltelefonnummer_duplisert")] + public string? IsMobileNumberDuplicated { get; init; } + + /// + /// Gets the mobile phone number of the person. + /// + [JsonPropertyName("mobiltelefonnummer")] + public string? MobileNumber { get; init; } + + /// + /// Gets the date and time when the mobile phone number was last verified. + /// + [JsonPropertyName("mobiltelefonnummer_sist_verifisert")] + public DateTime? MobileNumberLastVerified { get; init; } + + /// + /// Gets the date and time when the mobile phone number was last updated. + /// + [JsonPropertyName("mobiltelefonnummer_oppdatert")] + public DateTime? MobileNumberLastUpdated { get; init; } +} diff --git a/src/Altinn.Profile.Integrations/Entities/PersonContactDetails.cs b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferences.cs similarity index 78% rename from src/Altinn.Profile.Integrations/Entities/PersonContactDetails.cs rename to src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferences.cs index 9de6fd9..a853379 100644 --- a/src/Altinn.Profile.Integrations/Entities/PersonContactDetails.cs +++ b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferences.cs @@ -1,16 +1,16 @@ #nullable enable -namespace Altinn.Profile.Integrations.Entities; +namespace Altinn.Profile.Core.Person.ContactPreferences; /// /// Represents a person's contact details. /// -public record PersonContactDetails : IPersonContactDetails +public record PersonContactPreferences { /// - /// Gets the national identity number of the person. + /// Gets the email address of the person. /// - public required string NationalIdentityNumber { get; init; } + public string? Email { get; init; } /// /// Gets a value indicating whether the person opts out of being contacted. @@ -18,17 +18,17 @@ public record PersonContactDetails : IPersonContactDetails public bool? IsReserved { get; init; } /// - /// Gets the mobile phone number of the person. + /// Gets the language code of the person, represented as an ISO 639-1 code. /// - public string? MobilePhoneNumber { get; init; } + public string? LanguageCode { get; init; } /// - /// Gets the email address of the person. + /// Gets the mobile phone number of the person. /// - public string? EmailAddress { get; init; } + public string? MobileNumber { get; init; } /// - /// Gets the language code of the person, represented as an ISO 639-1 code. + /// Gets the national identity number of the person. /// - public string? LanguageCode { get; init; } + public required string NationalIdentityNumber { get; init; } } diff --git a/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferencesSnapshot.cs b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferencesSnapshot.cs new file mode 100644 index 0000000..73fd281 --- /dev/null +++ b/src/Altinn.Profile.Core/Person.ContactPreferences/PersonContactPreferencesSnapshot.cs @@ -0,0 +1,51 @@ +using System.Text.Json.Serialization; + +namespace Altinn.Profile.Core.Person.ContactPreferences; + +/// +/// Represents a log of changes to a person's contact preferences, including contact information, language preference, notification status, and other details. +/// +public class PersonContactPreferencesSnapshot +{ + /// + /// Gets the contact information details of the person. + /// + [JsonPropertyName("kontaktinformasjon")] + public PersonContactDetailsSnapshot? ContactDetailsSnapshot { get; init; } + + /// + /// Gets the language preference of the person. + /// + [JsonPropertyName("spraak")] + public string? Language { get; init; } + + /// + /// Gets the date and time when the person's language preference was last updated. + /// + [JsonPropertyName("spraak_oppdatert")] + public DateTime? LanguageLastUpdated { get; init; } + + /// + /// Gets the notification status of the person. + /// + [JsonPropertyName("varslingsstatus")] + public string? NotificationStatus { get; init; } + + /// + /// Gets the unique identifier of the person. + /// + [JsonPropertyName("personidentifikator")] + public string? PersonIdentifier { get; init; } + + /// + /// Gets the reservation details of the person. + /// + [JsonPropertyName("reservasjon")] + public string? Reservation { get; init; } + + /// + /// Gets the current status of the person. + /// + [JsonPropertyName("status")] + public string? Status { get; init; } +} diff --git a/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj b/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj index 8e98664..436159a 100644 --- a/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj +++ b/src/Altinn.Profile.Integrations/Altinn.Profile.Integrations.csproj @@ -9,7 +9,6 @@ - diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactAndReservationChangesException.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactAndReservationChangesException.cs new file mode 100644 index 0000000..4531090 --- /dev/null +++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactAndReservationChangesException.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Altinn.Profile.Integrations.ContactRegister; + +/// +/// Represents errors that occur during order processing operations. +/// +[ExcludeFromCodeCoverage] +public class ContactAndReservationChangesException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ContactAndReservationChangesException() : base() + { + } + + /// + /// Initializes a new instance of the class + /// with a specified error message. + /// + /// The message that describes the error. + public ContactAndReservationChangesException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class + /// with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ContactAndReservationChangesException(string message, Exception inner) : base(message, inner) + { + } +} diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterHttpClient.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterHttpClient.cs new file mode 100644 index 0000000..f7910f5 --- /dev/null +++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterHttpClient.cs @@ -0,0 +1,69 @@ +using System.Text; +using System.Text.Json; + +using Altinn.Profile.Core.ContactRegister; +using Altinn.Profile.Core.Extensions; + +namespace Altinn.Profile.Integrations.ContactRegister; + +/// +/// An HTTP client to interact with the contact register. +/// +public class ContactRegisterHttpClient : IContactRegisterHttpClient +{ + private readonly HttpClient _httpClient; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP client to interact with the contact register. + public ContactRegisterHttpClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + /// + /// Retrieves the changes in persons' contact details from the specified endpoint. + /// + /// The URL of the endpoint to retrieve contact details changes from. + /// The starting identifier for retrieving contact details changes. + /// + /// A task that represents the asynchronous operation with the returned values. + /// + /// The URL is invalid. - endpointUrl + public async Task GetContactDetailsChangesAsync(string endpointUrl, long startingIdentifier) + { + if (!endpointUrl.IsValidUrl()) + { + throw new ArgumentException("The endpoint URL is invalid.", nameof(endpointUrl)); + } + + if (startingIdentifier < 0) + { + throw new ArgumentException("The starting position is invalid.", nameof(endpointUrl)); + } + + var request = new HttpRequestMessage(HttpMethod.Post, endpointUrl) + { + Content = new StringContent($"{{\"fraEndringsId\": {startingIdentifier}}}", Encoding.UTF8, "application/json") + }; + + var response = await _httpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + throw new ContactAndReservationChangesException("Failed to retrieve contact details changes."); + } + + var responseData = await response.Content.ReadAsStringAsync(); + + var responseObject = JsonSerializer.Deserialize(responseData); + + if (responseObject == null || responseObject.ContactPreferencesSnapshots == null) + { + throw new ContactAndReservationChangesException("Failed to deserialize the response from the contact and reservation registry."); + } + + return responseObject; + } +} diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterService.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterService.cs new file mode 100644 index 0000000..61dc10c --- /dev/null +++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterService.cs @@ -0,0 +1,41 @@ +using Altinn.Profile.Core.ContactRegister; + +namespace Altinn.Profile.Integrations.ContactRegister; + +/// +/// Implementation of the change log service for handling changes in a person's contact preferences. +/// +internal class ContactRegisterService : IContactRegisterService +{ + private readonly IContactRegisterHttpClient _contactRegisterHttpClient; + private readonly ContactRegisterSettings _contactRegisterSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The settings used to configure the contact register. + /// The HTTP client used to retrieve contact details changes. + public ContactRegisterService(ContactRegisterSettings contactRegisterSettings, IContactRegisterHttpClient contactRegisterHttpClient) + { + _contactRegisterSettings = contactRegisterSettings; + _contactRegisterHttpClient = contactRegisterHttpClient; + } + + /// + /// Asynchronously retrieves the changes in contact preferences for all persons starting from a given number. + /// + /// The identifier from which to start retrieving the data. + /// + /// A task that represents the asynchronous operation. + /// + /// Thrown if the is null or empty. + public async Task RetrieveContactDetailsChangesAsync(long startingIdentifier) + { + if (string.IsNullOrWhiteSpace(_contactRegisterSettings.ChangesLogEndpoint)) + { + throw new InvalidOperationException("The endpoint URL must not be null or empty."); + } + + return await _contactRegisterHttpClient.GetContactDetailsChangesAsync(_contactRegisterSettings.ChangesLogEndpoint, startingIdentifier); + } +} diff --git a/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterSettings.cs b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterSettings.cs new file mode 100644 index 0000000..b3472b2 --- /dev/null +++ b/src/Altinn.Profile.Integrations/ContactRegister/ContactRegisterSettings.cs @@ -0,0 +1,21 @@ +#nullable enable + +using Altinn.ApiClients.Maskinporten.Config; + +namespace Altinn.Profile.Integrations.ContactRegister; + +/// +/// Represents the settings for managing contact details and reservation information for individuals. +/// +public class ContactRegisterSettings +{ + /// + /// Gets the endpoint URL used to retrieve updates in the contact information for one or more individuals. + /// + public string? ChangesLogEndpoint { get; init; } + + /// + /// Gets the settings required for Maskinporten authentication. + /// + public MaskinportenSettings? MaskinportenSettings { get; init; } +} diff --git a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetails.cs b/src/Altinn.Profile.Integrations/Entities/IPersonContactDetails.cs deleted file mode 100644 index cb1e7db..0000000 --- a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetails.cs +++ /dev/null @@ -1,34 +0,0 @@ -#nullable enable - -namespace Altinn.Profile.Integrations.Entities; - -/// -/// Represents a person's contact details. -/// -public interface IPersonContactDetails -{ - /// - /// Gets the national identity number of the person. - /// - string NationalIdentityNumber { get; } - - /// - /// Gets a value indicating whether the person opts out of being contacted. - /// - bool? IsReserved { get; } - - /// - /// Gets the mobile phone number of the person. - /// - string? MobilePhoneNumber { get; } - - /// - /// Gets the email address of the person. - /// - string? EmailAddress { get; } - - /// - /// Gets the language code of the person, represented as an ISO 639-1 code. - /// - string? LanguageCode { get; } -} diff --git a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetailsLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/IPersonContactDetailsLookupResult.cs deleted file mode 100644 index b717305..0000000 --- a/src/Altinn.Profile.Integrations/Entities/IPersonContactDetailsLookupResult.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable - -using System.Collections.Immutable; - -namespace Altinn.Profile.Integrations.Entities; - -/// -/// Represents the result of a lookup operation for contact details. -/// -public interface IPersonContactDetailsLookupResult -{ - /// - /// Gets a list of national identity numbers that could not be matched with any person contact details. - /// - /// - /// An of containing the unmatched national identity numbers. - /// - ImmutableList? UnmatchedNationalIdentityNumbers { get; } - - /// - /// Gets a list of person contact details that were successfully matched during the lookup. - /// - /// - /// An of containing the matched person contact details. - /// - ImmutableList? MatchedPersonContactDetails { get; } -} diff --git a/src/Altinn.Profile.Integrations/Entities/IPersonContactPreferencesLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/IPersonContactPreferencesLookupResult.cs new file mode 100644 index 0000000..9883f2d --- /dev/null +++ b/src/Altinn.Profile.Integrations/Entities/IPersonContactPreferencesLookupResult.cs @@ -0,0 +1,28 @@ +#nullable enable +using System.Collections.Immutable; + +using Altinn.Profile.Core.Person.ContactPreferences; + +namespace Altinn.Profile.Integrations.Entities; + +/// +/// Represents the result of a lookup operation for contact preferences. +/// +public interface IPersonContactPreferencesLookupResult +{ + /// + /// Gets a list of person contact preferences that were successfully matched during the lookup. + /// + /// + /// An of containing the matched person contact preferences. + /// + ImmutableList? MatchedPersonContactPreferences { get; } + + /// + /// Gets a list of national identity numbers that could not be matched with any person contact preferences. + /// + /// + /// An of containing the unmatched national identity numbers. + /// + ImmutableList? UnmatchedNationalIdentityNumbers { get; } +} diff --git a/src/Altinn.Profile.Integrations/Entities/PersonContactDetailsLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/PersonContactDetailsLookupResult.cs deleted file mode 100644 index 470e526..0000000 --- a/src/Altinn.Profile.Integrations/Entities/PersonContactDetailsLookupResult.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable - -using System.Collections.Immutable; - -namespace Altinn.Profile.Integrations.Entities; - -/// -/// Represents the result of a lookup operation for contact details. -/// -public record PersonContactDetailsLookupResult : IPersonContactDetailsLookupResult -{ - /// - /// Gets a list of national identity numbers that could not be matched with any person contact details. - /// - /// - /// An of containing the unmatched national identity numbers. - /// - public ImmutableList? UnmatchedNationalIdentityNumbers { get; init; } - - /// - /// Gets a list of person contact details that were successfully matched during the lookup. - /// - /// - /// An of containing the matched person contact details. - /// - public ImmutableList? MatchedPersonContactDetails { get; init; } -} diff --git a/src/Altinn.Profile.Integrations/Entities/PersonContactPreferencesLookupResult.cs b/src/Altinn.Profile.Integrations/Entities/PersonContactPreferencesLookupResult.cs new file mode 100644 index 0000000..749ce01 --- /dev/null +++ b/src/Altinn.Profile.Integrations/Entities/PersonContactPreferencesLookupResult.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System.Collections.Immutable; + +using Altinn.Profile.Core.Person.ContactPreferences; + +namespace Altinn.Profile.Integrations.Entities; + +/// +/// Represents the result of a lookup operation for contact preferences. +/// +public record PersonContactPreferencesLookupResult : IPersonContactPreferencesLookupResult +{ + /// + /// Gets a list of person contact preferences that were successfully matched during the lookup. + /// + /// + /// An of containing the matched person contact preferences. + /// + public ImmutableList? MatchedPersonContactPreferences { get; init; } + + /// + /// Gets a list of national identity numbers that could not be matched with any person contact preferences. + /// + /// + /// An of containing the unmatched national identity numbers. + /// + public ImmutableList? UnmatchedNationalIdentityNumbers { get; init; } +} diff --git a/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs b/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs index 0831560..5f41f99 100644 --- a/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs +++ b/src/Altinn.Profile.Integrations/Extensions/WebApplicationExtensions.cs @@ -20,12 +20,10 @@ public static class WebApplicationExtensions /// Configure and set up db /// /// app - /// is environment dev /// the configuration collection - public static void SetUpPostgreSql(this IApplicationBuilder app, bool isDevelopment, IConfiguration config) + public static void SetUpPostgreSql(this IApplicationBuilder app, IConfiguration config) { - PostgreSqlSettings? settings = config.GetSection("PostgreSQLSettings") - .Get() + PostgreSqlSettings? settings = config.GetSection("PostgreSQLSettings").Get() ?? throw new ArgumentNullException(nameof(config), "Required PostgreSQLSettings is missing from application configuration"); if (settings.EnableDBConnection) @@ -34,9 +32,7 @@ public static void SetUpPostgreSql(this IApplicationBuilder app, bool isDevelopm string connectionString = string.Format(settings.AdminConnectionString, settings.ProfileDbAdminPwd); - string fullWorkspacePath = isDevelopment ? - Path.Combine(Directory.GetParent(Environment.CurrentDirectory)!.FullName, settings.MigrationScriptPath) : - Path.Combine(Environment.CurrentDirectory, settings.MigrationScriptPath); + string fullWorkspacePath = Path.Combine(Environment.CurrentDirectory, settings.MigrationScriptPath); app.UseYuniql( new PostgreSqlDataService(traceService), diff --git a/src/Altinn.Profile.Integrations/Mappings/PersonContactDetailsProfile.cs b/src/Altinn.Profile.Integrations/Mappings/PersonContactDetailsProfile.cs deleted file mode 100644 index 64d7881..0000000 --- a/src/Altinn.Profile.Integrations/Mappings/PersonContactDetailsProfile.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Altinn.Profile.Integrations.Entities; - -namespace Altinn.Profile.Integrations.Mappings; - -/// -/// AutoMapper profile for mapping between and . -/// -/// -/// This profile defines the mapping rules to convert a object into a instance. -/// -public class PersonContactDetailsProfile : AutoMapper.Profile -{ - /// - /// Initializes a new instance of the class and configures the mappings. - /// - public PersonContactDetailsProfile() - { - CreateMap() - .ForMember(dest => dest.IsReserved, opt => opt.MapFrom(src => src.Reservation)) - .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => src.EmailAddress)) - .ForMember(dest => dest.LanguageCode, opt => opt.MapFrom(src => src.LanguageCode)) - .ForMember(dest => dest.NationalIdentityNumber, opt => opt.MapFrom(src => src.FnumberAk)) - .ForMember(dest => dest.MobilePhoneNumber, opt => opt.MapFrom(src => src.MobilePhoneNumber)); - } -} diff --git a/src/Altinn.Profile.Integrations/Mappings/PersonContactPreferencesProfile.cs b/src/Altinn.Profile.Integrations/Mappings/PersonContactPreferencesProfile.cs new file mode 100644 index 0000000..57a667f --- /dev/null +++ b/src/Altinn.Profile.Integrations/Mappings/PersonContactPreferencesProfile.cs @@ -0,0 +1,26 @@ +using Altinn.Profile.Core.Person.ContactPreferences; +using Altinn.Profile.Integrations.Entities; + +namespace Altinn.Profile.Integrations.Mappings; + +/// +/// AutoMapper profile for mapping between and . +/// +/// +/// This profile defines the mapping rules to convert a object into a instance. +/// +public class PersonContactPreferencesProfile : AutoMapper.Profile +{ + /// + /// Initializes a new instance of the class and configures the mappings. + /// + public PersonContactPreferencesProfile() + { + CreateMap() + .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.EmailAddress)) + .ForMember(dest => dest.IsReserved, opt => opt.MapFrom(src => src.Reservation)) + .ForMember(dest => dest.LanguageCode, opt => opt.MapFrom(src => src.LanguageCode)) + .ForMember(dest => dest.MobileNumber, opt => opt.MapFrom(src => src.MobilePhoneNumber)) + .ForMember(dest => dest.NationalIdentityNumber, opt => opt.MapFrom(src => src.FnumberAk)); + } +} diff --git a/src/Altinn.Profile.Integrations/Mappings/PersonMappingProfile.cs b/src/Altinn.Profile.Integrations/Mappings/PersonMappingProfile.cs new file mode 100644 index 0000000..fd35ae8 --- /dev/null +++ b/src/Altinn.Profile.Integrations/Mappings/PersonMappingProfile.cs @@ -0,0 +1,50 @@ +using Altinn.Profile.Core.Person.ContactPreferences; +using Altinn.Profile.Integrations.Entities; + +namespace Altinn.Profile.Integrations.Mappings; + +/// +/// AutoMapper profile for mapping between and . +/// +/// +/// This profile defines the mapping rules to convert a object into a instance. +/// +public class PersonMappingProfile : AutoMapper.Profile +{ + /// + /// Initializes a new instance of the class and configures the mappings. + /// + public PersonMappingProfile() + { + CreateMap() + .ForMember(dest => dest.LanguageCode, opt => opt.MapFrom(src => src.Language)) + .ForMember(dest => dest.FnumberAk, opt => opt.MapFrom(src => src.PersonIdentifier)) + .ForMember(dest => dest.Reservation, opt => opt.MapFrom(src => src.Reservation == "JA")) + .ForMember(dest => dest.EmailAddress, opt => opt.MapFrom(src => GetContactDetail(src, detail => detail.Email))) + .ForMember(dest => dest.MobilePhoneNumber, opt => opt.MapFrom(src => GetContactDetail(src, detail => detail.MobileNumber))) + .ForMember(dest => dest.EmailAddressLastUpdated, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.EmailLastUpdated))) + .ForMember(dest => dest.EmailAddressLastVerified, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.EmailLastVerified))) + .ForMember(dest => dest.MobilePhoneNumberLastUpdated, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.MobileNumberLastUpdated))) + .ForMember(dest => dest.MobilePhoneNumberLastVerified, opt => opt.MapFrom(src => GetContactDetailDate(src, detail => detail.MobileNumberLastVerified))); + } + + private static string? GetContactDetail(PersonContactPreferencesSnapshot src, Func selector) + { + return src.ContactDetailsSnapshot != null ? selector(src.ContactDetailsSnapshot) : null; + } + + private static DateTime? GetContactDetailDate(PersonContactPreferencesSnapshot src, Func selector) + { + if (src.ContactDetailsSnapshot != null) + { + var date = selector(src.ContactDetailsSnapshot); + + if (date.HasValue) + { + return date.Value.ToUniversalTime(); + } + } + + return null; + } +} diff --git a/src/Altinn.Profile.Integrations/Migration/v0.02/01-setup-grants.sql b/src/Altinn.Profile.Integrations/Migration/v0.02/01-setup-grants.sql new file mode 100644 index 0000000..2ed3f53 --- /dev/null +++ b/src/Altinn.Profile.Integrations/Migration/v0.02/01-setup-grants.sql @@ -0,0 +1,8 @@ +-- Grant access to the mailbox_supplier table +GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE contact_and_reservation.mailbox_supplier TO platform_profile; + +-- Grant access to the metadata table +GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE contact_and_reservation.metadata TO platform_profile; + +-- Grant access to the person table +GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE contact_and_reservation.person TO platform_profile; diff --git a/src/Altinn.Profile.Integrations/Repositories/IMetadataRepository.cs b/src/Altinn.Profile.Integrations/Repositories/IMetadataRepository.cs new file mode 100644 index 0000000..e6b67d2 --- /dev/null +++ b/src/Altinn.Profile.Integrations/Repositories/IMetadataRepository.cs @@ -0,0 +1,26 @@ +using Altinn.Profile.Core; + +namespace Altinn.Profile.Integrations.Repositories; + +/// +/// Defines a repository for handling metadata operations. +/// +public interface IMetadataRepository +{ + /// + /// Asynchronously retrieves the latest change number from the metadata repository. + /// + /// + /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure. + /// + Task> GetLatestChangeNumberAsync(); + + /// + /// Asynchronously updates the latest change number from the metadata repository. + /// + /// The new changed number. + /// + /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure. + /// + Task> UpdateLatestChangeNumberAsync(long newNumber); +} diff --git a/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs b/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs index 5a2a0f4..e920943 100644 --- a/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs +++ b/src/Altinn.Profile.Integrations/Repositories/IPersonRepository.cs @@ -1,8 +1,7 @@ -#nullable enable +using System.Collections.Immutable; -using System.Collections.Immutable; - -using Altinn.Profile.Core.Domain; +using Altinn.Profile.Core; +using Altinn.Profile.Core.ContactRegister; using Altinn.Profile.Integrations.Entities; namespace Altinn.Profile.Integrations.Repositories; @@ -10,14 +9,23 @@ namespace Altinn.Profile.Integrations.Repositories; /// /// Defines a repository for handling person data operations. /// -public interface IPersonRepository : IRepository +public interface IPersonRepository { /// /// Asynchronously retrieves the contact details for multiple persons by their national identity numbers. /// - /// A collection of national identity numbers to look up for. + /// A collection of national identity numbers to look up. + /// + /// A task that represents the asynchronous operation. The task result contains a object with an of objects representing the contact details of the persons on success, or a indicating failure. + /// + Task, bool>> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers); + + /// + /// Asynchronously synchronizes the changes in person contact preferences. + /// + /// The snapshots of person contact preferences to be synchronized. /// - /// A task that represents the asynchronous operation. The task result contains an of objects representing the contact details of the persons. + /// A task that represents the asynchronous operation. The task result contains a object with a indicating success or failure. /// - Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers); + Task SyncPersonContactPreferencesAsync(ContactRegisterChangesLog personContactPreferencesSnapshots); } diff --git a/src/Altinn.Profile.Integrations/Repositories/MetadataRepository.cs b/src/Altinn.Profile.Integrations/Repositories/MetadataRepository.cs new file mode 100644 index 0000000..22bcfe7 --- /dev/null +++ b/src/Altinn.Profile.Integrations/Repositories/MetadataRepository.cs @@ -0,0 +1,67 @@ +using Altinn.Profile.Core; +using Altinn.Profile.Integrations.Entities; +using Altinn.Profile.Integrations.Persistence; + +using Microsoft.EntityFrameworkCore; + +namespace Altinn.Profile.Integrations.Repositories; + +/// +/// Provides methods for handling metadata operations in the profile database. +/// +/// +/// Initializes a new instance of the class. +/// +/// The factory for creating database context instances. +public class MetadataRepository(IDbContextFactory contextFactory) : IMetadataRepository +{ + private readonly IDbContextFactory _contextFactory = contextFactory; + + /// + /// Asynchronously retrieves the latest change number from the metadata repository. + /// + /// + /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure. + /// + public async Task> GetLatestChangeNumberAsync() + { + using ProfileDbContext databaseContext = _contextFactory.CreateDbContext(); + + Metadata? metadataSingleRow = await databaseContext.Metadata.FirstOrDefaultAsync(); + + return metadataSingleRow != null ? metadataSingleRow.LatestChangeNumber : 0; + } + + /// + /// Asynchronously updates the latest change number from the metadata repository. + /// + /// The new changed number. + /// + /// A task that represents the asynchronous operation. The task result contains a object with a value on success, or a value indicating failure. + /// + public async Task> UpdateLatestChangeNumberAsync(long newNumber) + { + if (newNumber < 0) + { + throw new ArgumentException("The new change number must be non-negative.", nameof(newNumber)); + } + + using ProfileDbContext databaseContext = _contextFactory.CreateDbContext(); + Metadata? existingMetadata = await databaseContext.Metadata.FirstOrDefaultAsync(); + + if (existingMetadata != null) + { + databaseContext.Metadata.Remove(existingMetadata); + } + + Metadata metadata = new() + { + Exported = DateTime.UtcNow, + LatestChangeNumber = newNumber + }; + + await databaseContext.Metadata.AddAsync(metadata); + + return await databaseContext.SaveChangesAsync() > 0 ? newNumber : -1; + } +} diff --git a/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs b/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs index fe9041e..6a4be4e 100644 --- a/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs +++ b/src/Altinn.Profile.Integrations/Repositories/PersonRepository.cs @@ -2,9 +2,14 @@ using System.Collections.Immutable; +using Altinn.Profile.Core; +using Altinn.Profile.Core.ContactRegister; +using Altinn.Profile.Core.Person.ContactPreferences; using Altinn.Profile.Integrations.Entities; using Altinn.Profile.Integrations.Persistence; +using AutoMapper; + using Microsoft.EntityFrameworkCore; namespace Altinn.Profile.Integrations.Repositories; @@ -13,39 +18,102 @@ namespace Altinn.Profile.Integrations.Repositories; /// Defines a repository for handling person data operations. /// /// -internal class PersonRepository : ProfileRepository, IPersonRepository +/// +/// Initializes a new instance of the class. +/// +/// The mapper instance used for object-object mapping. +/// The factory for creating database context instances. +/// +/// Thrown when the , or is null. +/// +internal class PersonRepository(IMapper mapper, IDbContextFactory contextFactory) : IPersonRepository { - private readonly ProfileDbContext _context; + private readonly IMapper _mapper = mapper; + private readonly IDbContextFactory _contextFactory = contextFactory; /// - /// Initializes a new instance of the class. + /// Asynchronously retrieves the contact details for multiple persons by their national identity numbers. /// - /// The context. - /// Thrown when the object is null. - public PersonRepository(ProfileDbContext context) + /// A collection of national identity numbers to look up. + /// + /// A task that represents the asynchronous operation. The task result contains a object with an of objects representing the contact details of the persons on success, or a indicating failure. + /// + /// Thrown when the is null. + public async Task, bool>> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers) { - _context = context ?? throw new ArgumentNullException(nameof(context)); + ArgumentNullException.ThrowIfNull(nationalIdentityNumbers); + + if (!nationalIdentityNumbers.Any()) + { + return ImmutableList.Empty; + } + + using ProfileDbContext databaseContext = await _contextFactory.CreateDbContextAsync(); + + List people = await databaseContext.People.Where(e => nationalIdentityNumbers.Contains(e.FnumberAk)).ToListAsync(); + + return people.ToImmutableList(); } /// - /// Asynchronously retrieves the contact details for multiple persons by their national identity numbers. + /// Asynchronously synchronizes the changes in person contact preferences. /// - /// A collection of national identity numbers to look up for. + /// The snapshots of person contact preferences to be synchronized. /// - /// A task that represents the asynchronous operation. The task result contains an of objects representing the contact details of the persons. + /// A task that represents the asynchronous operation. The task result contains a object with a indicating success or failure. /// - /// Thrown when the is null. - public async Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers) + public async Task SyncPersonContactPreferencesAsync(ContactRegisterChangesLog personContactPreferencesSnapshots) { - ArgumentNullException.ThrowIfNull(nationalIdentityNumbers); + ArgumentNullException.ThrowIfNull(personContactPreferencesSnapshots); + ArgumentNullException.ThrowIfNull(personContactPreferencesSnapshots.ContactPreferencesSnapshots); - if (!nationalIdentityNumbers.Any()) + ImmutableList distinctContactPreferences = + GetDistinctContactPreferences(personContactPreferencesSnapshots.ContactPreferencesSnapshots); + + using ProfileDbContext databaseContext = await _contextFactory.CreateDbContextAsync(); + + foreach (PersonContactPreferencesSnapshot contactPreferenceSnapshot in distinctContactPreferences) { - return []; + Person person = _mapper.Map(contactPreferenceSnapshot); + + Person? existingPerson = await databaseContext.People.FirstOrDefaultAsync(e => e.FnumberAk.Trim() == person.FnumberAk.Trim()); + + if (existingPerson is null) + { + await databaseContext.People.AddAsync(person); + } + else + { + existingPerson.Reservation = person.Reservation; + existingPerson.EmailAddress = person.EmailAddress; + existingPerson.EmailAddressLastUpdated = person.EmailAddressLastUpdated; + existingPerson.EmailAddressLastVerified = person.EmailAddressLastVerified; + existingPerson.MobilePhoneNumber = person.MobilePhoneNumber; + existingPerson.MobilePhoneNumberLastUpdated = person.MobilePhoneNumberLastUpdated; + existingPerson.MobilePhoneNumberLastVerified = person.MobilePhoneNumberLastVerified; + existingPerson.LanguageCode = person.LanguageCode; + + databaseContext.People.Update(existingPerson); + } } - var people = await _context.People.Where(e => nationalIdentityNumbers.Contains(e.FnumberAk)).ToListAsync(); + return await databaseContext.SaveChangesAsync(); + } - return [.. people]; + /// + /// Finds distinct contact preferences by selecting the item with the largest values for the specified properties. + /// + /// The collection of contact preferences snapshots. + /// A list of distinct contact preferences. + private static ImmutableList GetDistinctContactPreferences(IEnumerable contactPreferencesSnapshots) + { + return contactPreferencesSnapshots.GroupBy(p => p.PersonIdentifier) + .Select(g => g.OrderByDescending(p => p.ContactDetailsSnapshot?.EmailLastUpdated) + .ThenByDescending(p => p.ContactDetailsSnapshot?.MobileNumberLastUpdated) + .ThenByDescending(p => p.ContactDetailsSnapshot?.EmailLastVerified) + .ThenByDescending(p => p.ContactDetailsSnapshot?.MobileNumberLastVerified) + .ThenByDescending(p => p.LanguageLastUpdated) + .First()) + .ToImmutableList(); } } diff --git a/src/Altinn.Profile.Integrations/Repositories/ProfileRepository.cs b/src/Altinn.Profile.Integrations/Repositories/ProfileRepository.cs deleted file mode 100644 index dee31f3..0000000 --- a/src/Altinn.Profile.Integrations/Repositories/ProfileRepository.cs +++ /dev/null @@ -1,134 +0,0 @@ -#nullable enable - -using Altinn.Profile.Core.Domain; - -namespace Altinn.Profile.Integrations.Repositories; - -/// -/// Generic repository for handling data access operations. -/// -/// The type of the entity. -/// -internal class ProfileRepository : IRepository - where T : class -{ - /// - /// Initializes a new instance of the class. - /// - internal ProfileRepository() - { - } - - /// - /// Asynchronously adds a new entity to the database. - /// - /// The entity to add. - /// - /// A task that represents the asynchronous operation. The task result contains the added entity. - /// - public Task AddAsync(T entity) - { - throw new NotImplementedException(); - } - - /// - /// Adds multiple entities to the database asynchronously. - /// - /// The entities to add. - /// A task that represents the asynchronous operation. - public Task AddRangeAsync(IEnumerable entities) - { - throw new NotImplementedException(); - } - - /// - /// Deletes an entity from the database asynchronously based on its identifier. - /// - /// The identifier of the entity to delete. - /// A task that represents the asynchronous operation. - public Task DeleteAsync(string id) - { - throw new NotImplementedException(); - } - - /// - /// Deletes multiple entities from the database asynchronously based on their identifiers. - /// - /// The identifiers of the entities to delete. - /// A task that represents the asynchronous operation. - public Task DeleteRangeAsync(IEnumerable ids) - { - throw new NotImplementedException(); - } - - /// - /// Checks whether an entity exists asynchronously based on its identifier. - /// - /// The identifier of the entity. - /// A task that represents the asynchronous operation. The task result contains true if the entity exists, otherwise false. - public Task ExistsAsync(string id) - { - throw new NotImplementedException(); - } - - /// - /// Retrieves all entities from the database asynchronously. - /// - /// A task that represents the asynchronous operation. The task result contains a collection of all entities. - public Task> GetAllAsync() - { - throw new NotImplementedException(); - } - - /// - /// Retrieves entities based on a filter, with optional sorting, pagination, and filtering. - /// - /// A function to filter the entities. - /// A function to order the entities. - /// Number of entities to skip for pagination. - /// Number of entities to take for pagination. - /// A task that represents the asynchronous operation. The task result contains a collection of entities matching the criteria. - public Task> GetAsync(Func? filter = null, Func, IOrderedEnumerable>? orderBy = null, int? skip = null, int? take = null) - { - throw new NotImplementedException(); - } - - /// - /// Retrieves an entity asynchronously based on its identifier. - /// - /// The identifier of the entity to retrieve. - /// A task that represents the asynchronous operation. The task result contains the entity if found, otherwise null. - public Task GetByIdAsync(string id) - { - throw new NotImplementedException(); - } - - /// - /// Saves the changes made in the context asynchronously. - /// - /// A task that represents the asynchronous operation. The task result contains the number of state entries written to the database. - public Task SaveChangesAsync() - { - throw new NotImplementedException(); - } - - /// - /// Updates an entity in the database asynchronously. - /// - /// The entity to update. - /// A task that represents the asynchronous operation. - public Task UpdateAsync(T entity) - { - throw new NotImplementedException(); - } - - /// - /// Updates multiple entities in the database asynchronously. - /// - /// The entities to update. - /// A task that represents the asynchronous operation. - public Task UpdateRangeAsync(IEnumerable entities) - { - throw new NotImplementedException(); - } -} diff --git a/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs b/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs index 05dae22..780b9d5 100644 --- a/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs +++ b/src/Altinn.Profile.Integrations/ServiceCollectionExtensions.cs @@ -1,6 +1,12 @@ using System.Diagnostics.CodeAnalysis; +using Altinn.ApiClients.Maskinporten.Extensions; +using Altinn.ApiClients.Maskinporten.Services; +using Altinn.Profile.Core.ContactRegister; + using Altinn.Profile.Core.Integrations; +using Altinn.Profile.Core.Person.ContactPreferences; +using Altinn.Profile.Integrations.ContactRegister; using Altinn.Profile.Integrations.Extensions; using Altinn.Profile.Integrations.Mappings; using Altinn.Profile.Integrations.Persistence; @@ -58,13 +64,41 @@ public static void AddRegisterService(this IServiceCollection services, IConfigu throw new InvalidOperationException("Database connection string is not properly configured."); } - services.AddDbContext(options => options.UseNpgsql(connectionString)); - - services.AddAutoMapper(typeof(PersonContactDetailsProfile)); - services.AddScoped(); services.AddScoped(); + services.AddScoped(); + + services.AddAutoMapper(typeof(PersonMappingProfile)); services.AddSingleton(); + + services.AddDbContextFactory(options => options.UseNpgsql(connectionString)); + } + + /// + /// Adds the Maskinporten client to the DI container. + /// + /// The service collection. + /// The configuration collection. + /// Thrown when the configuration is null. + /// Thrown when any of the required configuration values are missing or empty. + public static void AddMaskinportenClient(this IServiceCollection services, IConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config), "Configuration cannot be null."); + } + + var contactRegisterSettings = new ContactRegisterSettings(); + config.GetSection("ContactAndReservationSettings").Bind(contactRegisterSettings); + if (contactRegisterSettings.MaskinportenSettings == null) + { + throw new InvalidOperationException("Contact and reservation settings are not properly configured."); + } + + services.AddScoped(); + + services.AddSingleton(contactRegisterSettings); + services.AddMaskinportenHttpClient(contactRegisterSettings.MaskinportenSettings); } } diff --git a/src/Altinn.Profile.Integrations/Services/IPersonService.cs b/src/Altinn.Profile.Integrations/Services/IPersonService.cs index ad93f40..ec16f4e 100644 --- a/src/Altinn.Profile.Integrations/Services/IPersonService.cs +++ b/src/Altinn.Profile.Integrations/Services/IPersonService.cs @@ -11,20 +11,17 @@ namespace Altinn.Profile.Integrations.Services; public interface IPersonService { /// - /// Asynchronously retrieves the contact details for a single person based on their national identity number. + /// Asynchronously retrieves the contact preferences for multiple persons based on their national identity numbers. /// - /// The national identity number of the person. + /// A collection of national identity numbers. /// - /// A task that represents the asynchronous operation. The task result contains the person's contact details, or null if not found. + /// A task that represents the asynchronous operation. The task result contains a object, where represents the successful lookup result and indicates a failure. /// - Task GetContactDetailsAsync(string nationalIdentityNumber); + Task> GetContactPreferencesAsync(IEnumerable nationalIdentityNumbers); /// - /// Asynchronously retrieves the contact details for multiple persons based on their national identity numbers. + /// Asynchronously synchronizes the person contact preferences. /// - /// A collection of national identity numbers. - /// - /// A task that represents the asynchronous operation. The task result contains a object, where represents the successful lookup result and indicates a failure. - /// - Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers); + /// A task that represents the asynchronous operation. + Task SyncPersonContactPreferencesAsync(); } diff --git a/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs b/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs index 816646d..b74db94 100644 --- a/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs +++ b/src/Altinn.Profile.Integrations/Services/NationalIdentityNumberChecker.cs @@ -55,6 +55,6 @@ public IImmutableList GetValid(IEnumerable nationalIdentityNumbe /// public bool IsValid(string nationalIdentityNumber) { - return nationalIdentityNumber.IsValidSocialSecurityNumber(); + return nationalIdentityNumber.IsValidNationalIdentityNumber(); } } diff --git a/src/Altinn.Profile.Integrations/Services/PersonService.cs b/src/Altinn.Profile.Integrations/Services/PersonService.cs index 0c2fd21..0af58eb 100644 --- a/src/Altinn.Profile.Integrations/Services/PersonService.cs +++ b/src/Altinn.Profile.Integrations/Services/PersonService.cs @@ -3,6 +3,8 @@ using System.Collections.Immutable; using Altinn.Profile.Core; +using Altinn.Profile.Core.ContactRegister; +using Altinn.Profile.Core.Person.ContactPreferences; using Altinn.Profile.Integrations.Entities; using Altinn.Profile.Integrations.Repositories; @@ -17,68 +19,94 @@ public class PersonService : IPersonService { private readonly IMapper _mapper; private readonly IPersonRepository _personRepository; + private readonly IMetadataRepository _metadataRepository; + private readonly IContactRegisterService _changesLogService; private readonly INationalIdentityNumberChecker _nationalIdentityNumberChecker; /// /// Initializes a new instance of the class. /// - /// The mapper used for object mapping. + /// The objects mapper. /// The repository used for accessing the person data. + /// The service used for logging changes in contact preferences. + /// The repository used for accessing metadata. /// The service used for checking the validity of national identity numbers. - /// - /// Thrown if , , or is null. - /// - public PersonService(IMapper mapper, IPersonRepository personRepository, INationalIdentityNumberChecker nationalIdentityNumberChecker) + public PersonService( + IMapper mapper, + IPersonRepository personRepository, + IContactRegisterService changesLogService, + IMetadataRepository metadataRepository, + INationalIdentityNumberChecker nationalIdentityNumberChecker) { - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - _personRepository = personRepository ?? throw new ArgumentNullException(nameof(personRepository)); - _nationalIdentityNumberChecker = nationalIdentityNumberChecker ?? throw new ArgumentNullException(nameof(nationalIdentityNumberChecker)); + _mapper = mapper; + _personRepository = personRepository; + _changesLogService = changesLogService; + _metadataRepository = metadataRepository; + _nationalIdentityNumberChecker = nationalIdentityNumberChecker; } /// - /// Asynchronously retrieves the contact details for a single person based on their national identity number. - /// - /// The national identity number of the person. - /// - /// A task that represents the asynchronous operation. The task result contains the person's contact details, or null if not found. - /// - public async Task GetContactDetailsAsync(string nationalIdentityNumber) - { - if (!_nationalIdentityNumberChecker.IsValid(nationalIdentityNumber)) - { - return null; - } - - var personContactDetails = await _personRepository.GetContactDetailsAsync([nationalIdentityNumber]); - return _mapper.Map(personContactDetails.FirstOrDefault()); - } - - /// - /// Asynchronously retrieves the contact details for multiple persons based on their national identity numbers. + /// Asynchronously retrieves the contact preferences for multiple persons based on their national identity numbers. /// /// A collection of national identity numbers. /// - /// A task that represents the asynchronous operation. The task result contains a object, where represents the successful lookup result and indicates a failure. + /// A task that represents the asynchronous operation. The task result contains a + /// object, where represents the successful lookup result and indicates a failure. /// /// Thrown if is null. - public async Task> GetContactDetailsAsync(IEnumerable nationalIdentityNumbers) + public async Task> GetContactPreferencesAsync(IEnumerable nationalIdentityNumbers) { ArgumentNullException.ThrowIfNull(nationalIdentityNumbers); var validNationalIdentityNumbers = _nationalIdentityNumberChecker.GetValid(nationalIdentityNumbers); + if (validNationalIdentityNumbers == null || validNationalIdentityNumbers.Count == 0) + { + return false; + } - var matchedContactDetails = await _personRepository.GetContactDetailsAsync(validNationalIdentityNumbers); - - var matchedNationalIdentityNumbers = matchedContactDetails != null ? new HashSet(matchedContactDetails.Select(e => e.FnumberAk)) : []; + Result, bool> matchedContactDetails = await _personRepository.GetContactDetailsAsync(validNationalIdentityNumbers); - var unmatchedNationalIdentityNumbers = nationalIdentityNumbers.Where(e => !matchedNationalIdentityNumbers.Contains(e)); + HashSet matchedNationalIdentityNumbers = []; + IEnumerable unmatchedNationalIdentityNumbers = []; + IEnumerable matchedPersonContactDetails = []; - var matchedPersonContactDetails = matchedContactDetails != null ? matchedContactDetails.Select(_mapper.Map) : []; + matchedContactDetails.Match( + e => + { + if (e is not null && e.Count > 0) + { + matchedNationalIdentityNumbers = new HashSet(e.Select(e => e.FnumberAk)); + matchedPersonContactDetails = e.Select(_mapper.Map).ToImmutableList(); + unmatchedNationalIdentityNumbers = nationalIdentityNumbers.Where(e => !matchedNationalIdentityNumbers.Contains(e)); + } + }, + _ => { }); - return new PersonContactDetailsLookupResult + return new PersonContactPreferencesLookupResult { - MatchedPersonContactDetails = matchedPersonContactDetails.ToImmutableList(), - UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers.ToImmutableList() + MatchedPersonContactPreferences = matchedPersonContactDetails.Any() ? matchedPersonContactDetails.ToImmutableList() : null, + UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers.Any() ? unmatchedNationalIdentityNumbers.ToImmutableList() : null }; } + + /// + /// Asynchronously synchronizes the person contact preferences. + /// + /// A task that represents the asynchronous operation. + public async Task SyncPersonContactPreferencesAsync() + { + // Get the latest change number. + long latestChangeNumber = 0; + Result latestChangeNumberGetter = await _metadataRepository.GetLatestChangeNumberAsync(); + latestChangeNumberGetter.Match(e => latestChangeNumber = e, _ => latestChangeNumber = 0); + + // Retrieve the changes in contact preferences from the changes log. + ContactRegisterChangesLog contactDetailsChanges = await _changesLogService.RetrieveContactDetailsChangesAsync(latestChangeNumber); + + int synchornizedRowCount = await _personRepository.SyncPersonContactPreferencesAsync(contactDetailsChanges); + if (synchornizedRowCount > 0 && contactDetailsChanges.EndingIdentifier.HasValue) + { + await _metadataRepository.UpdateLatestChangeNumberAsync(contactDetailsChanges.EndingIdentifier.Value); + } + } } diff --git a/src/Altinn.Profile/Controllers/ContactDetailsController.cs b/src/Altinn.Profile/Controllers/PersonContactDetailsController.cs similarity index 51% rename from src/Altinn.Profile/Controllers/ContactDetailsController.cs rename to src/Altinn.Profile/Controllers/PersonContactDetailsController.cs index 8a5e235..e9bc6a9 100644 --- a/src/Altinn.Profile/Controllers/ContactDetailsController.cs +++ b/src/Altinn.Profile/Controllers/PersonContactDetailsController.cs @@ -12,31 +12,24 @@ namespace Altinn.Profile.Controllers; /// -/// Controller to retrieve the contact details for one or more persons. +/// Controller responsible for managing contact details for one or more persons. /// +/// +/// Initializes a new instance of the class. +/// +/// The logger instance used for logging. +/// The service for retrieving the contact details. [Authorize] [ApiController] [Consumes("application/json")] [Produces("application/json")] -[Route("profile/api/v1/contact/details")] -public class ContactDetailsController : ControllerBase +[Route("profile/api/v1/person/contact/details")] +public class PersonContactDetailsController( + ILogger logger, IPersonContactDetailsRetriever contactDetailsRetriever) + : ControllerBase { - private readonly ILogger _logger; - private readonly IContactDetailsRetriever _contactDetailsRetriever; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance used for logging. - /// The use case for retrieving the contact details. - /// - /// Thrown when the or is null. - /// - public ContactDetailsController(ILogger logger, IContactDetailsRetriever contactDetailsRetriever) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _contactDetailsRetriever = contactDetailsRetriever ?? throw new ArgumentNullException(nameof(contactDetailsRetriever)); - } + private readonly IPersonContactDetailsRetriever _contactDetailsRetriever = contactDetailsRetriever; + private readonly ILogger _logger = logger; /// /// Retrieves the contact details for persons based on their national identity numbers. @@ -44,13 +37,13 @@ public ContactDetailsController(ILogger logger, IConta /// A collection of national identity numbers. /// /// A task that represents the asynchronous operation, containing a response with persons' contact details. - /// Returns a with status 200 OK if successful. + /// Returns a with status 200 OK if successful. /// [HttpPost("lookup")] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ContactDetailsLookupResult), StatusCodes.Status200OK)] - public async Task> PostLookup([FromBody] UserContactPointLookup lookupCriteria) + [ProducesResponseType(typeof(PersonContactDetailsLookupResult), StatusCodes.Status200OK)] + public async Task> PostLookup([FromBody] UserContactDetailsLookupCriteria lookupCriteria) { if (!ModelState.IsValid) { @@ -66,10 +59,10 @@ public async Task> PostLookup([FromBody { var lookupResult = await _contactDetailsRetriever.RetrieveAsync(lookupCriteria); - return lookupResult.Match>( + return lookupResult.Match>( successResponse => { - return successResponse?.MatchedContactDetails?.Count > 0 ? Ok(successResponse) : NotFound(); + return successResponse?.MatchedPersonContactDetails?.Count > 0 ? Ok(successResponse) : NotFound(); }, failedResponse => NotFound()); } diff --git a/src/Altinn.Profile/Controllers/ContactDetailsInternalController.cs b/src/Altinn.Profile/Controllers/PersonContactDetailsInternalController.cs similarity index 62% rename from src/Altinn.Profile/Controllers/ContactDetailsInternalController.cs rename to src/Altinn.Profile/Controllers/PersonContactDetailsInternalController.cs index 1209689..1662a92 100644 --- a/src/Altinn.Profile/Controllers/ContactDetailsInternalController.cs +++ b/src/Altinn.Profile/Controllers/PersonContactDetailsInternalController.cs @@ -18,22 +18,21 @@ namespace Altinn.Profile.Controllers; [Consumes("application/json")] [Produces("application/json")] [ApiExplorerSettings(IgnoreApi = true)] -[Route("profile/api/v1/internal/contact/details")] -public class ContactDetailsInternalController : ControllerBase +[Route("profile/api/v1/internal/person/contact/details")] +public class PersonContactDetailsInternalController : ControllerBase { - private readonly ILogger _logger; - private readonly IContactDetailsRetriever _contactDetailsRetriever; + private readonly IPersonContactDetailsRetriever _contactDetailsRetriever; + private readonly ILogger _logger; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The logger instance used for logging. /// The use case for retrieving the contact details. - /// Thrown when the is null. - public ContactDetailsInternalController(ILogger logger, IContactDetailsRetriever contactDetailsRetriever) + public PersonContactDetailsInternalController(ILogger logger, IPersonContactDetailsRetriever contactDetailsRetriever) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _contactDetailsRetriever = contactDetailsRetriever ?? throw new ArgumentNullException(nameof(contactDetailsRetriever)); + _logger = logger; + _contactDetailsRetriever = contactDetailsRetriever; } /// @@ -42,14 +41,14 @@ public ContactDetailsInternalController(ILoggerA collection of national identity numbers. /// /// A task that represents the asynchronous operation, containing a response with persons' contact details. - /// Returns a with status 200 OK if successful, + /// Returns a with status 200 OK if successful, /// 400 Bad Request if the request is invalid, or 404 Not Found if no contact details are found. /// [HttpPost("lookup")] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ContactDetailsLookupResult), StatusCodes.Status200OK)] - public async Task> PostLookup([FromBody] UserContactPointLookup request) + [ProducesResponseType(typeof(PersonContactDetailsLookupResult), StatusCodes.Status200OK)] + public async Task> PostLookup([FromBody] UserContactDetailsLookupCriteria request) { if (!ModelState.IsValid) { @@ -65,10 +64,10 @@ public async Task> PostLookup([FromBody { var result = await _contactDetailsRetriever.RetrieveAsync(request); - return result.Match>( + return result.Match>( success => { - return success?.MatchedContactDetails?.Count > 0 ? Ok(success) : NotFound(); + return success?.MatchedPersonContactDetails?.Count > 0 ? Ok(success) : NotFound(); }, noMatch => NotFound()); } diff --git a/src/Altinn.Profile/Controllers/TriggerController.cs b/src/Altinn.Profile/Controllers/TriggerController.cs new file mode 100644 index 0000000..74d5e2d --- /dev/null +++ b/src/Altinn.Profile/Controllers/TriggerController.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; + +using Altinn.Profile.Integrations.Services; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Altinn.Profile.Controllers; + +/// +/// Controller responsible for managing changes in contact preferences for one or more persons. +/// +[ApiController] +[ApiExplorerSettings(IgnoreApi = true)] +[Consumes("application/json")] +[Produces("application/json")] +[Route("profile/api/v1/trigger")] +public class TriggerController : ControllerBase +{ + private readonly IPersonService _personService; + + /// + /// Initializes a new instance of the class. + /// + /// The service for retrieving the contact details. + public TriggerController(IPersonService personService) + { + _personService = personService; + } + + /// + /// Synchronizes the changes in the contact details for persons. + /// + /// + /// A task that represents the asynchronous operation. If successful, returns a status 200 OK. + /// + /// Starting the synchronisation work was successfull. + /// Returns a problem detail if an unexpected error occurs. + [HttpGet("syncpersonchanges")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task SyncChanges() + { + await _personService.SyncPersonContactPreferencesAsync(); + + return Ok(); + } +} diff --git a/src/Altinn.Profile/Controllers/UserContactPointController.cs b/src/Altinn.Profile/Controllers/UserContactPointController.cs index d45d328..f150943 100644 --- a/src/Altinn.Profile/Controllers/UserContactPointController.cs +++ b/src/Altinn.Profile/Controllers/UserContactPointController.cs @@ -12,6 +12,7 @@ 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. /// +[ApiController] [Route("profile/api/v1/users/contactpoint")] [ApiExplorerSettings(IgnoreApi = true)] [Consumes("application/json")] @@ -34,7 +35,7 @@ public UserContactPointController(IUserContactPoints contactPointService) /// Returns an overview of the availability of various contact points for the user [HttpPost("availability")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PostAvailabilityLookup([FromBody] UserContactPointLookup userContactPointLookup) + public async Task> PostAvailabilityLookup([FromBody] UserContactDetailsLookupCriteria userContactPointLookup) { if (userContactPointLookup.NationalIdentityNumbers.Count == 0) { @@ -54,7 +55,7 @@ public async Task> PostAvailabili /// Returns an overview of the contact points for the user [HttpPost("lookup")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> PostLookup([FromBody] UserContactPointLookup userContactPointLookup) + public async Task> PostLookup([FromBody] UserContactDetailsLookupCriteria userContactPointLookup) { Result result = await _contactPointService.GetContactPoints(userContactPointLookup.NationalIdentityNumbers); return result.Match>( diff --git a/src/Altinn.Profile/Models/ContactDetails.cs b/src/Altinn.Profile/Models/PersonContactDetails.cs similarity index 79% rename from src/Altinn.Profile/Models/ContactDetails.cs rename to src/Altinn.Profile/Models/PersonContactDetails.cs index bba8c59..09c72e6 100644 --- a/src/Altinn.Profile/Models/ContactDetails.cs +++ b/src/Altinn.Profile/Models/PersonContactDetails.cs @@ -5,37 +5,37 @@ namespace Altinn.Profile.Models; /// -/// Represents the contact information for a single person, including national identity number, contact methods, language preference, and opt-out status. +/// Represents the contact details for a single person, including the national identity number, mobile phone number, email address, language preference, and an opt-out status for being contacted. /// -public record ContactDetails +public record PersonContactDetails { /// - /// Gets the national identity number of the person. + /// Gets the email address of the person. /// - [JsonPropertyName("nationalIdentityNumber")] - public required string NationalIdentityNumber { get; init; } + [JsonPropertyName("emailAddress")] + public string? EmailAddress { get; init; } /// /// Gets a value indicating whether the person has opted out of being contacted. /// [JsonPropertyName("reservation")] - public bool? Reservation { get; init; } + public bool? IsReserved { get; init; } /// - /// Gets the mobile phone number of the person. + /// Gets the language code preferred by the person for communication. /// - [JsonPropertyName("mobilePhoneNumber")] - public string? MobilePhoneNumber { get; init; } + [JsonPropertyName("languageCode")] + public string? LanguageCode { get; init; } /// - /// Gets the email address of the person. + /// Gets the mobile phone number of the person. /// - [JsonPropertyName("emailAddress")] - public string? EmailAddress { get; init; } + [JsonPropertyName("mobilePhoneNumber")] + public string? MobilePhoneNumber { get; init; } /// - /// Gets the language code preferred by the person for communication. + /// Gets the national identity number of the person. /// - [JsonPropertyName("languageCode")] - public string? LanguageCode { get; init; } + [JsonPropertyName("nationalIdentityNumber")] + public required string NationalIdentityNumber { get; init; } } diff --git a/src/Altinn.Profile/Models/ContactDetailsLookupResult.cs b/src/Altinn.Profile/Models/PersonContactDetailsLookupResult.cs similarity index 53% rename from src/Altinn.Profile/Models/ContactDetailsLookupResult.cs rename to src/Altinn.Profile/Models/PersonContactDetailsLookupResult.cs index 1a9d4a6..c6650f2 100644 --- a/src/Altinn.Profile/Models/ContactDetailsLookupResult.cs +++ b/src/Altinn.Profile/Models/PersonContactDetailsLookupResult.cs @@ -6,16 +6,29 @@ namespace Altinn.Profile.Models; /// -/// Represents the results of a contact details lookup operation. +/// Represents the result of a contact details lookup operation for one or more persons. /// -public record ContactDetailsLookupResult +public record PersonContactDetailsLookupResult { /// - /// Gets a list of contact details that were successfully matched based on the national identity number. + /// Initializes a new instance of the record. /// - [JsonPropertyName("matchedContactDetails")] + /// The list of person contact details that were successfully matched based on the national identity number. + /// The list of national identity numbers that could not be matched with any contact details. + public PersonContactDetailsLookupResult( + ImmutableList matchedPersonContactDetails, + ImmutableList unmatchedNationalIdentityNumbers) + { + MatchedPersonContactDetails = matchedPersonContactDetails; + UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers; + } + + /// + /// Gets a list of person contact details that were successfully matched based on the national identity number. + /// + [JsonPropertyName("matchedPersonContactDetails")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public ImmutableList? MatchedContactDetails { get; init; } + public ImmutableList? MatchedPersonContactDetails { get; init; } /// /// Gets a list of national identity numbers that could not be matched with any contact details. @@ -23,17 +36,4 @@ public record ContactDetailsLookupResult [JsonPropertyName("unmatchedNationalIdentityNumbers")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ImmutableList? UnmatchedNationalIdentityNumbers { get; init; } - - /// - /// Initializes a new instance of the record. - /// - /// The list of contact details that were successfully matched based on the national identity number. - /// The list of national identity numbers that could not be matched with any contact details. - public ContactDetailsLookupResult( - ImmutableList matchedContactDetails, - ImmutableList unmatchedNationalIdentityNumbers) - { - MatchedContactDetails = matchedContactDetails; - UnmatchedNationalIdentityNumbers = unmatchedNationalIdentityNumbers; - } } diff --git a/src/Altinn.Profile/Models/UserContactPointLookup.cs b/src/Altinn.Profile/Models/UserContactDetailsLookupCriteria.cs similarity index 72% rename from src/Altinn.Profile/Models/UserContactPointLookup.cs rename to src/Altinn.Profile/Models/UserContactDetailsLookupCriteria.cs index 8ee6bb4..9f4982c 100644 --- a/src/Altinn.Profile/Models/UserContactPointLookup.cs +++ b/src/Altinn.Profile/Models/UserContactDetailsLookupCriteria.cs @@ -3,9 +3,9 @@ namespace Altinn.Profile.Models; /// -/// A class representing a user contact point lookup object. +/// Represents the lookup criteria to retrieve the contact details for one or more persons. /// -public class UserContactPointLookup +public class UserContactDetailsLookupCriteria { /// /// A collection of national identity numbers used to retrieve contact points, obtain contact details, or check the availability of contact points. diff --git a/src/Altinn.Profile/Program.cs b/src/Altinn.Profile/Program.cs index 2f97e5d..f061781 100644 --- a/src/Altinn.Profile/Program.cs +++ b/src/Altinn.Profile/Program.cs @@ -57,7 +57,7 @@ WebApplication app = builder.Build(); -app.SetUpPostgreSql(builder.Environment.IsDevelopment(), builder.Configuration); +app.SetUpPostgreSql(builder.Configuration); Configure(); @@ -162,7 +162,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddScoped(); + services.AddScoped(); services.AddAuthentication(JwtCookieDefaults.AuthenticationScheme) .AddJwtCookie(JwtCookieDefaults.AuthenticationScheme, options => @@ -194,6 +194,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration config) services.AddCoreServices(config); services.AddRegisterService(config); services.AddSblBridgeClients(config); + services.AddMaskinportenClient(config); if (!string.IsNullOrEmpty(applicationInsightsConnectionString)) { diff --git a/src/Altinn.Profile/UseCases/ContactDetailsRetriever.cs b/src/Altinn.Profile/UseCases/ContactDetailsRetriever.cs deleted file mode 100644 index f352262..0000000 --- a/src/Altinn.Profile/UseCases/ContactDetailsRetriever.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; - -using Altinn.Profile.Core; -using Altinn.Profile.Integrations.Entities; -using Altinn.Profile.Integrations.Services; -using Altinn.Profile.Models; - -namespace Altinn.Profile.UseCases; - -/// -/// Provides an implementation for retrieving the contact details for one or more persons. -/// -public class ContactDetailsRetriever : IContactDetailsRetriever -{ - private readonly IPersonService _personService; - - /// - /// Initializes a new instance of the class. - /// - /// The person service for retrieving contact details. - /// Thrown when is null. - public ContactDetailsRetriever(IPersonService personService) - { - _personService = personService ?? throw new ArgumentNullException(nameof(personService)); - } - - /// - /// Asynchronously retrieves the contact details for one or more persons based on the specified lookup criteria. - /// - /// The criteria used to look up contact details, including the national identity numbers of the persons. - /// - /// A task representing the asynchronous operation. - /// The task result contains a object, where represents the successful outcome and indicates a failure. - /// - /// Thrown when is null. - public async Task> RetrieveAsync(UserContactPointLookup lookupCriteria) - { - ArgumentNullException.ThrowIfNull(lookupCriteria); - - if (lookupCriteria.NationalIdentityNumbers == null || lookupCriteria.NationalIdentityNumbers.Count == 0) - { - return false; - } - - var contactDetails = await _personService.GetContactDetailsAsync(lookupCriteria.NationalIdentityNumbers); - - return contactDetails.Match( - MapToContactDetailsLookupResult, - _ => false); - } - - /// - /// Maps the person contact details to a . - /// - /// The person contact details to map. - /// The mapped . - /// Thrown when is null. - private ContactDetails MapToContactDetails(IPersonContactDetails contactDetails) - { - ArgumentNullException.ThrowIfNull(contactDetails); - - return new ContactDetails - { - Reservation = contactDetails.IsReserved, - EmailAddress = contactDetails.EmailAddress, - LanguageCode = contactDetails.LanguageCode, - MobilePhoneNumber = contactDetails.MobilePhoneNumber, - NationalIdentityNumber = contactDetails.NationalIdentityNumber - }; - } - - /// - /// Maps the person contact details lookup result to a . - /// - /// The lookup result containing the person contact details. - /// - /// A containing a if the mapping is successful, or false if the mapping fails. - /// - /// Thrown when is null. - private Result MapToContactDetailsLookupResult(IPersonContactDetailsLookupResult lookupResult) - { - ArgumentNullException.ThrowIfNull(lookupResult); - - var matchedContactDetails = lookupResult.MatchedPersonContactDetails?.Select(MapToContactDetails).ToImmutableList(); - - return new ContactDetailsLookupResult(matchedContactDetails, lookupResult.UnmatchedNationalIdentityNumbers); - } -} diff --git a/src/Altinn.Profile/UseCases/IContactDetailsRetriever.cs b/src/Altinn.Profile/UseCases/IPersonContactDetailsRetriever.cs similarity index 62% rename from src/Altinn.Profile/UseCases/IContactDetailsRetriever.cs rename to src/Altinn.Profile/UseCases/IPersonContactDetailsRetriever.cs index b3ff503..ac730c8 100644 --- a/src/Altinn.Profile/UseCases/IContactDetailsRetriever.cs +++ b/src/Altinn.Profile/UseCases/IPersonContactDetailsRetriever.cs @@ -8,15 +8,15 @@ namespace Altinn.Profile.UseCases; /// /// Defines a use case for retrieving the contact details for one or more persons. /// -public interface IContactDetailsRetriever +public interface IPersonContactDetailsRetriever { /// /// Asynchronously retrieves the contact details for one or more persons based on the specified lookup criteria. /// - /// The criteria used to look up contact details, including the national identity numbers of the persons. + /// The criteria used to look up contact . /// /// A task representing the asynchronous operation. - /// The task result contains a object, where represents the successful outcome and indicates a failure. + /// The task result contains a object, where represents the successful outcome and indicates a failure. /// - Task> RetrieveAsync(UserContactPointLookup lookupCriteria); + Task> RetrieveAsync(UserContactDetailsLookupCriteria lookupCriteria); } diff --git a/src/Altinn.Profile/UseCases/PersonContactDetailsRetriever.cs b/src/Altinn.Profile/UseCases/PersonContactDetailsRetriever.cs new file mode 100644 index 0000000..7237204 --- /dev/null +++ b/src/Altinn.Profile/UseCases/PersonContactDetailsRetriever.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +using Altinn.Profile.Core; +using Altinn.Profile.Core.Person.ContactPreferences; +using Altinn.Profile.Integrations.Entities; +using Altinn.Profile.Integrations.Services; +using Altinn.Profile.Models; + +namespace Altinn.Profile.UseCases; + +/// +/// Provides an implementation for retrieving the contact details for one or more persons. +/// +/// +/// Initializes a new instance of the class. +/// +/// The person service for retrieving contact details. +public class PersonContactDetailsRetriever(IPersonService personService) : IPersonContactDetailsRetriever +{ + private readonly IPersonService _personService = personService; + + /// + /// Asynchronously retrieves the contact details for one or more persons based on the specified lookup criteria. + /// + /// The criteria used to look up contact details. + /// + /// A task representing the asynchronous operation. + /// The task result contains a object, where represents the successful outcome and indicates a failure. + /// + /// Thrown when is null. + public async Task> RetrieveAsync(UserContactDetailsLookupCriteria lookupCriteria) + { + ArgumentNullException.ThrowIfNull(lookupCriteria); + + if (lookupCriteria.NationalIdentityNumbers == null || lookupCriteria.NationalIdentityNumbers.Count == 0) + { + return false; + } + + var contactDetails = await _personService.GetContactPreferencesAsync(lookupCriteria.NationalIdentityNumbers); + + return contactDetails.Match( + MapToContactDetailsLookupResult, + _ => false); + } + + /// + /// Maps the person contact details to a . + /// + /// The person contact details to map. + /// The mapped . + /// Thrown when is null. + private PersonContactDetails MapToContactDetails(PersonContactPreferences contactPreferences) + { + ArgumentNullException.ThrowIfNull(contactPreferences); + + return new PersonContactDetails + { + IsReserved = contactPreferences.IsReserved, + EmailAddress = contactPreferences.Email, + LanguageCode = contactPreferences.LanguageCode, + MobilePhoneNumber = contactPreferences.MobileNumber, + NationalIdentityNumber = contactPreferences.NationalIdentityNumber + }; + } + + /// + /// Maps the person contact details lookup result to a . + /// + /// The lookup result containing the person contact details. + /// + /// A containing a if the mapping is successful, or false if the mapping fails. + /// + /// Thrown when is null. + private Result MapToContactDetailsLookupResult(IPersonContactPreferencesLookupResult lookupResult) + { + ArgumentNullException.ThrowIfNull(lookupResult); + + var matchedContactDetails = lookupResult.MatchedPersonContactPreferences?.Select(MapToContactDetails).ToImmutableList(); + + return new PersonContactDetailsLookupResult(matchedContactDetails, lookupResult.UnmatchedNationalIdentityNumbers); + } +} diff --git a/src/Altinn.Profile/appsettings.Development.json b/src/Altinn.Profile/appsettings.Development.json index 01d01ec..a2ac3a3 100644 --- a/src/Altinn.Profile/appsettings.Development.json +++ b/src/Altinn.Profile/appsettings.Development.json @@ -1,6 +1,6 @@ { "PostgreSqlSettings": { - "MigrationScriptPath": "Altinn.Profile.Integrations/Migration" + "MigrationScriptPath": "../Altinn.Profile.Integrations/Migration" }, "Logging": { "LogLevel": { diff --git a/src/Altinn.Profile/appsettings.json b/src/Altinn.Profile/appsettings.json index e919f04..8add802 100644 --- a/src/Altinn.Profile/appsettings.json +++ b/src/Altinn.Profile/appsettings.json @@ -16,5 +16,14 @@ "ProfileDbAdminPwd": "Password", "ProfileDbPwd": "Password", "EnableDBConnection": true + }, + "ContactAndReservationSettings": { + "ChangesLogEndpoint": "https://test.kontaktregisteret.no/rest/v2/krr/hentEndringer", + "MaskinportenSettings": { + "Environment": "test", + "Scope": "krr:global/digitalpost.read krr:global/kontaktinformasjon.read krr:global/hentendring.read", + "ClientId": "value injected as secret", + "EncodedJwk": "value injected as secret" + } } } diff --git a/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj b/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj index ec727d7..19fed50 100644 --- a/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj +++ b/test/Altinn.Profile.Tests/Altinn.Profile.Tests.csproj @@ -71,6 +71,9 @@ Always + + PreserveNewest + diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsControllerTests.cs deleted file mode 100644 index ef8ebe0..0000000 --- a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsControllerTests.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System; -using System.Threading.Tasks; - -using Altinn.Profile.Controllers; -using Altinn.Profile.Models; -using Altinn.Profile.UseCases; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit; - -namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers; - -public class ContactDetailsControllerTests -{ - private readonly ContactDetailsController _controller; - private readonly Mock> _loggerMock; - private readonly Mock _mockContactDetailsRetriever; - - public ContactDetailsControllerTests() - { - _loggerMock = new Mock>(); - _mockContactDetailsRetriever = new Mock(); - _controller = new ContactDetailsController(_loggerMock.Object, _mockContactDetailsRetriever.Object); - } - - [Fact] - public void Constructor_WithNullContactDetailsRetriever_ThrowsArgumentNullException() - { - // Arrange - var loggerMock = new Mock>(); - - // Act & Assert - Assert.Throws(() => new ContactDetailsController(loggerMock.Object, null)); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Arrange - var contactDetailsRetrieverMock = new Mock(); - - // Act & Assert - Assert.Throws(() => new ContactDetailsController(null, contactDetailsRetrieverMock.Object)); - } - - [Fact] - public void Constructor_WithValidParameters_InitializesCorrectly() - { - // Arrange - var loggerMock = new Mock>(); - var contactDetailsRetrieverMock = new Mock(); - - // Act - var controller = new ContactDetailsController(loggerMock.Object, contactDetailsRetrieverMock.Object); - - // Assert - Assert.NotNull(controller); - } - - [Fact] - public async Task PostLookup_WhenExceptionOccurs_ReturnsInternalServerError_And_LogsError() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "27038893837" } }; - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ThrowsAsync(new Exception("Test exception")); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var problemResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode); - - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task PostLookup_WhenLookupCriteriaIsNull_ReturnsBadRequest() - { - // Arrange - UserContactPointLookup request = null; - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var badRequestResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); - Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value); - } - - [Fact] - public async Task PostLookup_WhenMatchedContactDetailsAreFound_ReturnsOk() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "05053423096" } }; - var contactDetails = new ContactDetails - { - LanguageCode = "en", - Reservation = false, - MobilePhoneNumber = "98765432", - EmailAddress = "user@example.com", - NationalIdentityNumber = "05053423096" - }; - var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var result = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - - var returnValue = Assert.IsType(result.Value); - Assert.NotNull(returnValue); - Assert.Single(returnValue.MatchedContactDetails); - Assert.Empty(returnValue.UnmatchedNationalIdentityNumbers); - } - - [Fact] - public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = null }; - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var badRequestResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); - Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value); - } - - [Fact] - public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "30083542175" } }; - var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [], unmatchedNationalIdentityNumbers: ["30083542175"]); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var notFoundResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode); - } - - [Fact] - public async Task PostLookup_WhenNoMatchedContactDetailsAreFound_ReturnsNotFound() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "99020312345" } }; - var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [], unmatchedNationalIdentityNumbers: ["99020312345"]); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var notFoundResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode); - } - - [Fact] - public async Task PostLookup_WhenServiceCallIsLongRunning_DoesNotBlock() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "02038112735" } }; - var contactDetails = new ContactDetails - { - LanguageCode = "nb", - Reservation = false, - MobilePhoneNumber = "12345678", - EmailAddress = "test@example.com", - NationalIdentityNumber = "02038112735" - }; - var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(() => - { - Task.Delay(5000).Wait(); // Simulate long-running task - return lookupResult; - }); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var result = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - Assert.Equal(lookupResult, Assert.IsType(result.Value)); - } - - [Fact] - public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest() - { - // Arrange - var invalidRequest = new UserContactPointLookup { NationalIdentityNumbers = { "14078112078" } }; - _controller.ModelState.AddModelError("InvalidKey", "Invalid error message"); - - // Act - var response = await _controller.PostLookup(invalidRequest); - - // Assert - var badRequestResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); - } - - [Fact] - public async Task PostLookup_WithInvalidSingleNationalIdentityNumber_ReturnsBadRequest() - { - // Arrange - var invalidRequest = new UserContactPointLookup { NationalIdentityNumbers = { "invalid_format" } }; - - // Act - var response = await _controller.PostLookup(invalidRequest); - - // Assert - var notFoundResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode); - } - - [Fact] - public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "10060339738", "16051327393" } }; - var contactDetails = new ContactDetails - { - LanguageCode = "nb", - Reservation = false, - MobilePhoneNumber = "12345678", - EmailAddress = "test@example.com", - NationalIdentityNumber = "10060339738" - }; - var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: ["16051327393"]); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var result = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - var returnValue = Assert.IsType(result.Value); - Assert.Equal(lookupResult, returnValue); - Assert.Single(returnValue.MatchedContactDetails); - Assert.Single(returnValue.UnmatchedNationalIdentityNumbers); - } - - [Fact] - public async Task PostLookup_WithNoNationalIdentityNumbers_ReturnsBadRequest() - { - // Arrange - var invalidRequest = new UserContactPointLookup { NationalIdentityNumbers = { } }; - - // Act - var response = await _controller.PostLookup(invalidRequest); - - // Assert - var badRequestResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); - Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value); - } - - [Fact] - public async Task PostLookup_WithValidRequest_ReturnsOk() - { - // Arrange - var request = new UserContactPointLookup { NationalIdentityNumbers = { "27038893837" } }; - var contactDetails = new ContactDetails - { - LanguageCode = "nb", - Reservation = false, - MobilePhoneNumber = "12345678", - EmailAddress = "test@example.com", - NationalIdentityNumber = "27038893837" - }; - var lookupResult = new ContactDetailsLookupResult(matchedContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); - - // Act - var response = await _controller.PostLookup(request); - - // Assert - var result = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - var returnValue = Assert.IsType(result.Value); - Assert.Equal(lookupResult, returnValue); - } -} diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsInternalControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsInternalControllerTests.cs deleted file mode 100644 index b4490f8..0000000 --- a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/ContactDetailsInternalControllerTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -using Altinn.Profile.Controllers; -using Altinn.Profile.Models; -using Altinn.Profile.UseCases; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit; - -namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers; - -public class ContactDetailsInternalControllerTests -{ - private readonly ContactDetailsInternalController _controller; - private readonly Mock> _loggerMock; - private readonly Mock _mockContactDetailsRetriever; - - public ContactDetailsInternalControllerTests() - { - _loggerMock = new Mock>(); - _mockContactDetailsRetriever = new Mock(); - _controller = new ContactDetailsInternalController(_loggerMock.Object, _mockContactDetailsRetriever.Object); - } - - [Fact] - public void Constructor_WithNullContactDetailsRetriever_ThrowsArgumentNullException() - { - Assert.Throws(() => new ContactDetailsInternalController(_loggerMock.Object, null)); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - var contactDetailsRetrieverMock = new Mock(); - Assert.Throws(() => new ContactDetailsInternalController(null, contactDetailsRetrieverMock.Object)); - } - - [Fact] - public void Constructor_WithValidParameters_InitializesCorrectly() - { - var contactDetailsRetrieverMock = new Mock(); - var controller = new ContactDetailsInternalController(_loggerMock.Object, contactDetailsRetrieverMock.Object); - Assert.NotNull(controller); - } - - [Fact] - public async Task PostLookup_WhenRetrievalThrowsException_LogsErrorAndReturnsProblemResult() - { - var request = new UserContactPointLookup - { - NationalIdentityNumbers = ["05025308508"] - }; - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)) - .ThrowsAsync(new Exception("Some error occurred")); - - var response = await _controller.PostLookup(request); - - _loggerMock.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")), - It.IsAny(), - It.IsAny>()), - Times.Once); - - var problemResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode); - Assert.Equal("An unexpected error occurred.", Convert.ToString(((ProblemDetails)problemResult.Value).Detail)); - } - - [Fact] - public async Task PostLookup_WithEmptyNationalIdentityNumbers_ReturnsBadRequest() - { - var request = new UserContactPointLookup { NationalIdentityNumbers = [] }; - var response = await _controller.PostLookup(request); - AssertBadRequest(response, "National identity numbers cannot be null or empty."); - } - - [Fact] - public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest() - { - var request = new UserContactPointLookup - { - NationalIdentityNumbers = ["17092037169"] - }; - _controller.ModelState.AddModelError("TestError", "Invalid data model"); - - var response = await _controller.PostLookup(request); - AssertBadRequest(response); - } - - [Fact] - public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults() - { - var request = new UserContactPointLookup - { - NationalIdentityNumbers = ["05025308508", "08110270527"] - }; - - var contactDetails = new ContactDetails - { - LanguageCode = "nb", - Reservation = false, - MobilePhoneNumber = "12345678", - EmailAddress = "test@example.com", - NationalIdentityNumber = "05025308508" - }; - - var lookupResult = new ContactDetailsLookupResult( - matchedContactDetails: [contactDetails], - unmatchedNationalIdentityNumbers: ["08110270527"]); - - _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)) - .ReturnsAsync(lookupResult); - - var response = await _controller.PostLookup(request); - - var result = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status200OK, result.StatusCode); - - var returnValue = Assert.IsType(result.Value); - AssertContactDetailsLookupResult(lookupResult, returnValue); - } - - [Fact] - public async Task PostLookup_WithNullNationalIdentityNumbers_ReturnsBadRequest() - { - var request = new UserContactPointLookup { NationalIdentityNumbers = null }; - var response = await _controller.PostLookup(request); - AssertBadRequest(response, "National identity numbers cannot be null or empty."); - } - - private static void AssertBadRequest(ActionResult response, string expectedMessage = null) - { - var badRequestResult = Assert.IsType(response.Result); - Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); - if (expectedMessage != null) - { - Assert.Equal(expectedMessage, badRequestResult.Value); - } - } - - private static void AssertContactDetailsLookupResult(ContactDetailsLookupResult expected, ContactDetailsLookupResult actual) - { - Assert.Equal(expected, actual); - Assert.Single(actual.MatchedContactDetails); - Assert.Single(actual.UnmatchedNationalIdentityNumbers); - - var matchedContactDetails = actual.MatchedContactDetails.FirstOrDefault(); - Assert.NotNull(matchedContactDetails); - Assert.Equal(expected.MatchedContactDetails.First().Reservation, matchedContactDetails.Reservation); - Assert.Equal(expected.MatchedContactDetails.First().EmailAddress, matchedContactDetails.EmailAddress); - Assert.Equal(expected.MatchedContactDetails.First().LanguageCode, matchedContactDetails.LanguageCode); - Assert.Equal(expected.MatchedContactDetails.First().MobilePhoneNumber, matchedContactDetails.MobilePhoneNumber); - Assert.Equal(expected.MatchedContactDetails.First().NationalIdentityNumber, matchedContactDetails.NationalIdentityNumber); - - var unmatchedNationalIdentityNumber = actual.UnmatchedNationalIdentityNumbers.FirstOrDefault(); - Assert.Equal(expected.UnmatchedNationalIdentityNumbers.First(), unmatchedNationalIdentityNumber); - } -} diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsControllerTests.cs new file mode 100644 index 0000000..853a218 --- /dev/null +++ b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsControllerTests.cs @@ -0,0 +1,407 @@ +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.Threading.Tasks; + +using Altinn.Profile.Controllers; +using Altinn.Profile.Models; +using Altinn.Profile.Tests.IntegrationTests.Utils; +using Altinn.Profile.UseCases; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Logging; + +using Moq; + +using Xunit; + +namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers; + +public class PersonContactDetailsControllerTests +{ + private readonly JsonSerializerOptions _serializerOptions; + private readonly PersonContactDetailsController _controller; + private readonly Mock> _loggerMock; + private readonly Mock _mockContactDetailsRetriever; + private readonly WebApplicationFactorySetup _webApplicationFactorySetup; + + public PersonContactDetailsControllerTests() + { + _loggerMock = new Mock>(); + _mockContactDetailsRetriever = new Mock(); + _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + _controller = new PersonContactDetailsController(_loggerMock.Object, _mockContactDetailsRetriever.Object); + _webApplicationFactorySetup = new WebApplicationFactorySetup(new WebApplicationFactory()); + } + + [Fact] + public void Constructor_WithValidParameters_InitializesCorrectly() + { + // Arrange + var loggerMock = new Mock>(); + var contactDetailsRetrieverMock = new Mock(); + + // Act + var controller = new PersonContactDetailsController(loggerMock.Object, contactDetailsRetrieverMock.Object); + + // Assert + Assert.NotNull(controller); + } + + [Fact] + public async Task PostLookup_WhenExceptionOccurs_ReturnsInternalServerError_And_LogsError() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "27038893837" } }; + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ThrowsAsync(new Exception("Test exception")); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var problemResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")), + It.IsAny(), + It.IsAny>()), + Times.Once); + } + + [Fact] + public async Task PostLookup_WhenLookupCriteriaIsNull_ReturnsBadRequest() + { + // Arrange + UserContactDetailsLookupCriteria request = null; + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var badRequestResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); + Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value); + } + + [Fact] + public async Task PostLookup_WhenMatchedContactDetailsAreFound_ReturnsOk() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "05053423096" } }; + var contactDetails = new PersonContactDetails + { + LanguageCode = "en", + IsReserved = false, + MobilePhoneNumber = "98765432", + EmailAddress = "user@example.com", + NationalIdentityNumber = "05053423096" + }; + var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var result = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + + var returnValue = Assert.IsType(result.Value); + Assert.NotNull(returnValue); + Assert.Single(returnValue.MatchedPersonContactDetails); + Assert.Empty(returnValue.UnmatchedNationalIdentityNumbers); + } + + [Fact] + public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = null }; + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var badRequestResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); + Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value); + } + + [Fact] + public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "30083542175" } }; + var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [], unmatchedNationalIdentityNumbers: ["30083542175"]); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var notFoundResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode); + } + + [Fact] + public async Task PostLookup_WhenNoMatchedContactDetailsAreFound_ReturnsNotFound() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "99020312345" } }; + var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [], unmatchedNationalIdentityNumbers: ["99020312345"]); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var notFoundResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode); + } + + [Fact] + public async Task PostLookup_WhenServiceCallIsLongRunning_DoesNotBlock() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "02038112735" } }; + var contactDetails = new PersonContactDetails + { + LanguageCode = "nb", + IsReserved = false, + MobilePhoneNumber = "12345678", + EmailAddress = "test@example.com", + NationalIdentityNumber = "02038112735" + }; + var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(() => + { + Task.Delay(5000).Wait(); // Simulate long-running task + return lookupResult; + }); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var result = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + Assert.Equal(lookupResult, Assert.IsType(result.Value)); + } + + [Fact] + public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest() + { + // Arrange + var invalidRequest = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "14078112078" } }; + _controller.ModelState.AddModelError("InvalidKey", "Invalid error message"); + + // Act + var response = await _controller.PostLookup(invalidRequest); + + // Assert + var badRequestResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); + } + + [Fact] + public async Task PostLookup_WithInvalidSingleNationalIdentityNumber_ReturnsBadRequest() + { + // Arrange + var invalidRequest = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "invalid_format" } }; + + // Act + var response = await _controller.PostLookup(invalidRequest); + + // Assert + var notFoundResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status404NotFound, notFoundResult.StatusCode); + } + + [Fact] + public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "10060339738", "16051327393" } }; + var contactDetails = new PersonContactDetails + { + LanguageCode = "nb", + IsReserved = false, + MobilePhoneNumber = "12345678", + EmailAddress = "test@example.com", + NationalIdentityNumber = "10060339738" + }; + var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: ["16051327393"]); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var result = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + var returnValue = Assert.IsType(result.Value); + Assert.Equal(lookupResult, returnValue); + Assert.Single(returnValue.MatchedPersonContactDetails); + Assert.Single(returnValue.UnmatchedNationalIdentityNumbers); + } + + [Fact] + public async Task PostLookup_WithNoNationalIdentityNumbers_ReturnsBadRequest() + { + // Arrange + var invalidRequest = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { } }; + + // Act + var response = await _controller.PostLookup(invalidRequest); + + // Assert + var badRequestResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); + Assert.Equal("National identity numbers cannot be null or empty.", badRequestResult.Value); + } + + [Fact] + public async Task PostLookup_WithValidRequest_ReturnsOk() + { + // Arrange + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = { "27038893837" } }; + var contactDetails = new PersonContactDetails + { + LanguageCode = "nb", + IsReserved = false, + MobilePhoneNumber = "12345678", + EmailAddress = "test@example.com", + NationalIdentityNumber = "27038893837" + }; + var lookupResult = new PersonContactDetailsLookupResult(matchedPersonContactDetails: [contactDetails], unmatchedNationalIdentityNumbers: []); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)).ReturnsAsync(lookupResult); + + // Act + var response = await _controller.PostLookup(request); + + // Assert + var result = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + var returnValue = Assert.IsType(result.Value); + Assert.Equal(lookupResult, returnValue); + } + + ////[Fact] + ////public async Task PostLookup_WithValidNationalIdentityNumbers_ReturnsValidResults_IntegrationTest() + ////{ + //// // Arrange + //// HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + //// var lookupCriteria = new PersonContactDetailsLookupCriteria + //// { + //// NationalIdentityNumbers = ["02018090573", "03070100664", "03074500217"] + //// }; + + //// HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup"); + //// httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + //// // Act + //// HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + //// // Assert + //// Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + //// string responseContent = await response.Content.ReadAsStringAsync(); + //// PersonContactDetailsLookupResult lookupResult = JsonSerializer.Deserialize(responseContent, _serializerOptions); + + //// Assert.NotNull(lookupResult); + //// Assert.NotNull(lookupResult.MatchedPersonContactDetails); + //// Assert.Null(lookupResult.UnmatchedNationalIdentityNumbers); + //// Assert.Equal(3, lookupResult.MatchedPersonContactDetails.Count); + ////} + + [Fact] + public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest_IntegrationTest() + { + // Arrange + var client = _webApplicationFactorySetup.GetTestServerClient(); + var lookupCriteria = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = null + }; + HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup"); + httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound_IntegrationTest() + { + // Arrange + var client = _webApplicationFactorySetup.GetTestServerClient(); + var lookupCriteria = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = ["false"] + }; + HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup"); + httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + ////[Fact] + ////public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults_IntegrationTest() + ////{ + //// // Arrange + //// HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + //// var lookupCriteria = new PersonContactDetailsLookupCriteria + //// { + //// NationalIdentityNumbers = ["07875499461", "none", "07844998311"] + //// }; + + //// HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/person/contact/details/lookup"); + //// httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + //// // Act + //// HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + //// // Assert + //// Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + //// string responseContent = await response.Content.ReadAsStringAsync(); + //// PersonContactDetailsLookupResult lookupResult = JsonSerializer.Deserialize(responseContent, _serializerOptions); + + //// Assert.NotNull(lookupResult); + //// Assert.NotNull(lookupResult.MatchedPersonContactDetails); + //// Assert.Single(lookupResult.UnmatchedNationalIdentityNumbers); + //// Assert.NotNull(lookupResult.UnmatchedNationalIdentityNumbers); + //// Assert.Equal(2, lookupResult.MatchedPersonContactDetails.Count); + ////} + + private static HttpRequestMessage CreatePostRequest(string requestUri) + { + int userId = 2516356; + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, requestUri); + string token = PrincipalUtil.GetToken(userId); + httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + return httpRequestMessage; + } +} diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsInternalControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsInternalControllerTests.cs new file mode 100644 index 0000000..1f175d0 --- /dev/null +++ b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/PersonContactDetailsInternalControllerTests.cs @@ -0,0 +1,268 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; + +using Altinn.Profile.Controllers; +using Altinn.Profile.Models; +using Altinn.Profile.Tests.IntegrationTests.Utils; +using Altinn.Profile.UseCases; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Logging; + +using Moq; + +using Xunit; + +namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers; + +public class PersonContactDetailsInternalControllerTests +{ + private readonly JsonSerializerOptions _serializerOptions; + private readonly PersonContactDetailsInternalController _controller; + private readonly Mock> _loggerMock; + private readonly Mock _mockContactDetailsRetriever; + private readonly WebApplicationFactorySetup _webApplicationFactorySetup; + + public PersonContactDetailsInternalControllerTests() + { + _loggerMock = new Mock>(); + _mockContactDetailsRetriever = new Mock(); + _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + _controller = new PersonContactDetailsInternalController(_loggerMock.Object, _mockContactDetailsRetriever.Object); + _webApplicationFactorySetup = new WebApplicationFactorySetup(new WebApplicationFactory()); + } + + [Fact] + public void Constructor_WithValidParameters_InitializesCorrectly() + { + var contactDetailsRetrieverMock = new Mock(); + var controller = new PersonContactDetailsInternalController(_loggerMock.Object, contactDetailsRetrieverMock.Object); + Assert.NotNull(controller); + } + + [Fact] + public async Task PostLookup_WhenRetrievalThrowsException_LogsErrorAndReturnsProblemResult() + { + var request = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = ["05025308508"] + }; + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)) + .ThrowsAsync(new Exception("Some error occurred")); + + var response = await _controller.PostLookup(request); + + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString().Contains("An error occurred while retrieving contact details.")), + It.IsAny(), + It.IsAny>()), + Times.Once); + + var problemResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode); + Assert.Equal("An unexpected error occurred.", Convert.ToString(((ProblemDetails)problemResult.Value).Detail)); + } + + [Fact] + public async Task PostLookup_WithEmptyNationalIdentityNumbers_ReturnsBadRequest() + { + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = [] }; + var response = await _controller.PostLookup(request); + AssertBadRequest(response, "National identity numbers cannot be null or empty."); + } + + [Fact] + public async Task PostLookup_WithInvalidModelState_ReturnsBadRequest() + { + var request = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = ["17092037169"] + }; + _controller.ModelState.AddModelError("TestError", "Invalid data model"); + + var response = await _controller.PostLookup(request); + AssertBadRequest(response); + } + + [Fact] + public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults() + { + var request = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = ["05025308508", "08110270527"] + }; + + var contactDetails = new PersonContactDetails + { + LanguageCode = "nb", + IsReserved = false, + MobilePhoneNumber = "12345678", + EmailAddress = "test@example.com", + NationalIdentityNumber = "05025308508" + }; + + var lookupResult = new PersonContactDetailsLookupResult( + matchedPersonContactDetails: [contactDetails], + unmatchedNationalIdentityNumbers: ["08110270527"]); + + _mockContactDetailsRetriever.Setup(x => x.RetrieveAsync(request)) + .ReturnsAsync(lookupResult); + + var response = await _controller.PostLookup(request); + + var result = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + + var returnValue = Assert.IsType(result.Value); + AssertContactDetailsLookupResult(lookupResult, returnValue); + } + + [Fact] + public async Task PostLookup_WithNullNationalIdentityNumbers_ReturnsBadRequest() + { + var request = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = null }; + var response = await _controller.PostLookup(request); + AssertBadRequest(response, "National identity numbers cannot be null or empty."); + } + + ////[Fact] + ////public async Task PostLookup_WithValidNationalIdentityNumbers_ReturnsValidResults_IntegrationTest() + ////{ + //// // Arrange + //// HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + //// var lookupCriteria = new PersonContactDetailsLookupCriteria + //// { + //// NationalIdentityNumbers = ["02018090573", "03070100664", "03074500217"] + //// }; + + //// HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/internal/person/contact/details/lookup"); + //// httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + //// // Act + //// HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + //// // Assert + //// Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + //// string responseContent = await response.Content.ReadAsStringAsync(); + //// PersonContactDetailsLookupResult lookupResult = JsonSerializer.Deserialize(responseContent, _serializerOptions); + + //// Assert.NotNull(lookupResult); + //// Assert.NotNull(lookupResult.MatchedPersonContactDetails); + //// Assert.Null(lookupResult.UnmatchedNationalIdentityNumbers); + //// Assert.Equal(3, lookupResult.MatchedPersonContactDetails.Count); + ////} + + [Fact] + public async Task PostLookup_WhenNationalIdentityNumbersIsNull_ReturnsBadRequest_IntegrationTest() + { + // Arrange + var client = _webApplicationFactorySetup.GetTestServerClient(); + var lookupCriteria = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = null + }; + HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/internal/person/contact/details/lookup"); + httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task PostLookup_WhenNoContactDetailsFound_ReturnsNotFound_IntegrationTest() + { + // Arrange + var client = _webApplicationFactorySetup.GetTestServerClient(); + var lookupCriteria = new UserContactDetailsLookupCriteria + { + NationalIdentityNumbers = ["false"] + }; + HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/internal/person/contact/details/lookup"); + httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + // Act + HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + ////[Fact] + ////public async Task PostLookup_WithMixedNationalIdentityNumbers_ReturnsMixedResults_IntegrationTest() + ////{ + //// // Arrange + //// HttpClient client = _webApplicationFactorySetup.GetTestServerClient(); + //// var lookupCriteria = new PersonContactDetailsLookupCriteria + //// { + //// NationalIdentityNumbers = ["02018090573", "no match", "03074500217"] + //// }; + + //// HttpRequestMessage httpRequestMessage = CreatePostRequest("/profile/api/v1/internal/person/contact/details/lookup"); + //// httpRequestMessage.Content = JsonContent.Create(lookupCriteria); + + //// // Act + //// HttpResponseMessage response = await client.SendAsync(httpRequestMessage); + + //// // Assert + //// Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + //// string responseContent = await response.Content.ReadAsStringAsync(); + //// PersonContactDetailsLookupResult lookupResult = JsonSerializer.Deserialize(responseContent, _serializerOptions); + + //// Assert.NotNull(lookupResult); + //// Assert.NotNull(lookupResult.MatchedPersonContactDetails); + //// Assert.Single(lookupResult.UnmatchedNationalIdentityNumbers); + //// Assert.NotNull(lookupResult.UnmatchedNationalIdentityNumbers); + //// Assert.Equal(2, lookupResult.MatchedPersonContactDetails.Count); + ////} + + private static HttpRequestMessage CreatePostRequest(string requestUri) + { + HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, requestUri); + return httpRequestMessage; + } + + private static void AssertBadRequest(ActionResult response, string expectedMessage = null) + { + var badRequestResult = Assert.IsType(response.Result); + Assert.Equal(StatusCodes.Status400BadRequest, badRequestResult.StatusCode); + if (expectedMessage != null) + { + Assert.Equal(expectedMessage, badRequestResult.Value); + } + } + + private static void AssertContactDetailsLookupResult(PersonContactDetailsLookupResult expected, PersonContactDetailsLookupResult actual) + { + Assert.Equal(expected, actual); + Assert.Single(actual.MatchedPersonContactDetails); + Assert.Single(actual.UnmatchedNationalIdentityNumbers); + + var matchedContactDetails = actual.MatchedPersonContactDetails.FirstOrDefault(); + Assert.NotNull(matchedContactDetails); + Assert.Equal(expected.MatchedPersonContactDetails.First().IsReserved, matchedContactDetails.IsReserved); + Assert.Equal(expected.MatchedPersonContactDetails.First().EmailAddress, matchedContactDetails.EmailAddress); + Assert.Equal(expected.MatchedPersonContactDetails.First().LanguageCode, matchedContactDetails.LanguageCode); + Assert.Equal(expected.MatchedPersonContactDetails.First().MobilePhoneNumber, matchedContactDetails.MobilePhoneNumber); + Assert.Equal(expected.MatchedPersonContactDetails.First().NationalIdentityNumber, matchedContactDetails.NationalIdentityNumber); + + var unmatchedNationalIdentityNumber = actual.UnmatchedNationalIdentityNumbers.FirstOrDefault(); + Assert.Equal(expected.UnmatchedNationalIdentityNumbers.First(), unmatchedNationalIdentityNumber); + } +} diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/TriggerControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/TriggerControllerTests.cs new file mode 100644 index 0000000..8c08709 --- /dev/null +++ b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/TriggerControllerTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Immutable; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +using Altinn.Profile.Controllers; +using Altinn.Profile.Core.ContactRegister; +using Altinn.Profile.Core.Person.ContactPreferences; +using Altinn.Profile.Integrations.Services; +using Altinn.Profile.Tests.IntegrationTests.Utils; + +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Logging; + +using Moq; + +using Xunit; + +namespace Altinn.Profile.Tests.IntegrationTests.API.Controllers; + +public class TriggerControllerTests : IClassFixture> +{ + private readonly Mock _personServiceMock; + private readonly Mock> _loggerMock; + private readonly WebApplicationFactorySetup _webApplicationFactorySetup; + + public TriggerControllerTests(WebApplicationFactory factory) + { + _personServiceMock = new Mock(); + _loggerMock = new Mock>(); + _webApplicationFactorySetup = new WebApplicationFactorySetup(factory); + } + + [Fact] + public async Task SyncChanges_WhenCalled_ReturnsOk() + { + // Arrange + ContactRegisterChangesLog changeLog = new ContactRegisterChangesLog + { + ContactPreferencesSnapshots = ImmutableList.Create() + }; + _webApplicationFactorySetup.ContactRegisterServiceMock.Setup( + c => c.GetContactDetailsChangesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(changeLog); + + var client = _webApplicationFactorySetup.GetTestServerClient(); + + HttpRequestMessage httpRequestMessage = CreateGetRequest("/profile/api/v1/trigger/syncpersonchanges"); + + // Act + var response = await client.SendAsync(httpRequestMessage); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + private static HttpRequestMessage CreateGetRequest(string requestUri) + { + HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, requestUri); + + return httpRequestMessage; + } +} diff --git a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/UserContactPointControllerTests.cs b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/UserContactPointControllerTests.cs index 4fd4299..14f31d1 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/UserContactPointControllerTests.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/API/Controllers/UserContactPointControllerTests.cs @@ -47,7 +47,7 @@ public UserContactPointControllerTests(WebApplicationFactory() { "01025101037" } }; @@ -93,7 +93,7 @@ public async Task PostAvailabilityLookup_SingleUser_DetailsReturned() public async Task PostAvailabilityLookup_SingleProfileNotFoundInBridge_RemainingUsersReturned() { // Arrange - UserContactPointLookup input = new() + UserContactDetailsLookupCriteria input = new() { NationalIdentityNumbers = new List() { "01025101037", "99999999999" } }; @@ -118,7 +118,7 @@ public async Task PostAvailabilityLookup_SingleProfileNotFoundInBridge_Remaining public async Task PostLookup_NoNationalIdentityNumbers_EmptyListReturned() { // Arrange - UserContactPointLookup input = new() + UserContactDetailsLookupCriteria input = new() { NationalIdentityNumbers = new List() { } }; @@ -142,7 +142,7 @@ public async Task PostLookup_NoNationalIdentityNumbers_EmptyListReturned() public async Task PostLookup_SingleProfileNotFoundInBridge_RemainingUsersReturned() { // Arrange - UserContactPointLookup input = new() + UserContactDetailsLookupCriteria input = new() { NationalIdentityNumbers = new List() { "01025101037", "99999999999" } }; @@ -167,7 +167,7 @@ public async Task PostLookup_SingleProfileNotFoundInBridge_RemainingUsersReturne public async Task PostLookup_SingleUser_DetailsReturned() { // Arrange - UserContactPointLookup input = new() + UserContactDetailsLookupCriteria input = new() { NationalIdentityNumbers = new List() { "01025101037" } }; diff --git a/test/Altinn.Profile.Tests/IntegrationTests/Utils/WebApplicationFactorySetup.cs b/test/Altinn.Profile.Tests/IntegrationTests/Utils/WebApplicationFactorySetup.cs index 5331364..6ebd4ee 100644 --- a/test/Altinn.Profile.Tests/IntegrationTests/Utils/WebApplicationFactorySetup.cs +++ b/test/Altinn.Profile.Tests/IntegrationTests/Utils/WebApplicationFactorySetup.cs @@ -2,7 +2,10 @@ using System.Net.Http; using Altinn.Common.AccessToken.Services; +using Altinn.Profile.Core; +using Altinn.Profile.Core.ContactRegister; using Altinn.Profile.Core.Integrations; +using Altinn.Profile.Integrations.ContactRegister; using Altinn.Profile.Integrations.SblBridge; using Altinn.Profile.Integrations.SblBridge.Unit.Profile; using Altinn.Profile.Integrations.SblBridge.User.Profile; @@ -33,6 +36,8 @@ public WebApplicationFactorySetup(WebApplicationFactory webApplicationFactory _webApplicationFactory = webApplicationFactory; } + public Mock ContactRegisterServiceMock { get; set; } = new(); + public Mock> UserProfileClientLogger { get; set; } = new(); public Mock> UnitProfileClientLogger { get; set; } = new(); @@ -64,6 +69,9 @@ public HttpClient GetTestServerClient() services.AddSingleton(); services.AddSingleton(memoryCache); + // Using a mock to stop tests from calling the contact register service. + services.AddSingleton(ContactRegisterServiceMock.Object); + // Using the real/actual implementation of IUserProfileService, but with a mocked message handler. // Haven't found any other ways of injecting a mocked message handler to simulate SBL Bridge. services.AddSingleton( diff --git a/test/Altinn.Profile.Tests/Profile.Core/Extensions/StringExtensionsTests.cs b/test/Altinn.Profile.Tests/Profile.Core/Extensions/StringExtensionsTests.cs index 30a9c70..446483e 100644 --- a/test/Altinn.Profile.Tests/Profile.Core/Extensions/StringExtensionsTests.cs +++ b/test/Altinn.Profile.Tests/Profile.Core/Extensions/StringExtensionsTests.cs @@ -49,10 +49,10 @@ public void IsValidSocialSecurityNumber_CacheReturnsCachedValue() // Act // First check: This will validate the SSN and store the result in the cache. - var firstCheck = ssn.IsValidSocialSecurityNumber(); + var firstCheck = ssn.IsValidNationalIdentityNumber(); // Second check: This should return the cached result. - var secondCheck = ssn.IsValidSocialSecurityNumber(); + var secondCheck = ssn.IsValidNationalIdentityNumber(); // Assert Assert.Equal(expectedFirstValidation, firstCheck); // Verify first validation result @@ -66,8 +66,8 @@ public void IsValidSocialSecurityNumber_CachedResult_UsesCache() var ssn = "08119043698"; // Act - var firstCheck = ssn.IsValidSocialSecurityNumber(); // First call - var secondCheck = ssn.IsValidSocialSecurityNumber(); // Should be cached + var firstCheck = ssn.IsValidNationalIdentityNumber(); // First call + var secondCheck = ssn.IsValidNationalIdentityNumber(); // Should be cached // Assert Assert.True(firstCheck); @@ -81,38 +81,28 @@ public void IsValidSocialSecurityNumber_InvalidFormat_ReturnsFalse() var invalidSsn = "0811a043698"; // Act - var result = invalidSsn.IsValidSocialSecurityNumber(); + var result = invalidSsn.IsValidNationalIdentityNumber(); // Assert Assert.False(result); } [Theory] - [InlineData("08119", false)] - [InlineData("081190A3698", false)] - [InlineData("081190 3698", false)] - public void IsValidSocialSecurityNumber_InvalidIndividualNumberPart_ReturnsFalse(string socialSecurityNumber, bool expected) - { - // Act - var result = socialSecurityNumber.IsValidSocialSecurityNumber(); - - // Assert - Assert.Equal(expected, result); - } - - [Theory] - [InlineData(null, false)] [InlineData("", false)] + [InlineData(null, false)] [InlineData("12345", false)] - [InlineData("12345678900", false)] - [InlineData("98765432100", false)] + [InlineData("08119", false)] [InlineData("08119043698", true)] [InlineData("23017126016", true)] [InlineData("04045325356", true)] + [InlineData("081190A3698", false)] + [InlineData("081190 3698", false)] + [InlineData("12345678900", false)] + [InlineData("98765432100", false)] public void IsValidSocialSecurityNumber_ValidatesCorrectly(string socialSecurityNumber, bool expected) { // Act - var result = socialSecurityNumber.IsValidSocialSecurityNumber(); + var result = socialSecurityNumber.IsValidNationalIdentityNumber(); // Assert Assert.Equal(expected, result); diff --git a/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonRepositoryTests.cs b/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonRepositoryTests.cs index c1d1684..3eaac5e 100644 --- a/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonRepositoryTests.cs +++ b/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonRepositoryTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Altinn.Profile.Integrations.Entities; @@ -8,8 +9,12 @@ using Altinn.Profile.Integrations.Repositories; using Altinn.Profile.Tests.Testdata; +using AutoMapper; + using Microsoft.EntityFrameworkCore; +using Moq; + using Xunit; namespace Altinn.Profile.Tests.Profile.Integrations; @@ -19,62 +24,98 @@ namespace Altinn.Profile.Tests.Profile.Integrations; /// public class PersonRepositoryTests : IDisposable { - private readonly ProfileDbContext _context; - private readonly PersonRepository _registerRepository; + private bool _isDisposed; + private readonly Mock _mapperMock; + private readonly ProfileDbContext _databaseContext; + private readonly PersonRepository _personRepository; private readonly List _personContactAndReservationTestData; + private readonly Mock> _databaseContextFactory; public PersonRepositoryTests() { - var options = new DbContextOptionsBuilder() + _mapperMock = new Mock(); + + var databaseContextOptions = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; - _context = new ProfileDbContext(options); - _registerRepository = new PersonRepository(_context); + _databaseContextFactory = new Mock>(); + + _databaseContextFactory.Setup(f => f.CreateDbContext()) + .Returns(new ProfileDbContext(databaseContextOptions)); + + _databaseContextFactory.Setup(f => f.CreateDbContextAsync(It.IsAny())) + .ReturnsAsync(() => new ProfileDbContext(databaseContextOptions)); + + _personRepository = new PersonRepository(_mapperMock.Object, _databaseContextFactory.Object); _personContactAndReservationTestData = new List(PersonTestData.GetContactAndReservationTestData()); - _context.People.AddRange(_personContactAndReservationTestData); - _context.SaveChanges(); + _databaseContext = _databaseContextFactory.Object.CreateDbContext(); + _databaseContext.People.AddRange(_personContactAndReservationTestData); + _databaseContext.SaveChanges(); } public void Dispose() { - _context.Database.EnsureDeleted(); - _context.Dispose(); + Dispose(true); GC.SuppressFinalize(this); } + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _databaseContext.Database.EnsureDeleted(); + _databaseContext.Dispose(); + } + + _isDisposed = true; + } + } + [Fact] public async Task GetContactDetailsAsync_WhenFound_ReturnsContactInfo() { // Act - var result = await _registerRepository.GetContactDetailsAsync(["17111933790"]); + var matchedPerson = (await _personRepository.GetContactDetailsAsync(["17111933790"])) + .Match( + e => e.Find(p => p.FnumberAk == "17111933790"), + _ => null); - var actual = result.FirstOrDefault(e => e.FnumberAk == "17111933790"); - var expected = _personContactAndReservationTestData.FirstOrDefault(e => e.FnumberAk == "17111933790"); + var expectedPerson = _personContactAndReservationTestData + .Find(e => e.FnumberAk == "17111933790"); // Assert - Assert.NotNull(actual); - AssertRegisterProperties(expected, actual); + Assert.NotNull(matchedPerson); + AssertRegisterProperties(expectedPerson, matchedPerson); } [Fact] public async Task GetContactDetailsAsync_WhenMultipleContactsFound_ReturnsMultipleContacts() { // Act - var result = await _registerRepository.GetContactDetailsAsync(["24064316776", "11044314101"]); - var expected = _personContactAndReservationTestData - .Where(e => e.FnumberAk == "24064316776" || e.FnumberAk == "11044314101"); + var contactDetailsGetter = await _personRepository.GetContactDetailsAsync(["24064316776", "11044314101"]); + + var matchedPersons = contactDetailsGetter.Match( + e => e, + _ => Enumerable.Empty()); + + var expectedPersons = _personContactAndReservationTestData + .Where(e => e.FnumberAk == "24064316776" || e.FnumberAk == "11044314101") + .ToList(); // Assert - Assert.Equal(2, result.Count); + Assert.Equal(2, matchedPersons.Count()); - foreach (var register in result) + foreach (var person in matchedPersons) { - var foundRegister = expected.FirstOrDefault(r => r.FnumberAk == register.FnumberAk); - Assert.NotNull(foundRegister); - AssertRegisterProperties(register, foundRegister); + var expectedPerson = expectedPersons.Find(r => r.FnumberAk == person.FnumberAk); + + Assert.NotNull(expectedPerson); + AssertRegisterProperties(expectedPerson, person); } } @@ -82,42 +123,45 @@ public async Task GetContactDetailsAsync_WhenMultipleContactsFound_ReturnsMultip public async Task GetContactDetailsAsync_WhenNoNationalIdentityNumbersProvided_ReturnsEmpty() { // Act - var result = await _registerRepository.GetContactDetailsAsync([]); + var contactDetailsGetter = await _personRepository.GetContactDetailsAsync([]); + + var matchedPersons = contactDetailsGetter.Match( + e => e, + _ => Enumerable.Empty()); // Assert - Assert.Empty(result); + Assert.Empty(matchedPersons); } [Fact] public async Task GetContactDetailsAsync_WhenNoneFound_ReturnsEmpty() { // Act - var result = await _registerRepository.GetContactDetailsAsync(["nonexistent1", "nonexistent2"]); - - // Assert - Assert.Empty(result); - } + var contactDetailsGetter = await _personRepository.GetContactDetailsAsync(["nonexistent1", "nonexistent2"]); - [Fact] - public async Task GetContactDetailsAsync_WhenNotFound_ReturnsEmpty() - { - // Act - var result = await _registerRepository.GetContactDetailsAsync(["nonexistent", "11044314120"]); + var matchedPersons = contactDetailsGetter.Match( + e => e, + _ => Enumerable.Empty()); // Assert - Assert.Empty(result); + Assert.Empty(matchedPersons); } [Fact] public async Task GetContactDetailsAsync_WhenValidAndInvalidNumbers_ReturnsCorrectResults() { // Act - var result = _personContactAndReservationTestData.Where(e => e.FnumberAk == "28026698350"); - var expected = await _registerRepository.GetContactDetailsAsync(["28026698350", "nonexistent2"]); + var result = _personContactAndReservationTestData.Find(e => e.FnumberAk == "28026698350"); + + var contactDetailsGetter = await _personRepository.GetContactDetailsAsync(["28026698350", "nonexistent2"]); + + var matchedPersons = contactDetailsGetter.Match( + e => e, + _ => Enumerable.Empty()); // Assert invalid result - Assert.Single(result); - AssertRegisterProperties(expected.FirstOrDefault(), result.FirstOrDefault()); + Assert.Single(matchedPersons); + AssertRegisterProperties(matchedPersons.FirstOrDefault(), result); } private static void AssertRegisterProperties(Person expected, Person actual) diff --git a/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonServiceTests.cs b/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonServiceTests.cs index 292275b..d22c6b6 100644 --- a/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonServiceTests.cs +++ b/test/Altinn.Profile.Tests/Profile.Integrations/Person/PersonServiceTests.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Threading.Tasks; +using Altinn.Profile.Core.ContactRegister; +using Altinn.Profile.Core.Person.ContactPreferences; using Altinn.Profile.Integrations.Entities; using Altinn.Profile.Integrations.Repositories; using Altinn.Profile.Integrations.Services; @@ -21,68 +23,54 @@ namespace Altinn.Profile.Tests.Profile.Integrations; public class PersonServiceTests { private readonly Mock _mapperMock; + private readonly PersonService _personService; private readonly Mock _personRepositoryMock; + private readonly Mock _metadataRepositoryMock; + private readonly Mock _contactRegisterServiceMock; private readonly Mock _nationalIdentityNumberCheckerMock; - private readonly PersonService _personService; - public PersonServiceTests() { _mapperMock = new Mock(); _personRepositoryMock = new Mock(); + _metadataRepositoryMock = new Mock(); + _contactRegisterServiceMock = new Mock(); _nationalIdentityNumberCheckerMock = new Mock(); - _personService = new PersonService(_mapperMock.Object, _personRepositoryMock.Object, _nationalIdentityNumberCheckerMock.Object); + _personService = new PersonService(_mapperMock.Object, _personRepositoryMock.Object, _contactRegisterServiceMock.Object, _metadataRepositoryMock.Object, _nationalIdentityNumberCheckerMock.Object); } + /// + /// Tests that returns all contacts when all numbers are valid and all contacts are found. + /// [Fact] public async Task GetContactDetailsAsync_WhenAllNumbersValidAndAllContactsFound_ReturnsAllContacts() { // Arrange var nationalIdentityNumbers = new List { "17092037169", "17033112912" }; - var firstRandomPerson = new Person - { - Reservation = false, - LanguageCode = "nb", - FnumberAk = "17092037169", - MailboxAddress = "1234 Test St", - MobilePhoneNumber = "+4791234567", - EmailAddress = "test@example.com", - X509Certificate = "certificate_data" - }; + var firstRandomPerson = CreatePerson( + fnumberAk: "17092037169", + reservation: false, + emailAddress: "test@example.com", + languageCode: "nb", + mobilePhoneNumber: "+4791234567"); - var secondRandomPerson = new Person - { - Reservation = true, - LanguageCode = "nb", - FnumberAk = "17033112912", - MailboxAddress = "1234 Test St", - MobilePhoneNumber = "+4791234567", - EmailAddress = "test@example.com", - X509Certificate = "certificate_data" - }; + var secondRandomPerson = CreatePerson( + fnumberAk: "17033112912", + reservation: true, + emailAddress: "test@example.com", + languageCode: "nb", + mobilePhoneNumber: "+4791234567"); var personList = new List { firstRandomPerson, secondRandomPerson }.ToImmutableList(); - var firstMappedContactDetails = new Mock(); - firstMappedContactDetails.SetupGet(x => x.IsReserved).Returns(firstRandomPerson.Reservation); - firstMappedContactDetails.SetupGet(x => x.EmailAddress).Returns(firstRandomPerson.EmailAddress); - firstMappedContactDetails.SetupGet(x => x.LanguageCode).Returns(firstRandomPerson.LanguageCode); - firstMappedContactDetails.SetupGet(x => x.NationalIdentityNumber).Returns(firstRandomPerson.FnumberAk); - firstMappedContactDetails.SetupGet(x => x.MobilePhoneNumber).Returns(firstRandomPerson.MobilePhoneNumber); - - _mapperMock.Setup(x => x.Map(firstRandomPerson)) - .Returns(firstMappedContactDetails.Object); - - var secondMappedContactDetails = new Mock(); - secondMappedContactDetails.SetupGet(x => x.IsReserved).Returns(secondRandomPerson.Reservation); - secondMappedContactDetails.SetupGet(x => x.EmailAddress).Returns(secondRandomPerson.EmailAddress); - secondMappedContactDetails.SetupGet(x => x.LanguageCode).Returns(secondRandomPerson.LanguageCode); - secondMappedContactDetails.SetupGet(x => x.NationalIdentityNumber).Returns(secondRandomPerson.FnumberAk); - secondMappedContactDetails.SetupGet(x => x.MobilePhoneNumber).Returns(secondRandomPerson.MobilePhoneNumber); + var firstMappedContactDetails = CreatePersonContactPreferences(firstRandomPerson); + var secondMappedContactDetails = CreatePersonContactPreferences(secondRandomPerson); - _mapperMock.Setup(x => x.Map(secondRandomPerson)) - .Returns(secondMappedContactDetails.Object); + _mapperMock.Setup(x => x.Map(firstRandomPerson)) + .Returns(firstMappedContactDetails); + _mapperMock.Setup(x => x.Map(secondRandomPerson)) + .Returns(secondMappedContactDetails); _personRepositoryMock .Setup(x => x.GetContactDetailsAsync(nationalIdentityNumbers)) @@ -93,16 +81,16 @@ public async Task GetContactDetailsAsync_WhenAllNumbersValidAndAllContactsFound_ .Returns(nationalIdentityNumbers.ToImmutableList()); // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumbers); + var result = await _personService.GetContactPreferencesAsync(nationalIdentityNumbers); // Assert - IEnumerable? unmatchedNationalIdentityNumbers = []; - IEnumerable? matchedPersonContactDetails = []; + IEnumerable? unmatchedNationalIdentityNumbers = null; + IEnumerable? matchedPersonContactDetails = null; result.Match( success => { - matchedPersonContactDetails = success.MatchedPersonContactDetails; + matchedPersonContactDetails = success.MatchedPersonContactPreferences; unmatchedNationalIdentityNumbers = success.UnmatchedNationalIdentityNumbers; }, failure => @@ -110,30 +98,21 @@ public async Task GetContactDetailsAsync_WhenAllNumbersValidAndAllContactsFound_ matchedPersonContactDetails = null; }); + Assert.NotNull(matchedPersonContactDetails); Assert.Equal(2, matchedPersonContactDetails.Count()); - Assert.Contains(matchedPersonContactDetails, detail => detail == firstMappedContactDetails.Object); - var firstContactDetails = matchedPersonContactDetails.FirstOrDefault(detail => detail.NationalIdentityNumber == firstRandomPerson.FnumberAk); - - Assert.NotNull(firstContactDetails); - Assert.Equal(firstRandomPerson.Reservation, firstContactDetails.IsReserved); - Assert.Equal(firstRandomPerson.EmailAddress, firstContactDetails.EmailAddress); - Assert.Equal(firstRandomPerson.LanguageCode, firstContactDetails.LanguageCode); - Assert.Equal(firstRandomPerson.FnumberAk, firstContactDetails.NationalIdentityNumber); - Assert.Equal(firstRandomPerson.MobilePhoneNumber, firstContactDetails.MobilePhoneNumber); - - Assert.Contains(matchedPersonContactDetails, detail => detail == secondMappedContactDetails.Object); - var secondContactDetails = matchedPersonContactDetails.FirstOrDefault(detail => detail.NationalIdentityNumber == secondRandomPerson.FnumberAk); - Assert.NotNull(secondContactDetails); - Assert.Equal(secondRandomPerson.Reservation, secondContactDetails.IsReserved); - Assert.Equal(secondRandomPerson.EmailAddress, secondContactDetails.EmailAddress); - Assert.Equal(secondRandomPerson.LanguageCode, secondContactDetails.LanguageCode); - Assert.Equal(secondRandomPerson.FnumberAk, secondContactDetails.NationalIdentityNumber); - Assert.Equal(secondRandomPerson.MobilePhoneNumber, secondContactDetails.MobilePhoneNumber); - - Assert.Empty(unmatchedNationalIdentityNumbers); + // Validate first contact details + AssertContactDetails(firstMappedContactDetails, matchedPersonContactDetails); + + // Validate second contact details + AssertContactDetails(secondMappedContactDetails, matchedPersonContactDetails); + + Assert.Null(unmatchedNationalIdentityNumbers); } + /// + /// Tests that returns the correct result when multiple national identity numbers are provided. + /// [Fact] public async Task GetContactDetailsAsync_WhenMultipleNationalIdentityNumbersAreProvided_ReturnsCorrectResult() { @@ -141,49 +120,29 @@ public async Task GetContactDetailsAsync_WhenMultipleNationalIdentityNumbersAreP var validNationalIdentityNumbers = new List { "12028193007", "01091235338" }; var nationalIdentityNumbers = new List { "12028193007", "01091235338", "invalid_number" }; - var firstRandomPerson = new Person - { - Reservation = false, - LanguageCode = "nb", - FnumberAk = "12028193007", - MailboxAddress = "1234 Test St", - MobilePhoneNumber = "+4791234567", - EmailAddress = "test@example.com", - X509Certificate = "certificate_data" - }; + var firstRandomPerson = CreatePerson( + fnumberAk: "12028193007", + reservation: false, + emailAddress: "test@example.com", + languageCode: "nb", + mobilePhoneNumber: "+4791234567"); - var secondRandomPerson = new Person - { - Reservation = true, - LanguageCode = "nb", - FnumberAk = "01091235338", - MailboxAddress = "1234 Test St", - MobilePhoneNumber = "+4791234567", - EmailAddress = "test@example.com", - X509Certificate = "certificate_data" - }; + var secondRandomPerson = CreatePerson( + fnumberAk: "01091235338", + reservation: true, + emailAddress: "test@example.com", + languageCode: "nb", + mobilePhoneNumber: "+4791234567"); var personList = new List { firstRandomPerson, secondRandomPerson }.ToImmutableList(); - var firstMappedContactDetails = new Mock(); - firstMappedContactDetails.SetupGet(x => x.IsReserved).Returns(firstRandomPerson.Reservation); - firstMappedContactDetails.SetupGet(x => x.EmailAddress).Returns(firstRandomPerson.EmailAddress); - firstMappedContactDetails.SetupGet(x => x.LanguageCode).Returns(firstRandomPerson.LanguageCode); - firstMappedContactDetails.SetupGet(x => x.NationalIdentityNumber).Returns(firstRandomPerson.FnumberAk); - firstMappedContactDetails.SetupGet(x => x.MobilePhoneNumber).Returns(firstRandomPerson.MobilePhoneNumber); - - _mapperMock.Setup(x => x.Map(firstRandomPerson)) - .Returns(firstMappedContactDetails.Object); + var firstMappedContactDetails = CreatePersonContactPreferences(firstRandomPerson); + var secondMappedContactDetails = CreatePersonContactPreferences(secondRandomPerson); - var secondMappedContactDetails = new Mock(); - secondMappedContactDetails.SetupGet(x => x.IsReserved).Returns(secondRandomPerson.Reservation); - secondMappedContactDetails.SetupGet(x => x.EmailAddress).Returns(secondRandomPerson.EmailAddress); - secondMappedContactDetails.SetupGet(x => x.LanguageCode).Returns(secondRandomPerson.LanguageCode); - secondMappedContactDetails.SetupGet(x => x.NationalIdentityNumber).Returns(secondRandomPerson.FnumberAk); - secondMappedContactDetails.SetupGet(x => x.MobilePhoneNumber).Returns(secondRandomPerson.MobilePhoneNumber); - - _mapperMock.Setup(x => x.Map(secondRandomPerson)) - .Returns(secondMappedContactDetails.Object); + _mapperMock.Setup(x => x.Map(firstRandomPerson)) + .Returns(firstMappedContactDetails); + _mapperMock.Setup(x => x.Map(secondRandomPerson)) + .Returns(secondMappedContactDetails); _personRepositoryMock .Setup(x => x.GetContactDetailsAsync(validNationalIdentityNumbers)) @@ -194,16 +153,16 @@ public async Task GetContactDetailsAsync_WhenMultipleNationalIdentityNumbersAreP .Returns(validNationalIdentityNumbers.ToImmutableList()); // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumbers); + var result = await _personService.GetContactPreferencesAsync(nationalIdentityNumbers); // Assert - IEnumerable? unmatchedNationalIdentityNumbers = []; - IEnumerable? matchedPersonContactDetails = []; + IEnumerable? unmatchedNationalIdentityNumbers = null; + IEnumerable? matchedPersonContactDetails = null; result.Match( success => { - matchedPersonContactDetails = success.MatchedPersonContactDetails; + matchedPersonContactDetails = success.MatchedPersonContactPreferences; unmatchedNationalIdentityNumbers = success.UnmatchedNationalIdentityNumbers; }, failure => @@ -211,138 +170,98 @@ public async Task GetContactDetailsAsync_WhenMultipleNationalIdentityNumbersAreP matchedPersonContactDetails = null; }); + Assert.NotNull(matchedPersonContactDetails); Assert.Equal(2, matchedPersonContactDetails.Count()); - var firstContactDetails = matchedPersonContactDetails.FirstOrDefault(detail => detail.NationalIdentityNumber == firstRandomPerson.FnumberAk); - - Assert.NotNull(firstContactDetails); - Assert.Equal(firstRandomPerson.Reservation, firstContactDetails.IsReserved); - Assert.Equal(firstRandomPerson.EmailAddress, firstContactDetails.EmailAddress); - Assert.Equal(firstRandomPerson.LanguageCode, firstContactDetails.LanguageCode); - Assert.Equal(firstRandomPerson.FnumberAk, firstContactDetails.NationalIdentityNumber); - Assert.Equal(firstRandomPerson.MobilePhoneNumber, firstContactDetails.MobilePhoneNumber); + // Validate first contact details + AssertContactDetails(firstMappedContactDetails, matchedPersonContactDetails); - Assert.Contains(matchedPersonContactDetails, detail => detail == secondMappedContactDetails.Object); - var secondContactDetails = matchedPersonContactDetails.FirstOrDefault(detail => detail.NationalIdentityNumber == secondRandomPerson.FnumberAk); - Assert.NotNull(secondContactDetails); - Assert.Equal(secondRandomPerson.Reservation, secondContactDetails.IsReserved); - Assert.Equal(secondRandomPerson.EmailAddress, secondContactDetails.EmailAddress); - Assert.Equal(secondRandomPerson.LanguageCode, secondContactDetails.LanguageCode); - Assert.Equal(secondRandomPerson.FnumberAk, secondContactDetails.NationalIdentityNumber); - Assert.Equal(secondRandomPerson.MobilePhoneNumber, secondContactDetails.MobilePhoneNumber); + // Validate second contact details + AssertContactDetails(secondMappedContactDetails, matchedPersonContactDetails); + Assert.NotNull(unmatchedNationalIdentityNumbers); Assert.Single(unmatchedNationalIdentityNumbers); Assert.Contains("invalid_number", unmatchedNationalIdentityNumbers); } + /// + /// Tests that returns null when the national identity number is invalid. + /// [Fact] public async Task GetContactDetailsAsync_WhenNationalIdentityNumberIsInvalid_ReturnsNull() { // Arrange var nationalIdentityNumber = "invalid_number"; + var nationalIdentityNumbers = new List { nationalIdentityNumber }; _nationalIdentityNumberCheckerMock .Setup(x => x.IsValid(nationalIdentityNumber)) .Returns(false); // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumber); + var result = await _personService.GetContactPreferencesAsync(nationalIdentityNumbers); // Assert - Assert.Null(result); + IEnumerable? unmatchedNationalIdentityNumbers = null; + IEnumerable? matchedPersonContactDetails = null; + result.Match( + success => + { + matchedPersonContactDetails = success.MatchedPersonContactPreferences; + unmatchedNationalIdentityNumbers = success.UnmatchedNationalIdentityNumbers; + }, + failure => + { + matchedPersonContactDetails = null; + }); + + Assert.Null(matchedPersonContactDetails); + Assert.Null(unmatchedNationalIdentityNumbers); _personRepositoryMock.Verify(x => x.GetContactDetailsAsync(It.IsAny>()), Times.Never); } + /// + /// Tests that returns the contact details when the national identity number is valid. + /// [Fact] public async Task GetContactDetailsAsync_WhenNationalIdentityNumberIsValid_ReturnsContactDetails() { // Arrange - var nationalIdentityNumber = "23080188641"; - var randomPerson = new Person - { - Reservation = false, - LanguageCode = "nb", - MailboxAddress = "1234 Test St", - MobilePhoneNumber = "+4791234567", - EmailAddress = "test@example.com", - FnumberAk = nationalIdentityNumber, - X509Certificate = "certificate_data" - }; - var randomPersons = new List { randomPerson }.ToImmutableList(); + var nationalIdentityNumbers = new List { "12028193007" }; - var personContactDetails = new Mock(); - personContactDetails.SetupGet(x => x.IsReserved).Returns(randomPerson.Reservation); - personContactDetails.SetupGet(x => x.EmailAddress).Returns(randomPerson.EmailAddress); - personContactDetails.SetupGet(x => x.LanguageCode).Returns(randomPerson.LanguageCode); - personContactDetails.SetupGet(x => x.NationalIdentityNumber).Returns(nationalIdentityNumber); - personContactDetails.SetupGet(x => x.MobilePhoneNumber).Returns(randomPerson.MobilePhoneNumber); + var randomPerson = CreatePerson( + fnumberAk: "12028193007", + reservation: false, + emailAddress: "test@example.com", + languageCode: "nb", + mobilePhoneNumber: "+4791234567"); - _mapperMock - .Setup(x => x.Map(randomPerson)) - .Returns(personContactDetails.Object); + var personList = new List { randomPerson }.ToImmutableList(); - _personRepositoryMock - .Setup(x => x.GetContactDetailsAsync(It.Is>(e => e.Contains(nationalIdentityNumber)))) - .ReturnsAsync(randomPersons); + var mappedContactDetails = CreatePersonContactPreferences(randomPerson); - _nationalIdentityNumberCheckerMock - .Setup(x => x.IsValid(nationalIdentityNumber)) - .Returns(true); - - // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumber); - - // Assert - Assert.NotNull(result); - Assert.Equal(personContactDetails.Object.IsReserved, result.IsReserved); - Assert.Equal(personContactDetails.Object.EmailAddress, result.EmailAddress); - Assert.Equal(personContactDetails.Object.LanguageCode, result.LanguageCode); - Assert.Equal(personContactDetails.Object.MobilePhoneNumber, result.MobilePhoneNumber); - Assert.Equal(personContactDetails.Object.NationalIdentityNumber, result.NationalIdentityNumber); - } - - [Fact] - public async Task GetContactDetailsAsync_WhenNationalIdentityNumberIsValidAndNoContactFound_ReturnsNull() - { - // Arrange - var nationalIdentityNumber = "23080188641"; - - _nationalIdentityNumberCheckerMock - .Setup(x => x.IsValid(nationalIdentityNumber)) - .Returns(true); + _mapperMock.Setup(x => x.Map(randomPerson)) + .Returns(mappedContactDetails); _personRepositoryMock - .Setup(x => x.GetContactDetailsAsync(It.IsAny>())) - .ReturnsAsync([]); - - // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumber); - - // Assert - Assert.Null(result); - } - - [Fact] - public async Task GetContactDetailsAsync_WhenNoValidNumbersProvided_ReturnsEmptyResult() - { - // Arrange - var nationalIdentityNumbers = new List { "invalid1", "invalid2" }; + .Setup(x => x.GetContactDetailsAsync(nationalIdentityNumbers)) + .ReturnsAsync(personList); _nationalIdentityNumberCheckerMock .Setup(x => x.GetValid(nationalIdentityNumbers)) .Returns(nationalIdentityNumbers.ToImmutableList()); // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumbers); + var result = await _personService.GetContactPreferencesAsync(nationalIdentityNumbers); // Assert - IEnumerable? unmatchedNationalIdentityNumbers = []; - IEnumerable? matchedPersonContactDetails = []; + IEnumerable? unmatchedNationalIdentityNumbers = null; + IEnumerable? matchedPersonContactDetails = null; result.Match( success => { - matchedPersonContactDetails = success.MatchedPersonContactDetails; + matchedPersonContactDetails = success.MatchedPersonContactPreferences; unmatchedNationalIdentityNumbers = success.UnmatchedNationalIdentityNumbers; }, failure => @@ -351,25 +270,68 @@ public async Task GetContactDetailsAsync_WhenNoValidNumbersProvided_ReturnsEmpty }); Assert.NotNull(matchedPersonContactDetails); - Assert.Empty(matchedPersonContactDetails); + Assert.Single(matchedPersonContactDetails); - Assert.NotNull(unmatchedNationalIdentityNumbers); - Assert.Equal(2, unmatchedNationalIdentityNumbers.Count()); + // Validate contact details + AssertContactDetails(mappedContactDetails, matchedPersonContactDetails); + + Assert.Null(unmatchedNationalIdentityNumbers); } - [Fact] - public async Task GetContactDetailsAsync_WhenRepositoryThrowsException_HandlesGracefully() + /// + /// Asserts that the contact details match the expected values. + /// + /// The expected contact preferences. + /// The actual contact details. + private static void AssertContactDetails(PersonContactPreferences expected, IEnumerable actualContactDetails) { - // Arrange - var nationalIdentityNumber = "26050711071"; - _personRepositoryMock - .Setup(repo => repo.GetByIdAsync(It.IsAny())) - .ThrowsAsync(new Exception("Repository failure")); + var contactDetails = actualContactDetails.FirstOrDefault(detail => detail.NationalIdentityNumber == expected.NationalIdentityNumber); + + Assert.NotNull(contactDetails); + Assert.Equal(expected.Email, contactDetails.Email); + Assert.Equal(expected.IsReserved, contactDetails.IsReserved); + Assert.Equal(expected.MobileNumber, contactDetails.MobileNumber); + Assert.Equal(expected.LanguageCode, contactDetails.LanguageCode); + Assert.Equal(expected.NationalIdentityNumber, contactDetails.NationalIdentityNumber); + } - // Act - var result = await _personService.GetContactDetailsAsync(nationalIdentityNumber); + /// + /// Creates a new instance of . + /// + /// The national identity number. + /// The reservation status. + /// The email address. + /// The language code. + /// The mobile phone number. + /// A new instance of . + private static Person CreatePerson(string fnumberAk, bool reservation, string emailAddress, string languageCode, string mobilePhoneNumber) + { + return new Person + { + FnumberAk = fnumberAk, + Reservation = reservation, + EmailAddress = emailAddress, + LanguageCode = languageCode, + MobilePhoneNumber = mobilePhoneNumber, + MailboxAddress = "1234 Test St", + X509Certificate = "certificate_data" + }; + } - // Assert - Assert.Null(result); + /// + /// Creates a new instance of . + /// + /// The person entity. + /// A new instance of . + private static PersonContactPreferences CreatePersonContactPreferences(Person person) + { + return new PersonContactPreferences + { + Email = person.EmailAddress, + IsReserved = person.Reservation, + LanguageCode = person.LanguageCode, + MobileNumber = person.MobilePhoneNumber, + NationalIdentityNumber = person.FnumberAk + }; } } diff --git a/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactDetailsProfileTests.cs b/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactDetailsProfileTests.cs deleted file mode 100644 index d9bc8ce..0000000 --- a/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactDetailsProfileTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Altinn.Profile.Integrations.Entities; -using Altinn.Profile.Integrations.Mappings; - -using AutoMapper; - -using Xunit; - -namespace Altinn.Profile.Tests.Profile.Integrations; - -public class PersonContactDetailsProfileTests -{ - private readonly IMapper _mapper; - - public PersonContactDetailsProfileTests() - { - var config = new MapperConfiguration(cfg => cfg.AddProfile()); - _mapper = config.CreateMapper(); - } - - [Fact] - public void Map_DifferentValues_CreatesCorrectMappings() - { - // Arrange - var person = new Person - { - Reservation = true, - LanguageCode = "no", - FnumberAk = "24021633239", - MobilePhoneNumber = "9876543210", - EmailAddress = "test@example.com", - }; - - // Act - var result = _mapper.Map(person); - - // Assert - Assert.True(result.IsReserved); - Assert.Equal(person.EmailAddress, result.EmailAddress); - Assert.Equal(person.LanguageCode, result.LanguageCode); - Assert.Equal(person.FnumberAk, result.NationalIdentityNumber); - Assert.Equal(person.MobilePhoneNumber, result.MobilePhoneNumber); - } - - [Fact] - public void Map_NullPerson_ReturnsNullPersonContactDetails() - { - // Arrange - Person person = null; - - // Act - var result = _mapper.Map(person); - - // Assert - Assert.Null(result); - } - - [Fact] - public void Map_OptionalProperties_WhenMissing_ReturnsDefaults() - { - // Arrange - var person = new Person - { - Reservation = false, - FnumberAk = "06082705358" - }; - - // Act - var result = _mapper.Map(person); - - // Assert - Assert.Null(result.LanguageCode); - Assert.Null(result.EmailAddress); - Assert.Null(result.MobilePhoneNumber); - Assert.Equal("06082705358", result.NationalIdentityNumber); - } - - [Fact] - public void Map_ReservationFalse_SetsIsReservedToFalse() - { - // Arrange - var person = new Person { Reservation = false }; - - // Act - var result = _mapper.Map(person); - - // Assert - Assert.False(result.IsReserved); - } - - [Fact] - public void Map_ReservationTrue_SetsIsReservedToTrue() - { - // Arrange - var person = new Person { Reservation = true }; - - // Act - var result = _mapper.Map(person); - - // Assert - Assert.True(result.IsReserved); - } - - [Fact] - public void Map_ValidPerson_ReturnsCorrectPersonContactDetails() - { - // Arrange - var person = new Person - { - Reservation = false, - LanguageCode = "en", - FnumberAk = "17080227000", - MobilePhoneNumber = "1234567890", - EmailAddress = "test@example.com" - }; - - // Act - var result = _mapper.Map(person); - - // Assert - Assert.NotNull(result); - Assert.False(result.IsReserved); - Assert.Equal(person.EmailAddress, result.EmailAddress); - Assert.Equal(person.LanguageCode, result.LanguageCode); - Assert.Equal(person.FnumberAk, result.NationalIdentityNumber); - Assert.Equal(person.MobilePhoneNumber, result.MobilePhoneNumber); - } -} diff --git a/test/Altinn.Profile.Tests/Profile.Integrations/UserContactDetailsRetrieverTests.cs b/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactDetailsRetrieverTests.cs similarity index 67% rename from test/Altinn.Profile.Tests/Profile.Integrations/UserContactDetailsRetrieverTests.cs rename to test/Altinn.Profile.Tests/Profile.Integrations/PersonContactDetailsRetrieverTests.cs index 9b52d21..20316a7 100644 --- a/test/Altinn.Profile.Tests/Profile.Integrations/UserContactDetailsRetrieverTests.cs +++ b/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactDetailsRetrieverTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; +using Altinn.Profile.Core.Person.ContactPreferences; using Altinn.Profile.Integrations.Entities; using Altinn.Profile.Integrations.Services; using Altinn.Profile.Models; @@ -16,15 +17,15 @@ namespace Altinn.Profile.Tests.Profile.Integrations; -public class UserContactDetailsRetrieverTests +public class PersonContactDetailsRetrieverTests { - private readonly ContactDetailsRetriever _retriever; + private readonly PersonContactDetailsRetriever _retriever; private readonly Mock _mockPersonService; - public UserContactDetailsRetrieverTests() + public PersonContactDetailsRetrieverTests() { _mockPersonService = new Mock(); - _retriever = new ContactDetailsRetriever(_mockPersonService.Object); + _retriever = new PersonContactDetailsRetriever(_mockPersonService.Object); } [Fact] @@ -38,7 +39,7 @@ public async Task RetrieveAsync_WhenLookupCriteriaIsNull_ThrowsArgumentNullExcep public async Task RetrieveAsync_WhenNationalIdentityNumbersIsEmpty_ReturnsFalse() { // Arrange - var lookupCriteria = new UserContactPointLookup { NationalIdentityNumbers = [] }; + var lookupCriteria = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = [] }; // Act var result = await _retriever.RetrieveAsync(lookupCriteria); @@ -52,12 +53,12 @@ public async Task RetrieveAsync_WhenNationalIdentityNumbersIsEmpty_ReturnsFalse( public async Task RetrieveAsync_WhenNoContactDetailsFound_ReturnsFalse() { // Arrange - var lookupCriteria = new UserContactPointLookup + var lookupCriteria = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = ["08119043698"] }; - _mockPersonService.Setup(s => s.GetContactDetailsAsync(lookupCriteria.NationalIdentityNumbers)).ReturnsAsync(false); + _mockPersonService.Setup(s => s.GetContactPreferencesAsync(lookupCriteria.NationalIdentityNumbers)).ReturnsAsync(false); // Act var result = await _retriever.RetrieveAsync(lookupCriteria); @@ -71,28 +72,28 @@ public async Task RetrieveAsync_WhenNoContactDetailsFound_ReturnsFalse() public async Task RetrieveAsync_WhenValidNationalIdentityNumbers_ReturnsExpectedContactDetailsLookupResult() { // Arrange - var lookupCriteria = new UserContactPointLookup + var lookupCriteria = new UserContactDetailsLookupCriteria { NationalIdentityNumbers = ["08053414843"] }; - var personContactDetails = new PersonContactDetails + var personContactDetails = new PersonContactPreferences { IsReserved = false, LanguageCode = "en", - MobilePhoneNumber = "1234567890", - EmailAddress = "test@example.com", + MobileNumber = "1234567890", + Email = "test@example.com", NationalIdentityNumber = "08053414843" }; - var lookupResult = new PersonContactDetailsLookupResult + var lookupResult = new PersonContactPreferencesLookupResult { UnmatchedNationalIdentityNumbers = [], - MatchedPersonContactDetails = [personContactDetails] + MatchedPersonContactPreferences = [personContactDetails] }; _mockPersonService - .Setup(e => e.GetContactDetailsAsync(lookupCriteria.NationalIdentityNumbers)) + .Setup(e => e.GetContactPreferencesAsync(lookupCriteria.NationalIdentityNumbers)) .ReturnsAsync(lookupResult); // Act @@ -101,12 +102,12 @@ public async Task RetrieveAsync_WhenValidNationalIdentityNumbers_ReturnsExpected // Assert Assert.True(result.IsSuccess); IEnumerable? unmatchedNationalIdentityNumbers = []; - IEnumerable? matchedPersonContactDetails = []; + IEnumerable? matchedPersonContactDetails = []; result.Match( success => { - matchedPersonContactDetails = success.MatchedContactDetails; + matchedPersonContactDetails = success.MatchedPersonContactDetails; unmatchedNationalIdentityNumbers = success.UnmatchedNationalIdentityNumbers; }, failure => @@ -119,10 +120,10 @@ public async Task RetrieveAsync_WhenValidNationalIdentityNumbers_ReturnsExpected var matchPersonContactDetails = matchedPersonContactDetails.FirstOrDefault(); Assert.NotNull(matchPersonContactDetails); - Assert.Equal(personContactDetails.IsReserved, matchPersonContactDetails.Reservation); + Assert.Equal(personContactDetails.IsReserved, matchPersonContactDetails.IsReserved); Assert.Equal(personContactDetails.LanguageCode, matchPersonContactDetails.LanguageCode); - Assert.Equal(personContactDetails.MobilePhoneNumber, matchPersonContactDetails.MobilePhoneNumber); - Assert.Equal(personContactDetails.EmailAddress, matchPersonContactDetails.EmailAddress); + Assert.Equal(personContactDetails.MobileNumber, matchPersonContactDetails.MobilePhoneNumber); + Assert.Equal(personContactDetails.Email, matchPersonContactDetails.EmailAddress); Assert.Equal(personContactDetails.NationalIdentityNumber, matchPersonContactDetails.NationalIdentityNumber); Assert.Empty(unmatchedNationalIdentityNumbers); diff --git a/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactPreferencesProfileTests.cs b/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactPreferencesProfileTests.cs new file mode 100644 index 0000000..559f8fb --- /dev/null +++ b/test/Altinn.Profile.Tests/Profile.Integrations/PersonContactPreferencesProfileTests.cs @@ -0,0 +1,127 @@ +////using Altinn.Profile.Integrations.Entities; +////using Altinn.Profile.Integrations.Mappings; + +////using AutoMapper; + +////using Xunit; + +////namespace Altinn.Profile.Tests.Profile.Integrations; + +////public class PersonContactPreferencesProfileTests +////{ +//// private readonly IMapper _mapper; + +//// public PersonContactPreferencesProfileTests() +//// { +//// var config = new MapperConfiguration(cfg => cfg.AddProfile()); +//// _mapper = config.CreateMapper(); +//// } + +//// [Fact] +//// public void Map_DifferentValues_CreatesCorrectMappings() +//// { +//// // Arrange +//// var person = new Person +//// { +//// Reservation = true, +//// LanguageCode = "no", +//// FnumberAk = "24021633239", +//// MobilePhoneNumber = "9876543210", +//// EmailAddress = "test@example.com", +//// }; + +//// // Act +//// var result = _mapper.Map(person); + +//// // Assert +//// Assert.True(result.IsReserved); +//// Assert.Equal(person.EmailAddress, result.Email); +//// Assert.Equal(person.LanguageCode, result.LanguageCode); +//// Assert.Equal(person.FnumberAk, result.NationalIdentityNumber); +//// Assert.Equal(person.MobilePhoneNumber, result.MobileNumber); +//// } + +//// [Fact] +//// public void Map_NullPerson_ReturnsNullPersonContactDetails() +//// { +//// // Arrange +//// Person person = null; + +//// // Act +//// var result = _mapper.Map(person); + +//// // Assert +//// Assert.Null(result); +//// } + +//// [Fact] +//// public void Map_OptionalProperties_WhenMissing_ReturnsDefaults() +//// { +//// // Arrange +//// var person = new Person +//// { +//// Reservation = false, +//// FnumberAk = "06082705358" +//// }; + +//// // Act +//// var result = _mapper.Map(person); + +//// // Assert +//// Assert.Null(result.LanguageCode); +//// Assert.Null(result.Email); +//// Assert.Null(result.MobileNumber); +//// Assert.Equal("06082705358", result.NationalIdentityNumber); +//// } + +//// [Fact] +//// public void Map_ReservationFalse_SetsIsReservedToFalse() +//// { +//// // Arrange +//// var person = new Person { Reservation = false }; + +//// // Act +//// var result = _mapper.Map(person); + +//// // Assert +//// Assert.False(result.IsReserved); +//// } + +//// [Fact] +//// public void Map_ReservationTrue_SetsIsReservedToTrue() +//// { +//// // Arrange +//// var person = new Person { Reservation = true }; + +//// // Act +//// var result = _mapper.Map(person); + +//// // Assert +//// Assert.True(result.IsReserved); +//// } + +//// [Fact] +//// public void Map_ValidPerson_ReturnsCorrectPersonContactDetails() +//// { +//// // Arrange +//// var person = new Person +//// { +//// Reservation = false, +//// LanguageCode = "en", +//// FnumberAk = "17080227000", +//// MobilePhoneNumber = "1234567890", +//// EmailAddress = "test@example.com" +//// }; + +//// // Act +//// var result = _mapper.Map(person); + +//// // Assert +//// Assert.NotNull(result); +//// Assert.False(result.IsReserved); +//// Assert.Equal(person.EmailAddress, result.Email); +//// Assert.Equal(person.LanguageCode, result.LanguageCode); +//// Assert.Equal(person.FnumberAk, result.NationalIdentityNumber); +//// Assert.Equal(person.MobilePhoneNumber, result.MobileNumber); +//// } +////} diff --git a/test/Altinn.Profile.Tests/appsettings.test.json b/test/Altinn.Profile.Tests/appsettings.test.json index e6e525c..8af95af 100644 --- a/test/Altinn.Profile.Tests/appsettings.test.json +++ b/test/Altinn.Profile.Tests/appsettings.test.json @@ -1,5 +1,6 @@ { "PostgreSqlSettings": { - "EnableDBConnection": false + "MigrationScriptPath": "../../../../../src/Altinn.Profile.Integrations/Migration", + "EnableDBConnection": true } } diff --git a/test/Altinn.Profile.Tests/xunit.runner.json b/test/Altinn.Profile.Tests/xunit.runner.json new file mode 100644 index 0000000..015166b --- /dev/null +++ b/test/Altinn.Profile.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} \ No newline at end of file