test(adm-009): FiscalController integration tests with JWT auth (Red→Green)
This commit is contained in:
696
tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs
Normal file
696
tests/SIGCM2.Api.Tests/Admin/FiscalControllerTests.cs
Normal file
@@ -0,0 +1,696 @@
|
||||
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 =
|
||||
"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<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'");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user