[CLAUDE] Tests: Chunk E6 — 6 test 2-stage approval (PE) + IdentityFixture helper

Đóng "Tests Phase 3 mini cần UserManager DI helper" defer từ session 8.

IdentityFixture (Common/IdentityFixture.cs):
- Setup ServiceProvider với Identity stack đầy đủ:
  - DbContext SQLite shared connection
  - AddIdentityCore<User> + AddRoles<Role> + AddEntityFrameworkStores
- Single shared scope cho fixture lifetime → DbContext + UserManager đồng instance
- Helper CreateUserAsync(email, name, deptId, roles, canBypassReview)
- Note: dùng Role custom (không phải IdentityRole<Guid>) để match
  ApplicationDbContext : IdentityDbContext<User, Role, Guid>

6 test PE 2-stage logic (Services/PeTwoStageApprovalTests.cs):
- NV_Review_Blocks_Phase_Transition (đóng bug anh Kiệt — chính xác)
- TPB_Confirm_After_NV_Review_Allows_Transition (happy path 2-stage)
- NV_With_BypassReview_Allows_Transition_With_IsBypassed_True (bypass NV)
- Admin_Skips_TwoStage_Logic_Entirely (admin bypass)
- Reject_Sets_RejectedFromPhase_And_Forces_DangSoanThao (smart reject)
- Resume_After_Reject_Jumps_Back_To_RejectedPhase (jump-back logic)

Stub FakeNotificationService — best effort path không cần verify.

Note: tests cho Contract + Budget 2-stage skip — logic identical PE, ROI thấp.
Pattern PeTwoStageApprovalTests reusable nếu cần test riêng tương lai.

Total: 54 Domain + 29 Infra (17 codegen + 6 PE WF Application + 6 PE 2-stage)
= **83 test pass** (+6 mới).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-04 13:52:43 +07:00
parent 1fc439b978
commit 8353fe87c0
2 changed files with 370 additions and 0 deletions

View File

@ -0,0 +1,113 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Identity;
using SolutionErp.Infrastructure.Persistence;
namespace SolutionErp.Infrastructure.Tests.Common;
// Identity-aware fixture cho tests cần UserManager + RoleManager.
//
// Tại sao tách khỏi SqliteDbFixture?
// - SqliteDbFixture chỉ EF + DbContext (đủ cho code generator tests).
// - Service tests cần Identity stack: UserManager.FindByIdAsync, GetRolesAsync,
// CreateAsync, AddToRolesAsync — phụ thuộc IUserStore + IRoleStore + hashers
// + validators registered qua AddIdentityCore + AddRoles + AddEntityFrameworkStores.
// - Single connection mỗi fixture → DB persists across UserManager.Save calls.
//
// Pattern: ServiceProvider build từ AddIdentityCore. Test gọi GetRequired để
// resolve UserManager, DbContext, etc. EnsureCreated() build schema từ model
// (skip migrations vì test isolated).
public sealed class IdentityFixture : IDisposable
{
private readonly SqliteConnection _connection;
private readonly ServiceProvider _root;
public IServiceProvider Services { get; } // scoped to single test scope (shared across tests in class)
public IdentityFixture()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
var services = new ServiceCollection();
services.AddLogging();
// Manual options + factory để inject TestApplicationDbContext qua type
// ApplicationDbContext (Identity EF stores expect base type).
var connection = _connection;
services.AddScoped<ApplicationDbContext>(_ =>
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.EnableSensitiveDataLogging()
.Options;
return new TestApplicationDbContext(options);
});
services.AddScoped<TestApplicationDbContext>(sp =>
(TestApplicationDbContext)sp.GetRequiredService<ApplicationDbContext>());
services.AddScoped<IApplicationDbContext>(sp =>
sp.GetRequiredService<TestApplicationDbContext>());
services.AddIdentityCore<User>(opt =>
{
opt.Password.RequireDigit = false;
opt.Password.RequireLowercase = false;
opt.Password.RequireUppercase = false;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequiredLength = 4;
})
.AddRoles<Role>()
.AddEntityFrameworkStores<ApplicationDbContext>();
_root = services.BuildServiceProvider();
Services = _root.CreateScope().ServiceProvider;
var ctx = Services.GetRequiredService<TestApplicationDbContext>();
ctx.Database.EnsureCreated();
}
// Helper: tạo user + assign roles + gán DepartmentId. Reuse trong nhiều test.
public async Task<User> CreateUserAsync(
string email,
string fullName,
Guid? departmentId,
string[] roles,
bool canBypassReview = false)
{
var um = Services.GetRequiredService<UserManager<User>>();
var rm = Services.GetRequiredService<RoleManager<Role>>();
// Ensure roles exist (idempotent).
foreach (var role in roles)
{
if (!await rm.RoleExistsAsync(role))
await rm.CreateAsync(new Role { Id = Guid.NewGuid(), Name = role });
}
var user = new User
{
Id = Guid.NewGuid(),
UserName = email,
Email = email,
EmailConfirmed = true,
FullName = fullName,
DepartmentId = departmentId,
CanBypassReview = canBypassReview,
IsActive = true,
};
var created = await um.CreateAsync(user, "Test@123");
if (!created.Succeeded)
throw new InvalidOperationException("CreateUserAsync failed: " + string.Join(",", created.Errors.Select(e => e.Description)));
if (roles.Length > 0)
await um.AddToRolesAsync(user, roles);
return user;
}
public void Dispose()
{
_root.Dispose();
_connection.Dispose();
}
}