feat(api): List + GetById usuarios — handlers, repo, endpoints [UDT-008]
This commit is contained in:
131
tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs
Normal file
131
tests/SIGCM2.Api.Tests/Usuarios/GetUsuarioByIdEndpointTests.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
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.Usuarios;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for GET /api/v1/users/{id} (UDT-008 B3).
|
||||
/// </summary>
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class GetUsuarioByIdEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
public GetUsuarioByIdEndpointTests(TestWebAppFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
_db = new SqlTestFixture(TestConnectionString);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync() => await _db.InitializeAsync();
|
||||
public async Task DisposeAsync() => await _db.DisposeAsync();
|
||||
|
||||
private async Task<string> GetAdminTokenAsync()
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new { username = "admin", password = "@Diego550@" });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return json.GetProperty("accessToken").GetString()!;
|
||||
}
|
||||
|
||||
private async Task<int> GetAdminIdAsync()
|
||||
{
|
||||
await using var conn = new SqlConnection(TestConnectionString);
|
||||
await conn.OpenAsync();
|
||||
return await conn.ExecuteScalarAsync<int>("SELECT Id FROM dbo.Usuario WHERE Username = 'admin'");
|
||||
}
|
||||
|
||||
private async Task<string> GetCajeroTokenAsync()
|
||||
{
|
||||
await using var conn = new SqlConnection(TestConnectionString);
|
||||
await conn.OpenAsync();
|
||||
await conn.ExecuteAsync("""
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'cajero_getbyid')
|
||||
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
|
||||
VALUES ('cajero_getbyid', '$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW', 'Cajero', 'Test', 'cajero', '[]', 1, 0)
|
||||
""");
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new { username = "cajero_getbyid", password = "@Diego550@" });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return json.GetProperty("accessToken").GetString()!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Id_200_Returns_Detail_Shape()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var adminId = await GetAdminIdAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/users/{adminId}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal(adminId, json.GetProperty("id").GetInt32());
|
||||
Assert.Equal("admin", json.GetProperty("username").GetString());
|
||||
Assert.True(json.TryGetProperty("nombre", out _));
|
||||
Assert.True(json.TryGetProperty("rol", out _));
|
||||
Assert.True(json.TryGetProperty("activo", out _));
|
||||
Assert.True(json.TryGetProperty("mustChangePassword", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Id_DoesNotContain_PasswordHash_In_Response()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var adminId = await GetAdminIdAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/users/{adminId}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var rawJson = await response.Content.ReadAsStringAsync();
|
||||
Assert.DoesNotContain("passwordHash", rawJson, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("permisosJson", rawJson, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Id_9999_Returns_404()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users/9999");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Id_No_Auth_Returns_401()
|
||||
{
|
||||
var response = await _client.GetAsync("/api/v1/users/1");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Id_No_Permission_Returns_403()
|
||||
{
|
||||
var token = await GetCajeroTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users/1");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
}
|
||||
152
tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs
Normal file
152
tests/SIGCM2.Api.Tests/Usuarios/ListUsuariosEndpointTests.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
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.Usuarios;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for GET /api/v1/users (UDT-008 B3).
|
||||
/// </summary>
|
||||
[Collection("ApiIntegration")]
|
||||
public sealed class ListUsuariosEndpointTests : IAsyncLifetime
|
||||
{
|
||||
private const string TestConnectionString =
|
||||
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly SqlTestFixture _db;
|
||||
|
||||
public ListUsuariosEndpointTests(TestWebAppFactory factory)
|
||||
{
|
||||
_client = factory.CreateClient();
|
||||
_db = new SqlTestFixture(TestConnectionString);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync() => await _db.InitializeAsync();
|
||||
public async Task DisposeAsync() => await _db.DisposeAsync();
|
||||
|
||||
private async Task<string> GetAdminTokenAsync()
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new { username = "admin", password = "@Diego550@" });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return json.GetProperty("accessToken").GetString()!;
|
||||
}
|
||||
|
||||
private async Task<string> GetCajeroTokenAsync()
|
||||
{
|
||||
// Seed a cajero user
|
||||
await using var conn = new SqlConnection(TestConnectionString);
|
||||
await conn.OpenAsync();
|
||||
await conn.ExecuteAsync("""
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = 'cajero_test')
|
||||
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
|
||||
VALUES ('cajero_test', '$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW', 'Cajero', 'Test', 'cajero', '[]', 1, 0)
|
||||
""");
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/api/v1/auth/login",
|
||||
new { username = "cajero_test", password = "@Diego550@" });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return json.GetProperty("accessToken").GetString()!;
|
||||
}
|
||||
|
||||
// ── happy path ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_200_Returns_Paged_Shape()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.True(json.TryGetProperty("items", out _));
|
||||
Assert.True(json.TryGetProperty("page", out _));
|
||||
Assert.True(json.TryGetProperty("pageSize", out _));
|
||||
Assert.True(json.TryGetProperty("total", out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Default_PageSize_Is_20()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal(20, json.GetProperty("pageSize").GetInt32());
|
||||
Assert.Equal(1, json.GetProperty("page").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Filter_Rol_Admin_Returns_Only_Admins()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users?rol=admin");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var items = json.GetProperty("items").EnumerateArray().ToList();
|
||||
Assert.All(items, item => Assert.Equal("admin", item.GetProperty("rol").GetString()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_PageSize_0_Returns_400()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users?pageSize=0");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_Page_0_Returns_400()
|
||||
{
|
||||
var token = await GetAdminTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users?page=0");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
// ── auth ──────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_No_Auth_Returns_401()
|
||||
{
|
||||
var response = await _client.GetAsync("/api/v1/users");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GET_Users_No_Permission_Returns_403()
|
||||
{
|
||||
var token = await GetCajeroTokenAsync();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/users");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _client.SendAsync(request);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Usuarios.GetById;
|
||||
using SIGCM2.Domain.Entities;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Usuarios;
|
||||
|
||||
public class GetUsuarioByIdQueryHandlerTests
|
||||
{
|
||||
private readonly IUsuarioRepository _repo = Substitute.For<IUsuarioRepository>();
|
||||
private readonly GetUsuarioByIdQueryHandler _handler;
|
||||
|
||||
public GetUsuarioByIdQueryHandlerTests()
|
||||
{
|
||||
_handler = new GetUsuarioByIdQueryHandler(_repo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Returns_UsuarioDetailDto_When_Found()
|
||||
{
|
||||
var usuario = new Usuario(5, "jperez", "$2a$12$hash", "Juan", "Pérez", "j@x.com", "cajero", "[]", true,
|
||||
fechaModificacion: null, ultimoLogin: null, mustChangePassword: false);
|
||||
|
||||
_repo.GetDetailAsync(5, Arg.Any<CancellationToken>()).Returns(usuario);
|
||||
|
||||
var result = await _handler.Handle(new GetUsuarioByIdQuery(5));
|
||||
|
||||
Assert.Equal(5, result.Id);
|
||||
Assert.Equal("jperez", result.Username);
|
||||
Assert.Equal("Juan", result.Nombre);
|
||||
Assert.Equal("Pérez", result.Apellido);
|
||||
Assert.Equal("j@x.com", result.Email);
|
||||
Assert.Equal("cajero", result.Rol);
|
||||
Assert.True(result.Activo);
|
||||
Assert.False(result.MustChangePassword);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_DoesNotReturn_PasswordHash_In_Dto()
|
||||
{
|
||||
var usuario = new Usuario(5, "jperez", "$2a$12$SECRETHASH", "Juan", "Pérez", null, "cajero", "[]", true);
|
||||
_repo.GetDetailAsync(5, Arg.Any<CancellationToken>()).Returns(usuario);
|
||||
|
||||
var result = await _handler.Handle(new GetUsuarioByIdQuery(5));
|
||||
|
||||
// UsuarioDetailDto must not expose PasswordHash
|
||||
var props = typeof(UsuarioDetailDto).GetProperties().Select(p => p.Name);
|
||||
Assert.DoesNotContain("PasswordHash", props);
|
||||
Assert.DoesNotContain("PermisosJson", props);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Throws_UsuarioNotFoundException_When_Not_Found()
|
||||
{
|
||||
_repo.GetDetailAsync(9999, Arg.Any<CancellationToken>()).Returns((Usuario?)null);
|
||||
|
||||
await Assert.ThrowsAsync<UsuarioNotFoundException>(
|
||||
() => _handler.Handle(new GetUsuarioByIdQuery(9999)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using NSubstitute;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Common;
|
||||
using SIGCM2.Application.Usuarios.List;
|
||||
using SIGCM2.Domain.Exceptions;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Usuarios;
|
||||
|
||||
public class ListUsuariosQueryHandlerTests
|
||||
{
|
||||
private readonly IUsuarioRepository _repo = Substitute.For<IUsuarioRepository>();
|
||||
private readonly ListUsuariosQueryHandler _handler;
|
||||
|
||||
public ListUsuariosQueryHandlerTests()
|
||||
{
|
||||
_handler = new ListUsuariosQueryHandler(_repo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Returns_PagedResult_With_Items()
|
||||
{
|
||||
var items = new List<UsuarioListItem>
|
||||
{
|
||||
new(1, "admin", "Admin", "Sys", null, "admin", true, null, null)
|
||||
};
|
||||
var paged = new PagedResult<UsuarioListItem>(items, 1, 20, 1);
|
||||
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(paged);
|
||||
|
||||
var query = new ListUsuariosQuery(1, 20, null, null, null);
|
||||
var result = await _handler.Handle(query);
|
||||
|
||||
Assert.Equal(1, result.Total);
|
||||
Assert.Single(result.Items);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Clamps_PageSize_Above_100_To_100()
|
||||
{
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedResult<UsuarioListItem>([], 1, 100, 0));
|
||||
|
||||
var query = new ListUsuariosQuery(1, 200, null, null, null);
|
||||
await _handler.Handle(query);
|
||||
|
||||
await _repo.Received(1).GetPagedAsync(
|
||||
Arg.Is<UsuariosQuery>(q => q.PageSize == 100),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Clamps_Page_Below_1_To_1()
|
||||
{
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
|
||||
|
||||
var query = new ListUsuariosQuery(0, 20, null, null, null);
|
||||
await _handler.Handle(query);
|
||||
|
||||
await _repo.Received(1).GetPagedAsync(
|
||||
Arg.Is<UsuariosQuery>(q => q.Page == 1),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Passes_Rol_Filter()
|
||||
{
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
|
||||
|
||||
var query = new ListUsuariosQuery(1, 20, "admin", null, null);
|
||||
await _handler.Handle(query);
|
||||
|
||||
await _repo.Received(1).GetPagedAsync(
|
||||
Arg.Is<UsuariosQuery>(q => q.Rol == "admin"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Passes_Activo_Filter()
|
||||
{
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
|
||||
|
||||
var query = new ListUsuariosQuery(1, 20, null, false, null);
|
||||
await _handler.Handle(query);
|
||||
|
||||
await _repo.Received(1).GetPagedAsync(
|
||||
Arg.Is<UsuariosQuery>(q => q.Activo == false),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Passes_Search_Filter()
|
||||
{
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
|
||||
|
||||
var query = new ListUsuariosQuery(1, 20, null, null, "juan");
|
||||
await _handler.Handle(query);
|
||||
|
||||
await _repo.Received(1).GetPagedAsync(
|
||||
Arg.Is<UsuariosQuery>(q => q.Search == "juan"),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_Returns_Empty_When_No_Items()
|
||||
{
|
||||
_repo.GetPagedAsync(Arg.Any<UsuariosQuery>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PagedResult<UsuarioListItem>([], 1, 20, 0));
|
||||
|
||||
var result = await _handler.Handle(new ListUsuariosQuery(1, 20, null, null, null));
|
||||
|
||||
Assert.Equal(0, result.Total);
|
||||
Assert.Empty(result.Items);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user