feat(api): ProductTypesController + ExceptionFilter 4 casos PRD-001

CRUD endpoints con validación FluentValidation inline; 4 nuevas excepciones mapeadas
en ExceptionFilter; conteos de permisos 25→26 actualizados; 12 e2e tests nuevos.
This commit is contained in:
2026-04-19 09:57:11 -03:00
parent 936d1dc353
commit 170789886b
5 changed files with 505 additions and 8 deletions

View File

@@ -50,8 +50,9 @@ public class AuthControllerTests
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
// V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
// V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25 total
Assert.Equal(25, permisos.GetArrayLength());
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
// V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26 total
Assert.Equal(26, permisos.GetArrayLength());
}
// Scenario: invalid credentials return 401 with opaque error

View File

@@ -129,7 +129,7 @@ public sealed class PermisosEndpointTests : IAsyncLifetime
// ── GET /api/v1/permisos — catalog ───────────────────────────────────────
[Fact]
public async Task GetPermisos_WithAdmin_Returns200With25Items()
public async Task GetPermisos_WithAdmin_Returns200With26Items()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/permisos", bearerToken: token);
@@ -140,8 +140,9 @@ public sealed class PermisosEndpointTests : IAsyncLifetime
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
// V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
// V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25 total
Assert.Equal(25, list.GetArrayLength());
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
// V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26 total
Assert.Equal(26, list.GetArrayLength());
}
[Fact]
@@ -184,7 +185,7 @@ public sealed class PermisosEndpointTests : IAsyncLifetime
// ── GET /api/v1/roles/{codigo}/permisos ──────────────────────────────────
[Fact]
public async Task GetRolPermisos_AdminRol_Returns200With25Items()
public async Task GetRolPermisos_AdminRol_Returns200With26Items()
{
var token = await GetBearerTokenAsync(AdminUsername, AdminPassword);
using var req = BuildRequest(HttpMethod.Get, "/api/v1/roles/admin/permisos", bearerToken: token);
@@ -195,8 +196,9 @@ public sealed class PermisosEndpointTests : IAsyncLifetime
// V011 (ADM-001) adds 'administracion:secciones:gestionar' → 22
// V013 (ADM-008) adds 'administracion:puntos_de_venta:gestionar' → 23
// V014 (ADM-009) adds 'administracion:fiscal:gestionar' → 24
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25 total
Assert.Equal(25, list.GetArrayLength());
// V016 (CAT-001) adds 'catalogo:rubros:gestionar' → 25
// V017 (PRD-001) adds 'catalogo:tipos:gestionar' → 26 total
Assert.Equal(26, list.GetArrayLength());
}
[Fact]

View File

@@ -0,0 +1,261 @@
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using SIGCM2.TestSupport;
namespace SIGCM2.Api.Tests.ProductTypes;
/// <summary>
/// PRD-001 — Integration tests for /api/v1/product-types and /api/v1/admin/product-types.
/// Read endpoints require authentication (any role).
/// Write endpoints require permission 'catalogo:tipos:gestionar'.
/// Verifies HTTP status codes, response shapes, and ExceptionFilter mappings.
/// </summary>
[Collection("ApiIntegration")]
public sealed class ProductTypesControllerTests : IAsyncLifetime
{
private const string ReadEndpoint = "/api/v1/product-types";
private const string AdminEndpoint = "/api/v1/admin/product-types";
private const string AdminUsername = "admin";
private const string AdminPassword = "@Diego550@";
private readonly HttpClient _client;
public ProductTypesControllerTests(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 HttpRequestMessage BuildRequest(HttpMethod method, string url, object? body = null, string? bearerToken = null)
{
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;
}
// ── 401 guards on READ endpoints ───────────────────────────────────────────
[Fact]
public async Task List_WithoutAuth_Returns401()
{
using var req = BuildRequest(HttpMethod.Get, ReadEndpoint);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
[Fact]
public async Task GetById_WithoutAuth_Returns401()
{
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/999999");
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
// ── 401 / 403 guards on WRITE endpoints ───────────────────────────────────
[Fact]
public async Task Create_WithoutAuth_Returns401()
{
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Test" });
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
// ── POST /api/v1/admin/product-types ──────────────────────────────────────
[Fact]
public async Task Create_WithAdmin_Returns201WithId()
{
var token = await GetAdminTokenAsync();
var uniqueName = $"Tipo_Create_{Guid.NewGuid():N}";
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
{
nombre = uniqueName,
hasDuration = true,
requiresText = false,
requiresCategory = false,
isBundle = false,
allowImages = true,
maxImages = 3,
maxImageSizeMB = 1.5,
maxImageWidth = (int?)null,
maxImageHeight = (int?)null
}, 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(uniqueName, json.GetProperty("nombre").GetString());
Assert.True(json.GetProperty("isActive").GetBoolean());
}
[Fact]
public async Task Create_DuplicateNombre_Returns409()
{
var token = await GetAdminTokenAsync();
var uniqueName = $"DupNombre_{Guid.NewGuid():N}";
using var req1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new
{
nombre = uniqueName,
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
allowImages = false
}, token);
var resp1 = await _client.SendAsync(req1);
Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);
using var req2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new
{
nombre = uniqueName,
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
allowImages = false
}, token);
var resp2 = await _client.SendAsync(req2);
Assert.Equal(HttpStatusCode.Conflict, resp2.StatusCode);
}
[Fact]
public async Task Create_InvalidBody_Returns400()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
{
nombre = string.Empty, // invalid
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
allowImages = false
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
}
// ── GET /api/v1/product-types ─────────────────────────────────────────────
[Fact]
public async Task List_WithAdmin_Returns200WithPaginatedResult()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}?page=1&pageSize=10", 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 _));
Assert.True(json.TryGetProperty("total", out _));
}
// ── GET /api/v1/product-types/{id} ────────────────────────────────────────
[Fact]
public async Task GetById_NotFound_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/999999999", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
[Fact]
public async Task GetById_ExistingId_Returns200()
{
var token = await GetAdminTokenAsync();
var uniqueName = $"Tipo_GetById_{Guid.NewGuid():N}";
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
{
nombre = uniqueName,
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
allowImages = false
}, token);
var createResp = await _client.SendAsync(createReq);
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var id = created.GetProperty("id").GetInt32();
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/{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(uniqueName, json.GetProperty("nombre").GetString());
}
// ── PUT /api/v1/admin/product-types/{id} ──────────────────────────────────
[Fact]
public async Task Update_NotFound_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/999999999", new
{
nombre = "No Existe",
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
allowImages = false
}, token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
// ── DELETE /api/v1/admin/product-types/{id} ───────────────────────────────
[Fact]
public async Task Deactivate_NotFound_Returns404()
{
var token = await GetAdminTokenAsync();
using var req = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/999999999", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
[Fact]
public async Task Deactivate_ExistingActive_Returns204()
{
var token = await GetAdminTokenAsync();
var uniqueName = $"Tipo_Deactivate_{Guid.NewGuid():N}";
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
{
nombre = uniqueName,
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
allowImages = false
}, token);
var createResp = await _client.SendAsync(createReq);
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
var id = created.GetProperty("id").GetInt32();
using var req = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{id}", bearerToken: token);
var resp = await _client.SendAsync(req);
Assert.Equal(HttpStatusCode.NoContent, resp.StatusCode);
}
}