feat(infra): MedioRepository + SeccionRepository + integration tests — ADM-001 B5
This commit is contained in:
@@ -32,6 +32,8 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IRolRepository, RolRepository>();
|
services.AddScoped<IRolRepository, RolRepository>();
|
||||||
services.AddScoped<IPermisoRepository, PermisoRepository>();
|
services.AddScoped<IPermisoRepository, PermisoRepository>();
|
||||||
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
|
services.AddScoped<IRolPermisoRepository, RolPermisoRepository>();
|
||||||
|
services.AddScoped<IMedioRepository, MedioRepository>();
|
||||||
|
services.AddScoped<ISeccionRepository, SeccionRepository>();
|
||||||
|
|
||||||
// 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"));
|
||||||
|
|||||||
188
src/api/SIGCM2.Infrastructure/Persistence/MedioRepository.cs
Normal file
188
src/api/SIGCM2.Infrastructure/Persistence/MedioRepository.cs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Dapper;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Common;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public sealed class MedioRepository : IMedioRepository
|
||||||
|
{
|
||||||
|
private readonly SqlConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public MedioRepository(SqlConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> AddAsync(Medio m, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO dbo.Medio (Codigo, Nombre, Tipo, PlataformaEmpresaId)
|
||||||
|
OUTPUT INSERTED.Id
|
||||||
|
VALUES (@Codigo, @Nombre, @Tipo, @PlataformaEmpresaId)
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
return await connection.ExecuteScalarAsync<int>(sql, new
|
||||||
|
{
|
||||||
|
m.Codigo,
|
||||||
|
m.Nombre,
|
||||||
|
Tipo = (int)m.Tipo,
|
||||||
|
m.PlataformaEmpresaId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Medio?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT Id, Codigo, Nombre, Tipo, PlataformaEmpresaId, Activo, FechaCreacion, FechaModificacion
|
||||||
|
FROM dbo.Medio
|
||||||
|
WHERE Id = @Id
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var row = await connection.QuerySingleOrDefaultAsync<MedioRow>(sql, new { Id = id });
|
||||||
|
return row is null ? null : MapRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsByCodigoAsync(string codigo, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT COUNT(1) FROM dbo.Medio WHERE Codigo = @Codigo
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var count = await connection.ExecuteScalarAsync<int>(sql, new { Codigo = codigo });
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(Medio m, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
UPDATE dbo.Medio
|
||||||
|
SET Nombre = @Nombre,
|
||||||
|
Tipo = @Tipo,
|
||||||
|
PlataformaEmpresaId = @PlataformaEmpresaId,
|
||||||
|
Activo = @Activo,
|
||||||
|
FechaModificacion = @FechaModificacion
|
||||||
|
WHERE Id = @Id
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(sql, new
|
||||||
|
{
|
||||||
|
m.Nombre,
|
||||||
|
Tipo = (int)m.Tipo,
|
||||||
|
m.PlataformaEmpresaId,
|
||||||
|
m.Activo,
|
||||||
|
FechaModificacion = m.FechaModificacion ?? DateTime.UtcNow,
|
||||||
|
m.Id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<Medio>> GetPagedAsync(MediosQuery q, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var page = Math.Max(1, q.Page);
|
||||||
|
var pageSize = Math.Clamp(q.PageSize, 1, 100);
|
||||||
|
var offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
var where = new StringBuilder("WHERE 1=1");
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("PageSize", pageSize);
|
||||||
|
parameters.Add("Offset", offset);
|
||||||
|
|
||||||
|
if (q.Activo.HasValue)
|
||||||
|
{
|
||||||
|
where.Append(" AND Activo = @Activo");
|
||||||
|
parameters.Add("Activo", q.Activo.Value ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q.Tipo.HasValue)
|
||||||
|
{
|
||||||
|
where.Append(" AND Tipo = @Tipo");
|
||||||
|
parameters.Add("Tipo", (int)q.Tipo.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(q.Search))
|
||||||
|
{
|
||||||
|
where.Append(" AND (Codigo LIKE @Search OR Nombre LIKE @Search)");
|
||||||
|
parameters.Add("Search", $"%{q.Search}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = $"""
|
||||||
|
SELECT
|
||||||
|
Id, Codigo, Nombre, Tipo, PlataformaEmpresaId, Activo, FechaCreacion, FechaModificacion,
|
||||||
|
COUNT(*) OVER() AS TotalCount
|
||||||
|
FROM dbo.Medio
|
||||||
|
{where}
|
||||||
|
ORDER BY Codigo
|
||||||
|
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var rows = await connection.QueryAsync<MedioPagedRow>(sql, parameters);
|
||||||
|
var list = rows.ToList();
|
||||||
|
|
||||||
|
var total = list.Count > 0 ? list[0].TotalCount : 0;
|
||||||
|
var items = list.Select(r => MapRow(r)).ToList();
|
||||||
|
|
||||||
|
return new PagedResult<Medio>(items, page, pageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── mapping ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static Medio MapRow(MedioRow r)
|
||||||
|
=> new(
|
||||||
|
id: r.Id,
|
||||||
|
codigo: r.Codigo,
|
||||||
|
nombre: r.Nombre,
|
||||||
|
tipo: (TipoMedio)r.Tipo,
|
||||||
|
plataformaEmpresaId: r.PlataformaEmpresaId,
|
||||||
|
activo: r.Activo,
|
||||||
|
fechaCreacion: r.FechaCreacion,
|
||||||
|
fechaModificacion: r.FechaModificacion);
|
||||||
|
|
||||||
|
private static Medio MapRow(MedioPagedRow r)
|
||||||
|
=> new(
|
||||||
|
id: r.Id,
|
||||||
|
codigo: r.Codigo,
|
||||||
|
nombre: r.Nombre,
|
||||||
|
tipo: (TipoMedio)r.Tipo,
|
||||||
|
plataformaEmpresaId: r.PlataformaEmpresaId,
|
||||||
|
activo: r.Activo,
|
||||||
|
fechaCreacion: r.FechaCreacion,
|
||||||
|
fechaModificacion: r.FechaModificacion);
|
||||||
|
|
||||||
|
private sealed record MedioRow(
|
||||||
|
int Id,
|
||||||
|
string Codigo,
|
||||||
|
string Nombre,
|
||||||
|
byte Tipo,
|
||||||
|
int? PlataformaEmpresaId,
|
||||||
|
bool Activo,
|
||||||
|
DateTime FechaCreacion,
|
||||||
|
DateTime? FechaModificacion);
|
||||||
|
|
||||||
|
private sealed record MedioPagedRow(
|
||||||
|
int Id,
|
||||||
|
string Codigo,
|
||||||
|
string Nombre,
|
||||||
|
byte Tipo,
|
||||||
|
int? PlataformaEmpresaId,
|
||||||
|
bool Activo,
|
||||||
|
DateTime FechaCreacion,
|
||||||
|
DateTime? FechaModificacion,
|
||||||
|
int TotalCount);
|
||||||
|
}
|
||||||
197
src/api/SIGCM2.Infrastructure/Persistence/SeccionRepository.cs
Normal file
197
src/api/SIGCM2.Infrastructure/Persistence/SeccionRepository.cs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Dapper;
|
||||||
|
using SIGCM2.Application.Abstractions.Persistence;
|
||||||
|
using SIGCM2.Application.Common;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
|
||||||
|
namespace SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
public sealed class SeccionRepository : ISeccionRepository
|
||||||
|
{
|
||||||
|
private readonly SqlConnectionFactory _connectionFactory;
|
||||||
|
|
||||||
|
public SeccionRepository(SqlConnectionFactory connectionFactory)
|
||||||
|
{
|
||||||
|
_connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> AddAsync(Seccion s, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// DF handles: Activo (1), FechaCreacion (SYSUTCDATETIME()).
|
||||||
|
// FK_Seccion_Medio: if MedioId does not exist, SQL Server raises FK violation — let it bubble.
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO dbo.Seccion (MedioId, Codigo, Nombre, Tipo)
|
||||||
|
OUTPUT INSERTED.Id
|
||||||
|
VALUES (@MedioId, @Codigo, @Nombre, @Tipo)
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
return await connection.ExecuteScalarAsync<int>(sql, new
|
||||||
|
{
|
||||||
|
s.MedioId,
|
||||||
|
s.Codigo,
|
||||||
|
s.Nombre,
|
||||||
|
s.Tipo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Seccion?> GetByIdAsync(int id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT Id, MedioId, Codigo, Nombre, Tipo, Activo, FechaCreacion, FechaModificacion
|
||||||
|
FROM dbo.Seccion
|
||||||
|
WHERE Id = @Id
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var row = await connection.QuerySingleOrDefaultAsync<SeccionRow>(sql, new { Id = id });
|
||||||
|
return row is null ? null : MapRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ExistsByCodigoInMedioAsync(int medioId, string codigo, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT COUNT(1) FROM dbo.Seccion WHERE MedioId = @MedioId AND Codigo = @Codigo
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var count = await connection.ExecuteScalarAsync<int>(sql, new { MedioId = medioId, Codigo = codigo });
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(Seccion s, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
UPDATE dbo.Seccion
|
||||||
|
SET Nombre = @Nombre,
|
||||||
|
Tipo = @Tipo,
|
||||||
|
Activo = @Activo,
|
||||||
|
FechaModificacion = @FechaModificacion
|
||||||
|
WHERE Id = @Id
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
await connection.ExecuteAsync(sql, new
|
||||||
|
{
|
||||||
|
s.Nombre,
|
||||||
|
s.Tipo,
|
||||||
|
s.Activo,
|
||||||
|
FechaModificacion = s.FechaModificacion ?? DateTime.UtcNow,
|
||||||
|
s.Id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedResult<Seccion>> GetPagedAsync(SeccionesQuery q, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var page = Math.Max(1, q.Page);
|
||||||
|
var pageSize = Math.Clamp(q.PageSize, 1, 100);
|
||||||
|
var offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
var where = new StringBuilder("WHERE 1=1");
|
||||||
|
var parameters = new DynamicParameters();
|
||||||
|
parameters.Add("PageSize", pageSize);
|
||||||
|
parameters.Add("Offset", offset);
|
||||||
|
|
||||||
|
if (q.MedioId.HasValue)
|
||||||
|
{
|
||||||
|
where.Append(" AND MedioId = @MedioId");
|
||||||
|
parameters.Add("MedioId", q.MedioId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(q.Tipo))
|
||||||
|
{
|
||||||
|
where.Append(" AND Tipo = @Tipo");
|
||||||
|
parameters.Add("Tipo", q.Tipo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q.Activo.HasValue)
|
||||||
|
{
|
||||||
|
where.Append(" AND Activo = @Activo");
|
||||||
|
parameters.Add("Activo", q.Activo.Value ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(q.Search))
|
||||||
|
{
|
||||||
|
where.Append(" AND (Codigo LIKE @Search OR Nombre LIKE @Search)");
|
||||||
|
parameters.Add("Search", $"%{q.Search}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADM-001: filter only Seccion.Activo; Medio.Activo check is left to Application/UI layer.
|
||||||
|
// Joining Medio to filter on m.Activo would affect performance for large catalogs and is
|
||||||
|
// not required by the current specs. REQ-SEC-003 (Deactivate Medio hides Secciones) is
|
||||||
|
// enforced at the Application handler level, not the query level.
|
||||||
|
var sql = $"""
|
||||||
|
SELECT
|
||||||
|
Id, MedioId, Codigo, Nombre, Tipo, Activo, FechaCreacion, FechaModificacion,
|
||||||
|
COUNT(*) OVER() AS TotalCount
|
||||||
|
FROM dbo.Seccion
|
||||||
|
{where}
|
||||||
|
ORDER BY MedioId, Codigo
|
||||||
|
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var connection = _connectionFactory.CreateConnection();
|
||||||
|
await connection.OpenAsync(ct);
|
||||||
|
|
||||||
|
var rows = await connection.QueryAsync<SeccionPagedRow>(sql, parameters);
|
||||||
|
var list = rows.ToList();
|
||||||
|
|
||||||
|
var total = list.Count > 0 ? list[0].TotalCount : 0;
|
||||||
|
var items = list.Select(r => MapRow(r)).ToList();
|
||||||
|
|
||||||
|
return new PagedResult<Seccion>(items, page, pageSize, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── mapping ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static Seccion MapRow(SeccionRow r)
|
||||||
|
=> new(
|
||||||
|
id: r.Id,
|
||||||
|
medioId: r.MedioId,
|
||||||
|
codigo: r.Codigo,
|
||||||
|
nombre: r.Nombre,
|
||||||
|
tipo: r.Tipo,
|
||||||
|
activo: r.Activo,
|
||||||
|
fechaCreacion: r.FechaCreacion,
|
||||||
|
fechaModificacion: r.FechaModificacion);
|
||||||
|
|
||||||
|
private static Seccion MapRow(SeccionPagedRow r)
|
||||||
|
=> new(
|
||||||
|
id: r.Id,
|
||||||
|
medioId: r.MedioId,
|
||||||
|
codigo: r.Codigo,
|
||||||
|
nombre: r.Nombre,
|
||||||
|
tipo: r.Tipo,
|
||||||
|
activo: r.Activo,
|
||||||
|
fechaCreacion: r.FechaCreacion,
|
||||||
|
fechaModificacion: r.FechaModificacion);
|
||||||
|
|
||||||
|
private sealed record SeccionRow(
|
||||||
|
int Id,
|
||||||
|
int MedioId,
|
||||||
|
string Codigo,
|
||||||
|
string Nombre,
|
||||||
|
string Tipo,
|
||||||
|
bool Activo,
|
||||||
|
DateTime FechaCreacion,
|
||||||
|
DateTime? FechaModificacion);
|
||||||
|
|
||||||
|
private sealed record SeccionPagedRow(
|
||||||
|
int Id,
|
||||||
|
int MedioId,
|
||||||
|
string Codigo,
|
||||||
|
string Nombre,
|
||||||
|
string Tipo,
|
||||||
|
bool Activo,
|
||||||
|
DateTime FechaCreacion,
|
||||||
|
DateTime? FechaModificacion,
|
||||||
|
int TotalCount);
|
||||||
|
}
|
||||||
262
tests/SIGCM2.Application.Tests/Medios/MedioRepositoryTests.cs
Normal file
262
tests/SIGCM2.Application.Tests/Medios/MedioRepositoryTests.cs
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Respawn;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Medios;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration tests for MedioRepository against SIGCM2_Test.
|
||||||
|
/// TDD: RED written before implementation, GREEN after MedioRepository was created.
|
||||||
|
/// Temporal: after UpdateAsync, dbo.Medio_History MUST have ≥1 row for that Id.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Database")]
|
||||||
|
public class MedioRepositoryTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private const string ConnectionString =
|
||||||
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||||
|
|
||||||
|
private SqlConnection _connection = null!;
|
||||||
|
private Respawner _respawner = null!;
|
||||||
|
private MedioRepository _repository = null!;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_connection = new SqlConnection(ConnectionString);
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
|
||||||
|
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
|
||||||
|
{
|
||||||
|
DbAdapter = DbAdapter.SqlServer,
|
||||||
|
TablesToIgnore =
|
||||||
|
[
|
||||||
|
new Respawn.Graph.Table("dbo", "Rol"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Permiso"),
|
||||||
|
new Respawn.Graph.Table("dbo", "RolPermiso"),
|
||||||
|
// *_History tables are system-versioned — engine rejects direct DELETE.
|
||||||
|
new Respawn.Graph.Table("dbo", "Usuario_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Rol_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Permiso_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "RolPermiso_History"),
|
||||||
|
// ADM-001 (V011): Medio + Seccion are temporal — history tables cannot be directly deleted.
|
||||||
|
new Respawn.Graph.Table("dbo", "Medio_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Seccion_History"),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await _respawner.ResetAsync(_connection);
|
||||||
|
await SeedRolCanonicalAsync();
|
||||||
|
|
||||||
|
var factory = new SqlConnectionFactory(ConnectionString);
|
||||||
|
_repository = new MedioRepository(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddAsync_ThenGetById_ReturnsAllColumns()
|
||||||
|
{
|
||||||
|
var medio = Medio.ForCreation("DIARIO01", "Diario Uno", TipoMedio.Diario, null);
|
||||||
|
|
||||||
|
var id = await _repository.AddAsync(medio);
|
||||||
|
var result = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(id, result!.Id);
|
||||||
|
Assert.Equal("DIARIO01", result.Codigo);
|
||||||
|
Assert.Equal("Diario Uno", result.Nombre);
|
||||||
|
Assert.Equal(TipoMedio.Diario, result.Tipo);
|
||||||
|
Assert.Null(result.PlataformaEmpresaId);
|
||||||
|
Assert.True(result.Activo);
|
||||||
|
Assert.True(result.FechaCreacion > DateTime.MinValue);
|
||||||
|
Assert.Null(result.FechaModificacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddAsync_WithPlataformaEmpresaId_PersistsValue()
|
||||||
|
{
|
||||||
|
var medio = Medio.ForCreation("RADIO99", "Radio Test", TipoMedio.Radio, 42);
|
||||||
|
|
||||||
|
var id = await _repository.AddAsync(medio);
|
||||||
|
var result = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(42, result!.PlataformaEmpresaId);
|
||||||
|
Assert.Equal(TipoMedio.Radio, result.Tipo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByIdAsync_NonExistent_ReturnsNull()
|
||||||
|
{
|
||||||
|
var result = await _repository.GetByIdAsync(999999);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ExistsByCodigoAsync ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExistsByCodigoAsync_AfterAdd_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var medio = Medio.ForCreation("EXIST01", "Existe", TipoMedio.Web, null);
|
||||||
|
await _repository.AddAsync(medio);
|
||||||
|
|
||||||
|
var exists = await _repository.ExistsByCodigoAsync("EXIST01");
|
||||||
|
|
||||||
|
Assert.True(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExistsByCodigoAsync_NotAdded_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var exists = await _repository.ExistsByCodigoAsync("NOEXISTE_XYZ");
|
||||||
|
|
||||||
|
Assert.False(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExistsByCodigoAsync_IsCaseSensitive_MatchesAsStored()
|
||||||
|
{
|
||||||
|
var medio = Medio.ForCreation("UPPER01", "Upper Medio", TipoMedio.Diario, null);
|
||||||
|
await _repository.AddAsync(medio);
|
||||||
|
|
||||||
|
// Stored as 'UPPER01'; searching lowercase should not match (SQL_Latin1 collation is CI
|
||||||
|
// on most default installs, but the contract is: match as stored).
|
||||||
|
var exactMatch = await _repository.ExistsByCodigoAsync("UPPER01");
|
||||||
|
Assert.True(exactMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UpdateAsync + Temporal ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ThenQuery_ReflectsNewValues()
|
||||||
|
{
|
||||||
|
var id = await _repository.AddAsync(Medio.ForCreation("UPD01", "Original", TipoMedio.Diario, null));
|
||||||
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
var updated = original!.WithUpdatedProfile("Actualizado", TipoMedio.Radio, 7);
|
||||||
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
|
var result = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("Actualizado", result!.Nombre);
|
||||||
|
Assert.Equal(TipoMedio.Radio, result.Tipo);
|
||||||
|
Assert.Equal(7, result.PlataformaEmpresaId);
|
||||||
|
Assert.NotNull(result.FechaModificacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ProducesHistoryRow()
|
||||||
|
{
|
||||||
|
// Temporal: SQL Server automatically writes the previous row version to Medio_History on UPDATE.
|
||||||
|
var id = await _repository.AddAsync(Medio.ForCreation("HIST01", "Historial", TipoMedio.Diario, null));
|
||||||
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
var updated = original!.WithUpdatedProfile("Historial v2", TipoMedio.Web, null);
|
||||||
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
|
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT COUNT(*) FROM dbo.Medio_History WHERE Id = @Id", new { Id = id });
|
||||||
|
|
||||||
|
Assert.True(historyCount >= 1, $"Expected ≥1 history row for Medio Id={id}, got {historyCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GetPagedAsync ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_FilterByActivo_ReturnsOnlyActiveWhenTrue()
|
||||||
|
{
|
||||||
|
var idActivo = await _repository.AddAsync(Medio.ForCreation("ACTV01", "Activo", TipoMedio.Diario, null));
|
||||||
|
var idInact = await _repository.AddAsync(Medio.ForCreation("INACT01", "Inactivo", TipoMedio.Diario, null));
|
||||||
|
|
||||||
|
// Deactivate second medio
|
||||||
|
var inact = await _repository.GetByIdAsync(idInact);
|
||||||
|
await _repository.UpdateAsync(inact!.WithActivo(false));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, Activo: true, Tipo: null, Search: null));
|
||||||
|
|
||||||
|
var codigos = result.Items.Select(m => m.Codigo).ToHashSet();
|
||||||
|
Assert.Contains("ACTV01", codigos);
|
||||||
|
Assert.DoesNotContain("INACT01", codigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_FilterByTipo_ReturnsOnlyMatchingTipo()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("RADIO01", "Radio Uno", TipoMedio.Radio, null));
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("WEB01", "Web Uno", TipoMedio.Web, null));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, Activo: null, Tipo: TipoMedio.Radio, Search: null));
|
||||||
|
|
||||||
|
Assert.All(result.Items, m => Assert.Equal(TipoMedio.Radio, m.Tipo));
|
||||||
|
Assert.Contains(result.Items, m => m.Codigo == "RADIO01");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_SearchByNombre_ReturnsMatches()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("SRCH01", "Buscable Nombre", TipoMedio.Diario, null));
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("SRCH02", "Otro Medio", TipoMedio.Diario, null));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, Activo: null, Tipo: null, Search: "Buscable"));
|
||||||
|
|
||||||
|
Assert.Single(result.Items, m => m.Codigo == "SRCH01");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_PaginationClamping_PageSizeClampedTo1Min()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("PAG01", "Paginacion", TipoMedio.Diario, null));
|
||||||
|
|
||||||
|
// pageSize=0 should clamp to 1
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 0, Activo: null, Tipo: null, Search: null));
|
||||||
|
|
||||||
|
Assert.Equal(1, result.PageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_TotalCount_ReflectsAllMatchingRows()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("CNT01", "Contador 1", TipoMedio.Diario, null));
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("CNT02", "Contador 2", TipoMedio.Diario, null));
|
||||||
|
await _repository.AddAsync(Medio.ForCreation("CNT03", "Contador 3", TipoMedio.Diario, null));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 2, Activo: null, Tipo: null, Search: "Contador"));
|
||||||
|
|
||||||
|
Assert.Equal(3, result.Total);
|
||||||
|
Assert.Equal(2, result.Items.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task SeedRolCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.Rol AS t
|
||||||
|
USING (VALUES
|
||||||
|
('admin', N'Administrador', N'Supervisor total'),
|
||||||
|
('cajero', N'Cajero', N'Mostrador contado'),
|
||||||
|
('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'),
|
||||||
|
('picadora', N'Picadora/Correctora', N'Edición de textos'),
|
||||||
|
('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'),
|
||||||
|
('productor', N'Productor', N'Carga restringida'),
|
||||||
|
('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'),
|
||||||
|
('reportes', N'Reportes', N'Solo lectura reportes')
|
||||||
|
) AS s (Codigo, Nombre, Descripcion)
|
||||||
|
ON t.Codigo = s.Codigo
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (Codigo, Nombre, Descripcion, Activo)
|
||||||
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, 1);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Respawn;
|
||||||
|
using SIGCM2.Domain.Entities;
|
||||||
|
using SIGCM2.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace SIGCM2.Application.Tests.Secciones;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration tests for SeccionRepository against SIGCM2_Test.
|
||||||
|
/// TDD: RED written before implementation, GREEN after SeccionRepository was created.
|
||||||
|
/// Temporal: after UpdateAsync, dbo.Seccion_History MUST have ≥1 row for that Id.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("Database")]
|
||||||
|
public class SeccionRepositoryTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private const string ConnectionString =
|
||||||
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||||
|
|
||||||
|
private SqlConnection _connection = null!;
|
||||||
|
private Respawner _respawner = null!;
|
||||||
|
private SeccionRepository _repository = null!;
|
||||||
|
private MedioRepository _medioRepository = null!;
|
||||||
|
private int _medioId;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_connection = new SqlConnection(ConnectionString);
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
|
||||||
|
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
|
||||||
|
{
|
||||||
|
DbAdapter = DbAdapter.SqlServer,
|
||||||
|
TablesToIgnore =
|
||||||
|
[
|
||||||
|
new Respawn.Graph.Table("dbo", "Rol"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Permiso"),
|
||||||
|
new Respawn.Graph.Table("dbo", "RolPermiso"),
|
||||||
|
// *_History tables are system-versioned — engine rejects direct DELETE.
|
||||||
|
new Respawn.Graph.Table("dbo", "Usuario_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Rol_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Permiso_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "RolPermiso_History"),
|
||||||
|
// ADM-001 (V011): Medio + Seccion are temporal — history tables cannot be directly deleted.
|
||||||
|
new Respawn.Graph.Table("dbo", "Medio_History"),
|
||||||
|
new Respawn.Graph.Table("dbo", "Seccion_History"),
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await _respawner.ResetAsync(_connection);
|
||||||
|
await SeedRolCanonicalAsync();
|
||||||
|
|
||||||
|
var factory = new SqlConnectionFactory(ConnectionString);
|
||||||
|
_repository = new SeccionRepository(factory);
|
||||||
|
_medioRepository = new MedioRepository(factory);
|
||||||
|
|
||||||
|
// Seed a canonical Medio for FK-valid Seccion tests.
|
||||||
|
_medioId = await _medioRepository.AddAsync(Medio.ForCreation("TESTMEDIO", "Medio de Prueba", TipoMedio.Diario, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
await _connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── AddAsync + GetByIdAsync roundtrip ─────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddAsync_ThenGetById_ReturnsAllColumns()
|
||||||
|
{
|
||||||
|
var seccion = Seccion.ForCreation(_medioId, "SEC01", "Sección Uno", "clasificados");
|
||||||
|
|
||||||
|
var id = await _repository.AddAsync(seccion);
|
||||||
|
var result = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal(id, result!.Id);
|
||||||
|
Assert.Equal(_medioId, result.MedioId);
|
||||||
|
Assert.Equal("SEC01", result.Codigo);
|
||||||
|
Assert.Equal("Sección Uno", result.Nombre);
|
||||||
|
Assert.Equal("clasificados", result.Tipo);
|
||||||
|
Assert.True(result.Activo);
|
||||||
|
Assert.True(result.FechaCreacion > DateTime.MinValue);
|
||||||
|
Assert.Null(result.FechaModificacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetByIdAsync_NonExistent_ReturnsNull()
|
||||||
|
{
|
||||||
|
var result = await _repository.GetByIdAsync(999999);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FK violation ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddAsync_WithInvalidMedioId_ThrowsSqlException()
|
||||||
|
{
|
||||||
|
var seccion = Seccion.ForCreation(99999, "FKERR01", "FK Error", "notables");
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<Microsoft.Data.SqlClient.SqlException>(
|
||||||
|
() => _repository.AddAsync(seccion));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ExistsByCodigoInMedioAsync ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExistsByCodigoInMedioAsync_AfterAdd_ReturnsTrue()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "EXIST01", "Existe", "clasificados"));
|
||||||
|
|
||||||
|
var exists = await _repository.ExistsByCodigoInMedioAsync(_medioId, "EXIST01");
|
||||||
|
|
||||||
|
Assert.True(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExistsByCodigoInMedioAsync_NotAdded_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var exists = await _repository.ExistsByCodigoInMedioAsync(_medioId, "NOEXISTE_XYZ");
|
||||||
|
|
||||||
|
Assert.False(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExistsByCodigoInMedioAsync_SameCodigoDifferentMedio_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Seccion with same Codigo exists for _medioId, but NOT for a different medioId.
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "SHARED01", "Shared Codigo", "suplementos"));
|
||||||
|
|
||||||
|
var otherMedioId = await _medioRepository.AddAsync(Medio.ForCreation("OTRO01", "Otro Medio", TipoMedio.Radio, null));
|
||||||
|
var exists = await _repository.ExistsByCodigoInMedioAsync(otherMedioId, "SHARED01");
|
||||||
|
|
||||||
|
Assert.False(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UpdateAsync + Temporal ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ThenQuery_ReflectsNewValues()
|
||||||
|
{
|
||||||
|
var id = await _repository.AddAsync(Seccion.ForCreation(_medioId, "UPD01", "Original", "clasificados"));
|
||||||
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
var updated = original!.WithUpdatedProfile("Actualizado", "notables");
|
||||||
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
|
var result = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("Actualizado", result!.Nombre);
|
||||||
|
Assert.Equal("notables", result.Tipo);
|
||||||
|
Assert.NotNull(result.FechaModificacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_ProducesHistoryRow()
|
||||||
|
{
|
||||||
|
// Temporal: SQL Server automatically writes the previous row version to Seccion_History on UPDATE.
|
||||||
|
var id = await _repository.AddAsync(Seccion.ForCreation(_medioId, "HIST01", "Historial", "clasificados"));
|
||||||
|
var original = await _repository.GetByIdAsync(id);
|
||||||
|
|
||||||
|
var updated = original!.WithUpdatedProfile("Historial v2", "suplementos");
|
||||||
|
await _repository.UpdateAsync(updated);
|
||||||
|
|
||||||
|
var historyCount = await _connection.ExecuteScalarAsync<int>(
|
||||||
|
"SELECT COUNT(*) FROM dbo.Seccion_History WHERE Id = @Id", new { Id = id });
|
||||||
|
|
||||||
|
Assert.True(historyCount >= 1, $"Expected ≥1 history row for Seccion Id={id}, got {historyCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GetPagedAsync ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_FilterByMedioId_ReturnsOnlySecciones_OfThatMedio()
|
||||||
|
{
|
||||||
|
var otherMedioId = await _medioRepository.AddAsync(Medio.ForCreation("OTHER02", "Otro Medio 2", TipoMedio.Radio, null));
|
||||||
|
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "M1S01", "M1 Sec 1", "clasificados"));
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(otherMedioId, "M2S01", "M2 Sec 1", "notables"));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, MedioId: _medioId, Tipo: null, Activo: null, Search: null));
|
||||||
|
|
||||||
|
Assert.All(result.Items, s => Assert.Equal(_medioId, s.MedioId));
|
||||||
|
Assert.Contains(result.Items, s => s.Codigo == "M1S01");
|
||||||
|
Assert.DoesNotContain(result.Items, s => s.Codigo == "M2S01");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_FilterByTipo_ReturnsOnlyMatchingTipo()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "CL01", "Clasificados", "clasificados"));
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "NT01", "Notables", "notables"));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, MedioId: null, Tipo: "clasificados", Activo: null, Search: null));
|
||||||
|
|
||||||
|
Assert.All(result.Items, s => Assert.Equal("clasificados", s.Tipo));
|
||||||
|
Assert.Contains(result.Items, s => s.Codigo == "CL01");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_FilterByActivo_ReturnsOnlyActive()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "ACTV01", "Activa", "clasificados"));
|
||||||
|
var inactId = await _repository.AddAsync(Seccion.ForCreation(_medioId, "INACT01", "Inactiva", "clasificados"));
|
||||||
|
|
||||||
|
var inact = await _repository.GetByIdAsync(inactId);
|
||||||
|
await _repository.UpdateAsync(inact!.WithActivo(false));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 50, MedioId: _medioId, Tipo: null, Activo: true, Search: null));
|
||||||
|
|
||||||
|
var codigos = result.Items.Select(s => s.Codigo).ToHashSet();
|
||||||
|
Assert.Contains("ACTV01", codigos);
|
||||||
|
Assert.DoesNotContain("INACT01", codigos);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPagedAsync_TotalCount_ReflectsAllMatchingRows()
|
||||||
|
{
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "P01", "Page 1", "suplementos"));
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "P02", "Page 2", "suplementos"));
|
||||||
|
await _repository.AddAsync(Seccion.ForCreation(_medioId, "P03", "Page 3", "suplementos"));
|
||||||
|
|
||||||
|
var result = await _repository.GetPagedAsync(new(Page: 1, PageSize: 2, MedioId: _medioId, Tipo: "suplementos", Activo: null, Search: null));
|
||||||
|
|
||||||
|
Assert.Equal(3, result.Total);
|
||||||
|
Assert.Equal(2, result.Items.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task SeedRolCanonicalAsync()
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
MERGE dbo.Rol AS t
|
||||||
|
USING (VALUES
|
||||||
|
('admin', N'Administrador', N'Supervisor total'),
|
||||||
|
('cajero', N'Cajero', N'Mostrador contado'),
|
||||||
|
('operador_ctacte', N'Operador Cta Cte', N'Cuenta corriente'),
|
||||||
|
('picadora', N'Picadora/Correctora', N'Edición de textos'),
|
||||||
|
('jefe_publicidad', N'Jefe de Publicidad', N'Supervisión de pauta'),
|
||||||
|
('productor', N'Productor', N'Carga restringida'),
|
||||||
|
('diagramacion', N'Diagramación/Taller', N'Solo lectura pauta'),
|
||||||
|
('reportes', N'Reportes', N'Solo lectura reportes')
|
||||||
|
) AS s (Codigo, Nombre, Descripcion)
|
||||||
|
ON t.Codigo = s.Codigo
|
||||||
|
WHEN NOT MATCHED BY TARGET THEN
|
||||||
|
INSERT (Codigo, Nombre, Descripcion, Activo)
|
||||||
|
VALUES (s.Codigo, s.Nombre, s.Descripcion, 1);
|
||||||
|
""";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user