Files
SIG-CM2.0/tests/SIGCM2.Api.Tests/Usuarios/DeactivateReactivateEndpointTests.cs
dmolinari e5b6c06f64 refactor(tests): Api.Tests apunta a SIGCM2_Test_Api via TestConnectionStrings
Todos los archivos de Api.Tests reemplazan la connection string hardcodeada
por TestConnectionStrings.ApiTestDb. Cada proyecto de tests ahora tiene su
propia base de datos aislada, eliminando la contención entre Application.Tests
y Api.Tests que causaba flakiness.
2026-04-18 21:44:40 -03:00

216 lines
8.7 KiB
C#

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 PATCH /api/v1/users/{id}/deactivate and /reactivate (UDT-008 B5).
/// </summary>
[Collection("ApiIntegration")]
public sealed class DeactivateReactivateEndpointTests : IAsyncLifetime
{
private const string TestConnectionString = TestConnectionStrings.ApiTestDb;
private readonly HttpClient _client;
private readonly SqlTestFixture _db;
public DeactivateReactivateEndpointTests(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<int> SeedCajeroAsync(string username, bool activo = true)
{
await using var conn = new SqlConnection(TestConnectionString);
await conn.OpenAsync();
var activoVal = activo ? 1 : 0;
return await conn.ExecuteScalarAsync<int>($"""
IF NOT EXISTS (SELECT 1 FROM dbo.Usuario WHERE Username = '{username}')
INSERT INTO dbo.Usuario (Username, PasswordHash, Nombre, Apellido, Rol, PermisosJson, Activo, MustChangePassword)
VALUES ('{username}', '$2a$12$rmq6tlSAQ8WXhR2CwLCSeuwCJKz/.8Eab95UQCUNfwe4dokeOqMcW', 'Test', 'Usuario', 'cajero', '[]', {activoVal}, 0);
SELECT Id FROM dbo.Usuario WHERE Username = '{username}'
""");
}
private async Task<string> GetCajeroTokenAsync()
{
await SeedCajeroAsync("cajero_deact_auth");
var response = await _client.PostAsJsonAsync("/api/v1/auth/login",
new { username = "cajero_deact_auth", password = "@Diego550@" });
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
return json.GetProperty("accessToken").GetString()!;
}
// ── deactivate ────────────────────────────────────────────────────────────
[Fact]
public async Task PATCH_Deactivate_200_Returns_UserDetail_Activo_False()
{
var token = await GetAdminTokenAsync();
var targetId = await SeedCajeroAsync("cajero_deact_happy", true);
var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/users/{targetId}/deactivate");
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.False(json.GetProperty("activo").GetBoolean());
}
[Fact]
public async Task PATCH_Deactivate_Idempotent_Returns_200()
{
var token = await GetAdminTokenAsync();
var targetId = await SeedCajeroAsync("cajero_deact_idempotent", false); // already inactive
var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/users/{targetId}/deactivate");
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.False(json.GetProperty("activo").GetBoolean());
}
[Fact]
public async Task PATCH_Deactivate_400_Last_Admin_Lockout()
{
var token = await GetAdminTokenAsync();
var adminId = await GetAdminIdAsync();
var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/users/{adminId}/deactivate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _client.SendAsync(request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.Contains("last-admin-lockout", body, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task PATCH_Deactivate_404_Not_Found()
{
var token = await GetAdminTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Patch, "/api/v1/users/9999/deactivate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PATCH_Deactivate_401_No_Auth()
{
var response = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Patch, "/api/v1/users/1/deactivate"));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task PATCH_Deactivate_403_No_Permission()
{
var token = await GetCajeroTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Patch, "/api/v1/users/1/deactivate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
// ── reactivate ────────────────────────────────────────────────────────────
[Fact]
public async Task PATCH_Reactivate_200_Returns_UserDetail_Activo_True()
{
var token = await GetAdminTokenAsync();
var targetId = await SeedCajeroAsync("cajero_react_happy", false); // inactive
var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/users/{targetId}/reactivate");
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.GetProperty("activo").GetBoolean());
}
[Fact]
public async Task PATCH_Reactivate_Idempotent_Returns_200()
{
var token = await GetAdminTokenAsync();
var targetId = await SeedCajeroAsync("cajero_react_idempotent", true); // already active
var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/users/{targetId}/reactivate");
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.GetProperty("activo").GetBoolean());
}
[Fact]
public async Task PATCH_Reactivate_404_Not_Found()
{
var token = await GetAdminTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Patch, "/api/v1/users/9999/reactivate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task PATCH_Reactivate_401_No_Auth()
{
var response = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Patch, "/api/v1/users/1/reactivate"));
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task PATCH_Reactivate_403_No_Permission()
{
var token = await GetCajeroTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Patch, "/api/v1/users/1/reactivate");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _client.SendAsync(request);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
}