Stable user lockout, role and claim calls
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
using PostFundManagement.Application.Abstractions.Services;
|
||||||
|
using PostFundManagement.Application.Extensions;
|
||||||
|
using PostFundManagement.Domain.Abstractions;
|
||||||
|
using PostFundManagement.Domain.Errors;
|
||||||
|
using PostFundManagement.Domain.Extensions;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Api.Endpoints.Users;
|
||||||
|
|
||||||
|
[ApiVersionTarget(1)]
|
||||||
|
public sealed class GetRoleClaimsEndpoint : IEndpoint
|
||||||
|
{
|
||||||
|
public void Map(IEndpointRouteBuilder builder)
|
||||||
|
{
|
||||||
|
builder.MapGet("api/users/system/roles/{roleId:Guid}/claims", async (Guid roleId, HttpContext context,
|
||||||
|
IUserService service, CancellationToken cancellationToken = default) =>
|
||||||
|
await service.GetRoleClaimsAsync(roleId, context.GetUserId(), cancellationToken) switch
|
||||||
|
{
|
||||||
|
{ IsSuccess: true, Value: var claims } => Results.Ok(claims),
|
||||||
|
var r when r.HasError<NotFoundError>() => Results.NotFound(r.Errors.ToProblems()),
|
||||||
|
var r when r.HasError<UpdateError>() => Results.BadRequest(r.Errors.ToProblems()),
|
||||||
|
var r => Results.InternalServerError(r.Errors.ToProblems())
|
||||||
|
})
|
||||||
|
.WithDescription("Get system role claims")
|
||||||
|
.RequireAuthorization(Constants.MainScope)
|
||||||
|
.WithName(typeof(GetRoleClaimsEndpoint).ToEndpointName())
|
||||||
|
.MapToApiVersion(new ApiVersion(1))
|
||||||
|
.Produces<Claim[]>(StatusCodes.Status200OK)
|
||||||
|
.Produces(StatusCodes.Status401Unauthorized)
|
||||||
|
.Produces<Problems>(StatusCodes.Status404NotFound)
|
||||||
|
.Produces<Problems>(StatusCodes.Status400BadRequest)
|
||||||
|
.Produces<Problems>(StatusCodes.Status500InternalServerError)
|
||||||
|
.WithTags(EndpointTags.Users);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,9 @@ public interface IUserService
|
|||||||
[Log]
|
[Log]
|
||||||
Task<Result<Domain.Models.Security.Claim[]>> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default);
|
Task<Result<Domain.Models.Security.Claim[]>> GetUserClaimsAsync(Guid targetUserId, Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
[Log]
|
||||||
|
Task<Result<Domain.Models.Security.Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
[Log]
|
[Log]
|
||||||
Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default);
|
Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using PostFundManagement.Application.Handlers;
|
||||||
using PostFundManagement.Application.Interceptors;
|
using PostFundManagement.Application.Interceptors;
|
||||||
using PostFundManagement.Application.Services.Shared;
|
using PostFundManagement.Application.Services.Shared;
|
||||||
using PostFundManagement.Domain.Abstractions;
|
using PostFundManagement.Domain.Abstractions;
|
||||||
@@ -24,19 +25,10 @@ public static class Security
|
|||||||
|
|
||||||
if (!authOptions.Authority.EndsWith('/')) authOptions.Authority += "/";
|
if (!authOptions.Authority.EndsWith('/')) authOptions.Authority += "/";
|
||||||
|
|
||||||
services.AddClientCredentialsTokenManagement()
|
services.AddTransient<ClientCredentialsTokenHandler>();
|
||||||
.AddClient(SkorubaCredentialClient, client =>
|
|
||||||
{
|
services.AddHttpClient(SkorubaHttpClient, client => client.BaseAddress = new Uri(configuration.GetConnectionString("SkorubaApi")!))
|
||||||
client.TokenEndpoint = new Uri($"{authOptions.Authority}connect/token");
|
.AddHttpMessageHandler<ClientCredentialsTokenHandler>();
|
||||||
client.ClientId = ClientId.Parse(authOptions.ClientId!);
|
|
||||||
client.ClientSecret = ClientSecret.Parse(authOptions.ClientSecret!);
|
|
||||||
client.Scope = Duende.AccessTokenManagement.Scope.ParseOrDefault(authOptions.Scope);
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddHttpClient(SkorubaHttpClient, client =>
|
|
||||||
{
|
|
||||||
client.BaseAddress = new Uri(configuration.GetConnectionString("SkorubaApi")!);
|
|
||||||
}).AddClientCredentialsTokenHandler(ClientCredentialsClientName.Parse(SkorubaCredentialClient));
|
|
||||||
|
|
||||||
services.AddScoped<AuditInterceptor>();
|
services.AddScoped<AuditInterceptor>();
|
||||||
services.AddScoped<LoggingInterceptor>();
|
services.AddScoped<LoggingInterceptor>();
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using IdentityModel.Client;
|
||||||
|
using PostFundManagement.Domain.Configuration.Security;
|
||||||
|
|
||||||
|
namespace PostFundManagement.Application.Handlers;
|
||||||
|
|
||||||
|
public class ClientCredentialsTokenHandler(ILogger<ClientCredentialsTokenHandler> logger,
|
||||||
|
IOptions<MachineIdentitySettings> options) : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly MachineIdentitySettings _settings = options.Value;
|
||||||
|
private string? _cachedToken;
|
||||||
|
private DateTime _tokenExpiry = DateTime.MinValue;
|
||||||
|
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = await GetAccessTokenAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
logger.LogError("Failed to acquire access token for Skoruba API");
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Unable to acquire access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||||
|
|
||||||
|
logger.LogDebug("ClientCredentialsTokenHandler: Added bearer token to request");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error acquiring client credentials token");
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await base.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
logger.LogWarning("HTTP request failed with status {StatusCode} for {Method} {RequestUri}",
|
||||||
|
response.StatusCode, request.Method, request.RequestUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error sending HTTP request with client credentials token");
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetAccessTokenAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_cachedToken) && DateTime.UtcNow < _tokenExpiry)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Using cached access token");
|
||||||
|
|
||||||
|
return _cachedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogDebug("Acquiring new access token from {Authority}", _settings.Authority);
|
||||||
|
|
||||||
|
using var tokenClient = new HttpClient();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_settings.Authority!.EndsWith('/')) _settings.Authority += "/";
|
||||||
|
|
||||||
|
var scope = _settings.Scope;
|
||||||
|
|
||||||
|
var tokenRequest = new ClientCredentialsTokenRequest
|
||||||
|
{
|
||||||
|
Address = $"{_settings.Authority}connect/token",
|
||||||
|
ClientId = _settings.ClientId!,
|
||||||
|
ClientSecret = _settings.ClientSecret,
|
||||||
|
GrantType = "client_credentials",
|
||||||
|
Scope = scope,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.LogDebug("Sending token request to {TokenEndpoint} for client {ClientId} with scope {Scope}",
|
||||||
|
tokenRequest.Address, tokenRequest.ClientId, tokenRequest.Scope);
|
||||||
|
|
||||||
|
var response = await tokenClient.RequestClientCredentialsTokenAsync(tokenRequest, cancellationToken);
|
||||||
|
|
||||||
|
if (response.IsError)
|
||||||
|
{
|
||||||
|
logger.LogError("Token request error: {Error} - {ErrorDescription}. Full response: {Raw}",
|
||||||
|
response.Error, response.ErrorDescription, response.Raw);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedToken = response.AccessToken;
|
||||||
|
|
||||||
|
var expiresIn = response.ExpiresIn > 0 ? response.ExpiresIn : 3600;
|
||||||
|
|
||||||
|
_tokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn * 0.9);
|
||||||
|
|
||||||
|
logger.LogDebug("Successfully acquired access token, expires at {Expiry}", _tokenExpiry);
|
||||||
|
|
||||||
|
return _cachedToken;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Exception occurred while acquiring client credentials token");
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,31 @@ public sealed class UserCacheService(UserService service, IDistributedCache cach
|
|||||||
public async Task<Result> DeleteUserRoleAsync(Guid targetUserId, Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
public async Task<Result> DeleteUserRoleAsync(Guid targetUserId, Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
await service.DeleteUserRoleAsync(targetUserId, roleId, userId, cancellationToken);
|
await service.DeleteUserRoleAsync(targetUserId, roleId, userId, cancellationToken);
|
||||||
|
|
||||||
|
public async Task<Result<Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
await Result.Try((Func<Task<Result<Claim[]>>>)(async () =>
|
||||||
|
{
|
||||||
|
if (!await featureManager.IsEnabledAsync(ServiceCachingFeature))
|
||||||
|
return await service.GetRoleClaimsAsync(roleId, userId, cancellationToken);
|
||||||
|
|
||||||
|
var cacheKey = HashService.ToMd5Hash($"getsystemroles-{userId}").Value;
|
||||||
|
|
||||||
|
var cached = await cache.GetStringAsync(cacheKey, cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(cached))
|
||||||
|
{
|
||||||
|
var claimResult = await service.GetRoleClaimsAsync(roleId, userId, cancellationToken);
|
||||||
|
|
||||||
|
if (claimResult.IsFailed) return claimResult;
|
||||||
|
|
||||||
|
await cache.SetStringAsync(cacheKey, claimResult.Value.ToJson(),
|
||||||
|
cacheOptions, cancellationToken);
|
||||||
|
|
||||||
|
return claimResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Ok(cached.FromJson<Claim[]>());
|
||||||
|
}));
|
||||||
|
|
||||||
public async Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default) =>
|
public async Task<Result<Role[]>> GetSystemRolesAsync(Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
await Result.Try((Func<Task<Result<Role[]>>>)(async () =>
|
await Result.Try((Func<Task<Result<Role[]>>>)(async () =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -321,4 +321,8 @@ public sealed class UserService(IDbContextFactory<ApplicationDbContext> contextF
|
|||||||
return Result.OkIf(roleAddResult.IsSuccess && changedTotal > 0, ValidationError.Create("Failed to add user role.")
|
return Result.OkIf(roleAddResult.IsSuccess && changedTotal > 0, ValidationError.Create("Failed to add user role.")
|
||||||
.CausedBy(roleAddResult.Errors));
|
.CausedBy(roleAddResult.Errors));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
public async Task<Result<Claim[]>> GetRoleClaimsAsync(Guid roleId, Guid userId, CancellationToken cancellationToken = default) =>
|
||||||
|
await Result.Try((Func<Task<Result<Claim[]>>>)(async () =>
|
||||||
|
await skorubaService.GetRoleClaimsAsync(roleId, cancellationToken)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public interface ISkorubaUserService
|
|||||||
Task<Result<Models.Security.Claim[]>> GetUserClaimsAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default);
|
Task<Result<Models.Security.Claim[]>> GetUserClaimsAsync(Guid userId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
[Log]
|
[Log]
|
||||||
Task<Result<Models.Security.Claim[]>> GetRoleClaimsAsync(Guid roleId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default);
|
Task<Result<Models.Security.Claim[]>> GetRoleClaimsAsync(Guid roleId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
[Log]
|
[Log]
|
||||||
Task<Result> InvalidateUserSessionAsync(Guid userId, CancellationToken cancellationToken = default);
|
Task<Result> InvalidateUserSessionAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ public sealed class MachineIdentitySettings
|
|||||||
public string? ClientId { get; set; }
|
public string? ClientId { get; set; }
|
||||||
|
|
||||||
public string? ClientSecret { get; set; }
|
public string? ClientSecret { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,39 @@
|
|||||||
|
|
||||||
namespace PostFundManagement.Domain.Models.Security;
|
namespace PostFundManagement.Domain.Models.Security;
|
||||||
|
|
||||||
|
public sealed class UserLockOutRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("userName")]
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("email")]
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("emailConfirmed")]
|
||||||
|
public bool EmailConfirmed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("phoneNumber")]
|
||||||
|
public string? PhoneNumber { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("phoneNumberConfirmed")]
|
||||||
|
public bool PhoneNumberConfirmed { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("LockoutEnabled")]
|
||||||
|
public bool LockoutEnabled { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("twoFactorEnabled")]
|
||||||
|
public bool TwoFactorEnabled { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("accessFailedCount")]
|
||||||
|
public int AccessFailedCount { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lockoutEnd")]
|
||||||
|
public DateTimeOffset? LockoutEnd { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record AddUserClaimRequest([Required] Guid UserId,
|
public sealed record AddUserClaimRequest([Required] Guid UserId,
|
||||||
[Required] int ClaimId,
|
[Required] int ClaimId,
|
||||||
[Required][MaxLength(LabelLength)] string ClaimType,
|
[Required][MaxLength(LabelLength)] string ClaimType,
|
||||||
|
|||||||
@@ -49,9 +49,17 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba
|
|||||||
if (user is null)
|
if (user is null)
|
||||||
return Result.Fail(NotFoundError.Create($"User {userId} not found."));
|
return Result.Fail(NotFoundError.Create($"User {userId} not found."));
|
||||||
|
|
||||||
var updated = user with
|
var updated = new UserLockOutRequest
|
||||||
{
|
{
|
||||||
|
UserId = userId.ToString(),
|
||||||
|
UserName = user.UserName,
|
||||||
|
Email = user.Email,
|
||||||
|
EmailConfirmed = user.EmailConfirmed,
|
||||||
|
PhoneNumber = user.PhoneNumber,
|
||||||
|
PhoneNumberConfirmed = user.PhoneNumberConfirmed,
|
||||||
LockoutEnabled = true,
|
LockoutEnabled = true,
|
||||||
|
TwoFactorEnabled = user.TwoFactorEnabled,
|
||||||
|
AccessFailedCount = user.AccessFailedCount,
|
||||||
LockoutEnd = disabled ? DateTimeOffset.UtcNow.AddYears(100) : null
|
LockoutEnd = disabled ? DateTimeOffset.UtcNow.AddYears(100) : null
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +105,7 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba
|
|||||||
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
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/{userId}/roles", payload, cancellationToken);
|
var response = await httpClient.PostAsJsonAsync($"api/users/roles", payload, cancellationToken);
|
||||||
|
|
||||||
return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to add user role."));
|
return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to add user role."));
|
||||||
}));
|
}));
|
||||||
@@ -106,7 +114,17 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba
|
|||||||
await Result.Try((Func<Task<Result>>)(async () =>
|
await Result.Try((Func<Task<Result>>)(async () =>
|
||||||
{
|
{
|
||||||
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
||||||
var response = await httpClient.DeleteAsync($"api/users/{userId}/roles/{roleId}", cancellationToken);
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Delete, "api/Users/Roles")
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(new
|
||||||
|
{
|
||||||
|
userId = userId.ToString(),
|
||||||
|
roleId = roleId.ToString()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await httpClient.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to remove user role."));
|
return Result.OkIf(response.IsSuccessStatusCode, CreateError.Create("Failed to remove user role."));
|
||||||
}));
|
}));
|
||||||
@@ -116,17 +134,20 @@ public class SkorubaUserService(IHttpClientFactory httpClientFactory) : ISkoruba
|
|||||||
{
|
{
|
||||||
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
||||||
|
|
||||||
var result = await httpClient.GetFromJsonAsync<RolesResponse>($"api/users/{userId}/roles?page={page}&pageSize={pageSize}", cancellationToken);
|
var result = await httpClient.GetFromJsonAsync<RolesResponse>($"api/Users/{userId}/Roles?page={page}&pageSize={pageSize}", cancellationToken);
|
||||||
|
|
||||||
return Result.FailIf(result is null, NotFoundError.Create($"Roles for user {userId} could not be retrieved."))
|
return Result.FailIf(result is null, NotFoundError.Create($"Roles for user {userId} could not be retrieved."))
|
||||||
.ToResult(result?.Roles?.ToArray() ?? []);
|
.ToResult(result?.Roles?.ToArray() ?? []);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
public async Task<Result<Claim[]>> GetRoleClaimsAsync(Guid roleId, int page = 1, int pageSize = 50, CancellationToken cancellationToken = default) =>
|
public async Task<Result<Claim[]>> GetRoleClaimsAsync(Guid roleId, CancellationToken cancellationToken = default) =>
|
||||||
await Result.Try((Func<Task<Result<Claim[]>>>)(async () =>
|
await Result.Try((Func<Task<Result<Claim[]>>>)(async () =>
|
||||||
{
|
{
|
||||||
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
var httpClient = httpClientFactory.CreateClient(SkorubaHttpClient);
|
||||||
|
|
||||||
|
var page = 1;
|
||||||
|
var pageSize = 100;
|
||||||
|
|
||||||
var result = await httpClient.GetFromJsonAsync<ClaimsResponse>($"api/roles/{roleId}/claims?page={page}&pageSize={pageSize}", cancellationToken);
|
var result = await httpClient.GetFromJsonAsync<ClaimsResponse>($"api/roles/{roleId}/claims?page={page}&pageSize={pageSize}", cancellationToken);
|
||||||
|
|
||||||
return Result.FailIf(result is null, NotFoundError.Create($"Claims for role {roleId} could not be retrieved."))
|
return Result.FailIf(result is null, NotFoundError.Create($"Claims for role {roleId} could not be retrieved."))
|
||||||
|
|||||||
@@ -74,14 +74,14 @@ public class SkorubaUserServiceGetUserRolesIntegrationTests
|
|||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var userId = Guid.Parse("6059f1e4-6a41-4447-a546-86ece5102004");
|
var userId = Guid.Parse("6059f1e4-6a41-4447-a546-86ece5102004");
|
||||||
var page = 2;
|
var page = 1;
|
||||||
var pageSize = 25;
|
var pageSize = 25;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _skorubaService.GetUserRolesAsync(userId, page, pageSize);
|
var result = await _skorubaService.GetUserRolesAsync(userId, page, pageSize);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.IsSuccess, result.Errors[0].Message);
|
Assert.True(result.IsSuccess, result.Errors.FirstOrDefault()?.Message);
|
||||||
Assert.Empty(result.Errors);
|
Assert.Empty(result.Errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user