feat(api): ProductsController + ExceptionFilter Product cases, fix permiso count to 27 (PRD-002)
This commit is contained in:
339
tests/SIGCM2.Api.Tests/Products/ProductsControllerTests.cs
Normal file
339
tests/SIGCM2.Api.Tests/Products/ProductsControllerTests.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using SIGCM2.TestSupport;
|
||||
|
||||
namespace SIGCM2.Api.Tests.Products;
|
||||
|
||||
/// <summary>
|
||||
/// PRD-002 — Integration tests for /api/v1/products and /api/v1/admin/products.
|
||||
/// Read endpoints require authentication (any role).
|
||||
/// Write endpoints require permission 'catalogo:productos:gestionar'.
|
||||
/// Verifies HTTP status codes, response shapes, and ExceptionFilter mappings.
|
||||
/// </summary>
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class ProductsControllerTests : IAsyncLifetime
|
||||
{
|
||||
private const string ReadEndpoint = "/api/v1/products";
|
||||
private const string AdminEndpoint = "/api/v1/admin/products";
|
||||
private const string AdminUsername = "admin";
|
||||
private const string AdminPassword = "@Diego550@";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public ProductsControllerTests(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;
|
||||
}
|
||||
|
||||
private async Task<(int MedioId, int ProductTypeId)> EnsureMedioAndProductTypeAsync(string token)
|
||||
{
|
||||
// Create a Medio via SQL (we don't have a Medio controller endpoint available here)
|
||||
// Use product-types endpoint to create a ProductType and insert Medio directly
|
||||
var medioResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/medios", new
|
||||
{
|
||||
codigo = $"PM{Guid.NewGuid():N}"[..6],
|
||||
nombre = $"Medio Test {Guid.NewGuid():N}"[..30],
|
||||
tipo = 1
|
||||
}, token));
|
||||
medioResp.EnsureSuccessStatusCode();
|
||||
var medioJson = await medioResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var medioId = medioJson.GetProperty("id").GetInt32();
|
||||
|
||||
var ptResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/product-types", new
|
||||
{
|
||||
nombre = $"PT_{Guid.NewGuid():N}"[..30],
|
||||
hasDuration = false, requiresText = false, requiresCategory = false, isBundle = false,
|
||||
allowImages = false
|
||||
}, token));
|
||||
ptResp.EnsureSuccessStatusCode();
|
||||
var ptJson = await ptResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var productTypeId = ptJson.GetProperty("id").GetInt32();
|
||||
|
||||
return (medioId, productTypeId);
|
||||
}
|
||||
|
||||
// ── 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 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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WithoutAuth_Returns401()
|
||||
{
|
||||
using var req = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/1", new { nombre = "Test", basePrice = 10m });
|
||||
var resp = await _client.SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deactivate_WithoutAuth_Returns401()
|
||||
{
|
||||
using var req = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/1");
|
||||
var resp = await _client.SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
|
||||
}
|
||||
|
||||
// ── POST /api/v1/admin/products ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WithAdmin_Returns201WithId()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
|
||||
var uniqueName = $"Prod_{Guid.NewGuid():N}"[..30];
|
||||
|
||||
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||
{
|
||||
nombre = uniqueName,
|
||||
medioId,
|
||||
productTypeId,
|
||||
basePrice = 100.50m
|
||||
}, 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_InvalidBody_Returns400()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
|
||||
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||
{
|
||||
nombre = string.Empty, // invalid
|
||||
medioId = 1,
|
||||
productTypeId = 1,
|
||||
basePrice = 10m
|
||||
}, token);
|
||||
|
||||
var resp = await _client.SendAsync(req);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_DuplicateNombre_Returns409()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
|
||||
var uniqueName = $"Dup_{Guid.NewGuid():N}"[..30];
|
||||
|
||||
var body = new { nombre = uniqueName, medioId, productTypeId, basePrice = 50m };
|
||||
|
||||
using var req1 = BuildRequest(HttpMethod.Post, AdminEndpoint, body, token);
|
||||
var resp1 = await _client.SendAsync(req1);
|
||||
Assert.Equal(HttpStatusCode.Created, resp1.StatusCode);
|
||||
|
||||
using var req2 = BuildRequest(HttpMethod.Post, AdminEndpoint, body, token);
|
||||
var resp2 = await _client.SendAsync(req2);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Conflict, resp2.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_MedioNotFound_Returns404()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
|
||||
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||
{
|
||||
nombre = $"Prod_{Guid.NewGuid():N}"[..30],
|
||||
medioId = 999999,
|
||||
productTypeId = 1,
|
||||
basePrice = 50m
|
||||
}, token);
|
||||
|
||||
var resp = await _client.SendAsync(req);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||
}
|
||||
|
||||
// ── GET /api/v1/products ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithAuth_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/products/{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 (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
|
||||
var uniqueName = $"GetById_{Guid.NewGuid():N}"[..28];
|
||||
|
||||
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||
{
|
||||
nombre = uniqueName, medioId, productTypeId, basePrice = 75m
|
||||
}, token);
|
||||
var createResp = await _client.SendAsync(createReq);
|
||||
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
|
||||
|
||||
var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var productId = createJson.GetProperty("id").GetInt32();
|
||||
|
||||
using var getReq = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/{productId}", bearerToken: token);
|
||||
var getResp = await _client.SendAsync(getReq);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, getResp.StatusCode);
|
||||
var getJson = await getResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal(productId, getJson.GetProperty("id").GetInt32());
|
||||
Assert.Equal(uniqueName, getJson.GetProperty("nombre").GetString());
|
||||
}
|
||||
|
||||
// ── DELETE /api/v1/admin/products/{id} ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Deactivate_ExistingId_Returns204()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
|
||||
var uniqueName = $"Del_{Guid.NewGuid():N}"[..28];
|
||||
|
||||
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||
{
|
||||
nombre = uniqueName, medioId, productTypeId, basePrice = 50m
|
||||
}, token);
|
||||
var createResp = await _client.SendAsync(createReq);
|
||||
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
|
||||
|
||||
var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var productId = createJson.GetProperty("id").GetInt32();
|
||||
|
||||
using var delReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{productId}", bearerToken: token);
|
||||
var delResp = await _client.SendAsync(delReq);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, delResp.StatusCode);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// ── PUT /api/v1/admin/products/{id} ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Update_ExistingProduct_Returns200()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var (medioId, productTypeId) = await EnsureMedioAndProductTypeAsync(token);
|
||||
var uniqueName = $"Upd_{Guid.NewGuid():N}"[..28];
|
||||
|
||||
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||
{
|
||||
nombre = uniqueName, medioId, productTypeId, basePrice = 50m
|
||||
}, token);
|
||||
var createResp = await _client.SendAsync(createReq);
|
||||
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
|
||||
|
||||
var createJson = await createResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var productId = createJson.GetProperty("id").GetInt32();
|
||||
|
||||
var newName = $"Upd2_{Guid.NewGuid():N}"[..28];
|
||||
using var updateReq = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/{productId}", new
|
||||
{
|
||||
nombre = newName,
|
||||
basePrice = 200m
|
||||
}, token);
|
||||
var updateResp = await _client.SendAsync(updateReq);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, updateResp.StatusCode);
|
||||
var updateJson = await updateResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal(newName, updateJson.GetProperty("nombre").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_NotFound_Returns404()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
using var req = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/999999999", new
|
||||
{
|
||||
nombre = "Test", basePrice = 10m
|
||||
}, token);
|
||||
var resp = await _client.SendAsync(req);
|
||||
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user