diff --git a/PostFundManagement.Application/Services/Users/UserService.cs b/PostFundManagement.Application/Services/Users/UserService.cs index 4639a68..5c25bcf 100644 --- a/PostFundManagement.Application/Services/Users/UserService.cs +++ b/PostFundManagement.Application/Services/Users/UserService.cs @@ -147,7 +147,7 @@ public sealed class UserService(IDbContextFactory contextF if (await context.Users.AnyAsync(u => u.Email == email, cancellationToken)) return Result.Fail(ValidationError.Create("User with this email already exists.")); - var createUserResult = await skorubaService.CreateUserAsync(request, userId, cancellationToken); + var createUserResult = await skorubaService.CreateUserAsync(request, cancellationToken); if (createUserResult.IsFailed) return Result.Fail(CreateError.Create("Failed to create user.")); diff --git a/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs b/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs index 1317e9e..f0f9faf 100644 --- a/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs +++ b/PostFundManagement.Domain/Abstractions/ISkorubaUserService.cs @@ -7,7 +7,11 @@ public interface ISkorubaUserService { [Log] [Audit] - Task> CreateUserAsync(CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default); + Task> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken = default); + + [Log] + [Audit] + Task RemoveUserAsync(Guid userId, CancellationToken cancellationToken = default); [Log] Task> GetUserAsync(Guid userId, CancellationToken cancellationToken = default); diff --git a/PostFundManagement.Domain/Models/Security/Contracts.cs b/PostFundManagement.Domain/Models/Security/Contracts.cs index 03defb6..86ea49e 100644 --- a/PostFundManagement.Domain/Models/Security/Contracts.cs +++ b/PostFundManagement.Domain/Models/Security/Contracts.cs @@ -2,7 +2,7 @@ namespace PostFundManagement.Domain.Models.Security; -public sealed class UserLockOutRequest +public abstract class SkorubaUser { [JsonPropertyName("id")] public string? UserId { get; set; } @@ -35,15 +35,15 @@ public sealed class UserLockOutRequest public DateTimeOffset? LockoutEnd { get; set; } } -public sealed record AddUserClaimRequest([Required] Guid UserId, - [Required] int ClaimId, - [Required][MaxLength(LabelLength)] string ClaimType, +public sealed class UserLockOutRequest : SkorubaUser; +public sealed class CreateUserRequest : SkorubaUser; +public sealed class UpdateUserRequest : SkorubaUser; + +public sealed record AddUserClaimRequest([Required] Guid UserId, + [Required] int ClaimId, + [Required][MaxLength(LabelLength)] string ClaimType, [Required][MaxLength(LabelLength)] string ClaimValue); -public sealed record CreateUserRequest(string UserName, string Email, string Password, bool EmailConfirmed = true, string? PhoneNumber = null, bool PhoneNumberConfirmed = false); - -public sealed record UpdateUserRequest(string UserId, string UserName, string Email, bool EmailConfirmed, string? PhoneNumber, bool PhoneNumberConfirmed, bool LockoutEnabled, DateTimeOffset? LockoutEnd, int AccessFailedCount, bool TwoFactorEnabled); - public sealed record Claim(string ClaimType, string ClaimValue, int? ClaimId = null); public sealed record ClaimsResponse(int TotalCount, int PageSize, ICollection Claims); diff --git a/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs b/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs index 9886324..366477f 100644 --- a/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs +++ b/PostFundManagement.Infrastructure/Security/SkorubaUserService.cs @@ -6,14 +6,14 @@ namespace PostFundManagement.Infrastructure.Security; public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkorubaUserService { - public async Task> CreateUserAsync(CreateUserRequest request, Guid userId, CancellationToken cancellationToken = default) => + public async Task> CreateUserAsync(CreateUserRequest request, CancellationToken cancellationToken = default) => await Result.Try((Func>>)(async () => { var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient); var response = await httpClient.PostAsJsonAsync("api/users", request, cancellationToken); if (!response.IsSuccessStatusCode) - return Result.Fail(CreateError.Create("Failed to create user.")); + return Result.Fail(CreateError.Create(response.ReasonPhrase!)); var created = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); @@ -72,9 +72,9 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba await Result.Try((Func>)(async () => { var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient); - var payload = new { UserId = userId, ClaimType = claimType, ClaimValue = claimValue }; + var payload = new { userId = userId, claimType = claimType, claimValue = claimValue }; - var response = await httpClient.PostAsJsonAsync($"api/users/{userId}/claims", payload, cancellationToken); + var response = await httpClient.PostAsJsonAsync($"api/users/claims", payload, cancellationToken); return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to add user claim.")); })); @@ -83,9 +83,12 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba await Result.Try((Func>)(async () => { var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient); - var response = await httpClient.DeleteAsync($"api/users/{userId}/claims/{claimId}", cancellationToken); + var response = await httpClient.DeleteAsync($"api/users/{userId}/Claims?claimId={claimId}", cancellationToken); - return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to remove user claim.")); + if(response.StatusCode == System.Net.HttpStatusCode.NotFound) + return Result.Fail(NotFoundError.Create($"Claim {claimId} for user {userId} not found.")); + + return Result.OkIf(response.IsSuccessStatusCode, ValidationError.Create("Failed to remove user claim.")); })); public async Task> GetUserClaimsAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) => @@ -103,7 +106,7 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba await Result.Try((Func>)(async () => { var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient); - var payload = new { UserId = userId, RoleId = roleId }; + var payload = new { userId = userId, roleId = roleId }; var response = await httpClient.PostAsJsonAsync($"api/users/roles", payload, cancellationToken); @@ -183,4 +186,15 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba return Result.FailIf(result is null, NotFoundError.Create("Available roles could not be retrieved.")) .ToResult(result?.Roles?.ToArray() ?? []); })); + + public async Task RemoveUserAsync(Guid userId, CancellationToken cancellationToken = default) => + await Result.Try((Func>)(async () => + { + var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient); + var result = await httpClient.DeleteAsync($"api/users/{userId}", cancellationToken); + + return result.StatusCode == System.Net.HttpStatusCode.NotFound + ? Result.Fail(NotFoundError.Create($"User {userId} not found.")) + : Result.OkIf(result.IsSuccessStatusCode, UpdateError.Create("Failed to remove user.")); + })); } \ No newline at end of file diff --git a/PostFundManagement.Tests/SkorubaUserServiceIntegrationTests.cs b/PostFundManagement.Tests/SkorubaUserServiceIntegrationTests.cs index 6df913d..e6dea26 100644 --- a/PostFundManagement.Tests/SkorubaUserServiceIntegrationTests.cs +++ b/PostFundManagement.Tests/SkorubaUserServiceIntegrationTests.cs @@ -1,22 +1,11 @@ using PostFundManagement.Application.Extensions; using PostFundManagement.Domain.Abstractions; +using PostFundManagement.Domain.Models.Security; using PostFundManagement.Infrastructure.Database.Services; using static PostFundManagement.Domain.Extensions.Constants; namespace PostFundManagement.Tests; -/// -/// Integration tests for SkorubaUserService.GetUserRolesAsync method. -/// -/// These tests hit the actual Skoruba API environment. -/// Requires the following configuration: -/// - MachineIdentitySettings with valid ClientId and ClientSecret from user secrets -/// - SkorubaApi connection string pointing to the API endpoint -/// - Network access to the Skoruba API -/// -/// To run these tests: -/// dotnet test --filter "FullyQualifiedName~SkorubaUserServiceGetUserRolesIntegrationTests" -/// public class SkorubaUserServiceGetUserRolesIntegrationTests { private readonly ISkorubaUserService _skorubaService; @@ -51,9 +40,6 @@ public class SkorubaUserServiceGetUserRolesIntegrationTests _httpClientFactory = serviceProvider.GetRequiredService(); } - /// - /// Integration test: Verify HttpClient is properly configured with SkorubaHttpClient name. - /// [DebugOnlyFact] public void HttpClientFactory_HasSkorubaHttpClientConfigured() { @@ -65,15 +51,72 @@ public class SkorubaUserServiceGetUserRolesIntegrationTests Assert.NotNull(httpClient.BaseAddress); } - /// - /// Integration test: Verify that the service properly formats API requests. - /// Uses a fake/invalid user ID to test the HTTP request flow. - /// + [DebugOnlyFact] + public async Task CreateUserAsync_MustSucced_WithErrorMessage() + { + // Arrange + var newUser = new CreateUserRequest + { + UserName = "testuser", + Email = "testuser@example.com", + EmailConfirmed = true, + }; + + // Act + var result = await _skorubaService.CreateUserAsync(newUser); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task DeleteUserAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("3b001832-c78d-4705-9bc2-0f2f2ebb35b7"); + + // Act + var result = await _skorubaService.RemoveUserAsync(userId); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task LockoutUserAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + + // Act + var result = await _skorubaService.SetUserLockoutAsync(userId, true); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task UnlockUserAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + + // Act + var result = await _skorubaService.SetUserLockoutAsync(userId, false); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + [DebugOnlyFact] public async Task GetUserRolesAsync_SendsProperlyFormattedRequest() { // Arrange - var userId = Guid.Parse("6059f1e4-6a41-4447-a546-86ece5102004"); + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); var page = 1; var pageSize = 25; @@ -85,9 +128,88 @@ public class SkorubaUserServiceGetUserRolesIntegrationTests Assert.Empty(result.Errors); } - /// - /// Integration test: Verify cancellation token is respected. - /// + [DebugOnlyFact] + public async Task AddUserRoleAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + var roleId = Guid.Parse("34b03edc-cda4-4380-b7a5-3310933b75ab"); // Contributor role ID + + // Act + var result = await _skorubaService.AddRoleAsync(userId, roleId); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task RemoveUserRoleAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + var roleId = Guid.Parse("34b03edc-cda4-4380-b7a5-3310933b75ab"); // Contributor role ID + + // Act + var result = await _skorubaService.RemoveRoleAsync(userId, roleId); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task GetUserClaimAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + var page = 1; + var pageSize = 25; + + // Act + var result = await _skorubaService.GetUserClaimsAsync(userId, page, pageSize); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task AddUserClaimAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + var claimType = "test-claim"; + var claimValue = "test-value"; + + // Act + var result = await _skorubaService.AddClaimAsync(userId, claimType, claimValue); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + + [DebugOnlyFact] + public async Task RemoveUserClaimAsync_MustSucced_WithErrorMessage() + { + // Arrange + var userId = Guid.Parse("285df6e4-c0f9-4c7c-8da7-b3867ffe1880"); + var claimType = "test-claim"; + + var userClaimsResult = await _skorubaService.GetUserClaimsAsync(userId); + var matchingClaim = userClaimsResult.Value?.FirstOrDefault(c => c.ClaimType == claimType); + + Assert.NotNull(matchingClaim); + + // Act + var result = await _skorubaService.RemoveClaimAsync(userId, matchingClaim!.ClaimId!.Value); + + // Assert + Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message); + Assert.Empty(result.Errors); + } + [DebugOnlyFact] public async Task GetUserRolesAsync_RespectsCancellationToken() { diff --git a/PostFundManagement.slnx b/PostFundManagement.slnx index c829b6c..169329b 100644 --- a/PostFundManagement.slnx +++ b/PostFundManagement.slnx @@ -7,11 +7,9 @@ - - - +