From dbda37eb30fefbb15527f6520a86e5499db20f19 Mon Sep 17 00:00:00 2001 From: pqhuy1987 Date: Wed, 13 May 2026 21:05:25 +0700 Subject: [PATCH] =?UTF-8?q?[CLAUDE]=20Tests:=20Plan=20C=20task=204=20?= =?UTF-8?q?=E2=80=94=20regression=20test=20#44=20silent=20403=20(Authorize?= =?UTF-8?q?=20policy=20ApprovalWorkflowsV2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 reflection-based tests verify ApprovalWorkflowsV2Controller Authorize policy split (gotcha #44 fix `f77ea38` S18): - class-level [Authorize] (any authenticated), NO Policy - GET Overview inherits class-level (no action policy) - POST Create + DELETE + PATCH user-selectable require Policy="Workflows.Create" Pattern reusable: catch future regression nếu ai add Policy lên class-level hoặc GET action → test fail ngay, không cần UAT reproduce silent 403. Add ProjectReference Api → Infrastructure.Tests cho reflection access. Verify: - dotnet test SolutionErp.slnx — 89/89 PASS (58 Domain + 31 Infra = 26+5 #44) Δ: 84 → 89 (+5) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Api/AuthorizePolicyRegressionTests.cs | 90 +++++++++++++++++++ .../SolutionErp.Infrastructure.Tests.csproj | 2 + 2 files changed, 92 insertions(+) create mode 100644 tests/SolutionErp.Infrastructure.Tests/Api/AuthorizePolicyRegressionTests.cs 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 @@ + +