Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions core/Extensions/DependencyInjectionExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static IServiceCollection AddDependencyInjection(this IServiceCollection
services.AddTransient<IImageService, ImageService>();
services.AddTransient<ISubscriptionService, SubscriptionService>();
services.AddTransient<INotificationService, NotificationService>();
services.AddTransient<IIdentityProviderService, AuthentikService>();

// Utils
services.AddTransient<IJwtUtils, JwtUtils>();
Expand Down
26 changes: 26 additions & 0 deletions core/Services/Abstractions/IIdentityProviderService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace api.core.Services.Abstractions;

/// <summary>
/// Interface to communicate with any ID Provider
/// </summary>
public interface IIdentityProviderService
{
/// <summary>
/// Fetches User's information from the ID Provider
/// </summary>
/// <param name="accessHeader">Header used to call the endpoint</param>
/// <returns>If result is null, UserInfo endpoint cannot be accessed</returns>
UserInfoDto? GetUserInfo(string accessHeader);
}

public class UserInfoDto
{
public string Sub { get; set; } = null!;
public string Email { get; set; } = null!;
public bool EmailVerified { get; set; }
public string Name { get; set; } = null!;
public string GivenName { get; set; } = null!;
public string PreferedUsername { get; set; } = null!;
public string Nickname { get; set; } = null!;
public List<string> Groups { get; set; } = null!;
}
2 changes: 1 addition & 1 deletion core/Services/Abstractions/IUserService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.Tracing;
using System.Diagnostics.Tracing;

using api.core.Data.Enums;
using api.core.Data.requests;
Expand Down
32 changes: 32 additions & 0 deletions core/Services/AuthentikService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using api.core.Services.Abstractions;

using System.Text.Json;

namespace api.core.Services;

/// <summary>
/// Service used to communicate with Authentik ID Provider
/// </summary>
public class AuthentikService : IIdentityProviderService
{
public UserInfoDto? GetUserInfo(string accessHeader)
{
using HttpClient client = new HttpClient()
{
BaseAddress = new Uri(Environment.GetEnvironmentVariable("OPENID_BASE_URL"))
};

using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "userinfo/");
request.Headers.Add("Authorization", accessHeader);

// TODO : Changer pour un comportement asynchrone
var response = client.SendAsync(request).Result;

if (!response.IsSuccessStatusCode)
{
return null;
}

return response.Content.ReadFromJsonAsync<UserInfoDto>(new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }).Result;
}
}
46 changes: 35 additions & 11 deletions core/Services/UserService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using api.core.data.entities;
using api.core.data.entities;
using api.core.Data.Exceptions;
using api.core.Data.Enums;
using api.core.Data.requests;
Expand All @@ -12,6 +12,7 @@
using api.core.Misc;
using api.core.Repositories.Abstractions;
using api.core.Data.Entities;
using api.core.Services.Abstractions;

namespace api.core.Services;

Expand All @@ -21,7 +22,8 @@ public class UserService(
ITagRepository tagRepository,
IActivityAreaRepository activityAreaRepository,
IImageService imageService,
IJwtUtils jwtUtils) : IUserService
IJwtUtils jwtUtils,
IIdentityProviderService identityProvider) : IUserService
{
private const string AVATAR_FILE_NAME = "avatar.webp";

Expand Down Expand Up @@ -57,16 +59,11 @@ public UserResponseDTO AddOrganizer(string id, UserCreateDTO organizerDto)
public UserResponseDTO GetUser(string authHeader)
{
string userId = jwtUtils.GetUserIdFromAuthHeader(authHeader);
UserResponseDTO? userRes = null;
var organizer = userRepository.GetOrganizer(userId);
if (organizer != null)
userRes = UserResponseDTO.Map(organizer!);
var user = userRepository.Get(userId);

var moderator = userRepository.GetModerator(userId);
if (moderator != null)
userRes = UserResponseDTO.Map(moderator!);

if (userRes == null) throw new Exception("No users associated with this ID");
UserResponseDTO? userRes = user == null ?
AddUser(authHeader, userId) :
UserResponseDTO.Map(user);

var fields = tagRepository.GetInterestFieldsForOrganizer(userId);
userRes.FieldsOfInterests = fields;
Expand Down Expand Up @@ -166,4 +163,31 @@ public string UpdateUserAvatar(string id, IFormFile avatarFile)
var url = fileShareService.FileGetDownloadUri($"{userId}/{AVATAR_FILE_NAME}");
return url.ToString();
}

/// <summary>
/// Adds a new user by calling the ID Provider endpoint for the users' information
/// </summary>
/// <param name="authHeader">The header used for calling de ID Provider's endpoint</param>
/// <param name="userId">User to add</param>
/// <returns>Formatted DTO of User's information</returns>
private UserResponseDTO AddUser(string authHeader, string userId)
{
UserInfoDto? userInfo = identityProvider.GetUserInfo(authHeader);

if (userInfo == null)
{
throw new Exception("No users associated with this ID");
}

User addedUser = userRepository.Add(new User
{
Email = userInfo.Email,
Id = userId,
ProfileDescription = userInfo.GivenName,
Organization = string.Empty,
IsActive = true
});

return UserResponseDTO.Map(addedUser);
}
}
80 changes: 65 additions & 15 deletions tests/Tests/Services/UserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using api.core.Repositories.Abstractions;
using Microsoft.IdentityModel.JsonWebTokens;
using api.core.Misc;
using api.core.Services.Abstractions;

namespace api.tests.Tests.Services;
public class UserServiceTests
Expand All @@ -25,6 +26,7 @@ public class UserServiceTests
private readonly Mock<IFileShareService> _fileShareServiceMock;
private readonly Mock<IImageService> _imageServiceMock;
private readonly Mock<IJwtUtils> _jwtUtilsMock;
private readonly Mock<IIdentityProviderService> _providerServiceMock;
private readonly UserService _userService;

public UserServiceTests()
Expand All @@ -35,6 +37,7 @@ public UserServiceTests()
_fileShareServiceMock = new Mock<IFileShareService>();
_imageServiceMock = new Mock<IImageService>();
_jwtUtilsMock = new Mock<IJwtUtils>();
_providerServiceMock = new Mock<IIdentityProviderService>();

_fileShareServiceMock.Setup(service => service.FileGetDownloadUri(It.IsAny<string>())).Returns(new Uri("http://example.com/avatar.webp"));
_userService = new UserService(
Expand All @@ -43,7 +46,8 @@ public UserServiceTests()
_tagRepositoryMock.Object,
_activityAreaRepositoryMock.Object,
_imageServiceMock.Object,
_jwtUtilsMock.Object);
_jwtUtilsMock.Object,
_providerServiceMock.Object);
}

[Fact]
Expand Down Expand Up @@ -112,7 +116,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById()
Role = UserRole.Organizer
};

_userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(organizer);
_userRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(organizer);
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId);

// Act
Expand All @@ -123,7 +127,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenOrganizerIsFoundById()
result.Id.Should().Be(organizerId);
result.Email.Should().Be(organizer.Email);

_userRepositoryMock.Verify(repo => repo.GetOrganizer(organizerId), Times.Once);
_userRepositoryMock.Verify(repo => repo.Get(organizerId), Times.Once);
}

[Fact]
Expand All @@ -141,8 +145,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenModeratorIsFoundById()
};

_jwtUtilsMock.Setup(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId);
_userRepositoryMock.Setup(repo => repo.GetOrganizer(moderatorId)).Returns((User?)null); // Simulate no organizer found
_userRepositoryMock.Setup(repo => repo.GetModerator(moderatorId)).Returns(moderator); // Simulate moderator found
_userRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns(moderator); // Simulate moderator found

// Act
var result = _userService.GetUser(moderatorId);
Expand All @@ -152,29 +155,76 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenModeratorIsFoundById()
result.Id.Should().Be(moderatorId);
result.Email.Should().Be(moderator.Email);

_userRepositoryMock.Verify(repo => repo.GetOrganizer(moderatorId), Times.Once);
_userRepositoryMock.Verify(repo => repo.GetModerator(moderatorId), Times.Once);
_userRepositoryMock.Verify(repo => repo.Get(moderatorId), Times.Once);
_jwtUtilsMock.Verify(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(moderatorId), Times.Once);
}

[Fact]
public void GetUser_ShouldThrowException_WhenNoUserIsAssociatedWithProvidedId()
public void GetUser_ShouldReturnUserResponseDTO_WhenNewUserIsFoundById()
{
// Arrange
var userId = "userId";
var user = new User
{
Id = userId,
Email = "jane.doe@example.com",
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};

var userInfo = new UserInfoDto
{
Email = "jane.doe@example.com",
EmailVerified = true,
GivenName = "Jane Doe",
Name = "Jane Doe",
Nickname = "jane.doe",
PreferedUsername = "jane.doe",
Sub = userId
};

_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(userId)).Returns(userId);
_userRepositoryMock.Setup(repo => repo.Get(userId)).Returns(null as User);
_providerServiceMock.Setup(provider => provider.GetUserInfo(userId)).Returns(userInfo);
_userRepositoryMock.Setup(repo =>
repo.Add(It.Is<User>(u => u.Id == userInfo.Sub && u.Email == userInfo.Email && u.ProfileDescription == userInfo.GivenName)))
.Returns(user);

// Act
var result = _userService.GetUser(userId);

// Assert
result.Should().NotBeNull();
result.Type.Should().BeEmpty();
result.Id.Should().Be(user.Id);
result.Email.Should().Be(user.Email);

_jwtUtilsMock.Verify(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(userId), Times.Once);
_userRepositoryMock.Verify(repo => repo.Get(userId), Times.Once);
_userRepositoryMock.Verify(repo => repo.Add(It.IsAny<User>()), Times.Once);
_providerServiceMock.Verify(provider => provider.GetUserInfo(userId), Times.Once);
}

[Fact]
public void GetUser_ShouldThrowException_WhenNoUserIsAssociatedWithProvidedIdNorValid()
{
// Arrange
var userId = "nobody";

// Setup both organizer and moderator repositories to return null, simulating that no user is found with the provided ID
_userRepositoryMock.Setup(repo => repo.GetOrganizer(userId)).Returns(null as User);
_userRepositoryMock.Setup(repo => repo.GetModerator(userId)).Returns(null as User);
_userRepositoryMock.Setup(repo => repo.Get(userId)).Returns(null as User);
_jwtUtilsMock.Setup(jwtUtil => jwtUtil.GetUserIdFromAuthHeader(userId)).Returns(userId);

// Setup the provider call as if the call was invalid (no connection or invalid token)
_providerServiceMock.Setup(provider => provider.GetUserInfo(userId)).Returns(null as UserInfoDto);

// Act
Action act = () => _userService.GetUser(userId);

// Assert
act.Should().Throw<Exception>().WithMessage("No users associated with this ID");
_userRepositoryMock.Verify(repo => repo.GetOrganizer(userId), Times.Once);
_userRepositoryMock.Verify(repo => repo.GetOrganizer(userId), Times.Once);
_userRepositoryMock.Verify(repo => repo.Get(userId), Times.Once);
_providerServiceMock.Verify(provider => provider.GetUserInfo(userId), Times.Once);
}


Expand Down Expand Up @@ -209,7 +259,7 @@ public void UpdateUser_ShouldReturnTrue_WhenOrganizerIsUpdatedSuccessfully()
};

_activityAreaRepositoryMock.Setup(repo => repo.Get(actAreaIdModified)).Returns(activity); // Simulate activity area found
_userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(existingOrganizer);
_userRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(existingOrganizer);
_userRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny<User>())).Returns(true);
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId);

Expand Down Expand Up @@ -250,7 +300,7 @@ public void UpdateUser_ShouldThrow_WhenActivityAreaIsNotFoundInTheList()
UpdatedAt = DateTime.UtcNow
};
_activityAreaRepositoryMock.Setup(repo => repo.Get(badActAreaIdModified)).Returns(null as ActivityArea); // Simulate activity area not found
_userRepositoryMock.Setup(repo => repo.GetOrganizer(organizerId)).Returns(existingOrganizer);
_userRepositoryMock.Setup(repo => repo.Get(organizerId)).Returns(existingOrganizer);
_userRepositoryMock.Setup(repo => repo.Update(organizerId, It.IsAny<User>())).Returns(true);
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId);

Expand Down Expand Up @@ -279,7 +329,7 @@ public void UpdateUser_ShouldReturnTrue_WhenModeratorIsUpdatedSuccessfully()
Role = UserRole.Moderator
};

_userRepositoryMock.Setup(repo => repo.GetModerator(moderatorId)).Returns(existingModerator); // Simulate moderator found
_userRepositoryMock.Setup(repo => repo.Get(moderatorId)).Returns(existingModerator); // Simulate moderator found
_userRepositoryMock.Setup(repo => repo.Update(moderatorId, It.IsAny<User>())).Returns(true);
_jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId);

Expand Down
Loading