diff --git a/tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs b/tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs new file mode 100644 index 0000000..2daf46b --- /dev/null +++ b/tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs @@ -0,0 +1,90 @@ +using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using SolutionErp.Api.Controllers; + +namespace SolutionErp.Infrastructure.Tests.Api; + +// Regression tests for gotcha #44 — Silent 403 từ class-level [Authorize(Policy = ...)] +// quá strict (Session 18, fix `f77ea38`). +// +// Bug ngày 2026-05-08: ApprovalWorkflowsV2Controller class-level +// `[Authorize(Policy = "Workflows.Read")]` → Drafter `nv.test` (chỉ có +// `PurchaseEvaluations.Read`) bị 403 silent khi GET /api/approval-workflows-v2. +// FE TanStack Query catch silent → Workspace dropdown empty không có error toast. +// +// Fix: split policy per action — class-level [Authorize] (any authenticated) +// cho list-pick GET, action-level [Authorize(Policy = "Workflows.Create")] +// cho POST/DELETE/PATCH admin-only. +// +// Regression coverage: nếu future ai đó add Policy="Workflows.Read" lên +// class-level OR add Policy lên GET action → test FAIL ngay, không cần UAT +// reproduce lại bug silent 403 mà FE catch không hiển thị. +public class AuthorizePolicyRegressionTests +{ + private static AuthorizeAttribute? GetClassLevelAuthorize(Type controllerType) + => controllerType.GetCustomAttributes(inherit: false).FirstOrDefault(); + + private static AuthorizeAttribute? GetActionAuthorize(Type controllerType, string methodName) + => controllerType + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .FirstOrDefault(m => m.Name == methodName) + ?.GetCustomAttributes(inherit: false) + .FirstOrDefault(); + + [Fact] + public void ApprovalWorkflowsV2Controller_ClassLevel_AuthorizeOnly_NoPolicy() + { + var attr = GetClassLevelAuthorize(typeof(ApprovalWorkflowsV2Controller)); + + attr.Should().NotBeNull("controller phải có [Authorize] class-level để chặn anonymous"); + attr!.Policy.Should().BeNull( + "class-level KHÔNG được hardcode Policy — sẽ block Drafter 403 silent khi GET. " + + "Gotcha #44: ràng buộc per-action thay vì class-level uniform."); + attr.Roles.Should().BeNull("class-level KHÔNG được hardcode Roles"); + } + + [Fact] + public void ApprovalWorkflowsV2Controller_Overview_GET_InheritsClassLevel_NoActionPolicy() + { + // GET Overview KHÔNG được override với [Authorize(Policy=...)] — Drafter + // cần list workflow để pick lúc create PE/HĐ (read-only, không expose + // business data nhạy cảm). + var attr = GetActionAuthorize(typeof(ApprovalWorkflowsV2Controller), nameof(ApprovalWorkflowsV2Controller.Overview)); + + attr.Should().BeNull( + "GET Overview phải inherit class-level [Authorize] (any authenticated). " + + "Nếu add [Authorize(Policy=...)] action-level → Drafter 403 silent (gotcha #44)."); + } + + [Fact] + public void ApprovalWorkflowsV2Controller_Create_POST_RequiresWorkflowsCreatePolicy() + { + // POST Create chỉ admin Designer được tạo workflow — phải có policy guard. + var attr = GetActionAuthorize(typeof(ApprovalWorkflowsV2Controller), nameof(ApprovalWorkflowsV2Controller.Create)); + + attr.Should().NotBeNull("POST Create phải có [Authorize(Policy = ...)] admin-only"); + attr!.Policy.Should().Be("Workflows.Create", + "POST Create chỉ admin được tạo workflow mới (Mig 22 V2 Designer)."); + } + + [Fact] + public void ApprovalWorkflowsV2Controller_Delete_RequiresWorkflowsCreatePolicy() + { + var attr = GetActionAuthorize(typeof(ApprovalWorkflowsV2Controller), nameof(ApprovalWorkflowsV2Controller.Delete)); + + attr.Should().NotBeNull("DELETE phải có [Authorize(Policy = ...)] admin-only"); + attr!.Policy.Should().Be("Workflows.Create", + "DELETE workflow chỉ admin (Designer)."); + } + + [Fact] + public void ApprovalWorkflowsV2Controller_SetUserSelectable_PATCH_RequiresWorkflowsCreatePolicy() + { + // Mig 25 — admin toggle stick/unstick "cho user pick lúc create phiếu". + var attr = GetActionAuthorize(typeof(ApprovalWorkflowsV2Controller), nameof(ApprovalWorkflowsV2Controller.SetUserSelectable)); + + attr.Should().NotBeNull("PATCH user-selectable phải có [Authorize(Policy = ...)] admin-only"); + attr!.Policy.Should().Be("Workflows.Create", + "PATCH user-selectable chỉ admin (Mig 25 Designer pin/unpin)."); + } +} diff --git a/tests/SolutionErp.Infrastructure.Tests/SolutionErp.Infrastructure.Tests.csproj b/tests/SolutionErp.Infrastructure.Tests/SolutionErp.Infrastructure.Tests.csproj index 11b0616..f852b87 100644 --- a/tests/SolutionErp.Infrastructure.Tests/SolutionErp.Infrastructure.Tests.csproj +++ b/tests/SolutionErp.Infrastructure.Tests/SolutionErp.Infrastructure.Tests.csproj @@ -21,6 +21,8 @@ + +