739 lines
31 KiB
C#
739 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 =
|
|
"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'");
|
|
}
|
|
|
|
// ── 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);
|
|
}
|
|
}
|
|
}
|