Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API endpoint for getting UserProfile by username #105

Merged
merged 5 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Altinn.Profile/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,25 @@ public async Task<ActionResult<UserProfile>> GetUserFromSSN([FromBody] string ss

return Ok(result);
}

/// <summary>
/// Gets the user profile for a given username
/// </summary>
/// <param name="username">The user's username</param>
/// <returns>User profile connected to given username</returns>
[HttpGet]
[Authorize(Policy = "PlatformAccess")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserProfile>> GetUserFromUsername([FromQuery] string username)
{
UserProfile result = await _userProfilesWrapper.GetUserByUsername(username);
if (result == null)
{
return NotFound();
}

return Ok(result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,25 @@ public async Task<UserProfile> GetUser(string ssn)

return user;
}

/// <inheritdoc/>
public async Task<UserProfile> GetUserByUsername(string username)
{
string uniqueCacheKey = "User_Username_" + username;

if (_memoryCache.TryGetValue(uniqueCacheKey, out UserProfile user))
{
return user;
}

user = await _decoratedService.GetUserByUsername(username);

if (user != null)
{
_memoryCache.Set(uniqueCacheKey, user, _cacheOptions);
}

return user;
}
}
}
23 changes: 21 additions & 2 deletions src/Altinn.Profile/Services/Implementation/UserProfilesWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
using Altinn.Platform.Profile.Models;
using Altinn.Profile.Configuration;
using Altinn.Profile.Services.Interfaces;

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -89,5 +87,26 @@

return user;
}

/// <inheritdoc />
public async Task<UserProfile> GetUserByUsername(string username)
{
UserProfile user;

Uri endpointUrl = new Uri($"{_generalSettings.BridgeApiEndpoint}users/?username={username}");

HttpResponseMessage response = await _client.GetAsync(endpointUrl);

if (!response.IsSuccessStatusCode)
{
_logger.LogError("Getting user {username} failed with {statusCode}", username, response.StatusCode);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.
return null;
}

string content = await response.Content.ReadAsStringAsync();
user = JsonSerializer.Deserialize<UserProfile>(content, _serializerOptions);

return user;
}
}
}
7 changes: 7 additions & 0 deletions src/Altinn.Profile/Services/Interfaces/IUserProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,12 @@ public interface IUserProfiles
/// <param name="ssn">The user's ssn.</param>
/// <returns>User profile connected to given ssn.</returns>
Task<UserProfile> GetUser(string ssn);

/// <summary>
/// Method that fetches a user based on username.
/// </summary>
/// <param name="username">The user's username.</param>
/// <returns>User profile connected to given username.</returns>
Task<UserProfile> GetUserByUsername(string username);
}
}
111 changes: 111 additions & 0 deletions test/Altinn.Profile.Tests/IntegrationTests/UserProfileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,117 @@ public async Task GetUsersBySsn_SblBridgeReturnsUnavailable_RespondsNotFound()
Assert.Equal("\"01017512345\"", requestContent);
}

[Fact]
public async Task GetUsersByUsername_SblBridgeFindsProfile_ResponseOk_ReturnsUserProfile()
{
// Arrange
const string Username = "OrstaECUser";
const int UserId = 2001072;

HttpRequestMessage sblRequest = null;
DelegatingHandlerStub messageHandler = new(async (request, token) =>
{
sblRequest = request;

UserProfile userProfile = await TestDataLoader.Load<UserProfile>(Username);
return new HttpResponseMessage() { Content = JsonContent.Create(userProfile) };
});
_webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler;

HttpRequestMessage httpRequestMessage = CreateGetRequest(UserId, $"/profile/api/v1/users/?username={Username}");

httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "unittest"));

HttpClient client = _webApplicationFactorySetup.GetTestServerClient();

// Act
HttpResponseMessage response = await client.SendAsync(httpRequestMessage);

// Assert
Assert.NotNull(sblRequest);
Assert.Equal(HttpMethod.Get, sblRequest.Method);
Assert.EndsWith($"users/?username={Username}", sblRequest.RequestUri.ToString());

string responseContent = await response.Content.ReadAsStringAsync();

UserProfile actualUser = JsonSerializer.Deserialize<UserProfile>(
responseContent, serializerOptionsCamelCase);

// These asserts check that deserializing with camel casing was successful.
Assert.Equal(UserId, actualUser.UserId);
Assert.Equal(Username, actualUser.UserName);
Assert.Equal(50005545, actualUser.Party.PartyId);
Assert.Equal("ØRSTA OG HEGGEDAL ", actualUser.Party.Name);
Assert.Equal("ØRSTA OG HEGGEDAL", actualUser.Party.Organization.Name);
Assert.Equal("nb", actualUser.ProfileSettingPreference.Language);
}

[Fact]
public async Task GetUsersByUsername_SblBridgeReturnsNotFound_ResponseNotFound()
{
// Arrange
const string Username = "NonExistingUsername";
const int UserId = 2222222;

HttpRequestMessage sblRequest = null;
DelegatingHandlerStub messageHandler = new(async (request, token) =>
{
sblRequest = request;

return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.NotFound });
});
_webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler;

HttpRequestMessage httpRequestMessage = CreateGetRequest(UserId, $"/profile/api/v1/users/?username={Username}");

httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "unittest"));

HttpClient client = _webApplicationFactorySetup.GetTestServerClient();

// Act
HttpResponseMessage response = await client.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);

Assert.NotNull(sblRequest);
Assert.Equal(HttpMethod.Get, sblRequest.Method);
Assert.EndsWith($"users/?username={Username}", sblRequest.RequestUri.ToString());
}

[Fact]
public async Task GetUsersByUsername_SblBridgeReturnsUnavailable_ResponseNotFound()
{
// Arrange
const string Username = "OrstaECUser";
const int UserId = 2222222;

HttpRequestMessage sblRequest = null;
DelegatingHandlerStub messageHandler = new(async (request, token) =>
{
sblRequest = request;

return await Task.FromResult(new HttpResponseMessage() { StatusCode = HttpStatusCode.ServiceUnavailable });
});
_webApplicationFactorySetup.SblBridgeHttpMessageHandler = messageHandler;

HttpRequestMessage httpRequestMessage = CreateGetRequest(UserId, $"/profile/api/v1/users/?username={Username}");

httpRequestMessage.Headers.Add("PlatformAccessToken", PrincipalUtil.GetAccessToken("ttd", "unittest"));

HttpClient client = _webApplicationFactorySetup.GetTestServerClient();

// Act
HttpResponseMessage response = await client.SendAsync(httpRequestMessage);

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);

Assert.NotNull(sblRequest);
Assert.Equal(HttpMethod.Get, sblRequest.Method);
Assert.EndsWith($"users/?username={Username}", sblRequest.RequestUri.ToString());
}

private static HttpRequestMessage CreateGetRequest(int userId, string requestUri)
{
HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, requestUri);
Expand Down
43 changes: 43 additions & 0 deletions test/Altinn.Profile.Tests/Testdata/UserProfile/OrstaECUser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"UserId": 2001072,
"UserType": 3,
"UserName": "OrstaECUser",
"ExternalIdentity": "",
"PhoneNumber": null,
"Email": null,
"PartyId": 50005545,
"Party": {
"PartyTypeName": 2,
"SSN": "",
"OrgNumber": "910459880",
"Person": null,
"Organization": {
"OrgNumber": "910459880",
"Name": "ØRSTA OG HEGGEDAL",
"UnitType": "AS",
"TelephoneNumber": "12345678",
"MobileNumber": "99999999",
"FaxNumber": "12345679",
"EMailAddress": "[email protected]",
"InternetAddress": null,
"MailingAddress": null,
"MailingPostalCode": "",
"MailingPostalCity": "",
"BusinessAddress": null,
"BusinessPostalCode": "",
"BusinessPostalCity": "",
"UnitStatus": "N"
},
"PartyId": 50005545,
"UnitType": "AS",
"Name": "ØRSTA OG HEGGEDAL ",
"IsDeleted": false,
"OnlyHierarchyElementWithNoAccess": false,
"ChildParties": null
},
"ProfileSettingPreference": {
"Language": "nb",
"PreSelectedPartyId": 0,
"DoNotPromptForParty": false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,79 @@ public async Task GetUserUserSSN_NullFromDecoratedService_CacheNotPopulated()
Assert.Null(actual);
Assert.False(memoryCache.TryGetValue("User_UserId_2001607", out UserProfile _));
}

/// <summary>
/// Tests that the userprofile available in the cache is returned to the caller without forwarding request to decorated service.
/// </summary>
[Fact]
public async Task GetUserByUsername_UserInCache_decoratedServiceNotCalled()
{
// Arrange
const string Username = "OrstaECUser";
const int UserId = 2001072;
MemoryCache memoryCache = new(new MemoryCacheOptions());

var userProfile = await TestDataLoader.Load<UserProfile>(Username);
memoryCache.Set("User_Username_OrstaECUser", userProfile);
var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object);

// Act
UserProfile actual = await target.GetUserByUsername(Username);

// Assert
_decoratedServiceMock.Verify(service => service.GetUser(It.IsAny<int>()), Times.Never());
Assert.NotNull(actual);
Assert.Equal(UserId, actual.UserId);
Assert.Equal(Username, actual.UserName);
}

/// <summary>
/// Tests that when the userprofile is not available in the cache, the request is forwarded to the decorated service.
/// </summary>
[Fact]
public async Task GetUserByUsername_UserNotInCache_decoratedServiceCalledMockPopulated()
{
// Arrange
const string Username = "OrstaECUser";
const int UserId = 2001072;
MemoryCache memoryCache = new(new MemoryCacheOptions());

var userProfile = await TestDataLoader.Load<UserProfile>(Username);
_decoratedServiceMock.Setup(service => service.GetUserByUsername(Username)).ReturnsAsync(userProfile);
var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object);

// Act
UserProfile actual = await target.GetUserByUsername(Username);

// Assert
_decoratedServiceMock.Verify(service => service.GetUserByUsername(Username), Times.Once());

Assert.NotNull(actual);
Assert.Equal(UserId, actual.UserId);
Assert.Equal(Username, actual.UserName);
Assert.True(memoryCache.TryGetValue("User_Username_OrstaECUser", out UserProfile _));
}

/// <summary>
/// Tests that if the result from decorated service is null, nothing is stored in cache and the null object returned to caller.
/// </summary>
[Fact]
public async Task GetUserByUsername_NullFromDecoratedService_CacheNotPopulated()
{
// Arrange
const string Username = "NonExistingUsername";
MemoryCache memoryCache = new(new MemoryCacheOptions());

_decoratedServiceMock.Setup(service => service.GetUserByUsername(Username)).ReturnsAsync((UserProfile)null);
var target = new UserProfileCachingDecorator(_decoratedServiceMock.Object, memoryCache, generalSettingsOptions.Object);

// Act
UserProfile actual = await target.GetUserByUsername(Username);

// Assert
_decoratedServiceMock.Verify(service => service.GetUserByUsername(Username), Times.Once());
Assert.Null(actual);
Assert.False(memoryCache.TryGetValue("User_Username_NonExistingUsername", out UserProfile _));
}
}
}
Loading