[CLAUDE] Tests: Plan C task 4 — regression test #44 silent 403 (Authorize policy ApprovalWorkflowsV2)

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) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-13 21:05:25 +07:00
parent 60efeeda63
commit dbda37eb30
2 changed files with 92 additions and 0 deletions

View File

@ -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<AuthorizeAttribute>(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<AuthorizeAttribute>(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).");
}
}