feat(application): PermisosOverride record + PermisoResolver static helper [UDT-009]

This commit is contained in:
2026-04-15 21:25:09 -03:00
parent be86c2fac9
commit da1eb83ac1
4 changed files with 339 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
using SIGCM2.Application.Common;
namespace SIGCM2.Application.Tests.Common;
/// <summary>
/// SUITE-B-RESOLVER — R-01 a R-09 (UDT-009)
/// Unit tests for PermisoResolver.Resolve static helper.
/// Pure unit: no DB, no mocks.
/// </summary>
public sealed class PermisoResolverTests
{
// R-01: Override vacío → effective = solo rol sin cambios
[Fact]
public void Resolve_EmptyOverride_ReturnsRolPermisosUnchanged()
{
var result = PermisoResolver.Resolve(["A", "B"], PermisosOverride.Empty);
Assert.Contains("A", result);
Assert.Contains("B", result);
Assert.Equal(2, result.Count);
}
// R-02: Grant nuevo permiso → se agrega al set
[Fact]
public void Resolve_GrantNewPermiso_AddsToEffective()
{
var overrides = new PermisosOverride(Grant: ["C"], Deny: []);
var result = PermisoResolver.Resolve(["A", "B"], overrides);
Assert.Contains("A", result);
Assert.Contains("B", result);
Assert.Contains("C", result);
Assert.Equal(3, result.Count);
}
// R-03: Deny permiso del rol → se quita del set
[Fact]
public void Resolve_DenyRolPermiso_RemovesFromEffective()
{
var overrides = new PermisosOverride(Grant: [], Deny: ["A"]);
var result = PermisoResolver.Resolve(["A", "B"], overrides);
Assert.DoesNotContain("A", result);
Assert.Contains("B", result);
Assert.Equal(1, result.Count);
}
// R-04: Grant duplicado (ya en rol) → idempotente, no duplicados
[Fact]
public void Resolve_GrantDuplicated_Idempotent()
{
var overrides = new PermisosOverride(Grant: ["B"], Deny: []);
var result = PermisoResolver.Resolve(["A", "B"], overrides);
Assert.Contains("A", result);
Assert.Contains("B", result);
Assert.Equal(2, result.Count); // no duplicates
}
// R-05: Deny código inexistente en rol → no-op
[Fact]
public void Resolve_DenyNonExistentCode_NoOp()
{
var overrides = new PermisosOverride(Grant: [], Deny: ["X"]);
var result = PermisoResolver.Resolve(["A", "B"], overrides);
Assert.Contains("A", result);
Assert.Contains("B", result);
Assert.Equal(2, result.Count);
}
// R-06: Grant + Deny combinados
[Fact]
public void Resolve_GrantAndDeny_Combined()
{
var overrides = new PermisosOverride(Grant: ["C"], Deny: ["A"]);
var result = PermisoResolver.Resolve(["A", "B"], overrides);
Assert.DoesNotContain("A", result);
Assert.Contains("B", result);
Assert.Contains("C", result);
Assert.Equal(2, result.Count);
}
// R-07: PermisosOverride.Empty literal → mismo que rol
[Fact]
public void Resolve_EmptyLiteral_ReturnsRolPermisosOnly()
{
var result = PermisoResolver.Resolve(["A", "B"], PermisosOverride.Empty);
Assert.Contains("A", result);
Assert.Contains("B", result);
Assert.Equal(2, result.Count);
}
// R-08: Rol vacío + grant → effective = solo el grant
[Fact]
public void Resolve_EmptyRol_WithGrant_ReturnsGrant()
{
var overrides = new PermisosOverride(Grant: ["C"], Deny: []);
var result = PermisoResolver.Resolve([], overrides);
Assert.Single(result);
Assert.Contains("C", result);
}
// R-09: Rol vacío + sin overrides → effective vacío
[Fact]
public void Resolve_EmptyRol_EmptyOverrides_ReturnsEmpty()
{
var result = PermisoResolver.Resolve([], PermisosOverride.Empty);
Assert.Empty(result);
}
// Extra: Deny gana sobre grant explícito (defense en runtime — validator lo bloquea antes)
[Fact]
public void Resolve_DenyWinsOver_ExplicitGrant()
{
// Mismo código en grant y deny → deny gana (algoritmo: grant primero, deny al final)
var overrides = new PermisosOverride(Grant: ["C"], Deny: ["C"]);
var result = PermisoResolver.Resolve(["A"], overrides);
Assert.DoesNotContain("C", result);
Assert.Contains("A", result);
}
}

View File

@@ -0,0 +1,116 @@
using SIGCM2.Application.Common;
namespace SIGCM2.Application.Tests.Common;
/// <summary>
/// SUITE-B-PERMISOS-OVERRIDE-PARSING — P-01 a P-08 (UDT-009)
/// Unit tests for PermisosOverride.FromJson parsing logic.
/// </summary>
public sealed class PermisosOverrideParsingTests
{
// P-01: JSON válido con grant y deny → record correcto
[Fact]
public void FromJson_ValidGrantAndDeny_ReturnsParsedRecord()
{
const string json = """{"grant":["textos:editar"],"deny":["ventas:contado:cobrar"]}""";
var result = PermisosOverride.FromJson(json);
Assert.Single(result.Grant);
Assert.Equal("textos:editar", result.Grant[0]);
Assert.Single(result.Deny);
Assert.Equal("ventas:contado:cobrar", result.Deny[0]);
}
// P-02: JSON vacío canónico → equivalente a Empty
[Fact]
public void FromJson_EmptyCanonical_ReturnsEmpty()
{
const string json = """{"grant":[],"deny":[]}""";
var result = PermisosOverride.FromJson(json);
Assert.Empty(result.Grant);
Assert.Empty(result.Deny);
}
// P-03: Legacy "[]" → Empty (backward compat)
[Fact]
public void FromJson_LegacyEmptyArray_ReturnsEmpty()
{
var result = PermisosOverride.FromJson("[]");
Assert.Same(PermisosOverride.Empty, result);
}
// P-04: Legacy '["*"]' → Empty (backward compat)
[Fact]
public void FromJson_LegacyWildcard_ReturnsEmpty()
{
var result = PermisosOverride.FromJson("""["*"]""");
Assert.Same(PermisosOverride.Empty, result);
}
// P-05: null → Empty
[Fact]
public void FromJson_Null_ReturnsEmpty()
{
var result = PermisosOverride.FromJson(null);
Assert.Same(PermisosOverride.Empty, result);
}
// P-06a: string vacío → Empty
[Fact]
public void FromJson_EmptyString_ReturnsEmpty()
{
var result = PermisosOverride.FromJson(string.Empty);
Assert.Same(PermisosOverride.Empty, result);
}
// P-06b: whitespace → Empty
[Fact]
public void FromJson_Whitespace_ReturnsEmpty()
{
var result = PermisosOverride.FromJson(" ");
Assert.Same(PermisosOverride.Empty, result);
}
// P-07: JSON malformado → Empty (tolerante en runtime)
[Fact]
public void FromJson_MalformedJson_ReturnsEmpty()
{
// Nota: FromJson es tolerante — catch(JsonException) → Empty.
// Ver tasks note 2: "P-07/P-08 verifican que JSON malformado → Empty (no FormatException)"
var result = PermisosOverride.FromJson("{grant:[");
Assert.Same(PermisosOverride.Empty, result);
}
// P-08: JSON de tipo incorrecto (número) → Empty (tolerante)
[Fact]
public void FromJson_WrongJsonType_ReturnsEmpty()
{
var result = PermisosOverride.FromJson("42");
Assert.Same(PermisosOverride.Empty, result);
}
// Extra: ToJson produce JSON re-parseable con shape correcto
[Fact]
public void ToJson_ProducesCanonicalJson()
{
var overrides = new PermisosOverride(
Grant: new[] { "textos:editar" },
Deny: new[] { "ventas:contado:cobrar" });
var json = overrides.ToJson();
var reparsed = PermisosOverride.FromJson(json);
Assert.Equal(overrides.Grant, reparsed.Grant);
Assert.Equal(overrides.Deny, reparsed.Deny);
}
}