Skip to content

Commit 469c239

Browse files
allinoxVedeler
andauthored
Toggle visibility of deleted units in AccountSelector (#1837)
* testing with new functionality * use local state for saving showDeleted state * add feature toggle and tests * rabbit qa * fix components buttons --------- Co-authored-by: Vedeler <acn-avede@ai-dev.no>
1 parent 3d68e3b commit 469c239

33 files changed

Lines changed: 374 additions & 49 deletions

File tree

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/ClientInterfaces/IProfileClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ public interface IProfileClient
1919
/// <returns>users profile settings</returns>
2020
Task<UserProfile> GetUserProfile(Guid uuid);
2121

22+
/// <summary>
23+
/// Updates the current user's profile settings in altinn profile
24+
/// </summary>
25+
/// <param name="settingsChange">the settings to be changed. Can be the full object or a partial one</param>
26+
/// <returns>users profile settings</returns>
27+
Task<ProfileSettingPreference> PatchCurrentUserProfileSetting(ProfileSettingPreference settingsChange);
28+
2229
/// <summary>
2330
/// Gets the organization's notification addresses from altinn profile
2431
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Configuration/FeatureFlags.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public class FeatureFlags
6161
/// </summary>
6262
public bool DisplayRequestsPage { get; set; }
6363

64+
/// <summary>
65+
/// Whether to display the deleted account toggle feature
66+
/// </summary>
67+
public bool DisplayDeletedAccountToggle { get; set; }
68+
6469
/// <summary>
6570
/// Whether to display the PRIV delegation feature
6671
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Extensions/HttpClientExtension.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ public static Task<HttpResponseMessage> PutAsync(this HttpClient httpClient, str
4949
return httpClient.SendAsync(request, CancellationToken.None);
5050
}
5151

52+
/// <summary>
53+
/// Extension that add authorization header to request.
54+
/// </summary>
55+
/// <param name="httpClient">The HttpClient.</param>
56+
/// <param name="authorizationToken">the authorization token (jwt).</param>
57+
/// <param name="requestUri">The request Uri.</param>
58+
/// <param name="content">The http content.</param>
59+
/// <param name="platformAccessToken">The platformAccess tokens.</param>
60+
/// <returns>A HttpResponseMessage.</returns>
61+
public static Task<HttpResponseMessage> PatchAsync(this HttpClient httpClient, string authorizationToken, string requestUri, HttpContent content, string platformAccessToken = null)
62+
{
63+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Patch, requestUri);
64+
request.Headers.Add("Authorization", "Bearer " + authorizationToken);
65+
request.Content = content;
66+
if (!string.IsNullOrEmpty(platformAccessToken))
67+
{
68+
request.Headers.Add("PlatformAccessToken", platformAccessToken);
69+
}
70+
71+
return httpClient.SendAsync(request, CancellationToken.None);
72+
}
73+
5274
/// <summary>
5375
/// Extension that add authorization header to request.
5476
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Models/Profile/ProfileSettingPreference.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
namespace Altinn.AccessManagement.UI.Core.Models.Profile
1+
#nullable enable
2+
namespace Altinn.AccessManagement.UI.Core.Models.Profile
23
{
34
/// <summary>
4-
/// Class describing a users profile setting preferences.
5+
/// Class describing a users profile setting preferences. This is lifted from the Profile API, but modified to have nullable properties
6+
/// to allow for partial updates.
57
/// </summary>
68
public class ProfileSettingPreference
79
{
@@ -12,7 +14,7 @@ public class ProfileSettingPreference
1214
"Design",
1315
"S2376:Write-only properties should not be used",
1416
Justification = "Write-only alias used to support incoming JSON 'languageType' while avoiding duplicate serialization output. Value is stored in Language.")]
15-
public string LanguageType
17+
public string? LanguageType
1618
{
1719
set
1820
{
@@ -23,21 +25,21 @@ public string LanguageType
2325
/// <summary>
2426
/// Gets or sets the user's language preference in Altinn.
2527
/// </summary>
26-
public string Language { get; set; }
28+
public string? Language { get; set; }
2729

2830
/// <summary>
2931
/// Gets or sets the user's preselected party.
3032
/// </summary>
3133
/// <remarks>
3234
/// This is being phased out in favor of PreselectedPartyUuid.
3335
/// </remarks>
34-
public int PreSelectedPartyId { get; set; }
36+
public int? PreSelectedPartyId { get; set; }
3537

3638
/// <summary>
3739
/// Gets or sets a value indicating whether the users want
3840
/// to be asked for the party on every form submission.
3941
/// </summary>
40-
public bool DoNotPromptForParty { get; set; }
42+
public bool? DoNotPromptForParty { get; set; }
4143

4244
/// <summary>
4345
/// The UUID of the preselected party. Optional.
@@ -47,7 +49,7 @@ public string LanguageType
4749
/// <summary>
4850
/// Indicates whether client units should be shown.
4951
/// </summary>
50-
public bool ShowClientUnits { get; set; }
52+
public bool? ShowClientUnits { get; set; }
5153

5254
/// <summary>
5355
/// Indicates whether sub-entities should be shown.
@@ -57,6 +59,8 @@ public string LanguageType
5759
/// <summary>
5860
/// Indicates whether deleted entities should be shown.
5961
/// </summary>
60-
public bool ShouldShowDeletedEntities { get; set; }
62+
public bool? ShouldShowDeletedEntities { get; set; }
6163
}
6264
}
65+
66+
#nullable restore

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Services/Interfaces/IUserService.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Altinn.AccessManagement.UI.Core.Models;
22
using Altinn.AccessManagement.UI.Core.Models.AccessManagement;
3+
using Altinn.AccessManagement.UI.Core.Models.Profile;
34
using Altinn.AccessManagement.UI.Core.Models.User;
45

56
namespace Altinn.AccessManagement.UI.Core.Services.Interfaces
@@ -16,6 +17,13 @@ public interface IUserService
1617
/// <returns>users preferred settings</returns>
1718
Task<UserProfileFE> GetUserProfile(int userId);
1819

20+
/// <summary>
21+
/// Updates the current user's profile settings in altinn profile
22+
/// </summary>
23+
/// <param name="shouldShowDeletedEntities">The new value of the field in the profile settings</param>
24+
/// <returns>users new profile preferences</returns>
25+
Task<ProfileSettingPreference> SetShowDeletedProfileSetting(bool shouldShowDeletedEntities);
26+
1927
/// <summary>
2028
/// Get the reportees for the user
2129
/// </summary>

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Core/Services/UserService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ public async Task<UserProfileFE> GetUserProfile(int userId)
4848
return userProfile == null ? null : new UserProfileFE(userProfile);
4949
}
5050

51+
/// <inheritdoc/>
52+
public async Task<ProfileSettingPreference> SetShowDeletedProfileSetting(bool shouldShowDeletedEntities)
53+
{
54+
ProfileSettingPreference change = new ProfileSettingPreference
55+
{
56+
ShouldShowDeletedEntities = shouldShowDeletedEntities
57+
};
58+
return await _profileClient.PatchCurrentUserProfileSetting(change);
59+
}
60+
5161
/// <inheritdoc/>
5262
public async Task<AuthorizedParty> GetPartyFromReporteeListIfExists(int partyId)
5363
{

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Integration/Clients/ProfileClient.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Text;
1+
using System;
2+
using System.Text;
23
using System.Text.Json;
34
using System.Text.Json.Serialization;
45
using Altinn.AccessManagement.UI.Core.ClientInterfaces;
@@ -86,6 +87,30 @@ public async Task<UserProfile> GetUserProfile(Guid uuid)
8687
return userProfile;
8788
}
8889

90+
/// <inheritdoc/>
91+
public async Task<ProfileSettingPreference> PatchCurrentUserProfileSetting(ProfileSettingPreference settingsChange)
92+
{
93+
try
94+
{
95+
string endpointUrl = $"users/current/profilesettings";
96+
string token = AltinnCore.Authentication.Utils.JwtTokenUtil.GetTokenFromContext(_httpContextAccessor.HttpContext, _platformSettings.JwtCookieName);
97+
var accessToken = await _accessTokenProvider.GetAccessToken();
98+
99+
StringContent requestBody = new StringContent(JsonSerializer.Serialize(settingsChange, _serializerOptions), Encoding.UTF8, "application/json");
100+
101+
HttpResponseMessage response = await _client.PatchAsync(token, endpointUrl, requestBody, accessToken);
102+
103+
var resString = await response.Content.ReadAsStringAsync();
104+
ProfileSettingPreference newSetting = await ClientUtils.DeserializeIfSuccessfullStatusCode<ProfileSettingPreference>(response);
105+
return newSetting;
106+
}
107+
catch (Exception ex)
108+
{
109+
_logger.LogError(ex, "AccessManagement.UI // ProfileClient // PatchCurrentUserProfileSetting // Exception");
110+
throw;
111+
}
112+
}
113+
89114
/// <inheritdoc/>
90115
public async Task<List<NotificationAddressResponse>> GetOrgNotificationAddresses(string orgNumber)
91116
{

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Mocks/Mocks/ProfileClientMock.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,68 @@ public Task<UserProfile> GetUserProfile(Guid uuid)
6262
return Task.FromResult<UserProfile>(null);
6363
}
6464

65+
/// <inheritdoc />
66+
public async Task<ProfileSettingPreference> PatchCurrentUserProfileSetting(ProfileSettingPreference settingsChange)
67+
{
68+
// Check authentication context for special test scenarios
69+
var httpContext = _httpContextAccessor?.HttpContext;
70+
if (httpContext?.User?.Identity?.IsAuthenticated == true)
71+
{
72+
var userId = AuthenticationHelper.GetUserId(_httpContextAccessor.HttpContext);
73+
// Special test scenario for 500 - internal server error
74+
if (userId == 500)
75+
{
76+
throw new HttpRequestException("Internal server error");
77+
}
78+
}
79+
80+
// static userId for testing
81+
var userUuid = new Guid("167536b5-f8ed-4c5a-8f48-0279507e53ae");
82+
83+
string path = GetDataPathForProfiles();
84+
if (File.Exists(path))
85+
{
86+
string content = File.ReadAllText(path);
87+
List<UserProfile> allProfiles = (List<UserProfile>)JsonSerializer.Deserialize(content, typeof(List<UserProfile>), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
88+
89+
var userProfile = allProfiles.FirstOrDefault(p => p.UserUuid == userUuid);
90+
if (userProfile?.ProfileSettingPreference != null)
91+
{
92+
// Create a copy of the existing ProfileSettingPreference
93+
var result = new ProfileSettingPreference
94+
{
95+
Language = userProfile.ProfileSettingPreference.Language,
96+
PreSelectedPartyId = userProfile.ProfileSettingPreference.PreSelectedPartyId,
97+
DoNotPromptForParty = userProfile.ProfileSettingPreference.DoNotPromptForParty,
98+
PreselectedPartyUuid = userProfile.ProfileSettingPreference.PreselectedPartyUuid,
99+
ShowClientUnits = userProfile.ProfileSettingPreference.ShowClientUnits,
100+
ShouldShowSubEntities = userProfile.ProfileSettingPreference.ShouldShowSubEntities,
101+
ShouldShowDeletedEntities = userProfile.ProfileSettingPreference.ShouldShowDeletedEntities
102+
};
103+
104+
// Apply changes (overwrite existing values)
105+
if (settingsChange.Language != null)
106+
result.Language = settingsChange.Language;
107+
if (settingsChange.PreSelectedPartyId.HasValue)
108+
result.PreSelectedPartyId = settingsChange.PreSelectedPartyId;
109+
if (settingsChange.DoNotPromptForParty.HasValue)
110+
result.DoNotPromptForParty = settingsChange.DoNotPromptForParty;
111+
if (settingsChange.PreselectedPartyUuid.HasValue)
112+
result.PreselectedPartyUuid = settingsChange.PreselectedPartyUuid;
113+
if (settingsChange.ShowClientUnits.HasValue)
114+
result.ShowClientUnits = settingsChange.ShowClientUnits;
115+
result.ShouldShowSubEntities = settingsChange.ShouldShowSubEntities; // ShouldShowSubEntities is not nullable, so always apply
116+
if (settingsChange.ShouldShowDeletedEntities.HasValue)
117+
result.ShouldShowDeletedEntities = settingsChange.ShouldShowDeletedEntities;
118+
119+
return await Task.FromResult(result);
120+
}
121+
}
122+
123+
// Return the settingsChange as-is if no existing profile found
124+
return await Task.FromResult(settingsChange);
125+
}
126+
65127
/// <inheritdoc/>
66128
public async Task<List<NotificationAddressResponse>> GetOrgNotificationAddresses(string orgNumber)
67129
{

backend/src/Altinn.AccessManagement.UI/Altinn.AccessManagement.UI.Tests/Controllers/UserControllerTest.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,109 @@ public void IsHovedadmin_NoHasRequestedPermissionItem_ReturnsFalse()
876876
var ok = Assert.IsType<OkObjectResult>(result.Result);
877877
Assert.False((bool)ok.Value);
878878
}
879+
880+
/// <summary>
881+
/// Test case: UpdateShouldShowDeletedPreference successfully updates preference to true
882+
/// Expected: Returns OK with updated ProfileSettingPreference
883+
/// </summary>
884+
[Fact]
885+
public async Task UpdateShouldShowDeletedPreference_SetToTrue_ReturnsUpdatedPreference()
886+
{
887+
// Arrange
888+
const int userId = 20004938;
889+
var token = PrincipalUtil.GetToken(userId, 1234, 2);
890+
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
891+
bool shouldShowDeleted = true;
892+
893+
// Act
894+
var response = await _client.PutAsJsonAsync("accessmanagement/api/v1/user/profile/settingspreferences/showdeleted", shouldShowDeleted);
895+
var actualResponse = await response.Content.ReadFromJsonAsync<ProfileSettingPreference>();
896+
897+
// Assert
898+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
899+
Assert.NotNull(actualResponse);
900+
Assert.True(actualResponse.ShouldShowDeletedEntities);
901+
}
902+
903+
/// <summary>
904+
/// Test case: UpdateShouldShowDeletedPreference successfully updates preference to false
905+
/// Expected: Returns OK with updated ProfileSettingPreference
906+
/// </summary>
907+
[Fact]
908+
public async Task UpdateShouldShowDeletedPreference_SetToFalse_ReturnsUpdatedPreference()
909+
{
910+
// Arrange
911+
const int userId = 20004938;
912+
var token = PrincipalUtil.GetToken(userId, 1234, 2);
913+
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
914+
bool shouldShowDeleted = false;
915+
916+
// Act
917+
var response = await _client.PutAsJsonAsync("accessmanagement/api/v1/user/profile/settingspreferences/showdeleted", shouldShowDeleted);
918+
var actualResponse = await response.Content.ReadFromJsonAsync<ProfileSettingPreference>();
919+
920+
// Assert
921+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
922+
Assert.NotNull(actualResponse);
923+
Assert.False(actualResponse.ShouldShowDeletedEntities);
924+
}
925+
926+
/// <summary>
927+
/// Test case: UpdateShouldShowDeletedPreference returns BadRequest when model state is invalid
928+
/// Expected: Returns BadRequest
929+
/// </summary>
930+
[Fact]
931+
public async Task UpdateShouldShowDeletedPreference_InvalidModelState_ReturnsBadRequest()
932+
{
933+
// Arrange
934+
const int userId = 20004938;
935+
var token = PrincipalUtil.GetToken(userId, 1234, 2);
936+
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
937+
938+
// Act - Send invalid JSON to trigger model state error
939+
var content = new StringContent("invalid-json", System.Text.Encoding.UTF8, "application/json");
940+
var response = await _client.PutAsync("accessmanagement/api/v1/user/profile/settingspreferences/showdeleted", content);
941+
942+
// Assert
943+
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
944+
}
945+
946+
/// <summary>
947+
/// Test case: UpdateShouldShowDeletedPreference returns InternalServerError when service throws exception
948+
/// Expected: Returns 500 Internal Server Error
949+
/// </summary>
950+
[Fact]
951+
public async Task UpdateShouldShowDeletedPreference_ServiceThrowsException_Returns500()
952+
{
953+
// Arrange
954+
const int userId = 500;
955+
var token = PrincipalUtil.GetToken(userId, 1234, 2);
956+
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
957+
bool shouldShowDeleted = true;
958+
959+
// Act
960+
var response = await _client.PutAsJsonAsync("accessmanagement/api/v1/user/profile/settingspreferences/showdeleted", shouldShowDeleted);
961+
962+
// Assert
963+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
964+
}
965+
966+
/// <summary>
967+
/// Test case: UpdateShouldShowDeletedPreference requires authentication
968+
/// Expected: Returns Unauthorized when no token is provided
969+
/// </summary>
970+
[Fact]
971+
public async Task UpdateShouldShowDeletedPreference_NoAuthentication_ReturnsUnauthorized()
972+
{
973+
// Arrange
974+
_client.DefaultRequestHeaders.Authorization = null;
975+
bool shouldShowDeleted = true;
976+
977+
// Act
978+
var response = await _client.PutAsJsonAsync("accessmanagement/api/v1/user/profile/settingspreferences/showdeleted", shouldShowDeleted);
979+
980+
// Assert
981+
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
982+
}
879983
}
880984
}

0 commit comments

Comments
 (0)