using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using SIGCM2.Api.Authorization; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Domain.Entities; namespace SIGCM2.Api.Tests.Authorization; /// /// Unit tests for PermissionAuthorizationHandler — SUITE-B-01 (UDT-006). /// Tests isolated from DB: IRolPermisoRepository is mocked via NSubstitute. /// public sealed class PermissionAuthorizationHandlerTests { private readonly IRolPermisoRepository _rolPermisoRepo = Substitute.For(); private readonly PermissionAuthorizationHandler _handler; public PermissionAuthorizationHandlerTests() { _handler = new PermissionAuthorizationHandler( _rolPermisoRepo, NullLogger.Instance); } // ── Helpers ────────────────────────────────────────────────────────────── private static ClaimsPrincipal AuthenticatedUserWithRol(string rolValue) { var identity = new ClaimsIdentity( new[] { new Claim("rol", rolValue) }, authenticationType: "TestAuth"); return new ClaimsPrincipal(identity); } private static ClaimsPrincipal AuthenticatedUserWithoutRolClaim() { var identity = new ClaimsIdentity( new[] { new Claim(ClaimTypes.Name, "someuser") }, authenticationType: "TestAuth"); return new ClaimsPrincipal(identity); } private static ClaimsPrincipal AnonymousUser() { return new ClaimsPrincipal(new ClaimsIdentity()); // not authenticated } private static Permiso MakePermiso(int id, string codigo) => Permiso.ForRead(id, codigo, codigo, null, codigo.Split(':')[0], true, DateTime.UtcNow); private static AuthorizationHandlerContext MakeContext( ClaimsPrincipal user, RequirePermissionAttribute requirement, HttpContext? httpContext = null) { var ctx = httpContext ?? new DefaultHttpContext(); return new AuthorizationHandlerContext( requirements: new[] { requirement }, user: user, resource: ctx); } // ── B-01-01: Usuario con permiso requerido → Succeed ───────────────────── [Fact] public async Task HandleAsync_Succeeds_WhenUserHasRequiredPermission() { // Arrange var user = AuthenticatedUserWithRol("admin"); var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("admin", Arg.Any()) .Returns(new List { MakePermiso(1, "administracion:usuarios:gestionar") } .AsReadOnly() as IReadOnlyList); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.True(context.HasSucceeded); } // ── B-01-02: Usuario sin el permiso requerido → Fail ────────────────────── [Fact] public async Task HandleAsync_Fails_WhenUserLacksPermission() { // Arrange var user = AuthenticatedUserWithRol("cajero"); var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any()) .Returns(new List { MakePermiso(10, "ventas:contado:crear") } .AsReadOnly() as IReadOnlyList); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.False(context.HasSucceeded); } // ── B-01-03: Multi-permiso OR — tiene uno de los dos → Succeed ──────────── [Fact] public async Task HandleAsync_Succeeds_WhenAnyOfMultiplePermissions_OR() { // Arrange var user = AuthenticatedUserWithRol("cajero"); var requirement = new RequirePermissionAttribute( "ventas:contado:crear", "administracion:usuarios:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any()) .Returns(new List { MakePermiso(10, "ventas:contado:crear"), MakePermiso(11, "ventas:contado:cobrar"), }.AsReadOnly() as IReadOnlyList); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.True(context.HasSucceeded); } // ── B-01-04: Multi-permiso OR — no tiene ninguno → Fail ─────────────────── [Fact] public async Task HandleAsync_Fails_WhenNoneOfMultiplePermissions() { // Arrange var user = AuthenticatedUserWithRol("cajero"); var requirement = new RequirePermissionAttribute( "administracion:usuarios:gestionar", "administracion:roles:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any()) .Returns(new List { MakePermiso(10, "ventas:contado:crear") } .AsReadOnly() as IReadOnlyList); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.False(context.HasSucceeded); } // ── B-01-05: Claim "rol" ausente → Fail; repo nunca llamado ─────────────── [Fact] public async Task HandleAsync_Fails_WhenRolClaimMissing() { // Arrange var user = AuthenticatedUserWithoutRolClaim(); var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar"); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.False(context.HasSucceeded); // Repo must NOT be called when claim is absent await _rolPermisoRepo.DidNotReceive() .GetByRolCodigoAsync(Arg.Any(), Arg.Any()); } // ── B-01-06: Repo devuelve lista vacía → Fail ───────────────────────────── [Fact] public async Task HandleAsync_Fails_WhenRoleHasNoPermissions() { // Arrange var user = AuthenticatedUserWithRol("reportes"); var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("reportes", Arg.Any()) .Returns(new List().AsReadOnly() as IReadOnlyList); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.False(context.HasSucceeded); } // ── B-01-07: Rol no existe en RolPermiso (mismo caso que lista vacía) ────── [Fact] public async Task HandleAsync_Fails_WhenRoleDoesNotExistInRolPermisoTable() { // Arrange var user = AuthenticatedUserWithRol("rol_fantasma"); var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("rol_fantasma", Arg.Any()) .Returns(new List().AsReadOnly() as IReadOnlyList); var context = MakeContext(user, requirement); // Act await _handler.HandleAsync(context); // Assert Assert.False(context.HasSucceeded); } // ── B-01-08: Fail stashea RequiredPermission en HttpContext.Items ────────── [Fact] public async Task HandleAsync_StashesRequiredPermission_InHttpContextItems_OnFail() { // Arrange var user = AuthenticatedUserWithRol("cajero"); var requirement = new RequirePermissionAttribute("administracion:usuarios:gestionar"); _rolPermisoRepo.GetByRolCodigoAsync("cajero", Arg.Any()) .Returns(new List { MakePermiso(10, "ventas:contado:crear") } .AsReadOnly() as IReadOnlyList); var httpContext = new DefaultHttpContext(); var context = MakeContext(user, requirement, httpContext); // Act await _handler.HandleAsync(context); // Assert — context.Resource is the HttpContext where items are stashed Assert.False(context.HasSucceeded); Assert.Equal("administracion:usuarios:gestionar", httpContext.Items["RequiredPermission"]); } }