[CLAUDE] App: YC-016 guard file-khi-duyệt KHKK theo TAY CẦM PHIẾU (slot 67 đóng)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m17s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m17s
Anh chốt "(c) THEO TAY CẦM PHIẾU": Nháp/Trả-lại (tay người soạn) ⇒ chỉ người soạn up file duyệt; Chờ-duyệt ⇒ chỉ thành viên quy trình đã pin (membership, không đòi đúng-lượt — chữ anh @S171 không thu hẹp); terminal DaDuyet/TuChoi ⇒ chặn cả hai (hồ sơ đã chốt). Guard mới EnsureApprovalAttachmentUploaderAsync (WorkflowGuards) wire vào UploadContractSigningPlanAttachmentCommandHandler — CHỈ rẽ nhánh Purpose=ApprovalAttachment; đường căn-cứ dossier giữ nguyên hành vi cũ (có control-test chặn nới guard ngoài phán quyết). Đóng lỗ đo prod @S172: drafter qua policy KeHoachKyKet.Update up được file vào phiếu không thuộc tay mình (#82 hai tầng — policy là quyền module, không phải quyền theo tay). FE 0-đổi (nút vốn chỉ trong dialog duyệt — display-layer; guard này là API-layer thật). Test 660 → 668 (+8: 3 nhánh tay × 2 chủ thể + ngoài-cuộc + terminal-2-vế + control đường-soạn). YC-016 vào sổ; YC-013 nhắc=2 (YC-015 verify 4-chân). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -1448,4 +1448,186 @@ public class ContractSigningPlanApprovalTests
|
||||
+ "là khoá cứng mọi phiếu đang pin bản đã nghỉ, đúng nhóm phiếu cần sửa nhất");
|
||||
plan.ApprovalWorkflowId.Should().Be(banMoi, "và pin không được tự đổi trong lượt sửa GhiChu");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// [YC-016 @S183 — anh chốt "(c) THEO TAY CẦM PHIẾU"] File-khi-duyệt (Purpose=ApprovalAttachment)
|
||||
// đi theo TAY cầm phiếu — 8 ca, mỗi ca 1 DbContext MỚI (khuôn (2) đầu file).
|
||||
// Guard: `ContractSigningPlanWorkflowGuards.EnsureApprovalAttachmentUploaderAsync`.
|
||||
// Control (ca 8): đường căn-cứ (DossierScan) KHÔNG bị rào mới đụng — hành vi cũ giữ nguyên.
|
||||
// ===================================================================
|
||||
|
||||
private sealed class FakeStorage : SolutionErp.Application.Common.Interfaces.IFileStorage
|
||||
{
|
||||
public Task<string> SaveAsync(string relativePath, Stream content, CancellationToken ct = default)
|
||||
=> Task.FromResult(relativePath);
|
||||
public Task<Stream> OpenReadAsync(string relativePath, CancellationToken ct = default)
|
||||
=> Task.FromResult<Stream>(new MemoryStream());
|
||||
public Task DeleteAsync(string relativePath, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public bool Exists(string relativePath) => true;
|
||||
}
|
||||
|
||||
private static async Task<ContractSigningPlanAttachmentDto> UploadAsync(
|
||||
KhkkWorkflowFixture f, TestCurrentUser actor, Guid planId,
|
||||
ContractSigningPlanAttachmentPurpose purpose = ContractSigningPlanAttachmentPurpose.ApprovalAttachment)
|
||||
{
|
||||
await using var db = f.NewDb(actor);
|
||||
var handler = new UploadContractSigningPlanAttachmentCommandHandler(db, new FakeStorage(), actor);
|
||||
await using var content = new MemoryStream(new byte[] { 1, 2, 3, 4 });
|
||||
return await handler.Handle(new UploadContractSigningPlanAttachmentCommand(
|
||||
planId, DossierItemId: null, "bien-ban.pdf", "application/pdf", 4, content,
|
||||
purpose, Note: "test"), CancellationToken.None);
|
||||
}
|
||||
|
||||
private static async Task<int> CountAttachmentsAsync(KhkkWorkflowFixture f, Guid planId)
|
||||
{
|
||||
await using var db = f.NewDb(TestCurrentUser.System());
|
||||
return await db.ContractSigningPlanAttachments
|
||||
.CountAsync(a => a.ContractSigningPlanId == planId, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_Nhap_NguoiSoan_DuocUp()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/701",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 100m)]);
|
||||
|
||||
var dto = await UploadAsync(f, drafter, planId);
|
||||
|
||||
dto.Purpose.Should().Be(ContractSigningPlanAttachmentPurpose.ApprovalAttachment);
|
||||
(await CountAttachmentsAsync(f, planId)).Should().Be(1,
|
||||
"Nháp = tay người soạn ⇒ người soạn up file duyệt được (YC-016 nhánh 1)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_TraLai_NguoiSoan_DuocUp()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/702",
|
||||
ContractSigningPlanPhase.TraLai, null, null, [new LineSeed(100m, 100m)]);
|
||||
|
||||
await UploadAsync(f, drafter, planId);
|
||||
|
||||
(await CountAttachmentsAsync(f, planId)).Should().Be(1,
|
||||
"Trả lại = phiếu về tay người soạn ⇒ vẫn là tay người soạn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_ChoDuyet_NguoiSoan_409_DiaBatDong()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/703",
|
||||
ContractSigningPlanPhase.ChoDuyet, 0, 1, [new LineSeed(100m, 100m)]);
|
||||
|
||||
var act = async () => await UploadAsync(f, drafter, planId);
|
||||
|
||||
(await act.Should().ThrowAsync<ConflictException>(
|
||||
"phiếu đã rời tay người soạn — up tiếp là nhét tài liệu vào hồ sơ đang duyệt"))
|
||||
.Which.Message.Should().Contain("chờ duyệt").And.Contain("người duyệt");
|
||||
(await CountAttachmentsAsync(f, planId)).Should().Be(0, "409 thì đĩa phải bất động");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_ChoDuyet_ThanhVienQuyTrinh_DuocUp()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver1 = await AddUserAsync(f, "NV duyệt Bước 1");
|
||||
var approver2 = await AddUserAsync(f, "NV duyệt Bước 2");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver1.Id]], [[approver2.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/704",
|
||||
ContractSigningPlanPhase.ChoDuyet, 0, 1, [new LineSeed(100m, 100m)]);
|
||||
|
||||
// approver2 thuộc BƯỚC SAU (chưa tới lượt) — membership là đủ, KHÔNG đòi đúng-lượt
|
||||
// (chữ anh @S171 không thu hẹp về acting-level; guard doc-comment khai rõ).
|
||||
await UploadAsync(f, As(approver2, "DeptManager"), planId);
|
||||
|
||||
(await CountAttachmentsAsync(f, planId)).Should().Be(1,
|
||||
"thành viên quy trình (kể cả bước sau) up được khi phiếu đang chờ duyệt");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_Nhap_NguoiDuyet_409()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/705",
|
||||
ContractSigningPlanPhase.DangSoanThao, null, null, [new LineSeed(100m, 100m)]);
|
||||
|
||||
var act = async () => await UploadAsync(f, As(approver, "DeptManager"), planId);
|
||||
|
||||
(await act.Should().ThrowAsync<ConflictException>(
|
||||
"Nháp = phiếu chưa trình — người duyệt chưa có việc gì với nó"))
|
||||
.Which.Message.Should().Contain("tay người soạn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_ChoDuyet_NgoaiCuoc_409()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var outsider = await AddUserAsync(f, "Người ngoài quy trình");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/706",
|
||||
ContractSigningPlanPhase.ChoDuyet, 0, 1, [new LineSeed(100m, 100m)]);
|
||||
|
||||
// outsider đã QUA policy module từ controller (ca đo prod @S172) — guard là rào CUỐI.
|
||||
var act = async () => await UploadAsync(f, As(outsider, "DeptManager"), planId);
|
||||
|
||||
(await act.Should().ThrowAsync<ConflictException>(
|
||||
"đây chính là ca đo prod @S172: qua policy nhưng KHÔNG thuộc tay nào"))
|
||||
.Which.Message.Should().Contain("người duyệt trong quy trình");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadDuyet_Terminal_ChanCaHai()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/707",
|
||||
ContractSigningPlanPhase.DaDuyet, null, null, [new LineSeed(100m, 100m)]);
|
||||
|
||||
var actDrafter = async () => await UploadAsync(f, drafter, planId);
|
||||
var actApprover = async () => await UploadAsync(f, As(approver, "DeptManager"), planId);
|
||||
|
||||
(await actDrafter.Should().ThrowAsync<ConflictException>())
|
||||
.Which.Message.Should().Contain("đã kết thúc");
|
||||
(await actApprover.Should().ThrowAsync<ConflictException>(
|
||||
"terminal = không ai cầm phiếu — hồ sơ đã chốt, cả hai đều bị chặn"))
|
||||
.Which.Message.Should().Contain("đã kết thúc");
|
||||
(await CountAttachmentsAsync(f, planId)).Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadCanCu_DuongSoan_KhongBiRaoMoiDung()
|
||||
{
|
||||
using var f = new KhkkWorkflowFixture();
|
||||
var drafter = As(await AddUserAsync(f, "Người soạn"), "Drafter");
|
||||
var approver = await AddUserAsync(f, "NV duyệt");
|
||||
var wf = await SeedWorkflowAsync(f, "KHKK-N1", steps: [[[approver.Id]]]);
|
||||
var planId = await SeedPlanAsync(f, drafter, wf, "KHKK/2026/708",
|
||||
ContractSigningPlanPhase.ChoDuyet, 0, 1, [new LineSeed(100m, 100m)]);
|
||||
|
||||
// CONTROL: purpose = DossierScan (đường căn-cứ, KHÔNG phải file-khi-duyệt) — guard mới
|
||||
// CHỈ rẽ nhánh ApprovalAttachment ⇒ hành vi cũ (cho qua) GIỮ NGUYÊN. Ca này chết nếu
|
||||
// ai đó nới guard sang mọi purpose "cho chắc" — đó là đổi hành vi NGOÀI phán quyết YC-016.
|
||||
await UploadAsync(f, drafter, planId, ContractSigningPlanAttachmentPurpose.DossierScan);
|
||||
|
||||
(await CountAttachmentsAsync(f, planId)).Should().Be(1,
|
||||
"đường căn-cứ giữ nguyên hành vi cũ — YC-016 chỉ phán về file-khi-duyệt");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user