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'"); } }