[CLAUDE] App+Api+FE-User+FE-Admin+Tests: W2 KHKK — CRUD phieu nhap + can cu b.8-9 + FE 3 page x2 app + 12 fix reviewer
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m56s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m56s
- BE mega-file ContractSigningPlanFeatures (Create 4-rao + auto-Lines snapshot per-winner + MaKeHoach @CREATE + UpdateDraft null-safe + Get/List/inbox-precompute-V2/deleted + Dossier + Attachments + Delete allow-list M6 + picker) + Controller 15 action policy per-action 15/15 (KeHoachKyKet.*) - FE pages/khkk 3 page + types x2 app SHA-pair + 4-place (6 leaf Khkk_* het coming-soon; WfView: user=matrix?type=10 [whitelist +10] / admin=Designer deep-link); root KeHoachKyKet -> /khkk/list - Reviewer adversarial: 12 finding (3 CRITICAL dut-2-bo FE-BE: route picker + peId body + dossier planId; 2 MAJOR security: detail+download 0 rao -> EnsureCanViewAsync mirror PE S89 nhap-rieng-tu; PUT-vs-POST de-ban-sao; peTenGoiThau; winnerSupplierNames; Content-Disposition RFC6266; LEFT-join Projects) -> 11 FIXED + 1 GIU (F-10 ke thua khuon PE, bit dong bo sau) - tests: +8 (574 total 0 fail) — authz reflection dong + Create/Delete/guard/khong-dot-ma-khi-409 + fixture-co-interceptor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Routing; // HttpMethodAttribute — đọc verb thật của action (W2 KHKK)
|
||||
using SolutionErp.Api.Controllers;
|
||||
using SolutionErp.Domain.Identity; // MenuKeys — chốt policy name có thật (F-1b S155)
|
||||
|
||||
@ -321,4 +322,121 @@ public class AuthorizePolicyRegressionTests
|
||||
"GET my-dashboard KHÔNG được gate Policy — mỗi user chỉ thấy số của chính mình. " +
|
||||
"Thêm policy sẽ khóa dashboard cá nhân của role không có Reports.Read (S118 giữ mở có chủ đích).");
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// [W2 KHKK — S161 2026-07-29] ContractSigningPlansController (GĐ2 "Kế hoạch ký kết HĐ").
|
||||
//
|
||||
// Shape KHÁC 3 controller ở trên — đừng ép về cùng một kiểu:
|
||||
// class-level CÓ Policy `KeHoachKyKet.Read` (mọi endpoint tối thiểu Read) + per-action
|
||||
// override GHI ĐỦ 15/15 action, KỂ CẢ action chỉ-đọc (viết lại Read cho tường minh).
|
||||
// Lý do ghi thành lời ở `ContractSigningPlansController.cs:12-19`.
|
||||
//
|
||||
// Test này quét TOÀN BỘ action (không liệt tên tay) vì rủi ro thật của module mới là
|
||||
// action thứ 16 thêm sau này QUÊN attribute — liệt tay thì action mới không bao giờ bị soi.
|
||||
// 3 lớp răng, mỗi lớp bắt một hỏng khác nhau:
|
||||
// (a) THIẾU attribute → endpoint tụt xuống class-level Read ⇒ ai đọc được cũng GHI được;
|
||||
// (b) SAI tên policy → `Program.cs` chỉ sinh policy cho key ∈ `MenuKeys.All`; tên lạ
|
||||
// = 500 lúc chạy (KHÔNG phải 403) — attribute vẫn "trông đúng";
|
||||
// (c) verb ghi mà policy Read → 403 không bao giờ xảy ra: user chỉ-xem GHI được vào phiếu.
|
||||
// ===================================================================
|
||||
|
||||
/// Bộ action của controller = method public khai TRỰC TIẾP trên class + có `[HttpXxx]`.
|
||||
/// (Lọc theo HttpMethodAttribute để bỏ property/ctor/record lồng bên trong.)
|
||||
private static (string Name, string Verb, AuthorizeAttribute? Authz)[] GetActionsWithVerb(Type controllerType)
|
||||
=> controllerType
|
||||
.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
.Select(m => new
|
||||
{
|
||||
m.Name,
|
||||
Http = m.GetCustomAttributes<HttpMethodAttribute>(inherit: false).FirstOrDefault(),
|
||||
Authz = m.GetCustomAttributes<AuthorizeAttribute>(inherit: false).FirstOrDefault(),
|
||||
})
|
||||
.Where(x => x.Http is not null)
|
||||
.Select(x => (x.Name, x.Http!.HttpMethods.First().ToUpperInvariant(), x.Authz))
|
||||
.ToArray();
|
||||
|
||||
[Fact]
|
||||
public void ContractSigningPlansController_EveryWriteEndpoint_HasAuthorizePolicy()
|
||||
{
|
||||
var type = typeof(ContractSigningPlansController);
|
||||
|
||||
// ---- (0) Class-level: 2 tầng — tầng 1 CÓ Policy Read (cố ý, khác ApprovalWorkflowsV2).
|
||||
var classAttr = GetClassLevelAuthorize(type);
|
||||
classAttr.Should().NotBeNull("controller phải có [Authorize] class-level chặn anonymous");
|
||||
classAttr!.Policy.Should().Be("KeHoachKyKet.Read",
|
||||
"tầng 1 của authz 2 tầng: MỌI endpoint KHKK tối thiểu phải có quyền đọc menu này. " +
|
||||
"Đây KHÔNG phải bẫy gotcha #44 (silent 403) vì module có menu key riêng — user được " +
|
||||
"cấp KeHoachKyKet mới thấy màn hình này, không phải mọi Drafter như ApprovalWorkflowsV2.");
|
||||
|
||||
var actions = GetActionsWithVerb(type);
|
||||
|
||||
// ---- Neo chống VACUOUS: foreach trên mảng rỗng luôn xanh. 15 action đo từ đĩa
|
||||
// 2026-07-29 (5 GET đọc + POST/PUT/DELETE phiếu + 3 dossier-item + 4 attachment).
|
||||
actions.Should().HaveCountGreaterThanOrEqualTo(15,
|
||||
"quét được {0} action — dưới 15 nghĩa là phép quét hỏng (đổi BindingFlags/attribute) " +
|
||||
"chứ không phải controller teo lại; lúc đó mọi assert dưới đây thành vô nghĩa",
|
||||
actions.Length);
|
||||
|
||||
// Verb GHI = mọi thứ không phải GET/HEAD. Không hardcode danh sách endpoint.
|
||||
var writeVerbs = new[] { "POST", "PUT", "PATCH", "DELETE" };
|
||||
|
||||
foreach (var (name, verb, authz) in actions)
|
||||
{
|
||||
// ---- (a) MỌI action phải có attribute riêng (kể cả GET — luật của module này).
|
||||
authz.Should().NotBeNull(
|
||||
"action {0} ({1}) thiếu [Authorize(Policy = ...)] — nó sẽ tụt xuống class-level " +
|
||||
"`KeHoachKyKet.Read`, nghĩa là ai ĐỌC được cũng GHI được (gotcha #82)", name, verb);
|
||||
authz!.Policy.Should().NotBeNullOrEmpty(
|
||||
"action {0} có [Authorize] TRẦN — `[Authorize]` không Policy = mọi user đăng nhập " +
|
||||
"lọt qua tầng 2 (bài F-1b S155)", name);
|
||||
|
||||
var parts = authz.Policy!.Split('.');
|
||||
parts.Should().HaveCount(2,
|
||||
"policy \"{0}\" của {1} phải đúng dạng {{menuKey}}.{{action}}", authz.Policy, name);
|
||||
|
||||
// ---- (b) Tên policy phải là tên CÓ THẬT trong bộ sinh policy của Program.cs.
|
||||
parts[0].Should().Be(MenuKeys.KeHoachKyKet,
|
||||
"action {0} phải gate bằng menu key của CHÍNH module này — Δ W1 chốt prefix " +
|
||||
"`KeHoachKyKet` (spec cũ ghi `ContractSigningPlans`, đã bỏ); gate nhầm key module " +
|
||||
"khác = cấp quyền chéo", name);
|
||||
MenuKeys.All.Should().Contain(parts[0],
|
||||
"policy \"{0}\" của {1} trỏ menu key KHÔNG có trong MenuKeys.All ⇒ Program.cs không " +
|
||||
"sinh policy đó ⇒ 500 lúc chạy chứ không phải 403", authz.Policy, name);
|
||||
MenuKeys.Actions.Should().Contain(parts[1],
|
||||
"policy \"{0}\" của {1} dùng action không có trong MenuKeys.Actions", authz.Policy, name);
|
||||
|
||||
// ---- (c) Verb GHI KHÔNG được gate bằng Read (403 sẽ không bao giờ xảy ra).
|
||||
if (writeVerbs.Contains(verb))
|
||||
{
|
||||
parts[1].Should().NotBe("Read",
|
||||
"{0} là {1} (ghi) mà chỉ gate `Read` ⇒ user chỉ-xem VẪN ghi được vào phiếu", name, verb);
|
||||
parts[1].Should().BeOneOf(["Create", "Update", "Delete"],
|
||||
"verb ghi {0} phải gate Create/Update/Delete", name);
|
||||
}
|
||||
else
|
||||
{
|
||||
parts[1].Should().Be("Read",
|
||||
"{0} là {1} (đọc) — gate mạnh hơn Read sẽ khóa nhầm người chỉ có quyền xem " +
|
||||
"(403 silent, gotcha #44)", name, verb);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- (d) Chốt 3 endpoint mấu chốt bằng TÊN — nếu ai đổi POST phiếu sang `Update`
|
||||
// thì luật (c) ở trên vẫn xanh (Update cũng là verb ghi hợp lệ).
|
||||
var byName = actions.ToDictionary(a => a.Name, a => a.Authz?.Policy);
|
||||
byName[nameof(ContractSigningPlansController.Create)].Should().Be("KeHoachKyKet.Create",
|
||||
"POST tạo phiếu = quyền Create");
|
||||
byName[nameof(ContractSigningPlansController.UpdateDraft)].Should().Be("KeHoachKyKet.Update",
|
||||
"PUT sửa nháp = quyền Update");
|
||||
byName[nameof(ContractSigningPlansController.Delete)].Should().Be("KeHoachKyKet.Delete",
|
||||
"DELETE phiếu = quyền Delete RIÊNG (không gộp Update) — xoá là hành vi khác sửa");
|
||||
|
||||
// Ghi vào phiếu (căn cứ b.8-9 / đính kèm) = Update chứ KHÔNG Create: "tạo" ở module
|
||||
// này nghĩa là tạo PHIẾU. Chốt lại để không ai "sửa cho nhất quán" nhầm hướng.
|
||||
byName[nameof(ContractSigningPlansController.CreateDossierItem)].Should().Be("KeHoachKyKet.Update",
|
||||
"thêm căn cứ = ghi vào phiếu đã có ⇒ Update; gate Create sẽ chặn oan người được giao " +
|
||||
"quyền sửa phiếu nhưng không được lập phiếu mới");
|
||||
byName[nameof(ContractSigningPlansController.UploadAttachment)].Should().Be("KeHoachKyKet.Update",
|
||||
"upload đính kèm = ghi vào phiếu đã có ⇒ Update");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user