diff --git a/src/api/SIGCM2.Api/Controllers/ProductTypesController.cs b/src/api/SIGCM2.Api/Controllers/ProductTypesController.cs
new file mode 100644
index 0000000..bfe3ea8
--- /dev/null
+++ b/src/api/SIGCM2.Api/Controllers/ProductTypesController.cs
@@ -0,0 +1,184 @@
+using FluentValidation;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using SIGCM2.Api.Authorization;
+using SIGCM2.Application.Abstractions;
+using SIGCM2.Application.Common;
+using SIGCM2.Application.ProductTypes.Create;
+using SIGCM2.Application.ProductTypes.Deactivate;
+using SIGCM2.Application.ProductTypes.GetById;
+using SIGCM2.Application.ProductTypes.List;
+using SIGCM2.Application.ProductTypes.Update;
+
+namespace SIGCM2.Api.Controllers;
+
+///
+/// PRD-001: ProductType catalog management.
+/// Read endpoints at /api/v1/product-types — require authentication (any role).
+/// Write endpoints at /api/v1/admin/product-types — require 'catalogo:tipos:gestionar'.
+///
+[ApiController]
+public sealed class ProductTypesController : ControllerBase
+{
+ private readonly IDispatcher _dispatcher;
+ private readonly IValidator _createValidator;
+ private readonly IValidator _updateValidator;
+
+ public ProductTypesController(
+ IDispatcher dispatcher,
+ IValidator createValidator,
+ IValidator updateValidator)
+ {
+ _dispatcher = dispatcher;
+ _createValidator = createValidator;
+ _updateValidator = updateValidator;
+ }
+
+ // ── READ endpoints ─────────────────────────────────────────────────────────
+
+ /// Returns a paginated list of ProductTypes. Requires authentication.
+ [HttpGet("api/v1/product-types")]
+ [Authorize]
+ [ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ public async Task ListProductTypes(
+ [FromQuery] int page = 1,
+ [FromQuery] int pageSize = 20,
+ [FromQuery] bool? activo = true,
+ [FromQuery] string? search = null)
+ {
+ var query = new ListProductTypesQuery(page, pageSize, activo, search);
+ var result = await _dispatcher.Send>(query);
+ return Ok(result);
+ }
+
+ /// Returns a single ProductType by id. Requires authentication.
+ [HttpGet("api/v1/product-types/{id:int}")]
+ [Authorize]
+ [ProducesResponseType(typeof(ProductTypeDetailDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetProductTypeById([FromRoute] int id)
+ {
+ var query = new GetProductTypeByIdQuery(id);
+ var result = await _dispatcher.Send(query);
+ return Ok(result);
+ }
+
+ // ── WRITE endpoints ────────────────────────────────────────────────────────
+
+ /// Creates a new ProductType. Requires catalogo:tipos:gestionar.
+ [HttpPost("api/v1/admin/product-types")]
+ [RequirePermission("catalogo:tipos:gestionar")]
+ [ProducesResponseType(typeof(ProductTypeCreatedDto), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task CreateProductType([FromBody] CreateProductTypeRequest request)
+ {
+ var command = new CreateProductTypeCommand(
+ Nombre: request.Nombre ?? string.Empty,
+ HasDuration: request.HasDuration,
+ RequiresText: request.RequiresText,
+ RequiresCategory: request.RequiresCategory,
+ IsBundle: request.IsBundle,
+ AllowImages: request.AllowImages,
+ MaxImages: request.MaxImages,
+ MaxImageSizeMB: request.MaxImageSizeMB,
+ MaxImageWidth: request.MaxImageWidth,
+ MaxImageHeight: request.MaxImageHeight);
+
+ var validation = await _createValidator.ValidateAsync(command);
+ if (!validation.IsValid)
+ {
+ var errors = validation.Errors
+ .GroupBy(e => e.PropertyName)
+ .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
+ return BadRequest(new { errors });
+ }
+
+ var result = await _dispatcher.Send(command);
+ return CreatedAtAction(nameof(GetProductTypeById), new { id = result.Id }, result);
+ }
+
+ /// Updates a ProductType. Requires catalogo:tipos:gestionar.
+ [HttpPut("api/v1/admin/product-types/{id:int}")]
+ [RequirePermission("catalogo:tipos:gestionar")]
+ [ProducesResponseType(typeof(ProductTypeUpdatedDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task UpdateProductType([FromRoute] int id, [FromBody] UpdateProductTypeRequest request)
+ {
+ var command = new UpdateProductTypeCommand(
+ Id: id,
+ Nombre: request.Nombre ?? string.Empty,
+ HasDuration: request.HasDuration,
+ RequiresText: request.RequiresText,
+ RequiresCategory: request.RequiresCategory,
+ IsBundle: request.IsBundle,
+ AllowImages: request.AllowImages,
+ MaxImages: request.MaxImages,
+ MaxImageSizeMB: request.MaxImageSizeMB,
+ MaxImageWidth: request.MaxImageWidth,
+ MaxImageHeight: request.MaxImageHeight);
+
+ var validation = await _updateValidator.ValidateAsync(command);
+ if (!validation.IsValid)
+ {
+ var errors = validation.Errors
+ .GroupBy(e => e.PropertyName)
+ .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
+ return BadRequest(new { errors });
+ }
+
+ var result = await _dispatcher.Send(command);
+ return Ok(result);
+ }
+
+ /// Soft-deletes (deactivates) a ProductType. Requires catalogo:tipos:gestionar.
+ [HttpDelete("api/v1/admin/product-types/{id:int}")]
+ [RequirePermission("catalogo:tipos:gestionar")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
+ public async Task DeactivateProductType([FromRoute] int id)
+ {
+ var command = new DeactivateProductTypeCommand(id);
+ await _dispatcher.Send(command);
+ return NoContent();
+ }
+}
+
+// ── Request body records ──────────────────────────────────────────────────────
+
+/// PRD-001: Create ProductType request body.
+public sealed record CreateProductTypeRequest(
+ string? Nombre,
+ bool HasDuration = false,
+ bool RequiresText = false,
+ bool RequiresCategory = false,
+ bool IsBundle = false,
+ bool AllowImages = false,
+ int? MaxImages = null,
+ decimal? MaxImageSizeMB = null,
+ int? MaxImageWidth = null,
+ int? MaxImageHeight = null);
+
+/// PRD-001: Update ProductType request body.
+public sealed record UpdateProductTypeRequest(
+ string? Nombre,
+ bool HasDuration = false,
+ bool RequiresText = false,
+ bool RequiresCategory = false,
+ bool IsBundle = false,
+ bool AllowImages = false,
+ int? MaxImages = null,
+ decimal? MaxImageSizeMB = null,
+ int? MaxImageWidth = null,
+ int? MaxImageHeight = null);
diff --git a/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs b/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs
index e936231..b40f2d3 100644
--- a/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs
+++ b/src/api/SIGCM2.Api/Filters/ExceptionFilter.cs
@@ -414,6 +414,55 @@ public sealed class ExceptionFilter : IExceptionFilter
context.ExceptionHandled = true;
break;
+ // PRD-001: ProductType exceptions
+ case ProductTypeNotFoundException productTypeNotFoundEx:
+ context.Result = new ObjectResult(new
+ {
+ error = "product_type_not_found",
+ message = productTypeNotFoundEx.Message
+ })
+ {
+ StatusCode = StatusCodes.Status404NotFound
+ };
+ context.ExceptionHandled = true;
+ break;
+
+ case ProductTypeNombreDuplicadoException productTypeDupEx:
+ context.Result = new ObjectResult(new
+ {
+ error = "product_type_nombre_duplicado",
+ message = productTypeDupEx.Message
+ })
+ {
+ StatusCode = StatusCodes.Status409Conflict
+ };
+ context.ExceptionHandled = true;
+ break;
+
+ case ProductTypeEnUsoException productTypeEnUsoEx:
+ context.Result = new ObjectResult(new
+ {
+ error = "product_type_en_uso",
+ message = productTypeEnUsoEx.Message
+ })
+ {
+ StatusCode = StatusCodes.Status409Conflict
+ };
+ context.ExceptionHandled = true;
+ break;
+
+ case ProductTypeFlagsIncoherentesException productTypeFlagsEx:
+ context.Result = new ObjectResult(new
+ {
+ error = "product_type_flags_incoherentes",
+ message = productTypeFlagsEx.Message
+ })
+ {
+ StatusCode = StatusCodes.Status422UnprocessableEntity
+ };
+ context.ExceptionHandled = true;
+ break;
+
// ADM-008: PuntoDeVenta exceptions
case PuntoDeVentaNotFoundException puntoDeVentaNotFoundEx:
context.Result = new ObjectResult(new
diff --git a/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs b/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs
index 5649e52..062b771 100644
--- a/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs
+++ b/tests/SIGCM2.Api.Tests/Auth/AuthControllerTests.cs
@@ -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
diff --git a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs
index 4fa9e24..372509b 100644
--- a/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs
+++ b/tests/SIGCM2.Api.Tests/Permisos/PermisosEndpointTests.cs
@@ -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]
diff --git a/tests/SIGCM2.Api.Tests/ProductTypes/ProductTypesControllerTests.cs b/tests/SIGCM2.Api.Tests/ProductTypes/ProductTypesControllerTests.cs
new file mode 100644
index 0000000..f8104cf
--- /dev/null
+++ b/tests/SIGCM2.Api.Tests/ProductTypes/ProductTypesControllerTests.cs
@@ -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;
+
+///
+/// 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.
+///
+[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 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;
+ }
+
+ // ── 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();
+ 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();
+ 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();
+ 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();
+ 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();
+ 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);
+ }
+}