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;
///
/// 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.
///
[Collection("ApiIntegration")]
public sealed class FiscalControllerTests : IAsyncLifetime
{
private const string TestConnectionString =
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
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 GetAdminTokenAsync()
{
var response = await _client.PostAsJsonAsync("/api/v1/auth/login", new
{
username = AdminUsername,
password = AdminPassword
});
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadFromJsonAsync();
return json.GetProperty("accessToken").GetString()!;
}
private async Task 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();
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 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();
return json.GetProperty("id").GetInt32();
}
private async Task 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();
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 ─────────────────────────────────────────────
/// [REQ-FISCAL-AUTH-001] GET /iva sin auth → 401.
[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);
}
/// [REQ-FISCAL-AUTH-001] GET /iva con cajero (sin permiso fiscal) → 403.
[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);
}
}
/// [REQ-FISCAL-AUTH-002] GET /iva con admin (tiene permiso) → 200.
[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();
Assert.True(json.TryGetProperty("items", out _), "Response must have 'items'");
Assert.True(json.TryGetProperty("total", out _), "Response must have 'total'");
}
// ── IVA: POST (CREATE) ────────────────────────────────────────────────────
/// POST /iva → 201 con id, campos correctos.
[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();
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);
}
}
/// [REQ-TIPOIVA-CREATE-002] POST /iva con codigo duplicado en misma vigencia → 409 duplicate_codigo.
[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();
Assert.Equal("duplicate_codigo", json.GetProperty("error").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: GET BY ID ────────────────────────────────────────────────────────
/// GET /iva/{id} inexistente → 404 tipo_iva_not_found.
[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();
Assert.Equal("tipo_iva_not_found", json.GetProperty("error").GetString());
}
/// GET /iva/{id} existente → 200 con campos correctos.
[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();
Assert.Equal(id, json.GetProperty("id").GetInt32());
Assert.Equal(codigo, json.GetProperty("codigo").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: PATCH (UPDATE COSMETICO) ─────────────────────────────────────────
/// PATCH /iva/{id} con campos cosméticos → 200 OK.
[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();
Assert.Equal("Descripcion Actualizada", json.GetProperty("descripcion").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// [REQ-TIPOIVA-UPDATE-002] PATCH /iva/{id} con "porcentaje" en body → 409 inmutable_usar_nueva_version.
[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();
Assert.Equal("inmutable_usar_nueva_version", json.GetProperty("error").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IVA: NUEVA VERSION ────────────────────────────────────────────────────
/// [REQ-TIPOIVA-NUEVAVER-001] POST /iva/{id}/nueva-version → 201 con predecesoraId+nuevaVersionId correctos.
[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();
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();
Assert.NotEqual(JsonValueKind.Null, predecesoraJson.GetProperty("vigenciaHasta").ValueKind);
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// [REQ-TIPOIVA-NUEVAVER-003] POST /iva/{id}/nueva-version sobre predecesora ya cerrada → 409 predecesora_ya_cerrada.
[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();
Assert.Equal("predecesora_ya_cerrada", json.GetProperty("error").GetString());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// POST /iva/{id}/nueva-version con vigenciaDesde inválida → 400 vigencia_desde_invalida.
[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();
// 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 ────────────────────────────────────────────────────────
/// GET /iva/{id}/historial → 200 con cadena ordenada.
[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();
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();
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 ─────────────────────────────────────────
/// POST /iva/{id}/deactivate → 200 con activo=false.
[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();
Assert.False(json.GetProperty("activo").GetBoolean());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
/// POST /iva/{id}/reactivate → 200 con activo=true.
[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();
Assert.True(json.GetProperty("activo").GetBoolean());
}
finally
{
await DeleteTipoDeIvaByCodigoAsync(codigo);
}
}
// ── IIBB: Tests espejo mínimos ────────────────────────────────────────────
/// [REQ-FISCAL-AUTH-001] GET /iibb sin auth → 401.
[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);
}
/// POST /iibb → 201 con id correcto.
[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();
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))");
}
}
/// PATCH /iibb/{id} con "alicuota" en body → 409 inmutable_usar_nueva_version.
[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();
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))");
}
}
/// GET /iibb/{id} inexistente → 404 ingresos_brutos_not_found.
[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();
Assert.Equal("ingresos_brutos_not_found", json.GetProperty("error").GetString());
}
/// GET /iibb con admin → 200 con paged result.
[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();
Assert.True(json.TryGetProperty("items", out _), "Response must have 'items'");
Assert.True(json.TryGetProperty("total", out _), "Response must have 'total'");
}
}