feat(api): Enhance OAuth2 token endpoint and debugging capabilities
- Added debug middleware for /connect/* endpoints to log request and response details for better troubleshooting. - Updated OAuth2 configuration to include "offline_access" scope and disabled access token encryption for development. - Improved DbContext registration in tests by removing all related registrations and ensuring in-memory database setup for testing purposes. - Addressed issues with the /connect/token endpoint not responding, outlining next steps for debugging and fixing the OpenIddict configuration.
This commit is contained in:
11
NOTE.MD
11
NOTE.MD
@@ -3,4 +3,13 @@
|
||||
- Role/Permission Management APIs - CRUD roles
|
||||
- Email Verification - Confirm email
|
||||
- 2FA/MFA - Two-factor authentication
|
||||
- Social Login - Google, Facebook, etc.
|
||||
- Social Login - Google, Facebook, etc.
|
||||
|
||||
|
||||
⚠️ Vấn đề phát hiện:
|
||||
/connect/token endpoint không phản hồi - OAuth2 token endpoint bị treo khi gửi request. Có vẻ OpenIddict Server middleware không xử lý request đúng cách.
|
||||
|
||||
Bạn muốn tôi làm gì tiếp:
|
||||
Fix OAuth2 /connect/token endpoint - Debug và sửa lỗi OpenIddict configuration
|
||||
Chỉ cần database connection - Nếu chỉ cần verify database connection thì đã hoàn thành
|
||||
Tiếp tục với task khác - Cần hướng dẫn cụ thể
|
||||
@@ -194,6 +194,23 @@ try
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Debug middleware for /connect/* endpoints
|
||||
// VI: Debug middleware cho /connect/* endpoints
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/connect"))
|
||||
{
|
||||
Log.Information(">>> [DEBUG] Request to {Path} - Method: {Method}",
|
||||
context.Request.Path, context.Request.Method);
|
||||
}
|
||||
await next();
|
||||
if (context.Request.Path.StartsWithSegments("/connect"))
|
||||
{
|
||||
Log.Information("<<< [DEBUG] Response from {Path} - Status: {StatusCode}",
|
||||
context.Request.Path, context.Response.StatusCode);
|
||||
}
|
||||
});
|
||||
|
||||
// EN: Authentication and Authorization / VI: Xác thực và phân quyền
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
@@ -102,27 +102,29 @@ public static class DependencyInjection
|
||||
|
||||
// EN: Register scopes
|
||||
// VI: Đăng ký scopes
|
||||
options.RegisterScopes("openid", "profile", "email", "roles", "api");
|
||||
options.RegisterScopes("openid", "profile", "email", "roles", "api", "offline_access");
|
||||
|
||||
// EN: Token lifetimes
|
||||
// VI: Thời hạn token
|
||||
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(15))
|
||||
.SetRefreshTokenLifetime(TimeSpan.FromDays(7));
|
||||
|
||||
// EN: Development settings - Disable HTTPS requirement for local dev
|
||||
// VI: Cài đặt development - Tắt yêu cầu HTTPS cho dev local
|
||||
// EN: Development settings
|
||||
// VI: Cài đặt development
|
||||
options.AddDevelopmentEncryptionCertificate()
|
||||
.AddDevelopmentSigningCertificate();
|
||||
.AddDevelopmentSigningCertificate()
|
||||
.DisableAccessTokenEncryption(); // EN: Disable encryption for dev / VI: Tắt mã hóa cho dev
|
||||
|
||||
// EN: Accept anonymous clients (for password flow)
|
||||
// VI: Chấp nhận anonymous clients (cho password flow)
|
||||
// EN: Accept anonymous clients (for password flow without client_id)
|
||||
// VI: Chấp nhận anonymous clients (cho password flow không cần client_id)
|
||||
options.AcceptAnonymousClients();
|
||||
|
||||
// EN: Disable client authentication
|
||||
// VI: Tắt client authentication (for development)
|
||||
// EN: Configure ASP.NET Core integration
|
||||
// VI: Cấu hình tích hợp ASP.NET Core
|
||||
options.UseAspNetCore()
|
||||
.EnableTokenEndpointPassthrough()
|
||||
.EnableUserinfoEndpointPassthrough();
|
||||
.EnableUserinfoEndpointPassthrough()
|
||||
.DisableTransportSecurityRequirement(); // EN: Allow HTTP for dev / VI: Cho phép HTTP cho dev
|
||||
})
|
||||
// EN: Register the OpenIddict validation components
|
||||
// VI: Đăng ký OpenIddict validation components
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Xunit;
|
||||
using IamService.API.Application.Commands.Auth;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.FunctionalTests.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Functional tests for AuthController endpoints.
|
||||
/// VI: Functional tests cho các endpoints của AuthController.
|
||||
/// </summary>
|
||||
public class AuthControllerTests : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly CustomWebApplicationFactory _factory;
|
||||
|
||||
public AuthControllerTests(CustomWebApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithValidData_ShouldReturn201()
|
||||
{
|
||||
// Arrange
|
||||
var request = new RegisterUserCommand(
|
||||
$"test_{Guid.NewGuid()}@example.com",
|
||||
"Password123!",
|
||||
"John",
|
||||
"Doe");
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<RegisterUserCommandResult>>();
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.Equal(request.Email, result.Data.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithInvalidEmail_ShouldReturn400()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
Email = "invalid-email",
|
||||
Password = "Password123!",
|
||||
FirstName = "John",
|
||||
LastName = "Doe"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithWeakPassword_ShouldReturn400()
|
||||
{
|
||||
// Arrange
|
||||
var request = new
|
||||
{
|
||||
Email = "test@example.com",
|
||||
Password = "weak",
|
||||
FirstName = "John",
|
||||
LastName = "Doe"
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_WithDuplicateEmail_ShouldReturn409()
|
||||
{
|
||||
// Arrange
|
||||
var email = $"duplicate_{Guid.NewGuid()}@example.com";
|
||||
var request = new RegisterUserCommand(email, "Password123!", "John", "Doe");
|
||||
|
||||
// First registration
|
||||
await _client.PostAsJsonAsync("/api/v1/auth/register", request);
|
||||
|
||||
// Act - Second registration with same email
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Token_WithValidCredentials_ShouldReturnTokens()
|
||||
{
|
||||
// Arrange - Register user first
|
||||
var email = $"login_{Guid.NewGuid()}@example.com";
|
||||
var password = "Password123!";
|
||||
|
||||
await _client.PostAsJsonAsync("/api/v1/auth/register", new
|
||||
{
|
||||
Email = email,
|
||||
Password = password,
|
||||
FirstName = "Login",
|
||||
LastName = "Test"
|
||||
});
|
||||
|
||||
// Activate user (in real scenario, need to verify email first)
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
user.Activate();
|
||||
await userManager.UpdateAsync(user);
|
||||
}
|
||||
|
||||
// Act - Login
|
||||
var tokenRequest = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "password",
|
||||
["username"] = email,
|
||||
["password"] = password,
|
||||
["scope"] = "openid profile email offline_access"
|
||||
});
|
||||
|
||||
var response = await _client.PostAsync("/connect/token", tokenRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<TokenResponse>();
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEmpty(result.access_token);
|
||||
Assert.Equal("Bearer", result.token_type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Token_WithInvalidCredentials_ShouldReturn400()
|
||||
{
|
||||
// Arrange
|
||||
var tokenRequest = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "password",
|
||||
["username"] = "nonexistent@example.com",
|
||||
["password"] = "WrongPassword123!",
|
||||
["scope"] = "openid"
|
||||
});
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsync("/connect/token", tokenRequest);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
private record TokenResponse(
|
||||
string access_token,
|
||||
string token_type,
|
||||
int expires_in,
|
||||
string? refresh_token,
|
||||
string? scope);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Http.Headers;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Xunit;
|
||||
using IamService.API.Application.Common;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.FunctionalTests.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Functional tests for UsersController endpoints.
|
||||
/// VI: Functional tests cho các endpoints của UsersController.
|
||||
/// </summary>
|
||||
public class UsersControllerTests : IClassFixture<CustomWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly CustomWebApplicationFactory _factory;
|
||||
|
||||
public UsersControllerTests(CustomWebApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<string> GetAccessTokenAsync()
|
||||
{
|
||||
// Register and login a user
|
||||
var email = $"user_{Guid.NewGuid()}@example.com";
|
||||
var password = "Password123!";
|
||||
|
||||
await _client.PostAsJsonAsync("/api/v1/auth/register", new
|
||||
{
|
||||
Email = email,
|
||||
Password = password,
|
||||
FirstName = "Test",
|
||||
LastName = "User"
|
||||
});
|
||||
|
||||
// Activate user
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
user.Activate();
|
||||
await userManager.UpdateAsync(user);
|
||||
}
|
||||
|
||||
// Get token
|
||||
var tokenRequest = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "password",
|
||||
["username"] = email,
|
||||
["password"] = password,
|
||||
["scope"] = "openid profile email offline_access"
|
||||
});
|
||||
|
||||
var response = await _client.PostAsync("/connect/token", tokenRequest);
|
||||
var result = await response.Content.ReadFromJsonAsync<TokenResponse>();
|
||||
return result?.access_token ?? throw new Exception("Failed to get token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUsers_WithoutAuth_ShouldReturn401()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/users");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUsers_WithAuth_ShouldReturn200()
|
||||
{
|
||||
// Arrange
|
||||
var token = await GetAccessTokenAsync();
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/users");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMe_WithAuth_ShouldReturnCurrentUser()
|
||||
{
|
||||
// Arrange
|
||||
var token = await GetAccessTokenAsync();
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/users/me");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<CurrentUserDto>>();
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.NotEmpty(result.Data.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMe_WithoutAuth_ShouldReturn401()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync("/api/v1/users/me");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_WithValidId_ShouldReturnUser()
|
||||
{
|
||||
// Arrange
|
||||
var email = $"getbyid_{Guid.NewGuid()}@example.com";
|
||||
var password = "Password123!";
|
||||
|
||||
// Register user
|
||||
var registerResponse = await _client.PostAsJsonAsync("/api/v1/auth/register", new
|
||||
{
|
||||
Email = email,
|
||||
Password = password,
|
||||
FirstName = "GetById",
|
||||
LastName = "Test"
|
||||
});
|
||||
var registerResult = await registerResponse.Content.ReadFromJsonAsync<ApiResponse<RegisterResult>>();
|
||||
var userId = registerResult?.Data?.UserId;
|
||||
|
||||
// Get auth token
|
||||
var token = await GetAccessTokenAsync();
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/api/v1/users/{userId}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserById_WithInvalidId_ShouldReturn404()
|
||||
{
|
||||
// Arrange
|
||||
var token = await GetAccessTokenAsync();
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
var randomId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/api/v1/users/{randomId}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
private record TokenResponse(string access_token, string token_type, int expires_in);
|
||||
private record RegisterResult(Guid UserId, string Email);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using IamService.Infrastructure;
|
||||
|
||||
namespace IamService.FunctionalTests;
|
||||
@@ -18,31 +19,30 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// EN: Remove the existing DbContext registration
|
||||
// VI: Xóa đăng ký DbContext hiện tại
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(DbContextOptions<IamServiceContext>));
|
||||
// EN: Remove ALL DbContext-related registrations
|
||||
// VI: Xóa TẤT CẢ các đăng ký liên quan đến DbContext
|
||||
var descriptorsToRemove = services.Where(
|
||||
d => d.ServiceType == typeof(DbContextOptions<IamServiceContext>) ||
|
||||
d.ServiceType == typeof(IamServiceContext) ||
|
||||
d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true ||
|
||||
d.ImplementationType?.FullName?.Contains("Npgsql") == true)
|
||||
.ToList();
|
||||
|
||||
if (descriptor != null)
|
||||
foreach (var descriptor in descriptorsToRemove)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
}
|
||||
|
||||
// EN: Remove DbContext service
|
||||
// VI: Xóa DbContext service
|
||||
var dbContextDescriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(IamServiceContext));
|
||||
|
||||
if (dbContextDescriptor != null)
|
||||
{
|
||||
services.Remove(dbContextDescriptor);
|
||||
}
|
||||
|
||||
// EN: Remove the DbContextOptions generic
|
||||
// VI: Xóa DbContextOptions generic
|
||||
services.RemoveAll(typeof(DbContextOptions));
|
||||
|
||||
// EN: Add in-memory database for testing
|
||||
// VI: Thêm in-memory database để test
|
||||
services.AddDbContext<IamServiceContext>(options =>
|
||||
{
|
||||
options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString());
|
||||
options.EnableSensitiveDataLogging();
|
||||
});
|
||||
|
||||
// EN: Ensure database is created with seed data
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using IamService.API.Application.Commands.Auth;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
using IamService.Domain.Exceptions;
|
||||
|
||||
namespace IamService.UnitTests.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for ChangePasswordCommandHandler.
|
||||
/// VI: Unit tests cho ChangePasswordCommandHandler.
|
||||
/// </summary>
|
||||
public class ChangePasswordCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
|
||||
private readonly Mock<ILogger<ChangePasswordCommandHandler>> _loggerMock;
|
||||
private readonly ChangePasswordCommandHandler _handler;
|
||||
|
||||
public ChangePasswordCommandHandlerTests()
|
||||
{
|
||||
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||
_userManagerMock = new Mock<UserManager<ApplicationUser>>(
|
||||
store.Object, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
_loggerMock = new Mock<ILogger<ChangePasswordCommandHandler>>();
|
||||
_handler = new ChangePasswordCommandHandler(_userManagerMock.Object, _loggerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidData_ShouldChangePassword()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
var command = new ChangePasswordCommand(userId, "OldPassword123!", "NewPassword123!");
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.FindByIdAsync(userId.ToString()))
|
||||
.ReturnsAsync(user);
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.ChangePasswordAsync(user, command.CurrentPassword, command.NewPassword))
|
||||
.ReturnsAsync(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithInvalidUserId_ShouldThrowDomainException()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var command = new ChangePasswordCommand(userId, "OldPassword123!", "NewPassword123!");
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.FindByIdAsync(userId.ToString()))
|
||||
.ReturnsAsync((ApplicationUser?)null);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<DomainException>(() =>
|
||||
_handler.Handle(command, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithWrongCurrentPassword_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
var command = new ChangePasswordCommand(userId, "WrongPassword!", "NewPassword123!");
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.FindByIdAsync(userId.ToString()))
|
||||
.ReturnsAsync(user);
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.ChangePasswordAsync(user, command.CurrentPassword, command.NewPassword))
|
||||
.ReturnsAsync(IdentityResult.Failed(
|
||||
new IdentityError { Code = "PasswordMismatch", Description = "Incorrect password" }));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_handler.Handle(command, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using IamService.API.Application.Commands.Auth;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.UnitTests.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for RegisterUserCommandHandler.
|
||||
/// VI: Unit tests cho RegisterUserCommandHandler.
|
||||
/// </summary>
|
||||
public class RegisterUserCommandHandlerTests
|
||||
{
|
||||
private readonly Mock<UserManager<ApplicationUser>> _userManagerMock;
|
||||
private readonly Mock<ILogger<RegisterUserCommandHandler>> _loggerMock;
|
||||
private readonly RegisterUserCommandHandler _handler;
|
||||
|
||||
public RegisterUserCommandHandlerTests()
|
||||
{
|
||||
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||
_userManagerMock = new Mock<UserManager<ApplicationUser>>(
|
||||
store.Object, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
_loggerMock = new Mock<ILogger<RegisterUserCommandHandler>>();
|
||||
_handler = new RegisterUserCommandHandler(_userManagerMock.Object, _loggerMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithValidData_ShouldCreateUser()
|
||||
{
|
||||
// Arrange
|
||||
var command = new RegisterUserCommand(
|
||||
"test@example.com",
|
||||
"Password123!",
|
||||
"John",
|
||||
"Doe");
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.FindByEmailAsync(command.Email))
|
||||
.ReturnsAsync((ApplicationUser?)null);
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
|
||||
.ReturnsAsync(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(command.Email, result.Email);
|
||||
Assert.NotEqual(Guid.Empty, result.UserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WithExistingEmail_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new RegisterUserCommand(
|
||||
"existing@example.com",
|
||||
"Password123!",
|
||||
"John",
|
||||
"Doe");
|
||||
|
||||
var existingUser = new ApplicationUser(command.Email, "Existing", "User");
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.FindByEmailAsync(command.Email))
|
||||
.ReturnsAsync(existingUser);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_handler.Handle(command, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_WhenCreateFails_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new RegisterUserCommand(
|
||||
"test@example.com",
|
||||
"weak",
|
||||
"John",
|
||||
"Doe");
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.FindByEmailAsync(command.Email))
|
||||
.ReturnsAsync((ApplicationUser?)null);
|
||||
|
||||
_userManagerMock
|
||||
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), command.Password))
|
||||
.ReturnsAsync(IdentityResult.Failed(
|
||||
new IdentityError { Code = "PasswordTooWeak", Description = "Password is too weak" }));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_handler.Handle(command, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using Xunit;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.UnitTests.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for ApplicationUser domain entity.
|
||||
/// VI: Unit tests cho domain entity ApplicationUser.
|
||||
/// </summary>
|
||||
public class ApplicationUserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithValidData_ShouldSetProperties()
|
||||
{
|
||||
// Arrange
|
||||
var email = "test@example.com";
|
||||
var firstName = "John";
|
||||
var lastName = "Doe";
|
||||
|
||||
// Act
|
||||
var user = new ApplicationUser(email, firstName, lastName);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(email, user.Email);
|
||||
Assert.Equal(firstName, user.FirstName);
|
||||
Assert.Equal(lastName, user.LastName);
|
||||
Assert.Equal($"{firstName} {lastName}", user.FullName);
|
||||
Assert.Equal(UserStatus.PendingVerification, user.Status);
|
||||
Assert.NotEqual(default, user.CreatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyEmail_ShouldThrowException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new ApplicationUser("", "John", "Doe"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyFirstName_ShouldThrowException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new ApplicationUser("test@example.com", "", "Doe"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyLastName_ShouldThrowException()
|
||||
{
|
||||
// Arrange & Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new ApplicationUser("test@example.com", "John", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_WithPendingUser_ShouldSetStatusToActive()
|
||||
{
|
||||
// Arrange
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
|
||||
// Act
|
||||
user.Activate();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(UserStatus.Active, user.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Disable_ShouldSetStatusToDisabled()
|
||||
{
|
||||
// Arrange
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
user.Activate();
|
||||
|
||||
// Act
|
||||
user.Disable();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(UserStatus.Disabled, user.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lock_ShouldSetStatusToLocked()
|
||||
{
|
||||
// Arrange
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
user.Activate();
|
||||
|
||||
// Act
|
||||
user.Lock();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(UserStatus.Locked, user.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateProfile_WithValidData_ShouldUpdateNames()
|
||||
{
|
||||
// Arrange
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
var newFirstName = "Jane";
|
||||
var newLastName = "Smith";
|
||||
|
||||
// Act
|
||||
user.UpdateProfile(newFirstName, newLastName);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(newFirstName, user.FirstName);
|
||||
Assert.Equal(newLastName, user.LastName);
|
||||
Assert.Equal($"{newFirstName} {newLastName}", user.FullName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateProfile_WithEmptyFirstName_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
user.UpdateProfile("", "Smith"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordLogin_ShouldUpdateLastLoginAt()
|
||||
{
|
||||
// Arrange
|
||||
var user = new ApplicationUser("test@example.com", "John", "Doe");
|
||||
var beforeLogin = DateTime.UtcNow;
|
||||
|
||||
// Act
|
||||
user.RecordLogin();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(user.LastLoginAt);
|
||||
Assert.True(user.LastLoginAt >= beforeLogin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Xunit;
|
||||
using IamService.Domain.AggregatesModel.UserAggregate;
|
||||
|
||||
namespace IamService.UnitTests.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Unit tests for UserStatus enumeration.
|
||||
/// VI: Unit tests cho UserStatus enumeration.
|
||||
/// </summary>
|
||||
public class UserStatusTests
|
||||
{
|
||||
[Fact]
|
||||
public void UserStatus_Active_ShouldHaveCorrectIdAndName()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(1, UserStatus.Active.Id);
|
||||
Assert.Equal("Active", UserStatus.Active.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserStatus_Locked_ShouldHaveCorrectIdAndName()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(2, UserStatus.Locked.Id);
|
||||
Assert.Equal("Locked", UserStatus.Locked.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserStatus_Disabled_ShouldHaveCorrectIdAndName()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(3, UserStatus.Disabled.Id);
|
||||
Assert.Equal("Disabled", UserStatus.Disabled.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserStatus_PendingVerification_ShouldHaveCorrectIdAndName()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal(4, UserStatus.PendingVerification.Id);
|
||||
Assert.Equal("PendingVerification", UserStatus.PendingVerification.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAll_ShouldReturnAllStatuses()
|
||||
{
|
||||
// Act
|
||||
var statuses = UserStatus.GetAll().ToList();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(4, statuses.Count);
|
||||
Assert.Contains(UserStatus.Active, statuses);
|
||||
Assert.Contains(UserStatus.Locked, statuses);
|
||||
Assert.Contains(UserStatus.Disabled, statuses);
|
||||
Assert.Contains(UserStatus.PendingVerification, statuses);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user