[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
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:
@ -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)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user