6 clases que instanciaban Respawner directamente migran a recibir SqlTestFixture vía ICollectionFixture. 8 clases restantes solo actualizan ConnectionString a TestConnectionStrings.AppTestDb. Cada clase ahora es responsable únicamente de sus seeds específicos; la limpieza de la base queda centralizada en el fixture.
101 lines
3.5 KiB
C#
101 lines
3.5 KiB
C#
using Dapper;
|
|
using FluentAssertions;
|
|
using Microsoft.Data.SqlClient;
|
|
using SIGCM2.Infrastructure.Audit;
|
|
using SIGCM2.Infrastructure.Persistence;
|
|
using Xunit;
|
|
|
|
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
|
|
|
/// UDT-010 Batch 5 — SecurityEventRepository integration tests against SIGCM2_Test.
|
|
[Collection("Database")]
|
|
public sealed class SecurityEventRepositoryTests : IAsyncLifetime
|
|
{
|
|
private const string ConnectionString = TestConnectionStrings.AppTestDb;
|
|
|
|
private SqlConnection _connection = null!;
|
|
private SecurityEventRepository _repo = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
_connection = new SqlConnection(ConnectionString);
|
|
await _connection.OpenAsync();
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.SecurityEvent;");
|
|
|
|
var factory = new SqlConnectionFactory(ConnectionString);
|
|
_repo = new SecurityEventRepository(factory);
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.SecurityEvent;");
|
|
await _connection.CloseAsync();
|
|
await _connection.DisposeAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertAsync_LoginSuccess_PersistsAllFields()
|
|
{
|
|
var sessionId = Guid.NewGuid();
|
|
var occurredAt = DateTime.UtcNow;
|
|
|
|
var id = await _repo.InsertAsync(
|
|
occurredAt: occurredAt,
|
|
actorUserId: 42,
|
|
attemptedUsername: null,
|
|
sessionId: sessionId,
|
|
action: "login",
|
|
result: "success",
|
|
failureReason: null,
|
|
ipAddress: "1.2.3.4",
|
|
userAgent: "ua/1.0",
|
|
metadata: """{"route":"/login"}""");
|
|
|
|
id.Should().BeGreaterThan(0);
|
|
|
|
var row = await _connection.QuerySingleAsync<(int? ActorUserId, Guid? SessionId, string Action, string Result, string? FailureReason)>(
|
|
"SELECT ActorUserId, SessionId, Action, Result, FailureReason FROM dbo.SecurityEvent WHERE Id = @Id",
|
|
new { Id = id });
|
|
row.ActorUserId.Should().Be(42);
|
|
row.SessionId.Should().Be(sessionId);
|
|
row.Action.Should().Be("login");
|
|
row.Result.Should().Be("success");
|
|
row.FailureReason.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertAsync_LoginFailure_SupportsNullActorAndAttemptedUsername()
|
|
{
|
|
var id = await _repo.InsertAsync(
|
|
occurredAt: DateTime.UtcNow,
|
|
actorUserId: null,
|
|
attemptedUsername: "juan",
|
|
sessionId: null,
|
|
action: "login",
|
|
result: "failure",
|
|
failureReason: "invalid_password",
|
|
ipAddress: "1.2.3.4",
|
|
userAgent: null,
|
|
metadata: null);
|
|
|
|
id.Should().BeGreaterThan(0);
|
|
var row = await _connection.QuerySingleAsync<(int? ActorUserId, string? AttemptedUsername, string Result, string? FailureReason)>(
|
|
"SELECT ActorUserId, AttemptedUsername, Result, FailureReason FROM dbo.SecurityEvent WHERE Id = @Id",
|
|
new { Id = id });
|
|
row.ActorUserId.Should().BeNull();
|
|
row.AttemptedUsername.Should().Be("juan");
|
|
row.Result.Should().Be("failure");
|
|
row.FailureReason.Should().Be("invalid_password");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertAsync_InvalidResult_FailsCheckConstraint()
|
|
{
|
|
var act = async () => await _repo.InsertAsync(
|
|
DateTime.UtcNow, null, null, null, "login", "neutral", null, null, null, null);
|
|
|
|
await act.Should().ThrowAsync<SqlException>()
|
|
.Where(e => e.Message.Contains("CK_SecurityEvent_Result"));
|
|
}
|
|
}
|