Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.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

738 lines
31 KiB
C#

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Dapper;
using Microsoft.Data.SqlClient;
using SIGCM2.TestSupport;
namespace SIGCM2.Api.Tests.Admin;
/// <summary>
/// ADM-009 Batch 5 — Integration tests for /api/v1/admin/fiscal
/// Requires permission 'administracion:fiscal:gestionar'.
/// All tests use real JWT RS256 auth via TestWebAppFactory.
/// </summary>
[Collection("ApiIntegration")]
public sealed class FiscalControllerTests : IAsyncLifetime
{
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
private const string IvaEndpoint = "/api/v1/admin/fiscal/iva";
private const string IibbEndpoint = "/api/v1/admin/fiscal/iibb";
private const string AdminUsername = "admin";
private const string AdminPassword = "@Diego550@";
private readonly HttpClient _client;
public FiscalControllerTests(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,
string contentType = "application/json")
{
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;
}
private HttpRequestMessage BuildRawRequest(
HttpMethod method,
string url,
string rawJson,
string? bearerToken = null)
{
var request = new HttpRequestMessage(method, url);
if (bearerToken is not null)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
request.Content = new StringContent(rawJson, Encoding.UTF8, "application/json");
return request;
}
private async Task<int> CreateTipoDeIvaAsync(string codigo, string descripcion, decimal porcentaje, bool aplicaIva, string vigenciaDesde, string token)
{
using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
{
codigo,
descripcion,
porcentaje,
aplicaIVA = aplicaIva,
vigenciaDesde
}, token);
var resp = await _client.SendAsync(req);
if (!resp.IsSuccessStatusCode)
{
var body = await resp.Content.ReadAsStringAsync();
Assert.Fail($"CreateTipoDeIva failed {resp.StatusCode}: {body}");
}
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("id").GetInt32();
}
private async Task<int> CreateIngresosBrutosAsync(string provincia, string descripcion, decimal alicuota, string vigenciaDesde, string token)
{
using var req = BuildRequest(HttpMethod.Post, IibbEndpoint, new
{
provincia,
descripcion,
alicuota,
vigenciaDesde
}, token);
var resp = await _client.SendAsync(req);
if (!resp.IsSuccessStatusCode)
{
var body = await resp.Content.ReadAsStringAsync();
Assert.Fail($"CreateIngresosBrutos failed {resp.StatusCode}: {body}");
}
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("id").GetInt32();
}
private static async Task DeleteTipoDeIvaByCodigoAsync(string codigo)
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
await conn.ExecuteAsync("ALTER TABLE dbo.TipoDeIva SET (SYSTEM_VERSIONING = OFF)");
await conn.ExecuteAsync("DELETE FROM dbo.TipoDeIva_History WHERE Codigo = @Codigo", new { Codigo = codigo });
await conn.ExecuteAsync("DELETE FROM dbo.TipoDeIva WHERE Codigo = @Codigo", new { Codigo = codigo });
await conn.ExecuteAsync(
"ALTER TABLE dbo.TipoDeIva SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TipoDeIva_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
}
private static async Task DeleteIngresosBrutosByProvinciaAsync(string provincia)
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
await conn.ExecuteAsync("ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = OFF)");
await conn.ExecuteAsync(
"DELETE FROM dbo.IngresosBrutos_History WHERE Provincia = @Provincia", new { Provincia = provincia });
await conn.ExecuteAsync(
"DELETE FROM dbo.IngresosBrutos WHERE Provincia = @Provincia", new { Provincia = provincia });
await conn.ExecuteAsync(
"ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.IngresosBrutos_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 });
}
// ── AUTH / PERMISSION GUARDS ─────────────────────────────────────────────
/// <summary>[REQ-FISCAL-AUTH-001] GET /iva sin auth → 401.</summary>
[Fact]
public async Task GetIva_WithoutAuth_Returns401()
{
using var req = new HttpRequestMessage(HttpMethod.Get, IvaEndpoint);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
/// <summary>[REQ-FISCAL-AUTH-001] GET /iva con cajero (sin permiso fiscal) → 403.</summary>
[Fact]
public async Task GetIva_WithCajeroRole_Returns403()
{
const string username = "adm009_fiscal_cajero_403";
try
{
var token = await GetCajeroTokenAsync(username);
using var req = BuildRequest(HttpMethod.Get, IvaEndpoint, bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
}
finally
{
await DeleteUsuarioIfExistsAsync(username);
}
}
/// <summary>[REQ-FISCAL-AUTH-002] GET /iva con admin (tiene permiso) → 200.</summary>
[Fact]
public async Task GetIva_WithAdmin_Returns200()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, IvaEndpoint, 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'");
}
// ── IVA: POST (CREATE) ────────────────────────────────────────────────────
/// <summary>POST /iva → 201 con id, campos correctos.</summary>
[Fact]
public async Task CreateIva_WithAdmin_Returns201()
{
// Codigo must match ^(EXENTO|NO_GRAVADO|IVA_\d+)$
const string codigo = "IVA_9901";
var token = await GetAdminTokenAsync();
try
{
using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
{
codigo,
descripcion = "IVA Test Creacion",
porcentaje = 15.5m,
aplicaIVA = true,
vigenciaDesde = "2025-01-01"
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(json.GetProperty("id").GetInt32() > 0);
Assert.Equal(codigo, json.GetProperty("codigo").GetString());
Assert.Equal(15.5m, json.GetProperty("porcentaje").GetDecimal());
Assert.True(json.GetProperty("activo").GetBoolean());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// <summary>[REQ-TIPOIVA-CREATE-002] POST /iva con codigo duplicado en misma vigencia → 409 duplicate_codigo.</summary>
[Fact]
public async Task CreateIva_DuplicateCodigo_Returns409()
{
const string codigo = "IVA_9902";
var token = await GetAdminTokenAsync();
try
{
await CreateTipoDeIvaAsync(codigo, "IVA Original", 10m, true, "2025-01-01", token);
using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
{
codigo,
descripcion = "IVA Duplicado",
porcentaje = 12m,
aplicaIVA = true,
vigenciaDesde = "2025-01-01"
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("duplicate_codigo", json.GetProperty("error").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: GET BY ID ────────────────────────────────────────────────────────
/// <summary>GET /iva/{id} inexistente → 404 tipo_iva_not_found.</summary>
[Fact]
public async Task GetIvaById_NotFound_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/999999", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("tipo_iva_not_found", json.GetProperty("error").GetString());
}
/// <summary>GET /iva/{id} existente → 200 con campos correctos.</summary>
[Fact]
public async Task GetIvaById_Existing_Returns200()
{
const string codigo = "IVA_9903";
var token = await GetAdminTokenAsync();
try
{
var id = await CreateTipoDeIvaAsync(codigo, "IVA GetById", 10m, true, "2025-01-01", token);
using var req = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/{id}", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(id, json.GetProperty("id").GetInt32());
Assert.Equal(codigo, json.GetProperty("codigo").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: PATCH (UPDATE COSMETICO) ─────────────────────────────────────────
/// <summary>PATCH /iva/{id} con campos cosméticos → 200 OK.</summary>
[Fact]
public async Task PatchIva_CosmeticFields_Returns200()
{
const string codigo = "IVA_9904";
var token = await GetAdminTokenAsync();
try
{
var id = await CreateTipoDeIvaAsync(codigo, "Descripcion Original", 10m, true, "2025-01-01", token);
using var req = BuildRawRequest(
HttpMethod.Patch,
$"{IvaEndpoint}/{id}",
"""{"codigo":"IVA_9904","descripcion":"Descripcion Actualizada","aplicaIVA":true,"activo":true}""",
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("Descripcion Actualizada", json.GetProperty("descripcion").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// <summary>[REQ-TIPOIVA-UPDATE-002] PATCH /iva/{id} con "porcentaje" en body → 409 inmutable_usar_nueva_version.</summary>
[Fact]
public async Task PatchIva_WithPorcentajeInBody_Returns409Inmutable()
{
const string codigo = "IVA_9905";
var token = await GetAdminTokenAsync();
try
{
var id = await CreateTipoDeIvaAsync(codigo, "IVA Patch Pct", 10m, true, "2025-01-01", token);
using var req = BuildRawRequest(
HttpMethod.Patch,
$"{IvaEndpoint}/{id}",
"""{"porcentaje":23.5}""",
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("inmutable_usar_nueva_version", json.GetProperty("error").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: NUEVA VERSION ────────────────────────────────────────────────────
/// <summary>[REQ-TIPOIVA-NUEVAVER-001] POST /iva/{id}/nueva-version → 201 con predecesoraId+nuevaVersionId correctos.</summary>
[Fact]
public async Task NuevaVersionIva_HappyPath_Returns201WithChain()
{
const string codigo = "IVA_9906";
var token = await GetAdminTokenAsync();
try
{
var predecesoraId = await CreateTipoDeIvaAsync(codigo, "IVA Nueva Version", 10m, true, "2024-01-01", token);
using var req = BuildRequest(
HttpMethod.Post,
$"{IvaEndpoint}/{predecesoraId}/nueva-version",
new { porcentaje = 12m, vigenciaDesde = "2025-01-01" },
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal(predecesoraId, json.GetProperty("predecesoraId").GetInt32());
var nuevaVersionId = json.GetProperty("nuevaVersionId").GetInt32();
Assert.True(nuevaVersionId > predecesoraId);
// Verificar que la predecesora quedó cerrada (VigenciaHasta != null)
using var getReq = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/{predecesoraId}", bearerToken: token);
var getResp = await _client.SendAsync(getReq);
var predecesoraJson = await getResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.NotEqual(JsonValueKind.Null, predecesoraJson.GetProperty("vigenciaHasta").ValueKind);
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// <summary>[REQ-TIPOIVA-NUEVAVER-003] POST /iva/{id}/nueva-version sobre predecesora ya cerrada → 409 predecesora_ya_cerrada.</summary>
[Fact]
public async Task NuevaVersionIva_PredecesoraYaCerrada_Returns409()
{
const string codigo = "IVA_9907";
var token = await GetAdminTokenAsync();
try
{
var predecesoraId = await CreateTipoDeIvaAsync(codigo, "IVA Predecesora Cerrada", 10m, true, "2024-01-01", token);
// Primera nueva version — cierra la predecesora
using var req1 = BuildRequest(
HttpMethod.Post,
$"{IvaEndpoint}/{predecesoraId}/nueva-version",
new { porcentaje = 12m, vigenciaDesde = "2025-01-01" },
token);
var resp1 = await _client.SendAsync(req1);
Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);
// Segunda sobre la predecesora original (ya cerrada)
using var req2 = BuildRequest(
HttpMethod.Post,
$"{IvaEndpoint}/{predecesoraId}/nueva-version",
new { porcentaje = 15m, vigenciaDesde = "2026-01-01" },
token);
var resp2 = await _client.SendAsync(req2);
Assert.Equal(HttpStatusCode.Conflict, resp2.StatusCode);
var json = await resp2.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("predecesora_ya_cerrada", json.GetProperty("error").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// <summary>POST /iva/{id}/nueva-version con vigenciaDesde inválida → 400 vigencia_desde_invalida.</summary>
[Fact]
public async Task NuevaVersionIva_VigenciaDesdeInvalida_Returns400()
{
const string codigo = "IVA_9908";
var token = await GetAdminTokenAsync();
try
{
var predecesoraId = await CreateTipoDeIvaAsync(codigo, "IVA Vig Invalida", 10m, true, "2025-06-01", token);
// vigenciaDesde anterior a la de la predecesora
using var req = BuildRequest(
HttpMethod.Post,
$"{IvaEndpoint}/{predecesoraId}/nueva-version",
new { porcentaje = 12m, vigenciaDesde = "2024-01-01" },
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
// Validate that the error body contains info about vigencia_desde_invalida
var bodyStr = json.GetRawText();
Assert.Contains("vigencia_desde_invalida", bodyStr);
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: HISTORIAL ────────────────────────────────────────────────────────
/// <summary>GET /iva/{id}/historial → 200 con cadena ordenada.</summary>
[Fact]
public async Task GetHistorialIva_Returns200WithOrderedChain()
{
const string codigo = "IVA_9909";
var token = await GetAdminTokenAsync();
try
{
// Crear cadena de 2 versiones
var v1Id = await CreateTipoDeIvaAsync(codigo, "IVA Historial v1", 10m, true, "2023-01-01", token);
using var nvReq = BuildRequest(
HttpMethod.Post,
$"{IvaEndpoint}/{v1Id}/nueva-version",
new { porcentaje = 15m, vigenciaDesde = "2025-01-01" },
token);
var nvResp = await _client.SendAsync(nvReq);
Assert.Equal(HttpStatusCode.Created, nvResp.StatusCode);
var nvJson = await nvResp.Content.ReadFromJsonAsync<JsonElement>();
var v2Id = nvJson.GetProperty("nuevaVersionId").GetInt32();
// GET historial desde v2 (la actual)
using var req = BuildRequest(HttpMethod.Get, $"{IvaEndpoint}/{v2Id}/historial", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var chain = await resp.Content.ReadFromJsonAsync<JsonElement>();
var items = chain.EnumerateArray().ToList();
Assert.Equal(2, items.Count);
// Version 1 tiene version=1, version 2 tiene version=2
Assert.Equal(1, items[0].GetProperty("version").GetInt32());
Assert.Equal(2, items[1].GetProperty("version").GetInt32());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: DEACTIVATE / REACTIVATE ─────────────────────────────────────────
/// <summary>POST /iva/{id}/deactivate → 200 con activo=false.</summary>
[Fact]
public async Task DeactivateIva_Returns200WithActivoFalse()
{
const string codigo = "IVA_9910";
var token = await GetAdminTokenAsync();
try
{
var id = await CreateTipoDeIvaAsync(codigo, "IVA Deactivate", 10m, true, "2025-01-01", token);
using var req = BuildRequest(HttpMethod.Post, $"{IvaEndpoint}/{id}/deactivate", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.False(json.GetProperty("activo").GetBoolean());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// <summary>POST /iva/{id}/reactivate → 200 con activo=true.</summary>
[Fact]
public async Task ReactivateIva_Returns200WithActivoTrue()
{
const string codigo = "IVA_9911";
var token = await GetAdminTokenAsync();
try
{
var id = await CreateTipoDeIvaAsync(codigo, "IVA Reactivate", 10m, true, "2025-01-01", token);
// Deactivate primero
using var deactReq = BuildRequest(HttpMethod.Post, $"{IvaEndpoint}/{id}/deactivate", bearerToken: token);
await _client.SendAsync(deactReq);
// Reactivate
using var reactReq = BuildRequest(HttpMethod.Post, $"{IvaEndpoint}/{id}/reactivate", bearerToken: token);
var reactResp = await _client.SendAsync(reactReq);
Assert.Equal(HttpStatusCode.OK, reactResp.StatusCode);
var json = await reactResp.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(json.GetProperty("activo").GetBoolean());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IIBB: Tests espejo mínimos ────────────────────────────────────────────
/// <summary>[REQ-FISCAL-AUTH-001] GET /iibb sin auth → 401.</summary>
[Fact]
public async Task GetIibb_WithoutAuth_Returns401()
{
using var req = new HttpRequestMessage(HttpMethod.Get, IibbEndpoint);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
/// <summary>POST /iibb → 201 con id correcto.</summary>
[Fact]
public async Task CreateIibb_WithAdmin_Returns201()
{
// Usar una provincia que no tenga datos de test previos
// Nota: El seed tiene todas las provincias con Alicuota=0.
// Para crear un nuevo registro necesitamos una provincia+vigenciaDesde únicos.
// Los repos aceptan combinación (Provincia, VigenciaDesde) única.
// Usamos "Formosa" con una fecha específica de test.
const string provincia = "Formosa";
const string vigenciaDesde = "2030-01-01"; // fecha futura para no colisionar con seed
var token = await GetAdminTokenAsync();
try
{
using var req = BuildRequest(HttpMethod.Post, IibbEndpoint, new
{
provincia,
descripcion = "IIBB Formosa Test",
alicuota = 2.5m,
vigenciaDesde
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(json.GetProperty("id").GetInt32() > 0);
Assert.Equal(provincia, json.GetProperty("provincia").GetString());
Assert.Equal(2.5m, json.GetProperty("alicuota").GetDecimal());
}
finally
{
// Limpiar solo la fila con la fecha de test, no las del seed
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
await conn.ExecuteAsync("ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = OFF)");
await conn.ExecuteAsync(
"DELETE FROM dbo.IngresosBrutos_History WHERE Provincia = 'Formosa' AND VigenciaDesde = '2030-01-01'");
await conn.ExecuteAsync(
"DELETE FROM dbo.IngresosBrutos WHERE Provincia = 'Formosa' AND VigenciaDesde = '2030-01-01'");
await conn.ExecuteAsync(
"ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.IngresosBrutos_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
}
}
/// <summary>PATCH /iibb/{id} con "alicuota" en body → 409 inmutable_usar_nueva_version.</summary>
[Fact]
public async Task PatchIibb_WithAlicuotaInBody_Returns409Inmutable()
{
const string provincia = "Jujuy";
const string vigenciaDesde = "2030-02-01";
var token = await GetAdminTokenAsync();
try
{
var id = await CreateIngresosBrutosAsync(provincia, "IIBB Jujuy Test", 1.5m, vigenciaDesde, token);
using var req = BuildRawRequest(
HttpMethod.Patch,
$"{IibbEndpoint}/{id}",
"""{"alicuota":3.0}""",
token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Conflict, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("inmutable_usar_nueva_version", json.GetProperty("error").GetString());
}
finally
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
await conn.ExecuteAsync("ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = OFF)");
await conn.ExecuteAsync(
"DELETE FROM dbo.IngresosBrutos_History WHERE Provincia = 'Jujuy' AND VigenciaDesde = '2030-02-01'");
await conn.ExecuteAsync(
"DELETE FROM dbo.IngresosBrutos WHERE Provincia = 'Jujuy' AND VigenciaDesde = '2030-02-01'");
await conn.ExecuteAsync(
"ALTER TABLE dbo.IngresosBrutos SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.IngresosBrutos_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
}
}
/// <summary>GET /iibb/{id} inexistente → 404 ingresos_brutos_not_found.</summary>
[Fact]
public async Task GetIibbById_NotFound_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, $"{IibbEndpoint}/999999", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
Assert.Equal("ingresos_brutos_not_found", json.GetProperty("error").GetString());
}
/// <summary>GET /iibb con admin → 200 con paged result.</summary>
[Fact]
public async Task GetIibb_WithAdmin_Returns200PagedResult()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, IibbEndpoint, 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'");
}
// ── UDT-011: DateOnly serialization format ────────────────────────────────
/// <summary>
/// [UDT-011 REQ-BE-JSON-001] POST /iva → vigenciaDesde en respuesta debe ser
/// "yyyy-MM-dd" (e.g. "2025-01-01"), no "2025-01-01T00:00:00" ni con sufijo "Z".
/// Valida que DateOnlyJsonConverter está activo en el pipeline de controllers.
/// </summary>
[Fact]
public async Task CreateIva_VigenciaDesde_SerializesAsDateOnlyString()
{
const string codigo = "IVA_9999";
var token = await GetAdminTokenAsync();
try
{
using var req = BuildRequest(HttpMethod.Post, IvaEndpoint, new
{
codigo,
descripcion = "IVA DateOnly Format Test",
porcentaje = 5.0m,
aplicaIVA = true,
vigenciaDesde = "2025-01-01"
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
// Read raw JSON string to inspect format (not deserialized)
var rawJson = await resp.Content.ReadAsStringAsync();
// vigenciaDesde MUST be "2025-01-01" — short date format
Assert.Contains("\"2025-01-01\"", rawJson);
// Must NOT contain datetime format or UTC suffix
Assert.DoesNotContain("T00:00:00", rawJson);
Assert.DoesNotContain("\"2025-01-01Z\"", rawJson);
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
}