diff --git a/tests/SIGCM2.Api.Tests/Products/ProductTypeDeactivationGuardTests.cs b/tests/SIGCM2.Api.Tests/Products/ProductTypeDeactivationGuardTests.cs new file mode 100644 index 0000000..1d244ed --- /dev/null +++ b/tests/SIGCM2.Api.Tests/Products/ProductTypeDeactivationGuardTests.cs @@ -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; + +/// +/// 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 +/// +[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 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 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; + } + + /// + /// E2E proof: seed Medio + ProductType + active Product → DELETE product-type → expect 409. + /// This verifies the IProductQueryRepository guard fires against real data in dbo.Product. + /// + [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(); + 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(); + 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(); + Assert.Equal("product_type_en_uso", body.GetProperty("error").GetString()); + } + + /// + /// Verify guard does NOT block deactivation when Products exist but are all inactive. + /// + [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(); + 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(); + 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(); + 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); + } +}