124 lines
4.9 KiB
C#
124 lines
4.9 KiB
C#
|
|
using Dapper;
|
||
|
|
using FluentAssertions;
|
||
|
|
using Microsoft.Data.SqlClient;
|
||
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
||
|
|
using NSubstitute;
|
||
|
|
using Quartz;
|
||
|
|
using SIGCM2.Application.Audit;
|
||
|
|
using SIGCM2.Infrastructure.Audit.Jobs;
|
||
|
|
using SIGCM2.Infrastructure.Persistence;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
||
|
|
|
||
|
|
/// UDT-010 Batch 11 — audit maintenance jobs integration tests.
|
||
|
|
/// Executes each IJob directly (no Quartz scheduler needed) against SIGCM2_Test.
|
||
|
|
[Collection("Database")]
|
||
|
|
public sealed class AuditJobsTests : IAsyncLifetime
|
||
|
|
{
|
||
|
|
private const string ConnectionString =
|
||
|
|
"Server=TECNICA3;Database=SIGCM2_Test;User Id=desarrollo;Password=desarrollo2026;TrustServerCertificate=True;";
|
||
|
|
|
||
|
|
private SqlConnection _connection = null!;
|
||
|
|
private SqlConnectionFactory _factory = null!;
|
||
|
|
|
||
|
|
public async Task InitializeAsync()
|
||
|
|
{
|
||
|
|
_connection = new SqlConnection(ConnectionString);
|
||
|
|
await _connection.OpenAsync();
|
||
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.AuditEvent; DELETE FROM dbo.SecurityEvent;");
|
||
|
|
_factory = new SqlConnectionFactory(ConnectionString);
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task DisposeAsync()
|
||
|
|
{
|
||
|
|
await _connection.ExecuteAsync("DELETE FROM dbo.AuditEvent; DELETE FROM dbo.SecurityEvent;");
|
||
|
|
await _connection.CloseAsync();
|
||
|
|
await _connection.DisposeAsync();
|
||
|
|
}
|
||
|
|
|
||
|
|
private static IJobExecutionContext MockContext()
|
||
|
|
{
|
||
|
|
var ctx = Substitute.For<IJobExecutionContext>();
|
||
|
|
ctx.CancellationToken.Returns(CancellationToken.None);
|
||
|
|
return ctx;
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task PartitionManager_ExtendsFunctionForward_Idempotent()
|
||
|
|
{
|
||
|
|
var job = new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance);
|
||
|
|
|
||
|
|
// First run: ensure the target boundary exists
|
||
|
|
await job.Execute(MockContext());
|
||
|
|
|
||
|
|
// Compute target as the job does
|
||
|
|
var now = DateTime.UtcNow;
|
||
|
|
var target = new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(2);
|
||
|
|
|
||
|
|
foreach (var pf in new[] { "pf_AuditEvent_Monthly", "pf_SecurityEvent_Monthly" })
|
||
|
|
{
|
||
|
|
var count = await _connection.ExecuteScalarAsync<int>("""
|
||
|
|
SELECT COUNT(*)
|
||
|
|
FROM sys.partition_functions pf
|
||
|
|
JOIN sys.partition_range_values prv ON prv.function_id = pf.function_id
|
||
|
|
WHERE pf.name = @Name AND CAST(prv.value AS DATETIME2(3)) = @Target;
|
||
|
|
""", new { Name = pf, Target = target });
|
||
|
|
count.Should().Be(1, $"{pf} should have boundary {target:yyyy-MM-dd}");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Second run must not throw (idempotent)
|
||
|
|
await job.Execute(MockContext());
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task RetentionEnforcer_PurgesAuditEventOlderThan10Years_AndSecurityOlderThan5Years()
|
||
|
|
{
|
||
|
|
await _connection.ExecuteAsync("""
|
||
|
|
INSERT INTO dbo.AuditEvent (OccurredAt, ActorUserId, Action, TargetType, TargetId)
|
||
|
|
VALUES
|
||
|
|
(@Ancient, 1, 'x.y', 'T', '1'), -- should be purged
|
||
|
|
(@Recent, 1, 'x.y', 'T', '2'); -- should stay
|
||
|
|
INSERT INTO dbo.SecurityEvent (OccurredAt, ActorUserId, Action, Result)
|
||
|
|
VALUES
|
||
|
|
(@Ancient5, 1, 'login', 'success'), -- should be purged
|
||
|
|
(@Recent, 1, 'login', 'success'); -- should stay
|
||
|
|
""", new
|
||
|
|
{
|
||
|
|
Ancient = DateTime.UtcNow.AddYears(-11),
|
||
|
|
Recent = DateTime.UtcNow.AddDays(-1),
|
||
|
|
Ancient5 = DateTime.UtcNow.AddYears(-6),
|
||
|
|
});
|
||
|
|
|
||
|
|
var job = new AuditRetentionEnforcerJob(_factory, NullLogger<AuditRetentionEnforcerJob>.Instance);
|
||
|
|
await job.Execute(MockContext());
|
||
|
|
|
||
|
|
var auditCount = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.AuditEvent;");
|
||
|
|
var securityCount = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM dbo.SecurityEvent;");
|
||
|
|
auditCount.Should().Be(1);
|
||
|
|
securityCount.Should().Be(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public async Task IntegrityCheck_AllOk_DoesNotEmitSecurityEvent()
|
||
|
|
{
|
||
|
|
var security = Substitute.For<ISecurityEventLogger>();
|
||
|
|
var job = new AuditIntegrityCheckJob(_factory, security, NullLogger<AuditIntegrityCheckJob>.Instance);
|
||
|
|
|
||
|
|
// Ensure partition manager has run first so next-3-months exist
|
||
|
|
await new AuditPartitionManagerJob(_factory, NullLogger<AuditPartitionManagerJob>.Instance).Execute(MockContext());
|
||
|
|
|
||
|
|
await job.Execute(MockContext());
|
||
|
|
|
||
|
|
await security.DidNotReceive().LogAsync(
|
||
|
|
action: "system.integrity_alert",
|
||
|
|
result: Arg.Any<string>(),
|
||
|
|
actorUserId: Arg.Any<int?>(),
|
||
|
|
attemptedUsername: Arg.Any<string?>(),
|
||
|
|
sessionId: Arg.Any<Guid?>(),
|
||
|
|
failureReason: Arg.Any<string?>(),
|
||
|
|
metadata: Arg.Any<object?>(),
|
||
|
|
ct: Arg.Any<CancellationToken>());
|
||
|
|
}
|
||
|
|
}
|