Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Admin/SeccionesControllerTests.cs
dmolinari e5b6c06f64 refactor(tests): Api.Tests apunta a SIGCM2_Test_Api via TestConnectionStrings
Todos los archivos de Api.Tests reemplazan la connection string hardcodeada
por TestConnectionStrings.ApiTestDb. Cada proyecto de tests ahora tiene su
propia base de datos aislada, eliminando la contención entre Application.Tests
y Api.Tests que causaba flakiness.
2026-04-18 21:44:40 -03:00

579 lines
23 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM2.TestSupport;
namespace SIGCM2.Api.Tests.Admin;
/// <summary>
/// ADM-001 B6 — Integration tests for /api/v1/admin/secciones.
/// All endpoints require permission 'administracion:secciones:gestionar'.
/// </summary>
[Collection("ApiIntegration")]
public sealed class SeccionesControllerTests : IAsyncLifetime
{
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
private const string Endpoint = "/api/v1/admin/secciones";
private const string MediosEndpoint = "/api/v1/admin/medios";
private const string AdminUsername = "admin";
private const string AdminPassword = "@Diego550@";
private readonly HttpClient _client;
public SeccionesControllerTests(TestWebAppFactory factory)
{
_client = factory.CreateClient();
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync() => Task.CompletedTask;
// ── Helpers ──────────────────────────────────────────────────────────────
private async Task<string> GetAdminTokenAsync()
{
var response = await _client.PostAsJsonAsync("/api/v1/auth/login", new
{
username = AdminUsername,
password = AdminPassword
});
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("accessToken").GetString()!;
}
private async Task<string> GetCajeroTokenAsync(string username)
{
var adminToken = await GetAdminTokenAsync();
using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new
{
username,
password = "Secure1234!",
nombre = "Cajero",
apellido = "Test",
email = (string?)null,
rol = "cajero"
}, adminToken);
var mkResp = await _client.SendAsync(mkUser);
if (mkResp.StatusCode != HttpStatusCode.Created && mkResp.StatusCode != HttpStatusCode.Conflict)
Assert.Fail($"Seed cajero failed: {mkResp.StatusCode}");
var loginResp = await _client.PostAsJsonAsync("/api/v1/auth/login", new
{
username,
password = "Secure1234!"
});
loginResp.EnsureSuccessStatusCode();
var loginJson = await loginResp.Content.ReadFromJsonAsync<JsonElement>();
return loginJson.GetProperty("accessToken").GetString()!;
}
private HttpRequestMessage BuildRequest(HttpMethod method, string url, object? body = null, string? bearerToken = null)
{
var request = new HttpRequestMessage(method, url);
if (bearerToken is not null)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
if (body is not null)
request.Content = JsonContent.Create(body);
return request;
}
/// <summary>Creates a Medio via the API and returns its id.</summary>
private async Task<int> CreateMedioAsync(string codigo, string nombre, string token)
{
using var req = BuildRequest(HttpMethod.Post, MediosEndpoint, new
{
codigo,
nombre,
tipo = 1
}, token);
var resp = await _client.SendAsync(req);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("id").GetInt32();
}
private static async Task DeleteMedioIfExistsAsync(string codigo)
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
var id = await conn.QuerySingleOrDefaultAsync<int?>(
"SELECT Id FROM dbo.Medio WHERE Codigo = @Codigo", new { Codigo = codigo });
if (id is null) return;
// Delete dependent secciones (disable versioning to clear history too)
await conn.ExecuteAsync("ALTER TABLE dbo.Seccion SET (SYSTEM_VERSIONING = OFF)");
await conn.ExecuteAsync("DELETE FROM dbo.Seccion_History WHERE MedioId = @id", new { id });
await conn.ExecuteAsync("DELETE FROM dbo.Seccion WHERE MedioId = @id", new { id });
await conn.ExecuteAsync(
"ALTER TABLE dbo.Seccion SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Seccion_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
// Delete the medio itself
await conn.ExecuteAsync("ALTER TABLE dbo.Medio SET (SYSTEM_VERSIONING = OFF)");
await conn.ExecuteAsync("DELETE FROM dbo.Medio_History WHERE Id = @id", new { id });
await conn.ExecuteAsync("DELETE FROM dbo.Medio WHERE Id = @id", new { id });
await conn.ExecuteAsync(
"ALTER TABLE dbo.Medio SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Medio_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
}
private static async Task DeleteUsuarioIfExistsAsync(string username)
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
await conn.ExecuteAsync("""
DELETE rt FROM dbo.RefreshToken rt
INNER JOIN dbo.Usuario u ON u.Id = rt.UsuarioId
WHERE u.Username = @Username
""", new { Username = username });
await conn.ExecuteAsync("DELETE FROM dbo.Usuario WHERE Username = @Username", new { Username = username });
}
private static async Task<int> CountAuditEventsAsync(string action, string targetType, string targetId)
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
return await conn.QuerySingleAsync<int>(
"SELECT COUNT(*) FROM dbo.AuditEvent WHERE Action = @Action AND TargetType = @TargetType AND TargetId = @TargetId",
new { Action = action, TargetType = targetType, TargetId = targetId });
}
// ── 401 / 403 guards ─────────────────────────────────────────────────────
[Fact]
public async Task CreateSeccion_WithoutAuth_Returns401()
{
using var req = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId = 1,
codigo = "SEC401",
nombre = "Seccion Test",
tipo = "clasificados"
});
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
[Fact]
public async Task CreateSeccion_WithCajeroRole_Returns403()
{
const string username = "adm001_sec_cajero_403";
try
{
var token = await GetCajeroTokenAsync(username);
using var req = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId = 1,
codigo = "SEC403",
nombre = "Seccion Test",
tipo = "clasificados"
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
// ── CREATE ────────────────────────────────────────────────────────────────
[Fact]
public async Task CreateSeccion_WithAdmin_Returns201AndAuditEvent()
{
const string medioCodigo = "TESTSECMED201";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Para Seccion 201", token);
using var req = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "SEC201",
nombre = "Seccion 201",
tipo = "clasificados"
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
Assert.NotNull(resp.Headers.Location);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(json.TryGetProperty("id", out var idEl));
var secId = idEl.GetInt32();
Assert.True(secId > 0);
Assert.Equal(medioId, json.GetProperty("medioId").GetInt32());
Assert.Equal("SEC201", json.GetProperty("codigo").GetString());
Assert.Equal("clasificados", json.GetProperty("tipo").GetString());
// Verify AuditEvent
var auditCount = await CountAuditEventsAsync("seccion.create", "Seccion", secId.ToString());
Assert.Equal(1, auditCount);
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
[Fact]
public async Task CreateSeccion_WithNonExistentMedioId_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId = 99999,
codigo = "SECNOTFOUND",
nombre = "Seccion Not Found",
tipo = "clasificados"
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("medio_not_found", json.GetProperty("error").GetString());
}
[Fact]
public async Task CreateSeccion_WithDuplicateCodigoInSameMedio_Returns409()
{
const string medioCodigo = "TESTSECDUP";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Dup Test", token);
// First seccion
using var first = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "DUPCODE",
nombre = "Seccion Original",
tipo = "clasificados"
}, token);
var firstResp = await _client.SendAsync(first);
Assert.Equal(HttpStatusCode.Created, firstResp.StatusCode);
// Second with same medioId + codigo
using var second = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "DUPCODE",
nombre = "Seccion Duplicada",
tipo = "notables"
}, token);
var secondResp = await _client.SendAsync(second);
Assert.Equal(HttpStatusCode.Conflict, secondResp.StatusCode);
var json = await secondResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("seccion_codigo_duplicado_en_medio", json.GetProperty("error").GetString());
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
[Fact]
public async Task CreateSeccion_SameCodigoDifferentMedio_Returns201()
{
const string medio1Codigo = "TESTSECMULTI1";
const string medio2Codigo = "TESTSECMULTI2";
var token = await GetAdminTokenAsync();
try
{
var medioId1 = await CreateMedioAsync(medio1Codigo, "Medio Multi 1", token);
var medioId2 = await CreateMedioAsync(medio2Codigo, "Medio Multi 2", token);
using var req1 = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId = medioId1,
codigo = "SHAREDCODE",
nombre = "Seccion en Medio 1",
tipo = "clasificados"
}, token);
var resp1 = await _client.SendAsync(req1);
Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);
// Same codigo but different medioId → should succeed (composite UQ)
using var req2 = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId = medioId2,
codigo = "SHAREDCODE",
nombre = "Seccion en Medio 2",
tipo = "notables"
}, token);
var resp2 = await _client.SendAsync(req2);
Assert.Equal(HttpStatusCode.Created, resp2.StatusCode);
}
finally
{
await DeleteMedioIfExistsAsync(medio1Codigo);
await DeleteMedioIfExistsAsync(medio2Codigo);
}
}
[Fact]
public async Task CreateSeccion_WithInactiveMedio_Returns404()
{
const string medioCodigo = "TESTSECDEACT";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Para Desactivar", token);
// Deactivate the medio
using var deactReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
var deactResp = await _client.SendAsync(deactReq);
Assert.Equal(HttpStatusCode.NoContent, deactResp.StatusCode);
// Try to create seccion in inactive medio
using var secReq = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "SECINACTIVE",
nombre = "Seccion en Medio Inactivo",
tipo = "clasificados"
}, token);
var secResp = await _client.SendAsync(secReq);
Assert.Equal(HttpStatusCode.NotFound, secResp.StatusCode);
var json = await secResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("medio_not_found", json.GetProperty("error").GetString());
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
// ── LIST ─────────────────────────────────────────────────────────────────
[Fact]
public async Task GetSecciones_WithAdmin_Returns200PagedResult()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, Endpoint, bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(json.TryGetProperty("items", out _), "Response must have 'items'");
Assert.True(json.TryGetProperty("total", out _), "Response must have 'total'");
}
// ── GET BY ID ────────────────────────────────────────────────────────────
[Fact]
public async Task GetSeccionById_NotFound_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, $"{Endpoint}/999999", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("seccion_not_found", json.GetProperty("error").GetString());
}
// ── CASCADA INACTIVIDAD (issue #16) ──────────────────────────────────────
[Fact]
public async Task UpdateSeccion_WhenMedioInactive_Returns409()
{
const string medioCodigo = "TSEC_UPD_INACT";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Inactivo Update", token);
using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "SECUPDINACT",
nombre = "Seccion Update Inactivo",
tipo = "clasificados"
}, token);
var createResp = await _client.SendAsync(createReq);
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var secId = created.GetProperty("id").GetInt32();
// Deactivate the medio
using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
var deactMedioResp = await _client.SendAsync(deactMedioReq);
Assert.Equal(HttpStatusCode.NoContent, deactMedioResp.StatusCode);
var auditBefore = await CountAuditEventsAsync("seccion.update", "Seccion", secId.ToString());
// Try to update seccion with inactive medio
using var updateReq = BuildRequest(HttpMethod.Put, $"{Endpoint}/{secId}", new
{
nombre = "Nombre Cambiado",
tipo = "notables"
}, token);
var updateResp = await _client.SendAsync(updateReq);
Assert.Equal(HttpStatusCode.Conflict, updateResp.StatusCode);
var json = await updateResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());
var auditAfter = await CountAuditEventsAsync("seccion.update", "Seccion", secId.ToString());
Assert.Equal(auditBefore, auditAfter);
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
[Fact]
public async Task DeactivateSeccion_WhenMedioInactive_Returns409()
{
const string medioCodigo = "TSEC_DEACT_INACT";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Inactivo Deactivate", token);
using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "SECDEACTINACT",
nombre = "Seccion Deactivate Inactivo",
tipo = "clasificados"
}, token);
var createResp = await _client.SendAsync(createReq);
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var secId = created.GetProperty("id").GetInt32();
// Deactivate the medio
using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
var deactMedioResp = await _client.SendAsync(deactMedioReq);
Assert.Equal(HttpStatusCode.NoContent, deactMedioResp.StatusCode);
var auditBefore = await CountAuditEventsAsync("seccion.deactivate", "Seccion", secId.ToString());
// Try to deactivate seccion with inactive medio
using var deactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/deactivate", bearerToken: token);
var deactResp = await _client.SendAsync(deactReq);
Assert.Equal(HttpStatusCode.Conflict, deactResp.StatusCode);
var json = await deactResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());
var auditAfter = await CountAuditEventsAsync("seccion.deactivate", "Seccion", secId.ToString());
Assert.Equal(auditBefore, auditAfter);
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
[Fact]
public async Task ReactivateSeccion_WhenMedioInactive_Returns409()
{
const string medioCodigo = "TSEC_REACT_INACT";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Inactivo Reactivate", token);
// Create seccion then deactivate it (while medio is still active)
using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "SECREACTINACT",
nombre = "Seccion Reactivate Inactivo",
tipo = "clasificados"
}, token);
var createResp = await _client.SendAsync(createReq);
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var secId = created.GetProperty("id").GetInt32();
// Deactivate seccion while medio is still active
using var deactSecReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/deactivate", bearerToken: token);
var deactSecResp = await _client.SendAsync(deactSecReq);
Assert.Equal(HttpStatusCode.NoContent, deactSecResp.StatusCode);
// Now deactivate the medio
using var deactMedioReq = BuildRequest(HttpMethod.Post, $"{MediosEndpoint}/{medioId}/deactivate", bearerToken: token);
var deactMedioResp = await _client.SendAsync(deactMedioReq);
Assert.Equal(HttpStatusCode.NoContent, deactMedioResp.StatusCode);
var auditBefore = await CountAuditEventsAsync("seccion.reactivate", "Seccion", secId.ToString());
// Try to reactivate seccion with inactive medio
using var reactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/reactivate", bearerToken: token);
var reactResp = await _client.SendAsync(reactReq);
Assert.Equal(HttpStatusCode.Conflict, reactResp.StatusCode);
var json = await reactResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("medio_inactivo", json.GetProperty("error").GetString());
var auditAfter = await CountAuditEventsAsync("seccion.reactivate", "Seccion", secId.ToString());
Assert.Equal(auditBefore, auditAfter);
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
// ── DEACTIVATE ────────────────────────────────────────────────────────────
[Fact]
public async Task DeactivateSeccion_WithAdmin_Returns204AndAuditEvent()
{
const string medioCodigo = "TESTSECDEACT2";
var token = await GetAdminTokenAsync();
try
{
var medioId = await CreateMedioAsync(medioCodigo, "Medio Para Sec Deactivate", token);
using var createReq = BuildRequest(HttpMethod.Post, Endpoint, new
{
medioId,
codigo = "SECDEACT",
nombre = "Seccion Para Desactivar",
tipo = "clasificados"
}, token);
var createResp = await _client.SendAsync(createReq);
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var secId = created.GetProperty("id").GetInt32();
using var deactReq = BuildRequest(HttpMethod.Post, $"{Endpoint}/{secId}/deactivate", bearerToken: token);
var deactResp = await _client.SendAsync(deactReq);
Assert.Equal(HttpStatusCode.NoContent, deactResp.StatusCode);
var auditCount = await CountAuditEventsAsync("seccion.deactivate", "Seccion", secId.ToString());
Assert.Equal(1, auditCount);
}
finally
{
await DeleteMedioIfExistsAsync(medioCodigo);
}
}
}