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:
Ho Ngoc Hai
2026-01-12 18:22:47 +07:00
parent 435e5c2dfa
commit 079b24f683
10 changed files with 787 additions and 25 deletions

11
NOTE.MD
View File

@@ -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ể

View File

@@ -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();

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}