[CLAUDE] Tests: +3 regression bug-class S42 — UpdateDraft giữ Quy trình duyệt (anh Kiệt FDC)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m38s

Test-after ROOT FIX S85 (commit 1263c0d — UpdateDraftHandler null-safe ApprovalWorkflowId).
PeUpdateDraftWorkflowPreserveTests: (1) null→PRESERVE W (reproduce bug-class S42), (2) non-null→
CHANGE W2, (3) bonus BudgetPeriodAmount null-safe đối chứng. Full suite 371→374 PASS, 0 fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-23 15:32:18 +07:00
parent 49ac0868db
commit 5349623347

View File

@ -0,0 +1,181 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.PurchaseEvaluations;
using SolutionErp.Domain.ApprovalWorkflowsV2;
using SolutionErp.Domain.PurchaseEvaluations;
using SolutionErp.Infrastructure.Tests.Common;
namespace SolutionErp.Infrastructure.Tests.Application;
// S85 TEST-AFTER cho ROOT FIX bug-class S42 (anh Kiệt FDC UAT R5).
// BUG (đã fix em-main): form Sửa header CLEAR Quy trình duyệt của phiếu.
// Trước: entity.ApprovalWorkflowId = request.ApprovalWorkflowId (ABSOLUTE-SET →
// FE form Sửa KHÔNG gửi field → request.ApprovalWorkflowId = null → CLEAR WF
// → kết submit "chưa chọn quy trình" = whack-a-mole bug-class S42).
// Sau (fix): if (request.ApprovalWorkflowId is not null)
// entity.ApprovalWorkflowId = request.ApprovalWorkflowId;
// → client không gửi (null) GIỮ WF cũ; CHỈ đổi khi FE gửi giá trị thật.
//
// Test theo CODE (single source of truth, S34) — PurchaseEvaluationFeatures.cs:
// - Line 285-286: null-safe guard (giống BudgetPeriodAmount/ExpectedRemaining/WorkItemId).
// - Line 253-266: KHI awId != entity.ApprovalWorkflowId (tức CÓ đổi) → validate WF
// tồn tại + ApplicableType khớp Type phiếu. PRESERVE case (request null) SHORT-CIRCUIT
// trước validate đó (vì `is not null` false) → KHÔNG cần seed WF cho case PRESERVE.
//
// Handler: UpdatePurchaseEvaluationDraftCommandHandler(IApplicationDbContext db, ICurrentUser).
// Phase guard: chỉ DangSoanThao / TraLai. Changelog Add → cần IdentityFixture (SaveChanges).
public class PeUpdateDraftWorkflowPreserveTests
{
private sealed class FakeCurrentUser : ICurrentUser
{
public Guid? UserId { get; init; } = Guid.NewGuid();
public string? Email { get; init; }
public string? FullName { get; init; } = "Người soạn";
public IReadOnlyList<string> Roles { get; init; } = System.Array.Empty<string>();
public bool IsAuthenticated => UserId is not null;
}
// Seed 1 PE Nháp với ApprovalWorkflowId gán sẵn (mô phỏng phiếu ĐÃ chọn WF).
private static PurchaseEvaluation BuildPe(
Guid? approvalWorkflowId,
decimal? budgetPeriodAmount = null,
string code = "PE-WFP-001")
=> new()
{
Id = Guid.NewGuid(),
Type = PurchaseEvaluationType.DuyetNcc,
Phase = PurchaseEvaluationPhase.DangSoanThao,
MaPhieu = code,
TenGoiThau = "Gói thầu test",
ProjectId = Guid.NewGuid(),
DrafterUserId = Guid.NewGuid(),
ApprovalWorkflowId = approvalWorkflowId,
BudgetPeriodAmount = budgetPeriodAmount,
};
// Seed 1 ApprovalWorkflow V2 thật, ApplicableType=DuyetNcc (khớp PE Type) để pass
// validate block khi đổi WF (CHANGE case). Trả về Id.
private static async Task<Guid> SeedWorkflowAsync(TestApplicationDbContext db, string code)
{
var wf = new ApprovalWorkflow
{
Id = Guid.NewGuid(),
Code = code,
Version = 1,
ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
Name = code,
IsActive = true,
IsUserSelectable = true,
};
db.ApprovalWorkflows.Add(wf);
await db.SaveChangesAsync(CancellationToken.None);
return wf.Id;
}
// ============================================================
// CASE 1 — PRESERVE (cốt lõi bug-class S42, reproduce + verify fix)
// ============================================================
[Fact]
public async Task UpdateDraft_NullApprovalWorkflowId_PreservesExistingWorkflow()
{
// Phiếu Nháp CÓ ApprovalWorkflowId=W → UpdateDraft với ApprovalWorkflowId=null
// (FE form Sửa header KHÔNG gửi field) → entity.ApprovalWorkflowId PHẢI VẪN == W.
// TRƯỚC FIX: absolute-set null → W bị clear (= bug). SAU FIX: null-safe → giữ W.
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
// PE.ApprovalWorkflowId là FK Restrict → seed WF thật (case này request null nên
// KHÔNG đi vào validate block, nhưng PE seed vẫn cần FK hợp lệ).
var w = await SeedWorkflowAsync(db, "QT-DN-V2-PRESERVE");
var pe = BuildPe(approvalWorkflowId: w, code: "PE-WFP-PRESERVE");
db.PurchaseEvaluations.Add(pe);
await db.SaveChangesAsync(CancellationToken.None);
// Update header KHÔNG kèm ApprovalWorkflowId (default null) — các field khác có giá trị.
var cmd = new UpdatePurchaseEvaluationDraftCommand(
pe.Id,
TenGoiThau: "Gói thầu đã sửa",
DiaDiem: "Công trường A",
MoTa: "Mô tả mới",
PaymentTerms: "30 ngày"); // ApprovalWorkflowId / WorkItemId / Budget* để default null
var handler = new UpdatePurchaseEvaluationDraftCommandHandler(db, new FakeCurrentUser());
await handler.Handle(cmd, CancellationToken.None);
var reload = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
reload.ApprovalWorkflowId.Should().Be(w,
"form Sửa KHÔNG gửi approvalWorkflowId (null) → null-safe guard GIỮ WF cũ (root fix bug-class S42)");
// Sanity: field text vẫn cập nhật bình thường (handler vẫn chạy, không phải no-op).
reload.TenGoiThau.Should().Be("Gói thầu đã sửa");
reload.DiaDiem.Should().Be("Công trường A");
}
// ============================================================
// CASE 2 — CHANGE (đổi WF khi CÓ gửi giá trị thật)
// ============================================================
[Fact]
public async Task UpdateDraft_NonNullApprovalWorkflowId_ChangesToNewWorkflow()
{
// Phiếu có WF=W1 → UpdateDraft với ApprovalWorkflowId=W2 (W2 != null) →
// entity.ApprovalWorkflowId == W2. CHO PHÉP đổi khi FE gửi giá trị.
// W2 phải là WF thật ApplicableType=DuyetNcc để pass validate block (line 253-266).
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
var w1 = await SeedWorkflowAsync(db, "QT-DN-V2-W1");
var w2 = await SeedWorkflowAsync(db, "QT-DN-V2-W2");
var pe = BuildPe(approvalWorkflowId: w1, code: "PE-WFP-CHANGE");
db.PurchaseEvaluations.Add(pe);
await db.SaveChangesAsync(CancellationToken.None);
var cmd = new UpdatePurchaseEvaluationDraftCommand(
pe.Id,
TenGoiThau: "Gói thầu test",
DiaDiem: null,
MoTa: null,
PaymentTerms: null,
ApprovalWorkflowId: w2); // FE GỬI W2 → đổi
var handler = new UpdatePurchaseEvaluationDraftCommandHandler(db, new FakeCurrentUser());
await handler.Handle(cmd, CancellationToken.None);
var reload = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
reload.ApprovalWorkflowId.Should().Be(w2,
"FE gửi ApprovalWorkflowId không-null → cho phép đổi WF (null-safe guard chỉ chặn null, không chặn đổi)");
}
// ============================================================
// CASE 3 (bonus) — mirror null-safe field BudgetPeriodAmount đã có (S61)
// ============================================================
[Fact]
public async Task UpdateDraft_NullBudgetPeriodAmount_PreservesExistingBudget()
{
// Cùng convention null-safe S42 (line 289-290): phiếu có BudgetPeriodAmount=100tr,
// UpdateDraft không gửi (null) → GIỮ 100tr (KHÔNG null-hóa). Đối chứng cho thấy
// ApprovalWorkflowId nay đã CÙNG pattern với Budget* (đồng nhất bug-class S42).
using var fix = new IdentityFixture();
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
const decimal budget = 100_000_000m;
var w = await SeedWorkflowAsync(db, "QT-DN-V2-BUDGET"); // FK Restrict → WF thật
var pe = BuildPe(approvalWorkflowId: w, budgetPeriodAmount: budget, code: "PE-WFP-BUDGET");
db.PurchaseEvaluations.Add(pe);
await db.SaveChangesAsync(CancellationToken.None);
var cmd = new UpdatePurchaseEvaluationDraftCommand(
pe.Id,
TenGoiThau: "Gói thầu test",
DiaDiem: null,
MoTa: null,
PaymentTerms: null); // BudgetPeriodAmount default null → giữ
var handler = new UpdatePurchaseEvaluationDraftCommandHandler(db, new FakeCurrentUser());
await handler.Handle(cmd, CancellationToken.None);
var reload = await db.PurchaseEvaluations.AsNoTracking().FirstAsync(x => x.Id == pe.Id);
reload.BudgetPeriodAmount.Should().Be(budget,
"BudgetPeriodAmount null-safe (S61) — null request GIỮ giá trị cũ; cùng pattern ApprovalWorkflowId (S85)");
}
}