feat(infra): BATCH 4 - Permiso/RolPermiso repos Dapper + tests integracion [UDT-005]
This commit is contained in:
@@ -29,6 +29,8 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IUsuarioRepository, UsuarioRepository>();
|
services.AddScoped<IUsuarioRepository, UsuarioRepository>();
|
||||||
services.AddScoped<IRefreshTokenRepository, RefreshTokenRepository>();
|
services.AddScoped<IRefreshTokenRepository, RefreshTokenRepository>();
|
||||||
services.AddScoped<IRolRepository, RolRepository>();
|
services.AddScoped<IRolRepository, RolRepository>();
|
||||||
|
services.AddScoped<IPermisoRepository, PermisoRepository>();
|
||||||
|
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
|
||||||
|
|
||||||
// JWT Options — bound lazily via IOptions so tests can override via ConfigureWebHost
|
// JWT Options — bound lazily via IOptions so tests can override via ConfigureWebHost
|
||||||
services.Configure<JwtOptions>(configuration.GetSection("Jwt"));
|
services.Configure<JwtOptions>(configuration.GetSection("Jwt"));
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using Dapper;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public sealed class PermisoRepository : IPermisoRepository
|
||||||
|
{
|
||||||
|
private readonly SqlConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public PermisoRepository(SqlConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Permiso>> ListAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT Id, Codigo, Nombre, Descripcion, Modulo, Activo, FechaCreacion
|
||||||
|
FROM dbo.Permiso
|
||||||
|
ORDER BY Id
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var rows = await connection.QueryAsync<PermisoRow>(sql);
|
||||||
|
return rows.Select(MapRow).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Permiso?> GetByCodigoAsync(string codigo, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT Id, Codigo, Nombre, Descripcion, Modulo, Activo, FechaCreacion
|
||||||
|
FROM dbo.Permiso
|
||||||
|
WHERE Codigo = @Codigo
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var row = await connection.QuerySingleOrDefaultAsync<PermisoRow>(sql, new { Codigo = codigo });
|
||||||
|
return row is null ? null : MapRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Permiso>> GetByCodigosAsync(
|
||||||
|
IEnumerable<string> codigos,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var codigosList = codigos.ToList();
|
||||||
|
if (codigosList.Count == 0)
|
||||||
|
return Array.Empty<Permiso>();
|
||||||
|
|
||||||
|
const string sql = """
|
||||||
|
SELECT Id, Codigo, Nombre, Descripcion, Modulo, Activo, FechaCreacion
|
||||||
|
FROM dbo.Permiso
|
||||||
|
WHERE Codigo IN @Codigos
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var rows = await connection.QueryAsync<PermisoRow>(sql, new { Codigos = codigosList });
|
||||||
|
return rows.Select(MapRow).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Permiso MapRow(PermisoRow row)
|
||||||
|
=> Permiso.ForRead(
|
||||||
|
id: row.Id,
|
||||||
|
codigo: row.Codigo,
|
||||||
|
nombre: row.Nombre,
|
||||||
|
descripcion: row.Descripcion,
|
||||||
|
modulo: row.Modulo,
|
||||||
|
activo: row.Activo,
|
||||||
|
fechaCreacion: row.FechaCreacion);
|
||||||
|
|
||||||
|
private sealed record PermisoRow(
|
||||||
|
int Id,
|
||||||
|
string Codigo,
|
||||||
|
string Nombre,
|
||||||
|
string? Descripcion,
|
||||||
|
string Modulo,
|
||||||
|
bool Activo,
|
||||||
|
DateTime FechaCreacion);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using Dapper;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public sealed class RolPermisoRepository : IRolPermisoRepository
|
||||||
|
{
|
||||||
|
private readonly SqlConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public RolPermisoRepository(SqlConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Permiso>> GetByRolCodigoAsync(
|
||||||
|
string rolCodigo,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT p.Id, p.Codigo, p.Nombre, p.Descripcion, p.Modulo, p.Activo, p.FechaCreacion
|
||||||
|
FROM dbo.RolPermiso rp
|
||||||
|
JOIN dbo.Rol r ON r.Id = rp.RolId
|
||||||
|
JOIN dbo.Permiso p ON p.Id = rp.PermisoId
|
||||||
|
WHERE r.Codigo = @RolCodigo
|
||||||
|
ORDER BY p.Id
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var rows = await connection.QueryAsync<PermisoRow>(sql, new { RolCodigo = rolCodigo });
|
||||||
|
return rows.Select(MapRow).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReplaceForRolAsync(
|
||||||
|
int rolId,
|
||||||
|
IEnumerable<int> permisoIds,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var ids = permisoIds.ToList();
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Step 1: Delete all existing permisos for this rol
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"DELETE FROM dbo.RolPermiso WHERE RolId = @RolId",
|
||||||
|
new { RolId = rolId },
|
||||||
|
transaction);
|
||||||
|
|
||||||
|
// Step 2: Insert the new set (bulk via multi-row VALUES)
|
||||||
|
if (ids.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var permisoId in ids)
|
||||||
|
{
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"""
|
||||||
|
INSERT INTO dbo.RolPermiso (RolId, PermisoId)
|
||||||
|
VALUES (@RolId, @PermisoId)
|
||||||
|
""",
|
||||||
|
new { RolId = rolId, PermisoId = permisoId },
|
||||||
|
transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(ct);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Permiso MapRow(PermisoRow row)
|
||||||
|
=> Permiso.ForRead(
|
||||||
|
id: row.Id,
|
||||||
|
codigo: row.Codigo,
|
||||||
|
nombre: row.Nombre,
|
||||||
|
descripcion: row.Descripcion,
|
||||||
|
modulo: row.Modulo,
|
||||||
|
activo: row.Activo,
|
||||||
|
fechaCreacion: row.FechaCreacion);
|
||||||
|
|
||||||
|
private sealed record PermisoRow(
|
||||||
|
int Id,
|
||||||
|
string Codigo,
|
||||||
|
string Nombre,
|
||||||
|
string? Descripcion,
|
||||||
|
string Modulo,
|
||||||
|
bool Activo,
|
||||||
|
DateTime FechaCreacion);
|
||||||
|
}
|
||||||
@@ -31,7 +31,12 @@ public class RefreshTokenRepositoryTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
DbAdapter = DbAdapter.SqlServer,
|
DbAdapter = DbAdapter.SqlServer,
|
||||||
// Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks.
|
// Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks.
|
||||||
TablesToIgnore = [new Respawn.Graph.Table("dbo", "Rol")]
|
TablesToIgnore =
|
||||||
|
[
|
||||||
|
new Respawn.Graph.Table("dbo", "Rol"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Permiso"),
|
||||||
|
new Respawn.Graph.Table("dbo", "RolPermiso"),
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
await _respawner.ResetAsync(_connection);
|
await _respawner.ResetAsync(_connection);
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Integration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration tests for PermisoRepository against SIGCM2_Test.
|
||||||
|
/// RED: written before the repository implementation exists.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Database")]
|
||||||
|
public class PermisoRepositoryTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private const string ConnectionString =
|
||||||
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||||
|
|
||||||
|
private SqlConnection _connection = null!;
|
||||||
|
private PermisoRepository _repository = null!;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_connection = new SqlConnection(ConnectionString);
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
|
||||||
|
// Ensure the 18 canonical permisos are present — idempotent MERGE.
|
||||||
|
// Needed because other test classes (RefreshTokenRepositoryTests, UsuarioRepositoryTests)
|
||||||
|
// may call Respawn.ResetAsync before us, which would clear Permiso even if listed in
|
||||||
|
// TablesToIgnore of the central SqlTestFixture (each class configures its own Respawner).
|
||||||
|
await SeedPermisosCanonicalAsync();
|
||||||
|
|
||||||
|
var factory = new SqlConnectionFactory(ConnectionString);
|
||||||
|
_repository = new PermisoRepository(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedPermisosCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.Permiso AS t
|
||||||
|
USING (VALUES
|
||||||
|
('ventas:contado:crear', N'Cargar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:modificar', N'Modificar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:cobrar', N'Cobrar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:facturar', N'Facturar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:ctacte:crear', N'Cargar orden cuenta corriente', NULL, 'ventas'),
|
||||||
|
('ventas:ctacte:facturar', N'Facturar lote cuenta corriente', NULL, 'ventas'),
|
||||||
|
('textos:editar', N'Editar textos', NULL, 'textos'),
|
||||||
|
('textos:reclamos:ver', N'Ver reclamos de textos', NULL, 'textos'),
|
||||||
|
('pauta:azanu:ver', N'Ver AZANU en pauta', NULL, 'pauta'),
|
||||||
|
('pauta:limpiar', N'Limpieza de pauta', NULL, 'pauta'),
|
||||||
|
('pauta:recursos:fueradehora', N'Recursos fuera de hora', NULL, 'pauta'),
|
||||||
|
('productores:deuda:ver', N'Ver deuda propia de productores', NULL, 'productores'),
|
||||||
|
('productores:pendientes:crear', N'Cargar pendientes de productores', NULL, 'productores'),
|
||||||
|
('productores:deuda:bypass', N'Bypass de deuda de productores', NULL, 'productores'),
|
||||||
|
('administracion:usuarios:gestionar', N'Gestionar usuarios del sistema', N'Crear, editar y desactivar usuarios', 'administracion'),
|
||||||
|
('administracion:tarifarios:gestionar', N'Gestionar tarifarios', N'Crear y modificar tarifarios de publicidad', 'administracion'),
|
||||||
|
('administracion:medios:gestionar', N'Gestionar medios publicitarios', N'Alta y configuracion de medios', 'administracion'),
|
||||||
|
('administracion:auditoria:ver', N'Ver logs de auditoria', N'Acceso al dashboard de auditoria', 'administracion')
|
||||||
|
) AS s (Codigo, Nombre, Descripcion, Modulo)
|
||||||
|
ON t.Codigo = s.Codigo
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (Codigo, Nombre, Descripcion, Modulo)
|
||||||
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ListAsync ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAsync_Returns18CanonicalSeeds()
|
||||||
|
{
|
||||||
|
var list = await _repository.ListAsync();
|
||||||
|
|
||||||
|
// V005 seeds exactly 18 canonical permisos
|
||||||
|
Assert.Equal(18, list.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAsync_ContainsExpectedCodigos()
|
||||||
|
{
|
||||||
|
var list = await _repository.ListAsync();
|
||||||
|
var codigos = list.Select(p => p.Codigo).ToHashSet();
|
||||||
|
|
||||||
|
Assert.Contains("ventas:contado:crear", codigos);
|
||||||
|
Assert.Contains("ventas:contado:facturar", codigos);
|
||||||
|
Assert.Contains("administracion:usuarios:gestionar", codigos);
|
||||||
|
Assert.Contains("administracion:auditoria:ver", codigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListAsync_AllItemsHaveNonEmptyCodigoAndNombre()
|
||||||
|
{
|
||||||
|
var list = await _repository.ListAsync();
|
||||||
|
|
||||||
|
foreach (var p in list)
|
||||||
|
{
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(p.Codigo));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(p.Nombre));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GetByCodigoAsync ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigoAsync_ExistingCodigo_ReturnsPermiso()
|
||||||
|
{
|
||||||
|
var permiso = await _repository.GetByCodigoAsync("ventas:contado:crear");
|
||||||
|
|
||||||
|
Assert.NotNull(permiso);
|
||||||
|
Assert.Equal("ventas:contado:crear", permiso!.Codigo);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(permiso.Nombre));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigoAsync_AnotherExistingCodigo_ReturnsCorrectPermiso()
|
||||||
|
{
|
||||||
|
var permiso = await _repository.GetByCodigoAsync("administracion:usuarios:gestionar");
|
||||||
|
|
||||||
|
Assert.NotNull(permiso);
|
||||||
|
Assert.Equal("administracion:usuarios:gestionar", permiso!.Codigo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigoAsync_NonExistentCodigo_ReturnsNull()
|
||||||
|
{
|
||||||
|
var permiso = await _repository.GetByCodigoAsync("permiso:inexistente:xyz");
|
||||||
|
|
||||||
|
Assert.Null(permiso);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GetByCodigosAsync ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigosAsync_ThreeValidCodigos_ReturnsThreeEntities()
|
||||||
|
{
|
||||||
|
var codigos = new[]
|
||||||
|
{
|
||||||
|
"ventas:contado:crear",
|
||||||
|
"ventas:contado:facturar",
|
||||||
|
"textos:editar"
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _repository.GetByCodigosAsync(codigos);
|
||||||
|
|
||||||
|
Assert.Equal(3, result.Count);
|
||||||
|
var returnedCodigos = result.Select(p => p.Codigo).ToHashSet();
|
||||||
|
foreach (var c in codigos)
|
||||||
|
Assert.Contains(c, returnedCodigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigosAsync_MixedExistingAndNonExisting_ReturnsOnlyExisting()
|
||||||
|
{
|
||||||
|
var codigos = new[]
|
||||||
|
{
|
||||||
|
"ventas:contado:crear",
|
||||||
|
"permiso:no:existe",
|
||||||
|
"textos:editar"
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _repository.GetByCodigosAsync(codigos);
|
||||||
|
|
||||||
|
// Only 2 of 3 exist
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
var returnedCodigos = result.Select(p => p.Codigo).ToHashSet();
|
||||||
|
Assert.Contains("ventas:contado:crear", returnedCodigos);
|
||||||
|
Assert.Contains("textos:editar", returnedCodigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigosAsync_EmptyList_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var result = await _repository.GetByCodigosAsync(Array.Empty<string>());
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByCodigosAsync_AllNonExisting_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var codigos = new[] { "no:existe:uno", "no:existe:dos" };
|
||||||
|
|
||||||
|
var result = await _repository.GetByCodigosAsync(codigos);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Integration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration tests for RolPermisoRepository against SIGCM2_Test.
|
||||||
|
/// RED: written before the repository implementation exists.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Database")]
|
||||||
|
public class RolPermisoRepositoryTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private const string ConnectionString =
|
||||||
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||||
|
|
||||||
|
private SqlConnection _connection = null!;
|
||||||
|
private RolPermisoRepository _repository = null!;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_connection = new SqlConnection(ConnectionString);
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
|
||||||
|
// Ensure the 18 canonical permisos exist — idempotent MERGE.
|
||||||
|
// Other test classes call Respawn.ResetAsync which may have cleared Permiso.
|
||||||
|
await SeedPermisosCanonicalAsync();
|
||||||
|
|
||||||
|
// Ensure canonical RolPermiso seeds are present.
|
||||||
|
await SeedRolPermisosCanonicalAsync();
|
||||||
|
|
||||||
|
// Restore RolPermiso seeds for 'cajero' (4 permisos) in case prior test modified them.
|
||||||
|
await RestoreCajeroPermisosAsync();
|
||||||
|
|
||||||
|
var factory = new SqlConnectionFactory(ConnectionString);
|
||||||
|
_repository = new RolPermisoRepository(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedPermisosCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.Permiso AS t
|
||||||
|
USING (VALUES
|
||||||
|
('ventas:contado:crear', N'Cargar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:modificar', N'Modificar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:cobrar', N'Cobrar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:facturar', N'Facturar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:ctacte:crear', N'Cargar orden cuenta corriente', NULL, 'ventas'),
|
||||||
|
('ventas:ctacte:facturar', N'Facturar lote cuenta corriente', NULL, 'ventas'),
|
||||||
|
('textos:editar', N'Editar textos', NULL, 'textos'),
|
||||||
|
('textos:reclamos:ver', N'Ver reclamos de textos', NULL, 'textos'),
|
||||||
|
('pauta:azanu:ver', N'Ver AZANU en pauta', NULL, 'pauta'),
|
||||||
|
('pauta:limpiar', N'Limpieza de pauta', NULL, 'pauta'),
|
||||||
|
('pauta:recursos:fueradehora', N'Recursos fuera de hora', NULL, 'pauta'),
|
||||||
|
('productores:deuda:ver', N'Ver deuda propia de productores', NULL, 'productores'),
|
||||||
|
('productores:pendientes:crear', N'Cargar pendientes de productores', NULL, 'productores'),
|
||||||
|
('productores:deuda:bypass', N'Bypass de deuda de productores', NULL, 'productores'),
|
||||||
|
('administracion:usuarios:gestionar', N'Gestionar usuarios del sistema', N'Crear, editar y desactivar usuarios', 'administracion'),
|
||||||
|
('administracion:tarifarios:gestionar', N'Gestionar tarifarios', N'Crear y modificar tarifarios de publicidad', 'administracion'),
|
||||||
|
('administracion:medios:gestionar', N'Gestionar medios publicitarios', N'Alta y configuracion de medios', 'administracion'),
|
||||||
|
('administracion:auditoria:ver', N'Ver logs de auditoria', N'Acceso al dashboard de auditoria', 'administracion')
|
||||||
|
) AS s (Codigo, Nombre, Descripcion, Modulo)
|
||||||
|
ON t.Codigo = s.Codigo
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (Codigo, Nombre, Descripcion, Modulo)
|
||||||
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedRolPermisosCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.RolPermiso AS t
|
||||||
|
USING (
|
||||||
|
SELECT r.Id AS RolId, p.Id AS PermisoId
|
||||||
|
FROM (VALUES
|
||||||
|
('admin', 'ventas:contado:crear'),
|
||||||
|
('admin', 'ventas:contado:modificar'),
|
||||||
|
('admin', 'ventas:contado:cobrar'),
|
||||||
|
('admin', 'ventas:contado:facturar'),
|
||||||
|
('admin', 'ventas:ctacte:crear'),
|
||||||
|
('admin', 'ventas:ctacte:facturar'),
|
||||||
|
('admin', 'textos:editar'),
|
||||||
|
('admin', 'textos:reclamos:ver'),
|
||||||
|
('admin', 'pauta:azanu:ver'),
|
||||||
|
('admin', 'pauta:limpiar'),
|
||||||
|
('admin', 'pauta:recursos:fueradehora'),
|
||||||
|
('admin', 'productores:deuda:ver'),
|
||||||
|
('admin', 'productores:pendientes:crear'),
|
||||||
|
('admin', 'productores:deuda:bypass'),
|
||||||
|
('admin', 'administracion:usuarios:gestionar'),
|
||||||
|
('admin', 'administracion:tarifarios:gestionar'),
|
||||||
|
('admin', 'administracion:medios:gestionar'),
|
||||||
|
('admin', 'administracion:auditoria:ver'),
|
||||||
|
('cajero', 'ventas:contado:crear'),
|
||||||
|
('cajero', 'ventas:contado:modificar'),
|
||||||
|
('cajero', 'ventas:contado:cobrar'),
|
||||||
|
('cajero', 'ventas:contado:facturar'),
|
||||||
|
('operador_ctacte', 'ventas:ctacte:crear'),
|
||||||
|
('operador_ctacte', 'ventas:ctacte:facturar'),
|
||||||
|
('picadora', 'textos:editar'),
|
||||||
|
('picadora', 'textos:reclamos:ver'),
|
||||||
|
('jefe_publicidad', 'textos:editar'),
|
||||||
|
('jefe_publicidad', 'textos:reclamos:ver'),
|
||||||
|
('jefe_publicidad', 'pauta:azanu:ver'),
|
||||||
|
('jefe_publicidad', 'pauta:limpiar'),
|
||||||
|
('jefe_publicidad', 'pauta:recursos:fueradehora'),
|
||||||
|
('jefe_publicidad', 'productores:deuda:ver'),
|
||||||
|
('jefe_publicidad', 'productores:deuda:bypass'),
|
||||||
|
('productor', 'productores:deuda:ver'),
|
||||||
|
('productor', 'productores:pendientes:crear'),
|
||||||
|
('diagramacion', 'pauta:azanu:ver')
|
||||||
|
) AS x (RolCodigo, PermisoCodigo)
|
||||||
|
JOIN dbo.Rol r ON r.Codigo = x.RolCodigo
|
||||||
|
JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
|
||||||
|
) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
// Restore cajero permisos so TablesToIgnore still reflects clean seed state.
|
||||||
|
await RestoreCajeroPermisosAsync();
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores the 4 canonical cajero permisos to match V006 seed state.
|
||||||
|
/// Uses MERGE so it's idempotent.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RestoreCajeroPermisosAsync()
|
||||||
|
{
|
||||||
|
// Delete any extra permisos assigned to cajero by tests
|
||||||
|
await _connection.ExecuteAsync("""
|
||||||
|
DELETE rp FROM dbo.RolPermiso rp
|
||||||
|
JOIN dbo.Rol r ON r.Id = rp.RolId
|
||||||
|
JOIN dbo.Permiso p ON p.Id = rp.PermisoId
|
||||||
|
WHERE r.Codigo = 'cajero'
|
||||||
|
AND p.Codigo NOT IN (
|
||||||
|
'ventas:contado:crear',
|
||||||
|
'ventas:contado:modificar',
|
||||||
|
'ventas:contado:cobrar',
|
||||||
|
'ventas:contado:facturar'
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
|
||||||
|
// Re-add the 4 canonical cajero permisos if missing
|
||||||
|
await _connection.ExecuteAsync("""
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.RolPermiso AS t
|
||||||
|
USING (
|
||||||
|
SELECT r.Id AS RolId, p.Id AS PermisoId
|
||||||
|
FROM (VALUES
|
||||||
|
('cajero', 'ventas:contado:crear'),
|
||||||
|
('cajero', 'ventas:contado:modificar'),
|
||||||
|
('cajero', 'ventas:contado:cobrar'),
|
||||||
|
('cajero', 'ventas:contado:facturar')
|
||||||
|
) AS x (RolCodigo, PermisoCodigo)
|
||||||
|
JOIN dbo.Rol r ON r.Codigo = x.RolCodigo
|
||||||
|
JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
|
||||||
|
) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GetByRolCodigoAsync ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_Admin_Returns18Permisos()
|
||||||
|
{
|
||||||
|
// admin has all 18 permisos assigned in V006 seed
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("admin");
|
||||||
|
|
||||||
|
Assert.Equal(18, permisos.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_Admin_ContainsAllModules()
|
||||||
|
{
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("admin");
|
||||||
|
var codigos = permisos.Select(p => p.Codigo).ToHashSet();
|
||||||
|
|
||||||
|
Assert.Contains("ventas:contado:crear", codigos);
|
||||||
|
Assert.Contains("administracion:auditoria:ver", codigos);
|
||||||
|
Assert.Contains("pauta:limpiar", codigos);
|
||||||
|
Assert.Contains("productores:deuda:bypass", codigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_Cajero_Returns4Permisos()
|
||||||
|
{
|
||||||
|
// cajero: ventas:contado:crear, :modificar, :cobrar, :facturar
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
|
||||||
|
Assert.Equal(4, permisos.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_Cajero_OnlyVentasContadoPermisos()
|
||||||
|
{
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
var codigos = permisos.Select(p => p.Codigo).ToHashSet();
|
||||||
|
|
||||||
|
Assert.Contains("ventas:contado:crear", codigos);
|
||||||
|
Assert.Contains("ventas:contado:modificar", codigos);
|
||||||
|
Assert.Contains("ventas:contado:cobrar", codigos);
|
||||||
|
Assert.Contains("ventas:contado:facturar", codigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_Reportes_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// 'reportes' rol has 0 permisos in V006 seed
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("reportes");
|
||||||
|
|
||||||
|
Assert.Empty(permisos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_NonExistentRol_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
// Unknown rol código — returns empty list, not an exception
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("rol_inexistente_xyz");
|
||||||
|
|
||||||
|
Assert.Empty(permisos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByRolCodigoAsync_ReturnsFullPermisoEntities()
|
||||||
|
{
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
|
||||||
|
var primero = permisos.First();
|
||||||
|
// All entity fields must be populated
|
||||||
|
Assert.True(primero.Id > 0);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(primero.Codigo));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(primero.Nombre));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ReplaceForRolAsync ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReplaceForRolAsync_ReplacesExistingSetWithNewSet()
|
||||||
|
{
|
||||||
|
// Get cajero's rol ID and a different permiso ID
|
||||||
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
||||||
|
var textoPermisoId = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT Id FROM dbo.Permiso WHERE Codigo = 'textos:editar'");
|
||||||
|
|
||||||
|
// Replace cajero's 4 permisos with just 1
|
||||||
|
await _repository.ReplaceForRolAsync(cajeroId, new[] { textoPermisoId });
|
||||||
|
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
Assert.Single(permisos);
|
||||||
|
Assert.Equal("textos:editar", permisos[0].Codigo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReplaceForRolAsync_Idempotent_SameCallTwiceProducesSameResult()
|
||||||
|
{
|
||||||
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
||||||
|
var permisoId = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT Id FROM dbo.Permiso WHERE Codigo = 'ventas:contado:crear'");
|
||||||
|
|
||||||
|
await _repository.ReplaceForRolAsync(cajeroId, new[] { permisoId });
|
||||||
|
await _repository.ReplaceForRolAsync(cajeroId, new[] { permisoId });
|
||||||
|
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
Assert.Single(permisos);
|
||||||
|
Assert.Equal("ventas:contado:crear", permisos[0].Codigo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReplaceForRolAsync_WithEmptyList_DeletesAllPermisos()
|
||||||
|
{
|
||||||
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
||||||
|
|
||||||
|
await _repository.ReplaceForRolAsync(cajeroId, Array.Empty<int>());
|
||||||
|
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
Assert.Empty(permisos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReplaceForRolAsync_PostReplace_GetReflectsNewSet()
|
||||||
|
{
|
||||||
|
var cajeroId = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT Id FROM dbo.Rol WHERE Codigo = 'cajero'");
|
||||||
|
|
||||||
|
// Get IDs of 2 specific permisos
|
||||||
|
var rows = await _connection.QueryAsync<(int Id, string Codigo)>(
|
||||||
|
"SELECT Id, Codigo FROM dbo.Permiso WHERE Codigo IN ('pauta:azanu:ver', 'pauta:limpiar')");
|
||||||
|
var ids = rows.Select(r => r.Id).ToArray();
|
||||||
|
|
||||||
|
await _repository.ReplaceForRolAsync(cajeroId, ids);
|
||||||
|
|
||||||
|
var permisos = await _repository.GetByRolCodigoAsync("cajero");
|
||||||
|
var codigos = permisos.Select(p => p.Codigo).ToHashSet();
|
||||||
|
|
||||||
|
Assert.Equal(2, permisos.Count);
|
||||||
|
Assert.Contains("pauta:azanu:ver", codigos);
|
||||||
|
Assert.Contains("pauta:limpiar", codigos);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,12 @@ public class UsuarioRepositoryTests : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
DbAdapter = DbAdapter.SqlServer,
|
DbAdapter = DbAdapter.SqlServer,
|
||||||
// Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks.
|
// Rol is a lookup table seeded by migration V003 — never wipe or Usuario FK breaks.
|
||||||
TablesToIgnore = [new Respawn.Graph.Table("dbo", "Rol")]
|
TablesToIgnore =
|
||||||
|
[
|
||||||
|
new Respawn.Graph.Table("dbo", "Rol"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Permiso"),
|
||||||
|
new Respawn.Graph.Table("dbo", "RolPermiso"),
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset DB, re-seed Rol canonical table (lookup) and admin user for each test class run.
|
// Reset DB, re-seed Rol canonical table (lookup) and admin user for each test class run.
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
|||||||
{
|
{
|
||||||
await _respawner.ResetAsync(_connection);
|
await _respawner.ResetAsync(_connection);
|
||||||
await SeedRolCanonicalAsync();
|
await SeedRolCanonicalAsync();
|
||||||
|
await SeedPermisosCanonicalAsync();
|
||||||
|
await SeedRolPermisosCanonicalAsync();
|
||||||
await SeedAdminAsync();
|
await SeedAdminAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +83,93 @@ public sealed class SqlTestFixture : IAsyncLifetime
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SeedPermisosCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.Permiso AS t
|
||||||
|
USING (VALUES
|
||||||
|
('ventas:contado:crear', N'Cargar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:modificar', N'Modificar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:cobrar', N'Cobrar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:contado:facturar', N'Facturar orden contado', NULL, 'ventas'),
|
||||||
|
('ventas:ctacte:crear', N'Cargar orden cuenta corriente', NULL, 'ventas'),
|
||||||
|
('ventas:ctacte:facturar', N'Facturar lote cuenta corriente', NULL, 'ventas'),
|
||||||
|
('textos:editar', N'Editar textos', NULL, 'textos'),
|
||||||
|
('textos:reclamos:ver', N'Ver reclamos de textos', NULL, 'textos'),
|
||||||
|
('pauta:azanu:ver', N'Ver AZANU en pauta', NULL, 'pauta'),
|
||||||
|
('pauta:limpiar', N'Limpieza de pauta', NULL, 'pauta'),
|
||||||
|
('pauta:recursos:fueradehora', N'Recursos fuera de hora', NULL, 'pauta'),
|
||||||
|
('productores:deuda:ver', N'Ver deuda propia de productores', NULL, 'productores'),
|
||||||
|
('productores:pendientes:crear', N'Cargar pendientes de productores', NULL, 'productores'),
|
||||||
|
('productores:deuda:bypass', N'Bypass de deuda de productores', NULL, 'productores'),
|
||||||
|
('administracion:usuarios:gestionar', N'Gestionar usuarios del sistema', N'Crear, editar y desactivar usuarios', 'administracion'),
|
||||||
|
('administracion:tarifarios:gestionar', N'Gestionar tarifarios', N'Crear y modificar tarifarios de publicidad', 'administracion'),
|
||||||
|
('administracion:medios:gestionar', N'Gestionar medios publicitarios', N'Alta y configuracion de medios', 'administracion'),
|
||||||
|
('administracion:auditoria:ver', N'Ver logs de auditoria', N'Acceso al dashboard de auditoria', 'administracion')
|
||||||
|
) AS s (Codigo, Nombre, Descripcion, Modulo)
|
||||||
|
ON t.Codigo = s.Codigo
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (Codigo, Nombre, Descripcion, Modulo)
|
||||||
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, s.Modulo);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedRolPermisosCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.RolPermiso AS t
|
||||||
|
USING (
|
||||||
|
SELECT r.Id AS RolId, p.Id AS PermisoId
|
||||||
|
FROM (VALUES
|
||||||
|
('admin', 'ventas:contado:crear'),
|
||||||
|
('admin', 'ventas:contado:modificar'),
|
||||||
|
('admin', 'ventas:contado:cobrar'),
|
||||||
|
('admin', 'ventas:contado:facturar'),
|
||||||
|
('admin', 'ventas:ctacte:crear'),
|
||||||
|
('admin', 'ventas:ctacte:facturar'),
|
||||||
|
('admin', 'textos:editar'),
|
||||||
|
('admin', 'textos:reclamos:ver'),
|
||||||
|
('admin', 'pauta:azanu:ver'),
|
||||||
|
('admin', 'pauta:limpiar'),
|
||||||
|
('admin', 'pauta:recursos:fueradehora'),
|
||||||
|
('admin', 'productores:deuda:ver'),
|
||||||
|
('admin', 'productores:pendientes:crear'),
|
||||||
|
('admin', 'productores:deuda:bypass'),
|
||||||
|
('admin', 'administracion:usuarios:gestionar'),
|
||||||
|
('admin', 'administracion:tarifarios:gestionar'),
|
||||||
|
('admin', 'administracion:medios:gestionar'),
|
||||||
|
('admin', 'administracion:auditoria:ver'),
|
||||||
|
('cajero', 'ventas:contado:crear'),
|
||||||
|
('cajero', 'ventas:contado:modificar'),
|
||||||
|
('cajero', 'ventas:contado:cobrar'),
|
||||||
|
('cajero', 'ventas:contado:facturar'),
|
||||||
|
('operador_ctacte', 'ventas:ctacte:crear'),
|
||||||
|
('operador_ctacte', 'ventas:ctacte:facturar'),
|
||||||
|
('picadora', 'textos:editar'),
|
||||||
|
('picadora', 'textos:reclamos:ver'),
|
||||||
|
('jefe_publicidad', 'textos:editar'),
|
||||||
|
('jefe_publicidad', 'textos:reclamos:ver'),
|
||||||
|
('jefe_publicidad', 'pauta:azanu:ver'),
|
||||||
|
('jefe_publicidad', 'pauta:limpiar'),
|
||||||
|
('jefe_publicidad', 'pauta:recursos:fueradehora'),
|
||||||
|
('jefe_publicidad', 'productores:deuda:ver'),
|
||||||
|
('jefe_publicidad', 'productores:deuda:bypass'),
|
||||||
|
('productor', 'productores:deuda:ver'),
|
||||||
|
('productor', 'productores:pendientes:crear'),
|
||||||
|
('diagramacion', 'pauta:azanu:ver')
|
||||||
|
) AS x (RolCodigo, PermisoCodigo)
|
||||||
|
JOIN dbo.Rol r ON r.Codigo = x.RolCodigo
|
||||||
|
JOIN dbo.Permiso p ON p.Codigo = x.PermisoCodigo
|
||||||
|
) AS s ON t.RolId = s.RolId AND t.PermisoId = s.PermisoId
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (RolId, PermisoId) VALUES (s.RolId, s.PermisoId);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SeedAdminAsync()
|
private async Task SeedAdminAsync()
|
||||||
{
|
{
|
||||||
const string sql = """
|
const string sql = """
|
||||||
|
|||||||
Reference in New Issue
Block a user