Skip to content

Commit

Permalink
API endpoint for getting UserProfile by username
Browse files Browse the repository at this point in the history
#104
Adds an API endpoint in altinn-profile:
profile/api/v1/users/?username={username}

which consumes the newly added SBL bridge API:
/sblbridge/profile/api/users/?username={username}
  • Loading branch information
Jon Kjetil Øye committed Oct 31, 2023
1 parent 7413143 commit 87ff2de
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 2 deletions.
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 @@ public async Task<UserProfile> GetUser(string ssn)

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": "test@test.test",
"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 _));
}
}
}

0 comments on commit 87ff2de

Please sign in to comment.