using System.Data; using Dapper; using Microsoft.Extensions.Logging; using Npgsql; using Xunit; using KArtSell.Modules.ModelOperations.Compliance; namespace KArtSell.Integration.Tests.Compliance; public class AuditTrailTests : IAsyncLifetime { private readonly IDbConnection _db; private readonly AuditSql _sql; public AuditTrailTests() { _db = new NpgsqlConnection(TestConnectionString); _sql = new AuditSql(LoggerFactory.Create(b => b.AddConsole()).CreateLogger()); } public async Task InitializeAsync() { _db.Open(); await _db.ExecuteAsync(@" DELETE FROM compliance.gdpr_retention; DELETE FROM compliance.audit_events; "); } public Task DisposeAsync() { _db?.Dispose(); return Task.CompletedTask; } [Fact] public async Task InsertAuditEvent_CreatesImmutableRecord() { // Arrange var eventId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); var entityId = Guid.NewGuid(); // Act await _sql.InsertAuditEventAsync( _db, eventId, AuditEventTypes.ModelActivated, AuditEntityTypes.Model, entityId, "sre@company.com", "SRE", DateTime.UtcNow, "SUCCESS", null, new Dictionary { { "modelVersion", "1.0.0" } }, new[] { "s3://evidence/pbo-0.95.json" }, "192.168.1.100", "PostmanRuntime/7.32.3", correlationId, CancellationToken.None); // Assert var @event = await _sql.GetAuditEventByIdAsync(_db, eventId, CancellationToken.None); Assert.NotNull(@event); Assert.Equal(AuditEventTypes.ModelActivated, @event.EventType); Assert.Equal(entityId, @event.EntityId); Assert.Equal("sre@company.com", @event.ActorEmail); Assert.Single(@event.EvidenceLinks!); } [Fact] public async Task QueryAuditEvents_WithFilters_ReturnsMatching() { // Arrange var entityId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); await _sql.InsertAuditEventAsync( _db, Guid.NewGuid(), AuditEventTypes.ModelActivated, AuditEntityTypes.Model, entityId, "sre@company.com", "SRE", DateTime.UtcNow, "SUCCESS", null, null, null, null, null, correlationId, CancellationToken.None); await _sql.InsertAuditEventAsync( _db, Guid.NewGuid(), AuditEventTypes.ApprovalApproved, AuditEntityTypes.Approval, Guid.NewGuid(), "checker@company.com", "CHECKER", DateTime.UtcNow, "SUCCESS", null, null, null, null, null, Guid.NewGuid(), CancellationToken.None); // Act var (events, total) = await _sql.QueryAuditEventsAsync( _db, eventType: AuditEventTypes.ModelActivated, take: 50, ct: CancellationToken.None); // Assert Assert.Equal(1, total); Assert.Single(events); Assert.Equal(AuditEventTypes.ModelActivated, events[0].EventType); } [Fact] public async Task InsertGdprRetention_TracksPersonalData() { // Arrange var eventId = Guid.NewGuid(); var customerId = Guid.NewGuid(); var retentionId = Guid.NewGuid(); await _sql.InsertAuditEventAsync( _db, eventId, AuditEventTypes.ModelActivated, AuditEntityTypes.Model, Guid.NewGuid(), "customer@company.com", null, DateTime.UtcNow, "SUCCESS", null, null, null, null, null, Guid.NewGuid(), CancellationToken.None); // Act await _sql.InsertGdprRetentionAsync( _db, retentionId, eventId, customerId, new[] { GdprDataCategories.PersonallyIdentifiableInformation, GdprDataCategories.EmailAddress }, DateTime.UtcNow.AddYears(7), CancellationToken.None); // Assert var retention = await _db.QuerySingleAsync( "SELECT * FROM compliance.gdpr_retention WHERE id = @Id", new { Id = retentionId }); Assert.NotNull(retention); Assert.Equal(customerId, retention.CustomerId); Assert.Equal(GdprPurgeStatus.Pending, retention.PurgeStatus); } [Fact] public async Task MarkGdprPurged_RedactsPersonalData() { // Arrange var customerId = Guid.NewGuid(); var eventId = Guid.NewGuid(); var retentionId = Guid.NewGuid(); await _sql.InsertAuditEventAsync( _db, eventId, AuditEventTypes.ModelActivated, AuditEntityTypes.Model, Guid.NewGuid(), "customer@company.com", null, DateTime.UtcNow, "SUCCESS", null, new Dictionary { { "customer_id", customerId.ToString() } }, null, null, null, Guid.NewGuid(), CancellationToken.None); await _sql.InsertGdprRetentionAsync( _db, retentionId, eventId, customerId, new[] { GdprDataCategories.PersonallyIdentifiableInformation }, DateTime.UtcNow.AddYears(7), CancellationToken.None); // Act await _sql.MarkGdprPurgedAsync(_db, customerId, CancellationToken.None); // Assert var retention = await _db.QuerySingleAsync( "SELECT * FROM compliance.gdpr_retention WHERE id = @Id", new { Id = retentionId }); Assert.Equal(GdprPurgeStatus.Purged, retention.PurgeStatus); Assert.NotNull(retention.PurgedAt); } [Fact] public async Task RedactAuditEventDetails_AnonymizesPersonalInfo() { // Arrange var eventId = Guid.NewGuid(); var customerId = Guid.NewGuid(); await _sql.InsertAuditEventAsync( _db, eventId, AuditEventTypes.ModelActivated, AuditEntityTypes.Model, Guid.NewGuid(), "customer@company.com", null, DateTime.UtcNow, "SUCCESS", null, new Dictionary { { "actor_email", "customer@company.com" }, { "customer_id", customerId.ToString() } }, null, null, null, Guid.NewGuid(), CancellationToken.None); // Act await _sql.RedactAuditEventDetailsAsync(_db, eventId, CancellationToken.None); // Assert var @event = await _sql.GetAuditEventByIdAsync(_db, eventId, CancellationToken.None); Assert.NotNull(@event); Assert.Equal("", @event.Details?["actor_email"].ToString()); Assert.Equal("", @event.Details?["customer_id"].ToString()); } private const string TestConnectionString = "Host=localhost;Port=5432;Database=kartselldb;Username=kartsell;Password=kartsell4321@!"; }