feat: PRD-002 Product CRUD #40
@@ -0,0 +1,158 @@
|
|||||||
|
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 Batch 8 — Guard proof: verifies that deactivating a ProductType with active Products
|
||||||
|
/// returns HTTP 409 Conflict. This test closes the W1 (dormant guard) issue from PRD-001:
|
||||||
|
/// - PRD-001: NullProductQueryRepository always returned false → guard never fired
|
||||||
|
/// - PRD-002: ProductQueryRepository now queries dbo.Product → guard fires correctly
|
||||||
|
/// </summary>
|
||||||
|
[Collection("ApiIntegration")]
|
||||||
|
public sealed class ProductTypeDeactivationGuardTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private const string AdminUsername = "admin";
|
||||||
|
private const string AdminPassword = "@Diego550@";
|
||||||
|
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
public ProductTypeDeactivationGuardTests(TestWebAppFactory factory)
|
||||||
|
{
|
||||||
|
_client = factory.CreateClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitializeAsync() => Task.CompletedTask;
|
||||||
|
public Task DisposeAsync() => Task.CompletedTask;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// E2E proof: seed Medio + ProductType + active Product → DELETE product-type → expect 409.
|
||||||
|
/// This verifies the IProductQueryRepository guard fires against real data in dbo.Product.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeactivateProductType_WithActiveProducts_Returns409Conflict()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
// 1. Create a Medio
|
||||||
|
var medioResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/medios", new
|
||||||
|
{
|
||||||
|
codigo = $"GD{Guid.NewGuid():N}"[..6],
|
||||||
|
nombre = $"Guarda Medio {Guid.NewGuid():N}"[..30],
|
||||||
|
tipo = 1
|
||||||
|
}, token));
|
||||||
|
medioResp.EnsureSuccessStatusCode();
|
||||||
|
var medioJson = await medioResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var medioId = medioJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// 2. Create a ProductType
|
||||||
|
var ptResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/product-types", new
|
||||||
|
{
|
||||||
|
nombre = $"Guardado 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();
|
||||||
|
|
||||||
|
// 3. Create an active Product for this ProductType
|
||||||
|
var prodResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/products", new
|
||||||
|
{
|
||||||
|
nombre = $"Prod Guarda {Guid.NewGuid():N}"[..28],
|
||||||
|
medioId,
|
||||||
|
productTypeId,
|
||||||
|
basePrice = 100m
|
||||||
|
}, token));
|
||||||
|
prodResp.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
// 4. Attempt to deactivate the ProductType — should be blocked by guard
|
||||||
|
using var deactivateReq = BuildRequest(HttpMethod.Delete, $"/api/v1/admin/product-types/{productTypeId}", bearerToken: token);
|
||||||
|
var deactivateResp = await _client.SendAsync(deactivateReq);
|
||||||
|
|
||||||
|
// CRITICAL ASSERTION: 409 Conflict — guard fires because Product is active
|
||||||
|
Assert.Equal(HttpStatusCode.Conflict, deactivateResp.StatusCode);
|
||||||
|
|
||||||
|
var body = await deactivateResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("product_type_en_uso", body.GetProperty("error").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verify guard does NOT block deactivation when Products exist but are all inactive.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DeactivateProductType_WithOnlyInactiveProducts_Returns204()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
// 1. Create Medio
|
||||||
|
var medioResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/medios", new
|
||||||
|
{
|
||||||
|
codigo = $"GI{Guid.NewGuid():N}"[..6],
|
||||||
|
nombre = $"Guarda Inact {Guid.NewGuid():N}"[..28],
|
||||||
|
tipo = 1
|
||||||
|
}, token));
|
||||||
|
medioResp.EnsureSuccessStatusCode();
|
||||||
|
var medioJson = await medioResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var medioId = medioJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// 2. Create ProductType
|
||||||
|
var ptResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/product-types", new
|
||||||
|
{
|
||||||
|
nombre = $"PT Inact {Guid.NewGuid():N}"[..28],
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 3. Create then deactivate Product
|
||||||
|
var prodResp = await _client.SendAsync(BuildRequest(HttpMethod.Post, "/api/v1/admin/products", new
|
||||||
|
{
|
||||||
|
nombre = $"Prod Inact {Guid.NewGuid():N}"[..28],
|
||||||
|
medioId,
|
||||||
|
productTypeId,
|
||||||
|
basePrice = 50m
|
||||||
|
}, token));
|
||||||
|
prodResp.EnsureSuccessStatusCode();
|
||||||
|
var prodJson = await prodResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var productId = prodJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// Deactivate the Product first
|
||||||
|
using var deactivateProdReq = BuildRequest(HttpMethod.Delete, $"/api/v1/admin/products/{productId}", bearerToken: token);
|
||||||
|
var deactivateProdResp = await _client.SendAsync(deactivateProdReq);
|
||||||
|
Assert.Equal(HttpStatusCode.NoContent, deactivateProdResp.StatusCode);
|
||||||
|
|
||||||
|
// 4. Now deactivate ProductType — should succeed since no active products
|
||||||
|
using var deactivatePtReq = BuildRequest(HttpMethod.Delete, $"/api/v1/admin/product-types/{productTypeId}", bearerToken: token);
|
||||||
|
var deactivatePtResp = await _client.SendAsync(deactivatePtReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NoContent, deactivatePtResp.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user