[CLAUDE] PurchaseEvaluation · Tests: phiếu nháp riêng tư người tạo (chặn leak role + V2-approver)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m56s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m56s
Màn hình CEO ở "chỗ thao tác" hiện bản nháp / phiếu chưa xong của NGƯỜI KHÁC (anh Kiệt FDC). Phiếu nháp (DangSoanThao) lộ qua 2 đường: cond #2 role-eligible (Drafter/DeptManager → DangSoanThao, mọi Drafter thấy nháp nhau) + cond #3 V2-approver (workflow pin lúc TẠO phiếu nên approver như CEO thấy mọi nháp trước khi gửi duyệt = leak chính). Luật mới: nháp = RIÊNG TƯ người tạo (DrafterUserId) + Admin thấy hết. 4 chỗ: - GetEligiblePhases: bỏ DangSoanThao khỏi role-eligible (cond #2). - Inbox (mảng admin): bỏ DangSoanThao — nháp khỏi Inbox hẳn kể cả admin (anh Kiệt chốt). - List query cond #3: guard != DangSoanThao (approver không thấy nháp). - Detail authz: guard != DangSoanThao cho V2-approver (không mở nháp người khác bằng link). Phiếu ĐÃ gửi duyệt giữ nguyên (approver vẫn thấy). Phần admin-screen tinh chỉnh sâu để sau. +PeDraftVisibilityTests (2 test, test-before security): owner thấy nháp mình · drafter khác không · CEO/V2-approver không · admin thấy hết · phiếu đã gửi vẫn hiện (no regression). Full suite 419→421 PASS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,121 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SolutionErp.Application.PurchaseEvaluations;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Master;
|
||||
using SolutionErp.Domain.PurchaseEvaluations;
|
||||
using SolutionErp.Infrastructure.Tests.Common;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Application;
|
||||
|
||||
// [S89 anh Kiệt FDC] Phiếu NHÁP (DangSoanThao) = RIÊNG TƯ người tạo (+ Admin thấy hết).
|
||||
// Trước đây "chỗ thao tác" màn hình CEO hiện nháp của người khác qua 2 đường:
|
||||
// cond #2 role-eligible (Drafter/DeptManager → DangSoanThao) — mọi Drafter thấy nháp nhau;
|
||||
// cond #3 V2-approver — CEO là approver trên workflow → thấy MỌI nháp (workflow pin lúc
|
||||
// TẠO phiếu, chưa gửi duyệt) = leak chính trên màn hình CEO.
|
||||
// Fix: nháp chỉ thấy/mở qua DrafterUserId ownership (+ Admin bypass). Phiếu ĐÃ gửi duyệt
|
||||
// giữ nguyên (approver vẫn thấy). Test-before security guard (rules §7 — critical authz).
|
||||
// Dùng IdentityFixture: ApproverUserId/DrafterUserId là FK → cần User rows thật.
|
||||
public class PeDraftVisibilityTests
|
||||
{
|
||||
private static (Guid projectId, Guid wfId) SeedProjectAndWorkflow(
|
||||
TestApplicationDbContext db, Guid approverUserId)
|
||||
{
|
||||
var project = new Project { Id = Guid.NewGuid(), Code = "VISP", Name = "Dự án Visibility" };
|
||||
db.Projects.Add(project);
|
||||
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Code = "QT-VIS-V2",
|
||||
Version = 1,
|
||||
ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
|
||||
Name = "QT visibility test",
|
||||
IsActive = true,
|
||||
IsUserSelectable = true,
|
||||
};
|
||||
var step = new ApprovalWorkflowStep { ApprovalWorkflowId = wf.Id, Order = 1, Name = "Bước 1" };
|
||||
step.Levels.Add(new ApprovalWorkflowLevel
|
||||
{
|
||||
ApprovalWorkflowStepId = step.Id,
|
||||
Order = 1,
|
||||
Name = "Cấp 1",
|
||||
ApproverUserId = approverUserId, // CEO-like: approver trên workflow này
|
||||
});
|
||||
wf.Steps.Add(step);
|
||||
db.ApprovalWorkflows.Add(wf);
|
||||
return (project.Id, wf.Id);
|
||||
}
|
||||
|
||||
private static Guid SeedPe(
|
||||
TestApplicationDbContext db, Guid projectId, Guid wfId, Guid drafterId,
|
||||
PurchaseEvaluationPhase phase, string code)
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
db.PurchaseEvaluations.Add(new PurchaseEvaluation
|
||||
{
|
||||
Id = id,
|
||||
MaPhieu = code,
|
||||
Type = PurchaseEvaluationType.DuyetNcc,
|
||||
Phase = phase,
|
||||
TenGoiThau = $"Gói {code}",
|
||||
ProjectId = projectId,
|
||||
DrafterUserId = drafterId,
|
||||
ApprovalWorkflowId = wfId, // workflow pin lúc tạo (kể cả nháp) — Features.cs:149
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
private static async Task<List<Guid>> ListVisibleIds(
|
||||
TestApplicationDbContext db, Guid? userId, params string[] roles)
|
||||
{
|
||||
var cu = new TestCurrentUser { UserId = userId, Roles = roles };
|
||||
var handler = new ListPurchaseEvaluationsQueryHandler(db, cu);
|
||||
var res = await handler.Handle(
|
||||
new ListPurchaseEvaluationsQuery { PageSize = 100 }, CancellationToken.None);
|
||||
return res.Items.Select(x => x.Id).ToList();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Draft_VisibleToOwnerOnly_NotOtherDrafter_NotApprover_AdminSeesAll()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
|
||||
var drafterA = await fix.CreateUserAsync("draftera@test.local", "Drafter A", null, new[] { AppRoles.Drafter });
|
||||
var drafterB = await fix.CreateUserAsync("drafterb@test.local", "Drafter B", null, new[] { AppRoles.Drafter });
|
||||
var ceo = await fix.CreateUserAsync("ceo@test.local", "CEO Director", null, new[] { AppRoles.Director });
|
||||
|
||||
var (projectId, wfId) = SeedProjectAndWorkflow(db, approverUserId: ceo.Id);
|
||||
var draft = SeedPe(db, projectId, wfId, drafterA.Id,
|
||||
PurchaseEvaluationPhase.DangSoanThao, "PE-DRAFT");
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
// Owner thấy nháp của CHÍNH MÌNH (cond #1 ownership).
|
||||
(await ListVisibleIds(db, drafterA.Id, AppRoles.Drafter)).Should().Contain(draft);
|
||||
// Drafter KHÁC (cùng role) KHÔNG thấy nháp của A (cond #2 leak ĐÃ vá).
|
||||
(await ListVisibleIds(db, drafterB.Id, AppRoles.Drafter)).Should().NotContain(draft);
|
||||
// V2-approver (CEO trên workflow) KHÔNG thấy nháp (cond #3 leak ĐÃ vá — đúng màn hình CEO).
|
||||
(await ListVisibleIds(db, ceo.Id, AppRoles.Director)).Should().NotContain(draft);
|
||||
// Admin thấy hết (anh Kiệt: "full admin với thấy hết").
|
||||
(await ListVisibleIds(db, Guid.NewGuid(), AppRoles.Admin)).Should().Contain(draft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmittedPhieu_StillVisibleToApprover_NotRegressed()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
|
||||
var drafterA = await fix.CreateUserAsync("draftera2@test.local", "Drafter A2", null, new[] { AppRoles.Drafter });
|
||||
var ceo = await fix.CreateUserAsync("ceo2@test.local", "CEO2 Director", null, new[] { AppRoles.Director });
|
||||
|
||||
var (projectId, wfId) = SeedProjectAndWorkflow(db, approverUserId: ceo.Id);
|
||||
// Phiếu ĐÃ gửi duyệt (ChoDuyet) — approver PHẢI vẫn thấy (regression guard: chỉ
|
||||
// chặn lúc CÒN nháp, không chặn sau khi đã gửi duyệt).
|
||||
var submitted = SeedPe(db, projectId, wfId, drafterA.Id,
|
||||
PurchaseEvaluationPhase.ChoDuyet, "PE-SUB");
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
(await ListVisibleIds(db, ceo.Id, AppRoles.Director)).Should().Contain(submitted);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user