From 4a4bf09261e7dc653ba2f104549bcbdc137435be Mon Sep 17 00:00:00 2001 From: superjekk Date: Tue, 30 Dec 2025 23:04:14 -0500 Subject: [PATCH 1/4] =?UTF-8?q?FCT=20:=20Service=20d'appel=20du=20fourniss?= =?UTF-8?q?eur=20d'Identit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un service d'appel des fournisseurs d'Identité a été créé afin de pouvoir interroger le serveur d'Identité sans passer par le client --- .../Abstractions/IIdentityProviderService.cs | 26 +++++++++++++++ core/Services/AuthentikService.cs | 32 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 core/Services/Abstractions/IIdentityProviderService.cs create mode 100644 core/Services/AuthentikService.cs diff --git a/core/Services/Abstractions/IIdentityProviderService.cs b/core/Services/Abstractions/IIdentityProviderService.cs new file mode 100644 index 0000000..a587baf --- /dev/null +++ b/core/Services/Abstractions/IIdentityProviderService.cs @@ -0,0 +1,26 @@ +namespace api.core.Services.Abstractions; + +/// +/// Interface to communicate with any ID Provider +/// +public interface IIdentityProviderService +{ + /// + /// Fetches User's information from the ID Provider + /// + /// Header used to call the endpoint + /// If result is null, UserInfo endpoint cannot be accessed + 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 Groups { get; set; } = null!; +} \ No newline at end of file diff --git a/core/Services/AuthentikService.cs b/core/Services/AuthentikService.cs new file mode 100644 index 0000000..1c7d8f5 --- /dev/null +++ b/core/Services/AuthentikService.cs @@ -0,0 +1,32 @@ +using api.core.Services.Abstractions; + +using System.Text.Json; + +namespace api.core.Services; + +/// +/// Service used to communicate with Authentik ID Provider +/// +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(new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }).Result; + } +} From f395708a59f231cee15cc5e4be3fa8484fde894e Mon Sep 17 00:00:00 2001 From: superjekk Date: Wed, 31 Dec 2025 19:02:28 -0500 Subject: [PATCH 2/4] FCT : Ajout d'utilisateur valide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lorsque l'on récupère un utilisateur, si l'utilisateur n'existe pas encore, on le crée --- .../DependencyInjectionExtension.cs | 1 + core/Services/Abstractions/IUserService.cs | 4 +- core/Services/UserService.cs | 39 ++++++--- tests/Tests/Services/UserServiceTests.cs | 80 +++++++++++++++---- 4 files changed, 97 insertions(+), 27 deletions(-) diff --git a/core/Extensions/DependencyInjectionExtension.cs b/core/Extensions/DependencyInjectionExtension.cs index a3e52ed..0a65e17 100644 --- a/core/Extensions/DependencyInjectionExtension.cs +++ b/core/Extensions/DependencyInjectionExtension.cs @@ -41,6 +41,7 @@ public static IServiceCollection AddDependencyInjection(this IServiceCollection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // Utils services.AddTransient(); diff --git a/core/Services/Abstractions/IUserService.cs b/core/Services/Abstractions/IUserService.cs index c487b3e..bc10f8f 100644 --- a/core/Services/Abstractions/IUserService.cs +++ b/core/Services/Abstractions/IUserService.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.Tracing; +using System.Diagnostics.Tracing; using api.core.Data.Enums; using api.core.Data.requests; @@ -21,4 +21,6 @@ public interface IUserService public bool ToggleUserActiveState(string id); public string UpdateUserAvatar(string id, IFormFile avatarFile); + + public UserResponseDTO AddUser(string authHeader, string userId); } diff --git a/core/Services/UserService.cs b/core/Services/UserService.cs index 7b56b99..877ea3b 100644 --- a/core/Services/UserService.cs +++ b/core/Services/UserService.cs @@ -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; @@ -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; @@ -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"; @@ -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; @@ -166,4 +163,24 @@ public string UpdateUserAvatar(string id, IFormFile avatarFile) var url = fileShareService.FileGetDownloadUri($"{userId}/{AVATAR_FILE_NAME}"); return url.ToString(); } + + /// + /// Adds a new user by calling the ID Provider endpoint for the users' information + /// + /// The header used for calling de ID Provider's endpoint + /// User to add + /// + public 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 }); + + return UserResponseDTO.Map(addedUser); + } } diff --git a/tests/Tests/Services/UserServiceTests.cs b/tests/Tests/Services/UserServiceTests.cs index 0e7ce00..336e6ba 100644 --- a/tests/Tests/Services/UserServiceTests.cs +++ b/tests/Tests/Services/UserServiceTests.cs @@ -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 @@ -25,6 +26,7 @@ public class UserServiceTests private readonly Mock _fileShareServiceMock; private readonly Mock _imageServiceMock; private readonly Mock _jwtUtilsMock; + private readonly Mock _providerServiceMock; private readonly UserService _userService; public UserServiceTests() @@ -35,6 +37,7 @@ public UserServiceTests() _fileShareServiceMock = new Mock(); _imageServiceMock = new Mock(); _jwtUtilsMock = new Mock(); + _providerServiceMock = new Mock(); _fileShareServiceMock.Setup(service => service.FileGetDownloadUri(It.IsAny())).Returns(new Uri("http://example.com/avatar.webp")); _userService = new UserService( @@ -43,7 +46,8 @@ public UserServiceTests() _tagRepositoryMock.Object, _activityAreaRepositoryMock.Object, _imageServiceMock.Object, - _jwtUtilsMock.Object); + _jwtUtilsMock.Object, + _providerServiceMock.Object); } [Fact] @@ -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 @@ -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] @@ -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); @@ -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(u => u.Id == userInfo.Sub && u.Email == userInfo.Email))) + .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()), 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().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); } @@ -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())).Returns(true); _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId); @@ -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())).Returns(true); _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(organizerId)).Returns(organizerId); @@ -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())).Returns(true); _jwtUtilsMock.Setup(jwtUtils => jwtUtils.GetUserIdFromAuthHeader(moderatorId)).Returns(moderatorId); From 2ec63664e8076264e355f389077e02f2d198211e Mon Sep 17 00:00:00 2001 From: superjekk Date: Thu, 1 Jan 2026 19:54:15 -0500 Subject: [PATCH 3/4] =?UTF-8?q?BUGFIX=20:=20Appel=20au=20point=20d'entr?= =?UTF-8?q?=C3=A9e=20Userinfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/Services/AuthentikService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Services/AuthentikService.cs b/core/Services/AuthentikService.cs index 1c7d8f5..2ede95f 100644 --- a/core/Services/AuthentikService.cs +++ b/core/Services/AuthentikService.cs @@ -16,7 +16,7 @@ public class AuthentikService : IIdentityProviderService BaseAddress = new Uri(Environment.GetEnvironmentVariable("OPENID_BASE_URL")) }; - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "userinfo"); + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "userinfo/"); request.Headers.Add("Authorization", accessHeader); // TODO : Changer pour un comportement asynchrone From 06b7b7a431c0e794edc925f82022939ee60f673c Mon Sep 17 00:00:00 2001 From: superjekk Date: Thu, 1 Jan 2026 20:09:56 -0500 Subject: [PATCH 4/4] =?UTF-8?q?BUGFIX=20:=20Valeurs=20par=20d=C3=A9faut=20?= =?UTF-8?q?manquantes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certaines valeurs par défaut manquantes à la création d'un utilisateur par défaut ont été ajoutées --- core/Services/Abstractions/IUserService.cs | 2 -- core/Services/UserService.cs | 13 ++++++++++--- tests/Tests/Services/UserServiceTests.cs | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/Services/Abstractions/IUserService.cs b/core/Services/Abstractions/IUserService.cs index bc10f8f..f2246b9 100644 --- a/core/Services/Abstractions/IUserService.cs +++ b/core/Services/Abstractions/IUserService.cs @@ -21,6 +21,4 @@ public interface IUserService public bool ToggleUserActiveState(string id); public string UpdateUserAvatar(string id, IFormFile avatarFile); - - public UserResponseDTO AddUser(string authHeader, string userId); } diff --git a/core/Services/UserService.cs b/core/Services/UserService.cs index 877ea3b..50eae10 100644 --- a/core/Services/UserService.cs +++ b/core/Services/UserService.cs @@ -169,8 +169,8 @@ public string UpdateUserAvatar(string id, IFormFile avatarFile) /// /// The header used for calling de ID Provider's endpoint /// User to add - /// - public UserResponseDTO AddUser(string authHeader, string userId) + /// Formatted DTO of User's information + private UserResponseDTO AddUser(string authHeader, string userId) { UserInfoDto? userInfo = identityProvider.GetUserInfo(authHeader); @@ -179,7 +179,14 @@ public UserResponseDTO AddUser(string authHeader, string userId) throw new Exception("No users associated with this ID"); } - User addedUser = userRepository.Add(new User { Email = userInfo.Email, Id = userId }); + User addedUser = userRepository.Add(new User + { + Email = userInfo.Email, + Id = userId, + ProfileDescription = userInfo.GivenName, + Organization = string.Empty, + IsActive = true + }); return UserResponseDTO.Map(addedUser); } diff --git a/tests/Tests/Services/UserServiceTests.cs b/tests/Tests/Services/UserServiceTests.cs index 336e6ba..79676f7 100644 --- a/tests/Tests/Services/UserServiceTests.cs +++ b/tests/Tests/Services/UserServiceTests.cs @@ -187,7 +187,7 @@ public void GetUser_ShouldReturnUserResponseDTO_WhenNewUserIsFoundById() _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(u => u.Id == userInfo.Sub && u.Email == userInfo.Email))) + repo.Add(It.Is(u => u.Id == userInfo.Sub && u.Email == userInfo.Email && u.ProfileDescription == userInfo.GivenName))) .Returns(user); // Act