From 2a7269581c96bd0d3ce939b4dceaeb847df76a89 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 29 Jul 2026 12:34:15 +0700 Subject: [PATCH] [CLAUDE] Infra: go nhanh S92 khoi revoker (owner supersede: hien thi het) + skeleton menu GD2/GD4 + ComingSoonPage x2 - RevokeTemporarilyHiddenModulesAsync: GO Contracts+Ct_*/Master-set khoi predicate (bai tra gia: grant SQL tay bi revoker lat o restart #423 - 447/494 row roi; regrant=447 da chay truoc commit nay). Hrm/Off/Personal GIU (y owner: vung dang-lam chi admin) - reviewKeys += ContractMenuKeys + MasterMenuKeys + KhkkKeys + HdcKeys (CanRead-only all-role, fresh-env hoi tu) - Seeder tree.Add: Khkk_G1 + 4 leaves (khuon Duyet NCC) + Hdc_x7 (tai dung nhan 01.-07.) - prod da INSERT truc tiep (perm=156) - ComingSoonPage mirror x2 (md5-identical) + route /coming-soon x2 + staticMap 11 leaf x2 (gotcha #50) - Test: AdminOnlyModulesRevokeTests tach StillHidden/Reopened + REGRESSION-GUARD S159 (re-add nhanh S92 => do); ProcurementMasterAccessSeedTests doc lai isolation bang CO CAO (spec-change S92->S159, update test + code chung commit) - 562 test PASS - nghiem thu cuoi = restart THAT khi deploy run nay (khuon S91) Co-Authored-By: Claude Opus 5 --- fe-admin/src/App.tsx | 3 + fe-admin/src/components/Layout.tsx | 13 ++++ fe-admin/src/pages/ComingSoonPage.tsx | 54 ++++++++++++++++ fe-user/src/App.tsx | 3 + fe-user/src/components/Layout.tsx | 13 ++++ fe-user/src/pages/ComingSoonPage.tsx | 54 ++++++++++++++++ .../Persistence/DbInitializer.cs | 63 ++++++++++++++++--- .../AdminOnlyModulesRevokeTests.cs | 42 +++++++++---- .../ProcurementMasterAccessSeedTests.cs | 16 ++--- 9 files changed, 234 insertions(+), 27 deletions(-) create mode 100644 fe-admin/src/pages/ComingSoonPage.tsx create mode 100644 fe-user/src/pages/ComingSoonPage.tsx diff --git a/fe-admin/src/App.tsx b/fe-admin/src/App.tsx index 08f2b91..4f7fac9 100644 --- a/fe-admin/src/App.tsx +++ b/fe-admin/src/App.tsx @@ -5,6 +5,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute' import { Layout } from '@/components/Layout' import { LoginPage } from '@/pages/LoginPage' import { DashboardPage } from '@/pages/DashboardPage' +import { ComingSoonPage } from '@/pages/ComingSoonPage' import { SuppliersPage } from '@/pages/master/SuppliersPage' import { ProjectsPage } from '@/pages/master/ProjectsPage' import { DepartmentsPage } from '@/pages/master/DepartmentsPage' @@ -55,6 +56,8 @@ function App() { } > } /> + {/* [S159] trang dich cho menu skeleton GD2/GD4 */} + } /> } /> } /> } /> diff --git a/fe-admin/src/components/Layout.tsx b/fe-admin/src/components/Layout.tsx index 144b46c..9cd9465 100644 --- a/fe-admin/src/components/Layout.tsx +++ b/fe-admin/src/components/Layout.tsx @@ -29,6 +29,19 @@ const TYPE_CODE_TO_INT: Record = { // Ct__ sub-menu entries. function resolvePath(key: string): string | null { const staticMap: Record = { + // [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ + // active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT). + Khkk_List: '/coming-soon?stage=2&m=list', + Khkk_Create: '/coming-soon?stage=2&m=create', + Khkk_Pending: '/coming-soon?stage=2&m=pending', + Khkk_Approved: '/coming-soon?stage=2&m=approved', + Hdc_ThauPhu: '/coming-soon?stage=4&t=01', + Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02', + Hdc_NhaCungCap: '/coming-soon?stage=4&t=03', + Hdc_DichVu: '/coming-soon?stage=4&t=04', + Hdc_MuaBan: '/coming-soon?stage=4&t=05', + Hdc_NguyenTacNcc: '/coming-soon?stage=4&t=06', + Hdc_NguyenTacDv: '/coming-soon?stage=4&t=07', Dashboard: '/dashboard', Suppliers: '/master/suppliers', Projects: '/master/projects', diff --git a/fe-admin/src/pages/ComingSoonPage.tsx b/fe-admin/src/pages/ComingSoonPage.tsx new file mode 100644 index 0000000..8ab4b31 --- /dev/null +++ b/fe-admin/src/pages/ComingSoonPage.tsx @@ -0,0 +1,54 @@ +// ComingSoonPage — trang đích cho các mục menu skeleton GĐ2/GĐ4 (module chưa build). +// [S159 2026-07-29] Owner: menu hiển thị đủ 4 giai đoạn + phân list như Duyệt NCC +// NGAY TỪ BÂY GIỜ để mọi người góp ý — leaf chưa có module trỏ về đây thay vì +// link chết. MIRROR ×2 app BYTE-IDENTICAL (nút quay lại dùng navigate(-1) để +// không phụ thuộc route riêng từng app). +import { useNavigate, useSearchParams } from 'react-router-dom' +import { ArrowLeft, Hourglass } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { PePipelineStrip } from '@/components/pe/PePipelineStrip' + +const STAGE_INFO: Record = { + '2': { + name: 'Kế hoạch ký kết HĐ', + steps: 'Bước 7 → 12', + desc: 'Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — duyệt mẫu/shopdrawing, so sánh giá và chốt giá trị ký kết hợp đồng.', + }, + '4': { + name: 'Hợp đồng cứng', + steps: 'Bước 19 → 21', + desc: 'Lưu bản hợp đồng đã ký cứng (tải lên bản scan sau khi ký, đóng dấu và phát hành).', + }, +} + +export function ComingSoonPage() { + const navigate = useNavigate() + const [params] = useSearchParams() + const stage = params.get('stage') ?? '2' + const info = STAGE_INFO[stage] ?? STAGE_INFO['2'] + + return ( +
+
+ +
+ + + +

{info.name}

+
+ Giai đoạn {stage} · {info.steps} +
+

{info.desc}

+

+ Phân hệ đang được triển khai — cấu trúc menu hiển thị trước để mọi người góp ý. +

+ +
+
+
+ ) +} diff --git a/fe-user/src/App.tsx b/fe-user/src/App.tsx index 248fc58..ec8c6af 100644 --- a/fe-user/src/App.tsx +++ b/fe-user/src/App.tsx @@ -5,6 +5,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute' import { Layout } from '@/components/Layout' import { LoginPage } from '@/pages/LoginPage' import { UserDashboardPage } from '@/pages/UserDashboardPage' +import { ComingSoonPage } from '@/pages/ComingSoonPage' import { InboxPage } from '@/pages/InboxPage' import { SuppliersPage } from '@/pages/master/SuppliersPage' import { ProjectsPage } from '@/pages/master/ProjectsPage' @@ -47,6 +48,8 @@ function App() { } > } /> + {/* [S159] trang dich cho menu skeleton GD2/GD4 */} + } /> } /> } /> } /> diff --git a/fe-user/src/components/Layout.tsx b/fe-user/src/components/Layout.tsx index c0bfd11..7346172 100644 --- a/fe-user/src/components/Layout.tsx +++ b/fe-user/src/components/Layout.tsx @@ -54,6 +54,19 @@ const INT_TO_PE_CODE: Record = Object.fromEntries( // /my-contracts (user's own drafts), Duyệt to /inbox (pending THEIR approval). function resolvePath(key: string): string | null { const staticMap: Record = { + // [S159-đợt3] skeleton GĐ2/GĐ4 — leaf trỏ trang "Sắp triển khai" (query giữ + // active-state riêng từng mục; gotcha #50: thiếu route = sidebar drop SILENT). + Khkk_List: '/coming-soon?stage=2&m=list', + Khkk_Create: '/coming-soon?stage=2&m=create', + Khkk_Pending: '/coming-soon?stage=2&m=pending', + Khkk_Approved: '/coming-soon?stage=2&m=approved', + Hdc_ThauPhu: '/coming-soon?stage=4&t=01', + Hdc_GiaoKhoan: '/coming-soon?stage=4&t=02', + Hdc_NhaCungCap: '/coming-soon?stage=4&t=03', + Hdc_DichVu: '/coming-soon?stage=4&t=04', + Hdc_MuaBan: '/coming-soon?stage=4&t=05', + Hdc_NguyenTacNcc: '/coming-soon?stage=4&t=06', + Hdc_NguyenTacDv: '/coming-soon?stage=4&t=07', Dashboard: '/dashboard', Contracts: '/my-contracts', PurchaseEvaluations: '/purchase-evaluations', diff --git a/fe-user/src/pages/ComingSoonPage.tsx b/fe-user/src/pages/ComingSoonPage.tsx new file mode 100644 index 0000000..8ab4b31 --- /dev/null +++ b/fe-user/src/pages/ComingSoonPage.tsx @@ -0,0 +1,54 @@ +// ComingSoonPage — trang đích cho các mục menu skeleton GĐ2/GĐ4 (module chưa build). +// [S159 2026-07-29] Owner: menu hiển thị đủ 4 giai đoạn + phân list như Duyệt NCC +// NGAY TỪ BÂY GIỜ để mọi người góp ý — leaf chưa có module trỏ về đây thay vì +// link chết. MIRROR ×2 app BYTE-IDENTICAL (nút quay lại dùng navigate(-1) để +// không phụ thuộc route riêng từng app). +import { useNavigate, useSearchParams } from 'react-router-dom' +import { ArrowLeft, Hourglass } from 'lucide-react' +import { Button } from '@/components/ui/Button' +import { PePipelineStrip } from '@/components/pe/PePipelineStrip' + +const STAGE_INFO: Record = { + '2': { + name: 'Kế hoạch ký kết HĐ', + steps: 'Bước 7 → 12', + desc: 'Phiếu nối tiếp từ phiếu Duyệt NCC đã duyệt — duyệt mẫu/shopdrawing, so sánh giá và chốt giá trị ký kết hợp đồng.', + }, + '4': { + name: 'Hợp đồng cứng', + steps: 'Bước 19 → 21', + desc: 'Lưu bản hợp đồng đã ký cứng (tải lên bản scan sau khi ký, đóng dấu và phát hành).', + }, +} + +export function ComingSoonPage() { + const navigate = useNavigate() + const [params] = useSearchParams() + const stage = params.get('stage') ?? '2' + const info = STAGE_INFO[stage] ?? STAGE_INFO['2'] + + return ( +
+
+ +
+ + + +

{info.name}

+
+ Giai đoạn {stage} · {info.steps} +
+

{info.desc}

+

+ Phân hệ đang được triển khai — cấu trúc menu hiển thị trước để mọi người góp ý. +

+ +
+
+
+ ) +} diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 4df9355..f0466de 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -1773,6 +1773,14 @@ public static class DbInitializer // FE staticMap trỏ /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây). (MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, "FileSignature"), (MenuKeys.HopDongCung, "Hợp đồng cứng", null, 32, "Stamp"), + // [S159-đợt3] skeleton menu GĐ2/GĐ4 (owner vẽ: "viết hoa + phân list như + // Duyệt NCC" / "thêm Sub đánh số như Duyệt Hợp đồng"). Root CÓ CON ⇒ render + // MenuGroup uppercase tự động. Leaves → /coming-soon (staticMap ×2 app). + ("Khkk_G1", "1. Kế hoạch ký kết HĐ (NCC-TP)", MenuKeys.KeHoachKyKet, 1, "FileCheck"), + ("Khkk_List", "Danh sách", "Khkk_G1", 1, "List"), + ("Khkk_Create", "Thao tác", "Khkk_G1", 2, "Plus"), + ("Khkk_Pending", "Đang duyệt", "Khkk_G1", 3, "CheckCircle2"), + ("Khkk_Approved", "Đã duyệt", "Khkk_G1", 4, "CheckCheck"), (MenuKeys.Forms, "Biểu mẫu", null, 40, "FileSpreadsheet"), (MenuKeys.Reports, "Báo cáo", null, 50, "BarChart3"), (MenuKeys.System, "Hệ thống", null, 90, "Settings"), @@ -1847,9 +1855,13 @@ public static class DbInitializer // Per-type sub-menu under Contracts: 1 group + 3 leaves each // (Danh sách / Thao tác / Duyệt). var order = 31; + var hdcOrder = 1; foreach (var code in MenuKeys.ContractTypeCodes) { var label = typeLabels.GetValueOrDefault(code, code); + // [S159-đợt3] GĐ4 skeleton — 1 leaf/loại HĐ dưới root Hợp đồng cứng, tái dùng + // đúng nhãn đã đánh số 01.-07. (leaf → /coming-soon?stage=4). + tree.Add(("Hdc_" + code, label, MenuKeys.HopDongCung, hdcOrder++, "FileText")); tree.Add((MenuKeys.ContractTypeGroup(code), label, MenuKeys.Contracts, order++, "FileText")); tree.Add((MenuKeys.ContractTypeList(code), "Danh sách", MenuKeys.ContractTypeGroup(code), order++, "List")); tree.Add((MenuKeys.ContractTypeCreate(code), "Thao tác", MenuKeys.ContractTypeGroup(code), order++, "Plus")); @@ -2115,6 +2127,31 @@ public static class DbInitializer // → "PeWf_*"/"PeWorkflows" KHÔNG match (loại admin Designer). static bool IsPeKey(string key) => key.StartsWith("Pe_"); + // [S159] các bộ key grant CanRead-only mọi role (xem chú thích tại reviewKeys). + static IEnumerable ContractMenuKeys() + { + yield return MenuKeys.Contracts; + foreach (var c in MenuKeys.ContractTypeCodes) + { + yield return MenuKeys.ContractTypeGroup(c); + yield return MenuKeys.ContractTypeList(c); + yield return MenuKeys.ContractTypeCreate(c); + yield return MenuKeys.ContractTypePending(c); + } + } + static IEnumerable MasterMenuKeys() => new[] + { + MenuKeys.Master, MenuKeys.Catalogs, MenuKeys.CatalogUnits, MenuKeys.CatalogMaterials, + MenuKeys.CatalogServices, MenuKeys.CatalogWorkItems, MenuKeys.Departments, + MenuKeys.Projects, MenuKeys.Suppliers, + }; + static IEnumerable KhkkKeys() => new[] + { + "Khkk_G1", "Khkk_List", "Khkk_Create", "Khkk_Pending", "Khkk_Approved", + }; + static IEnumerable HdcKeys() + => MenuKeys.ContractTypeCodes.Select(c => "Hdc_" + c); + // MenuKeys.All chứa root PurchaseEvaluations nhưng KHÔNG chứa Pe_* leaf // (sinh động qua factory). Build leaf giống SeedPurchaseEvaluationPermissionDefaultsAsync // để upgrade đúng row Pe_* thật trong DB (1 root + 5 leaf × 2 type). @@ -2137,6 +2174,14 @@ public static class DbInitializer // [S159] 2 root placeholder GĐ2/GĐ4 — CanRead-only mọi role (IsPeKey=false // ⇒ nhánh read-only). Prod đã INSERT trực tiếp; đây giữ fresh-DB hội tụ. .Concat(new[] { MenuKeys.KeHoachKyKet, MenuKeys.HopDongCung }) + // [S159] skeleton GĐ2 (Khkk_*) + GĐ4 (Hdc_*) — leaves trỏ /coming-soon. + .Concat(KhkkKeys()).Concat(HdcKeys()) + // [S159] owner "hiển thị hết": HĐ (Contracts+Ct_*) + DANH MỤC (Master-set) + // CanRead-only mọi role — cặp với việc GỠ nhánh [S92] khỏi revoker ở trên. + // Lưu ý: nhánh grant key-thường là skip-existing ⇒ trên prod row false + // KHÔNG được nâng ở đây — re-grant prod đã chạy SQL trực tiếp cùng ngày; + // đây là đường hội tụ cho fresh-DB/env khác. + .Concat(ContractMenuKeys()).Concat(MasterMenuKeys()) .Distinct() .ToArray(); var roles = await roleManager.Roles.ToListAsync(); @@ -2214,14 +2259,16 @@ public static class DbInitializer var rows = await db.Permissions .Where(p => p.RoleId != adminRoleId && (p.MenuKey.StartsWith("Hrm") || p.MenuKey.StartsWith("Off") - || p.MenuKey == MenuKeys.Personal - // [S92 2026-06-29] +Hợp đồng (Contracts + Ct_*) + Danh mục (Master + - // Suppliers + Projects + Departments + Catalog*) — anh chốt "chỉ Admin - // thấy". Pe_* (Duyệt NCC) KHÔNG nằm đây → module PE giữ nguyên cho user. - || p.MenuKey == MenuKeys.Contracts || p.MenuKey.StartsWith("Ct_") - || p.MenuKey == MenuKeys.Master || p.MenuKey == MenuKeys.Suppliers - || p.MenuKey == MenuKeys.Projects || p.MenuKey == MenuKeys.Departments - || p.MenuKey.StartsWith("Catalog")) + || p.MenuKey == MenuKeys.Personal) + // [S159 2026-07-29] 🔴 GỠ nhánh [S92] (Contracts+Ct_* / Master+Suppliers+ + // Projects+Departments+Catalog*) — owner SUPERSEDE chính quyết định S92 + // "chỉ Admin thấy": nay "đang phát triển → cho hiển thị hết để mọi người + // góp ý; Duyệt NCC để như cũ". Bài trả giá cùng ngày: grant 38 key bằng + // SQL tay bị CHÍNH revoker này lật lại ở app-restart kế (Run #423 — + // 447/494 row rơi, 11/13 role về 0; cicd-verify-1a47a61.md mục 9). + // Đúng chiều-ngược gotcha #75/#76: seeder ungated thắng data-change tay + // ⇒ đổi trạng-thái bền = đổi CODE, nghiệm thu = restart THẬT. + // Hrm*/Off*/Personal GIỮ trong revoker (vùng đang-làm, ý ❸ owner). && (p.CanRead || p.CanCreate || p.CanUpdate || p.CanDelete)) .ToListAsync(); diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/AdminOnlyModulesRevokeTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/AdminOnlyModulesRevokeTests.cs index dc73d31..658da70 100644 --- a/tests/SolutionErp.Infrastructure.Tests/Application/AdminOnlyModulesRevokeTests.cs +++ b/tests/SolutionErp.Infrastructure.Tests/Application/AdminOnlyModulesRevokeTests.cs @@ -32,26 +32,37 @@ namespace SolutionErp.Infrastructure.Tests.Application; // (Cascade) → PHẢI seed MenuItem rows + Role TRƯỚC khi seed Permission (nếu không SQLite FK Error 19). public class AdminOnlyModulesRevokeTests { - // Đại diện 5 nhóm anh chốt admin-only — sau revoke PHẢI CanRead=false (non-Admin). - // Dùng const/factory MenuKeys (KHÔNG hardcode string) để bắt đồng-bộ nếu key đổi. - private static readonly string[] S92HiddenKeys = + // [S159 2026-07-29] Owner SUPERSEDE S92: "đang phát triển → cho hiển thị hết để mọi + // người góp ý; Duyệt NCC để như cũ" ⇒ nhánh Contracts/Ct_*/Master-set GỠ khỏi revoker. + // Tập ẩn CÒN LẠI = 3 nhóm đang-làm (Hrm/Off/Personal — ý ❸ owner cùng ngày). + private static readonly string[] StillHiddenKeys = + { + MenuKeys.Hrm, // "Hrm" — match StartsWith("Hrm") + MenuKeys.Off, // "Off" — match StartsWith("Off") + MenuKeys.Personal, // "Personal" — match == Personal + }; + + // [S159] Bộ key S92 cũ nay MỞ LẠI — revoker KHÔNG ĐƯỢC chạm nữa. Giữ danh sách này + // làm REGRESSION-GUARD: bài trả giá 2026-07-29 = grant 38 key bằng SQL tay bị chính + // revoker lật ở app-restart kế (Run #423, 447/494 row rơi, 11/13 role về 0 — + // cicd-verify-1a47a61.md mục 9). Ai re-add nhánh S92 vào predicate ⇒ test này ĐỎ. + private static readonly string[] ReopenedKeys = { // HỢP ĐỒNG (Contracts + Ct_*) MenuKeys.Contracts, - MenuKeys.ContractTypeGroup("ThauPhu"), // "Ct_ThauPhu" — match StartsWith("Ct_") + MenuKeys.ContractTypeGroup("ThauPhu"), // "Ct_ThauPhu" // DANH MỤC (Master + 3 leaf + Catalog*) MenuKeys.Master, MenuKeys.Suppliers, MenuKeys.Projects, MenuKeys.Departments, - MenuKeys.Catalogs, // "Catalogs" — match StartsWith("Catalog") - MenuKeys.CatalogWorkItems, // "CatalogWorkItems" — match StartsWith("Catalog") - // Nhóm cũ S58 (regression — vẫn phải ẩn) - MenuKeys.Hrm, // "Hrm" — match StartsWith("Hrm") - MenuKeys.Off, // "Off" — match StartsWith("Off") - MenuKeys.Personal, // "Personal" — match == Personal + MenuKeys.Catalogs, + MenuKeys.CatalogWorkItems, }; + private static readonly string[] S92HiddenKeys = + StillHiddenKeys.Concat(ReopenedKeys).ToArray(); // giữ tên cho seed-helper cũ + // Pe_* (Duyệt NCC) + root PurchaseEvaluations — KHÔNG bị revoke (giữ cho user thường). private static readonly string[] PeKeptKeys = { @@ -130,15 +141,22 @@ public class AdminOnlyModulesRevokeTests await InvokeRevokeAsync(fix); - foreach (var key in S92HiddenKeys) + // Vế 1 — 3 nhóm đang-làm VẪN bị revoke (ý ❸ giữ). + foreach (var key in StillHiddenKeys) { var row = await GetPermAsync(db, nonAdminId, key); row.Should().NotBeNull($"{key} vẫn có row (revoke chỉ hạ cờ, KHÔNG xóa — giữ vết flip lại)"); - row!.CanRead.Should().BeFalse($"{key} bị revoke CanRead — menu ẩn khỏi non-Admin (chỉ Admin thấy)"); + row!.CanRead.Should().BeFalse($"{key} bị revoke CanRead — nhóm đang-làm ẩn khỏi non-Admin"); row.CanCreate.Should().BeFalse($"{key} revoke CanCreate"); row.CanUpdate.Should().BeFalse($"{key} revoke CanUpdate"); row.CanDelete.Should().BeFalse($"{key} revoke CanDelete"); } + + // Vế 2 — ⭐ REGRESSION-GUARD S159: bộ key MỞ LẠI phải GIỮ NGUYÊN CanRead=true + // sau revoke (revoker không còn nhánh S92; re-add là test này đỏ ngay). + foreach (var key in ReopenedKeys) + (await CanReadAsync(db, nonAdminId, key)).Should().BeTrue( + $"{key} — owner S159 mở lại cho mọi role; revoker KHÔNG được lật grant này nữa"); } // Admin loại trừ khỏi revoke → giữ nguyên CanRead=true (vẫn thấy 5 nhóm). diff --git a/tests/SolutionErp.Infrastructure.Tests/Application/ProcurementMasterAccessSeedTests.cs b/tests/SolutionErp.Infrastructure.Tests/Application/ProcurementMasterAccessSeedTests.cs index 9d44301..ac86323 100644 --- a/tests/SolutionErp.Infrastructure.Tests/Application/ProcurementMasterAccessSeedTests.cs +++ b/tests/SolutionErp.Infrastructure.Tests/Application/ProcurementMasterAccessSeedTests.cs @@ -189,12 +189,14 @@ public class ProcurementMasterAccessSeedTests await InvokeRevokeAsync(fix); // revoke CẢ HAI role về false await InvokeSeedProcurementAsync(fix); // CHỈ nâng lại Procurement - // ⭐ ISOLATION: DeptManager row TỒN TẠI (revoke giữ vết) NHƯNG mọi cờ = false — seeder - // Procurement KHÔNG chạm role khác. Assert cả Master + Suppliers (2 key task chốt) + 7 key kia. + // ⭐ ISOLATION (đọc lại theo S159): revoker KHÔNG còn chạm Master-set ⇒ CanRead=true + // pre-grant của DeptManager GIỮ NGUYÊN (chính sách all-role read — owner 2026-07-29 + // supersede S92). Isolation của Procurement-seed nay đo bằng CỜ CAO: seeder chỉ nâng + // CanCreate/Update cho Procurement, KHÔNG rò cờ cao sang role khác. foreach (var key in AllGrantKeys) await AssertFlagsAsync(db, otherId, key, - r: false, c: false, u: false, d: false, - "DeptManager KHÔNG được Procurement-grant chạm — S92 'chỉ Admin thấy' giữ nguyên role khác"); + r: true, c: false, u: false, d: false, + "S159 all-role read GIỮ; cờ cao KHÔNG rò sang DeptManager từ Procurement-grant"); // CONTRAST (chống vacuous-pass): Procurement THẬT SỰ được nâng → chứng seeder CÓ chạy, // nên isolation ở trên KHÔNG phải "false vì seeder no-op". @@ -246,10 +248,10 @@ public class ProcurementMasterAccessSeedTests var procCount = await db.Permissions.AsNoTracking().CountAsync(p => p.RoleId == procurementId); procCount.Should().Be(AllGrantKeys.Length, "idempotent — chạy lại KHÔNG tạo row trùng"); - // Role khác vẫn bị ẩn sau lần 2 (isolation bền vững). + // Isolation bền vững sau lần 2 (S159: CanRead all-role GIỮ true; cờ cao vẫn false). foreach (var key in AllGrantKeys) await AssertFlagsAsync(db, otherId, key, - r: false, c: false, u: false, d: false, - "idempotent — lần seed thứ 2 KHÔNG rò sang DeptManager"); + r: true, c: false, u: false, d: false, + "idempotent — lần seed thứ 2 KHÔNG rò CỜ CAO sang DeptManager (CanRead all-role S159 giữ)"); } }