feat: CAT-001 Árbol N-ario de Rubros #30
151
src/api/SIGCM2.Api/Controllers/RubrosController.cs
Normal file
151
src/api/SIGCM2.Api/Controllers/RubrosController.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SIGCM2.Api.Authorization;
|
||||||
|
using SIGCM2.Application.Abstractions;
|
||||||
|
using SIGCM2.Application.Rubros.Create;
|
||||||
|
using SIGCM2.Application.Rubros.Deactivate;
|
||||||
|
using SIGCM2.Application.Rubros.Dtos;
|
||||||
|
using SIGCM2.Application.Rubros.GetById;
|
||||||
|
using SIGCM2.Application.Rubros.GetTree;
|
||||||
|
using SIGCM2.Application.Rubros.Move;
|
||||||
|
using SIGCM2.Application.Rubros.Update;
|
||||||
|
|
||||||
|
namespace SIGCM2.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CAT-001: Rubro N-ary tree management.
|
||||||
|
/// Read endpoints at /api/v1/rubros — require authentication (any role).
|
||||||
|
/// Write endpoints at /api/v1/admin/rubros — require 'catalogo:rubros:gestionar'.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
public sealed class RubrosController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
|
||||||
|
public RubrosController(IDispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── READ endpoints ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Returns the full Rubro tree. Requires authentication.</summary>
|
||||||
|
[HttpGet("api/v1/rubros/tree")]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<RubroTreeNodeDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<IActionResult> GetRubroTree([FromQuery] bool incluirInactivos = false)
|
||||||
|
{
|
||||||
|
var query = new GetRubroTreeQuery(incluirInactivos);
|
||||||
|
var result = await _dispatcher.Send<GetRubroTreeQuery, IReadOnlyList<RubroTreeNodeDto>>(query);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a single Rubro by id. Requires authentication.</summary>
|
||||||
|
[HttpGet("api/v1/rubros/{id:int}")]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResponseType(typeof(RubroDetailDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<IActionResult> GetRubroById([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var query = new GetRubroByIdQuery(id);
|
||||||
|
var result = await _dispatcher.Send<GetRubroByIdQuery, RubroDetailDto>(query);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WRITE endpoints ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Creates a new Rubro. Requires catalogo:rubros:gestionar.</summary>
|
||||||
|
[HttpPost("api/v1/admin/rubros")]
|
||||||
|
[RequirePermission("catalogo:rubros:gestionar")]
|
||||||
|
[ProducesResponseType(typeof(RubroCreatedDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||||
|
public async Task<IActionResult> CreateRubro([FromBody] CreateRubroRequest request)
|
||||||
|
{
|
||||||
|
var command = new CreateRubroCommand(
|
||||||
|
Nombre: request.Nombre ?? string.Empty,
|
||||||
|
ParentId: request.ParentId,
|
||||||
|
TarifarioBaseId: request.TarifarioBaseId);
|
||||||
|
|
||||||
|
var result = await _dispatcher.Send<CreateRubroCommand, RubroCreatedDto>(command);
|
||||||
|
return CreatedAtAction(nameof(GetRubroById), new { id = result.Id }, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Updates a Rubro's nombre. Requires catalogo:rubros:gestionar.</summary>
|
||||||
|
[HttpPut("api/v1/admin/rubros/{id:int}")]
|
||||||
|
[RequirePermission("catalogo:rubros:gestionar")]
|
||||||
|
[ProducesResponseType(typeof(RubroUpdatedDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<IActionResult> UpdateRubro([FromRoute] int id, [FromBody] UpdateRubroRequest request)
|
||||||
|
{
|
||||||
|
var command = new UpdateRubroCommand(
|
||||||
|
Id: id,
|
||||||
|
Nombre: request.Nombre ?? string.Empty);
|
||||||
|
|
||||||
|
var result = await _dispatcher.Send<UpdateRubroCommand, RubroUpdatedDto>(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Soft-deletes (deactivates) a Rubro. Requires catalogo:rubros:gestionar.</summary>
|
||||||
|
[HttpDelete("api/v1/admin/rubros/{id:int}")]
|
||||||
|
[RequirePermission("catalogo:rubros:gestionar")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<IActionResult> DeactivateRubro([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var command = new DeactivateRubroCommand(id);
|
||||||
|
await _dispatcher.Send<DeactivateRubroCommand, RubroStatusDto>(command);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Moves a Rubro to a new parent. Requires catalogo:rubros:gestionar.</summary>
|
||||||
|
[HttpPatch("api/v1/admin/rubros/{id:int}/mover")]
|
||||||
|
[RequirePermission("catalogo:rubros:gestionar")]
|
||||||
|
[ProducesResponseType(typeof(RubroMovedDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||||
|
public async Task<IActionResult> MoveRubro([FromRoute] int id, [FromBody] MoveRubroRequest request)
|
||||||
|
{
|
||||||
|
var command = new MoveRubroCommand(
|
||||||
|
Id: id,
|
||||||
|
NuevoParentId: request.NuevoParentId,
|
||||||
|
NuevoOrden: request.NuevoOrden);
|
||||||
|
|
||||||
|
var result = await _dispatcher.Send<MoveRubroCommand, RubroMovedDto>(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Request body records ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>CAT-001: Create rubro request body.</summary>
|
||||||
|
public sealed record CreateRubroRequest(
|
||||||
|
string? Nombre,
|
||||||
|
int? ParentId,
|
||||||
|
int? TarifarioBaseId);
|
||||||
|
|
||||||
|
/// <summary>CAT-001: Update rubro request body.</summary>
|
||||||
|
public sealed record UpdateRubroRequest(
|
||||||
|
string? Nombre);
|
||||||
|
|
||||||
|
/// <summary>CAT-001: Move rubro request body.</summary>
|
||||||
|
public sealed record MoveRubroRequest(
|
||||||
|
int? NuevoParentId,
|
||||||
|
int NuevoOrden);
|
||||||
@@ -60,6 +60,13 @@ using SIGCM2.Application.Usuarios.Reactivate;
|
|||||||
using SIGCM2.Application.Usuarios.ResetPassword;
|
using SIGCM2.Application.Usuarios.ResetPassword;
|
||||||
using SIGCM2.Application.Usuarios.Permisos;
|
using SIGCM2.Application.Usuarios.Permisos;
|
||||||
using SIGCM2.Application.Usuarios.Update;
|
using SIGCM2.Application.Usuarios.Update;
|
||||||
|
using SIGCM2.Application.Rubros.Create;
|
||||||
|
using SIGCM2.Application.Rubros.Update;
|
||||||
|
using SIGCM2.Application.Rubros.Deactivate;
|
||||||
|
using SIGCM2.Application.Rubros.Move;
|
||||||
|
using SIGCM2.Application.Rubros.GetTree;
|
||||||
|
using SIGCM2.Application.Rubros.GetById;
|
||||||
|
using SIGCM2.Application.Rubros.Dtos;
|
||||||
|
|
||||||
namespace SIGCM2.Application;
|
namespace SIGCM2.Application;
|
||||||
|
|
||||||
@@ -145,6 +152,14 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<ICommandHandler<ListIngresosBrutosQuery, PagedResult<IngresosBrutosDto>>, ListIngresosBrutosQueryHandler>();
|
services.AddScoped<ICommandHandler<ListIngresosBrutosQuery, PagedResult<IngresosBrutosDto>>, ListIngresosBrutosQueryHandler>();
|
||||||
services.AddScoped<ICommandHandler<GetHistorialIngresosBrutosQuery, IReadOnlyList<HistorialCadenaIibbDto>>, GetHistorialIngresosBrutosQueryHandler>();
|
services.AddScoped<ICommandHandler<GetHistorialIngresosBrutosQuery, IReadOnlyList<HistorialCadenaIibbDto>>, GetHistorialIngresosBrutosQueryHandler>();
|
||||||
|
|
||||||
|
// Rubros (CAT-001)
|
||||||
|
services.AddScoped<ICommandHandler<CreateRubroCommand, RubroCreatedDto>, CreateRubroCommandHandler>();
|
||||||
|
services.AddScoped<ICommandHandler<UpdateRubroCommand, RubroUpdatedDto>, UpdateRubroCommandHandler>();
|
||||||
|
services.AddScoped<ICommandHandler<DeactivateRubroCommand, RubroStatusDto>, DeactivateRubroCommandHandler>();
|
||||||
|
services.AddScoped<ICommandHandler<MoveRubroCommand, RubroMovedDto>, MoveRubroCommandHandler>();
|
||||||
|
services.AddScoped<ICommandHandler<GetRubroTreeQuery, IReadOnlyList<RubroTreeNodeDto>>, GetRubroTreeQueryHandler>();
|
||||||
|
services.AddScoped<ICommandHandler<GetRubroByIdQuery, RubroDetailDto>, GetRubroByIdQueryHandler>();
|
||||||
|
|
||||||
// FluentValidation validators (scans entire Application assembly)
|
// FluentValidation validators (scans entire Application assembly)
|
||||||
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
services.AddValidatorsFromAssemblyContaining<LoginCommandValidator>();
|
||||||
|
|
||||||
|
|||||||
670
tests/SIGCM2.Api.Tests/Rubros/RubrosControllerTests.cs
Normal file
670
tests/SIGCM2.Api.Tests/Rubros/RubrosControllerTests.cs
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using SIGCM2.TestSupport;
|
||||||
|
|
||||||
|
namespace SIGCM2.Api.Tests.Rubros;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CAT-001 — Integration tests for /api/v1/rubros and /api/v1/admin/rubros.
|
||||||
|
/// Read endpoints require authentication (any role).
|
||||||
|
/// Write endpoints require permission 'catalogo:rubros:gestionar'.
|
||||||
|
/// Verifies audit events after each mutating operation.
|
||||||
|
/// </summary>
|
||||||
|
[Collection("ApiIntegration")]
|
||||||
|
public sealed class RubrosControllerTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private const string TestConnectionString =
|
||||||
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||||
|
|
||||||
|
private const string ReadEndpoint = "/api/v1/rubros";
|
||||||
|
private const string AdminEndpoint = "/api/v1/admin/rubros";
|
||||||
|
private const string AdminUsername = "admin";
|
||||||
|
private const string AdminPassword = "@Diego550@";
|
||||||
|
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
|
||||||
|
public RubrosControllerTests(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 async Task<string> GetCajeroTokenAsync(string username)
|
||||||
|
{
|
||||||
|
var adminToken = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var mkUser = BuildRequest(HttpMethod.Post, "/api/v1/users", new
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password = "Secure1234!",
|
||||||
|
nombre = "Cajero",
|
||||||
|
apellido = "Test",
|
||||||
|
email = (string?)null,
|
||||||
|
rol = "cajero"
|
||||||
|
}, adminToken);
|
||||||
|
var mkResp = await _client.SendAsync(mkUser);
|
||||||
|
if (mkResp.StatusCode != HttpStatusCode.Created && mkResp.StatusCode != HttpStatusCode.Conflict)
|
||||||
|
Assert.Fail($"Seed cajero failed: {mkResp.StatusCode}");
|
||||||
|
|
||||||
|
var loginResp = await _client.PostAsJsonAsync("/api/v1/auth/login", new
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password = "Secure1234!"
|
||||||
|
});
|
||||||
|
loginResp.EnsureSuccessStatusCode();
|
||||||
|
var loginJson = await loginResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
return loginJson.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 static async Task<int> CountAuditEventsAsync(string action, string targetType, string targetId)
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(TestConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
return await conn.QuerySingleAsync<int>(
|
||||||
|
"SELECT COUNT(*) FROM dbo.AuditEvent WHERE Action = @Action AND TargetType = @TargetType AND TargetId = @TargetId",
|
||||||
|
new { Action = action, TargetType = targetType, TargetId = targetId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DeleteRubroIfExistsAsync(int id)
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(TestConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// Need to disable system versioning to delete from history + main table
|
||||||
|
await conn.ExecuteAsync("ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = OFF)");
|
||||||
|
await conn.ExecuteAsync("DELETE FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id });
|
||||||
|
// Delete children first (recursive), then the target
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
WITH ToDelete AS (
|
||||||
|
SELECT Id FROM dbo.Rubro WHERE Id = @Id
|
||||||
|
UNION ALL
|
||||||
|
SELECT r.Id FROM dbo.Rubro r INNER JOIN ToDelete t ON r.ParentId = t.Id
|
||||||
|
)
|
||||||
|
DELETE r FROM dbo.Rubro r INNER JOIN ToDelete td ON r.Id = td.Id
|
||||||
|
""", new { Id = id });
|
||||||
|
await conn.ExecuteAsync(
|
||||||
|
"ALTER TABLE dbo.Rubro SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Rubro_History, HISTORY_RETENTION_PERIOD = 10 YEARS))");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DeleteUsuarioIfExistsAsync(string username)
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(TestConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
await conn.ExecuteAsync("""
|
||||||
|
DELETE rt FROM dbo.RefreshToken rt
|
||||||
|
INNER JOIN dbo.Usuario u ON u.Id = rt.UsuarioId
|
||||||
|
WHERE u.Username = @Username
|
||||||
|
""", new { Username = username });
|
||||||
|
await conn.ExecuteAsync("DELETE FROM dbo.Usuario WHERE Username = @Username", new { Username = username });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 401 / 403 guards on READ endpoints ────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTree_WithoutAuth_Returns401()
|
||||||
|
{
|
||||||
|
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree");
|
||||||
|
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 CreateRubro_WithoutAuth_Returns401()
|
||||||
|
{
|
||||||
|
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Test", parentId = (int?)null });
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateRubro_WithCajeroRole_Returns403()
|
||||||
|
{
|
||||||
|
const string username = "cat001_rubro_cajero_403";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = await GetCajeroTokenAsync(username);
|
||||||
|
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Test403", parentId = (int?)null }, token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, resp.StatusCode);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteUsuarioIfExistsAsync(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /api/v1/rubros/tree ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTree_WithAdmin_Returns200WithTree()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
// Create a root rubro for the tree
|
||||||
|
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||||
|
{
|
||||||
|
nombre = "TreeRoot_GetTree",
|
||||||
|
parentId = (int?)null,
|
||||||
|
tarifarioBaseId = (int?)null
|
||||||
|
}, token);
|
||||||
|
var createResp = await _client.SendAsync(createReq);
|
||||||
|
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
|
||||||
|
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var rootId = created.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree", bearerToken: token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||||
|
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal(JsonValueKind.Array, json.ValueKind);
|
||||||
|
// Should contain our created root
|
||||||
|
var nombres = json.EnumerateArray().Select(n => n.GetProperty("nombre").GetString()).ToList();
|
||||||
|
Assert.Contains("TreeRoot_GetTree", nombres);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(rootId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetTree_IncluirInactivosTrue_IncludesInactivos()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
// Create then deactivate a rubro
|
||||||
|
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||||
|
{
|
||||||
|
nombre = "RubroInactivo_GetTree",
|
||||||
|
parentId = (int?)null,
|
||||||
|
}, token);
|
||||||
|
var createResp = await _client.SendAsync(createReq);
|
||||||
|
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
|
||||||
|
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var rubroId = created.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Deactivate it
|
||||||
|
using var deleteReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{rubroId}", bearerToken: token);
|
||||||
|
await _client.SendAsync(deleteReq);
|
||||||
|
|
||||||
|
// Without incluirInactivos → should not appear
|
||||||
|
using var req1 = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree", bearerToken: token);
|
||||||
|
var resp1 = await _client.SendAsync(req1);
|
||||||
|
var json1 = await resp1.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var nombres1 = json1.EnumerateArray().Select(n => n.GetProperty("nombre").GetString()).ToList();
|
||||||
|
Assert.DoesNotContain("RubroInactivo_GetTree", nombres1);
|
||||||
|
|
||||||
|
// With incluirInactivos=true → should appear
|
||||||
|
using var req2 = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/tree?incluirInactivos=true", bearerToken: token);
|
||||||
|
var resp2 = await _client.SendAsync(req2);
|
||||||
|
var json2 = await resp2.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var nombres2 = json2.EnumerateArray().Select(n => n.GetProperty("nombre").GetString()).ToList();
|
||||||
|
Assert.Contains("RubroInactivo_GetTree", nombres2);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(rubroId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GET /api/v1/rubros/{id} ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetById_ExistingRubro_Returns200()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||||
|
{
|
||||||
|
nombre = "RubroGetById",
|
||||||
|
parentId = (int?)null,
|
||||||
|
}, token);
|
||||||
|
var createResp = await _client.SendAsync(createReq);
|
||||||
|
Assert.Equal(HttpStatusCode.Created, createResp.StatusCode);
|
||||||
|
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var rubroId = created.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/{rubroId}", bearerToken: token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||||
|
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("RubroGetById", json.GetProperty("nombre").GetString());
|
||||||
|
Assert.Equal(rubroId, json.GetProperty("id").GetInt32());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(rubroId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetById_NotFound_Returns404()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
using var req = BuildRequest(HttpMethod.Get, $"{ReadEndpoint}/999999", bearerToken: token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||||
|
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("rubro_not_found", json.GetProperty("error").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── POST /api/v1/admin/rubros ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateRubro_Root_Returns201WithAuditEvent()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new
|
||||||
|
{
|
||||||
|
nombre = "RubroCreate201",
|
||||||
|
parentId = (int?)null,
|
||||||
|
tarifarioBaseId = (int?)null
|
||||||
|
}, token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
|
||||||
|
Assert.NotNull(resp.Headers.Location);
|
||||||
|
|
||||||
|
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var id = json.GetProperty("id").GetInt32();
|
||||||
|
Assert.True(id > 0);
|
||||||
|
Assert.Equal("RubroCreate201", json.GetProperty("nombre").GetString());
|
||||||
|
Assert.True(json.GetProperty("activo").GetBoolean());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var auditCount = await CountAuditEventsAsync("rubro.created", "Rubro", id.ToString());
|
||||||
|
Assert.Equal(1, auditCount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateRubro_Child_Returns201()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var parentReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentCreate", parentId = (int?)null }, token);
|
||||||
|
var parentResp = await _client.SendAsync(parentReq);
|
||||||
|
Assert.Equal(HttpStatusCode.Created, parentResp.StatusCode);
|
||||||
|
var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parentId = parentJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ChildCreate", parentId }, token);
|
||||||
|
var childResp = await _client.SendAsync(childReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Created, childResp.StatusCode);
|
||||||
|
var childJson = await childResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal(parentId, childJson.GetProperty("parentId").GetInt32());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateRubro_DuplicateNombreUnderParent_Returns409()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var parentReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentDup409", parentId = (int?)null }, token);
|
||||||
|
var parentResp = await _client.SendAsync(parentReq);
|
||||||
|
Assert.Equal(HttpStatusCode.Created, parentResp.StatusCode);
|
||||||
|
var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parentId = parentJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First child
|
||||||
|
using var child1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Duplicado", parentId }, token);
|
||||||
|
var r1 = await _client.SendAsync(child1);
|
||||||
|
Assert.Equal(HttpStatusCode.Created, r1.StatusCode);
|
||||||
|
|
||||||
|
// Second child with same nombre
|
||||||
|
using var child2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Duplicado", parentId }, token);
|
||||||
|
var r2 = await _client.SendAsync(child2);
|
||||||
|
Assert.Equal(HttpStatusCode.Conflict, r2.StatusCode);
|
||||||
|
var json = await r2.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("rubro_nombre_duplicado", json.GetProperty("error").GetString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateRubro_ParentNotFound_Returns404()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
using var req = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "OrphanChild", parentId = 999999 }, token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PUT /api/v1/admin/rubros/{id} ─────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRubro_Returns200WithAuditEvent()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "OriginalNombre", parentId = (int?)null }, 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();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var updateReq = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/{id}", new { nombre = "NombreActualizado" }, token);
|
||||||
|
var updateResp = await _client.SendAsync(updateReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, updateResp.StatusCode);
|
||||||
|
var updated = await updateResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("NombreActualizado", updated.GetProperty("nombre").GetString());
|
||||||
|
|
||||||
|
var auditCount = await CountAuditEventsAsync("rubro.updated", "Rubro", id.ToString());
|
||||||
|
Assert.Equal(1, auditCount);
|
||||||
|
|
||||||
|
// Verify Rubro_History row
|
||||||
|
await using var conn = new SqlConnection(TestConnectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
var histCount = await conn.QuerySingleAsync<int>(
|
||||||
|
"SELECT COUNT(*) FROM dbo.Rubro_History WHERE Id = @Id", new { Id = id });
|
||||||
|
Assert.True(histCount >= 1, "Should have ≥1 row in Rubro_History after update");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRubro_NotFound_Returns404()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
using var req = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/999999", new { nombre = "Test" }, token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRubro_DuplicateNombreSibling_Returns409()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var parent = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentUpdate409", parentId = (int?)null }, token);
|
||||||
|
var parentResp = await _client.SendAsync(parent);
|
||||||
|
var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parentId = parentJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var c1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Sibling1", parentId }, token);
|
||||||
|
var r1 = await _client.SendAsync(c1);
|
||||||
|
var j1 = await r1.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var id1 = j1.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
using var c2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "Sibling2", parentId }, token);
|
||||||
|
var r2 = await _client.SendAsync(c2);
|
||||||
|
var j2 = await r2.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
// Try to rename Sibling1 → Sibling2 (conflict)
|
||||||
|
using var updateReq = BuildRequest(HttpMethod.Put, $"{AdminEndpoint}/{id1}", new { nombre = "Sibling2" }, token);
|
||||||
|
var updateResp = await _client.SendAsync(updateReq);
|
||||||
|
Assert.Equal(HttpStatusCode.Conflict, updateResp.StatusCode);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DELETE /api/v1/admin/rubros/{id} ──────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteRubro_LeafRubro_Returns204WithAuditEvent()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var createReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "RubroToDelete", parentId = (int?)null }, 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();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var deleteReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{id}", bearerToken: token);
|
||||||
|
var deleteResp = await _client.SendAsync(deleteReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NoContent, deleteResp.StatusCode);
|
||||||
|
|
||||||
|
// Verify audit event (handler uses "rubro.deleted")
|
||||||
|
var auditCount = await CountAuditEventsAsync("rubro.deleted", "Rubro", id.ToString());
|
||||||
|
Assert.Equal(1, auditCount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteRubro_WithActiveChildren_Returns409()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var parentReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ParentWithChildren", parentId = (int?)null }, token);
|
||||||
|
var parentResp = await _client.SendAsync(parentReq);
|
||||||
|
var parentJson = await parentResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parentId = parentJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add a child
|
||||||
|
using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "ChildActive", parentId }, token);
|
||||||
|
await _client.SendAsync(childReq);
|
||||||
|
|
||||||
|
// Try to delete parent (has active children → 409)
|
||||||
|
using var deleteReq = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/{parentId}", bearerToken: token);
|
||||||
|
var deleteResp = await _client.SendAsync(deleteReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Conflict, deleteResp.StatusCode);
|
||||||
|
var json = await deleteResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("rubro_tiene_hijos_activos", json.GetProperty("error").GetString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(parentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteRubro_NotFound_Returns404()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
using var req = BuildRequest(HttpMethod.Delete, $"{AdminEndpoint}/999999", bearerToken: token);
|
||||||
|
var resp = await _client.SendAsync(req);
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PATCH /api/v1/admin/rubros/{id}/mover ─────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MoveRubro_Returns200WithAuditEvent()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var p1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveParent1", parentId = (int?)null }, token);
|
||||||
|
var p1Resp = await _client.SendAsync(p1);
|
||||||
|
var p1Json = await p1Resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parent1Id = p1Json.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var p2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveParent2", parentId = (int?)null }, token);
|
||||||
|
var p2Resp = await _client.SendAsync(p2);
|
||||||
|
var p2Json = await p2Resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parent2Id = p2Json.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveChild", parentId = parent1Id }, token);
|
||||||
|
var childResp = await _client.SendAsync(childReq);
|
||||||
|
var childJson = await childResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var childId = childJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// Move child from parent1 to parent2
|
||||||
|
using var moveReq = BuildRequest(HttpMethod.Patch, $"{AdminEndpoint}/{childId}/mover", new
|
||||||
|
{
|
||||||
|
nuevoParentId = parent2Id,
|
||||||
|
nuevoOrden = 0
|
||||||
|
}, token);
|
||||||
|
var moveResp = await _client.SendAsync(moveReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, moveResp.StatusCode);
|
||||||
|
var moved = await moveResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal(parent2Id, moved.GetProperty("parentId").GetInt32());
|
||||||
|
|
||||||
|
var auditCount = await CountAuditEventsAsync("rubro.moved", "Rubro", childId.ToString());
|
||||||
|
Assert.Equal(1, auditCount);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(parent1Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MoveRubro_CycleDetected_Returns400()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var rootReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "CycleRoot", parentId = (int?)null }, token);
|
||||||
|
var rootResp = await _client.SendAsync(rootReq);
|
||||||
|
var rootJson = await rootResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var rootId = rootJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var childReq = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "CycleChild", parentId = rootId }, token);
|
||||||
|
var childResp = await _client.SendAsync(childReq);
|
||||||
|
var childJson = await childResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var childId = childJson.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// Try to move root under its own child → cycle
|
||||||
|
using var moveReq = BuildRequest(HttpMethod.Patch, $"{AdminEndpoint}/{rootId}/mover", new
|
||||||
|
{
|
||||||
|
nuevoParentId = childId,
|
||||||
|
nuevoOrden = 0
|
||||||
|
}, token);
|
||||||
|
var moveResp = await _client.SendAsync(moveReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, moveResp.StatusCode);
|
||||||
|
var json = await moveResp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
Assert.Equal("rubro_cycle_detected", json.GetProperty("error").GetString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(rootId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task MoveRubro_DuplicateNombreUnderNewParent_Returns409()
|
||||||
|
{
|
||||||
|
var token = await GetAdminTokenAsync();
|
||||||
|
|
||||||
|
using var p1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveDupParent1", parentId = (int?)null }, token);
|
||||||
|
var p1Resp = await _client.SendAsync(p1);
|
||||||
|
var p1Json = await p1Resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parent1Id = p1Json.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var p2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "MoveDupParent2", parentId = (int?)null }, token);
|
||||||
|
var p2Resp = await _client.SendAsync(p2);
|
||||||
|
var p2Json = await p2Resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var parent2Id = p2Json.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// Add "SameName" under parent1
|
||||||
|
using var c1 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "SameName", parentId = parent1Id }, token);
|
||||||
|
var c1Resp = await _client.SendAsync(c1);
|
||||||
|
var c1Json = await c1Resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||||
|
var c1Id = c1Json.GetProperty("id").GetInt32();
|
||||||
|
|
||||||
|
// Add "SameName" under parent2 already
|
||||||
|
using var c2 = BuildRequest(HttpMethod.Post, AdminEndpoint, new { nombre = "SameName", parentId = parent2Id }, token);
|
||||||
|
await _client.SendAsync(c2);
|
||||||
|
|
||||||
|
// Try to move c1 (SameName) under parent2 → duplicate
|
||||||
|
using var moveReq = BuildRequest(HttpMethod.Patch, $"{AdminEndpoint}/{c1Id}/mover", new
|
||||||
|
{
|
||||||
|
nuevoParentId = parent2Id,
|
||||||
|
nuevoOrden = 0
|
||||||
|
}, token);
|
||||||
|
var moveResp = await _client.SendAsync(moveReq);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Conflict, moveResp.StatusCode);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await DeleteRubroIfExistsAsync(parent1Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user