feat(configdb): map AuditEvent to AuditLog table with PK and five named indexes (#23)
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
|
|
||||||
|
namespace ScadaLink.ConfigurationDatabase.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the <see cref="AuditEvent"/> record to the central <c>AuditLog</c> table
|
||||||
|
/// described in alog.md §4. Column lengths/types and the five named indexes are
|
||||||
|
/// fixed by that specification — keep this in sync with the doc.
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditEvent>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<AuditEvent> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("AuditLog");
|
||||||
|
|
||||||
|
builder.HasKey(e => e.EventId);
|
||||||
|
|
||||||
|
// Enum-as-string columns: bounded varchar(32) ASCII.
|
||||||
|
builder.Property(e => e.Channel)
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(e => e.Kind)
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(e => e.Status)
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
builder.Property(e => e.ForwardState)
|
||||||
|
.HasConversion<string>()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
// Ascii identifier columns — never carry user-supplied unicode.
|
||||||
|
builder.Property(e => e.SourceSiteId)
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
builder.Property(e => e.SourceInstanceId)
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
builder.Property(e => e.SourceScript)
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
builder.Property(e => e.Actor)
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
builder.Property(e => e.Target)
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.IsUnicode(false);
|
||||||
|
|
||||||
|
// Bounded unicode message column.
|
||||||
|
builder.Property(e => e.ErrorMessage)
|
||||||
|
.HasMaxLength(1024);
|
||||||
|
|
||||||
|
// ErrorDetail, RequestSummary, ResponseSummary, Extra: leave as nvarchar(max).
|
||||||
|
|
||||||
|
// Indexes — names locked to alog.md §4 for reconciliation/migration discoverability.
|
||||||
|
builder.HasIndex(e => e.OccurredAtUtc)
|
||||||
|
.IsDescending(true)
|
||||||
|
.HasDatabaseName("IX_AuditLog_OccurredAtUtc");
|
||||||
|
|
||||||
|
builder.HasIndex(e => new { e.SourceSiteId, e.OccurredAtUtc })
|
||||||
|
.IsDescending(false, true)
|
||||||
|
.HasDatabaseName("IX_AuditLog_Site_Occurred");
|
||||||
|
|
||||||
|
builder.HasIndex(e => e.CorrelationId)
|
||||||
|
.HasFilter("[CorrelationId] IS NOT NULL")
|
||||||
|
.HasDatabaseName("IX_AuditLog_CorrelationId");
|
||||||
|
|
||||||
|
builder.HasIndex(e => new { e.Channel, e.Status, e.OccurredAtUtc })
|
||||||
|
.IsDescending(false, false, true)
|
||||||
|
.HasDatabaseName("IX_AuditLog_Channel_Status_Occurred");
|
||||||
|
|
||||||
|
builder.HasIndex(e => new { e.Target, e.OccurredAtUtc })
|
||||||
|
.IsDescending(false, true)
|
||||||
|
.HasFilter("[Target] IS NOT NULL")
|
||||||
|
.HasDatabaseName("IX_AuditLog_Target_Occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
|||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
public DbSet<AuditLogEntry> AuditLogEntries => Set<AuditLogEntry>();
|
public DbSet<AuditLogEntry> AuditLogEntries => Set<AuditLogEntry>();
|
||||||
|
public DbSet<AuditEvent> AuditLogs => Set<AuditEvent>();
|
||||||
|
|
||||||
// Data Protection Keys (for shared ASP.NET Data Protection across nodes)
|
// Data Protection Keys (for shared ASP.NET Data Protection across nodes)
|
||||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
|
using ScadaLink.ConfigurationDatabase;
|
||||||
|
using ScadaLink.ConfigurationDatabase.Configurations;
|
||||||
|
|
||||||
|
namespace ScadaLink.ConfigurationDatabase.Tests.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schema-level tests for <see cref="AuditLogEntityTypeConfiguration"/> (#23 M1 Bundle B).
|
||||||
|
/// Verifies that <see cref="AuditEvent"/> maps to the AuditLog table with the
|
||||||
|
/// PK, property set, column types/lengths, and five named indexes specified in alog.md §4.
|
||||||
|
/// Inspects EF model metadata via the existing in-memory SQLite test context — no
|
||||||
|
/// database round-trips required.
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLogEntityTypeConfigurationTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ScadaLinkDbContext _context;
|
||||||
|
|
||||||
|
public AuditLogEntityTypeConfigurationTests()
|
||||||
|
{
|
||||||
|
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_context.Database.CloseConnection();
|
||||||
|
_context.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configure_MapsToAuditLogTable_WithEventIdAsPrimaryKey()
|
||||||
|
{
|
||||||
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
|
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
Assert.Equal("AuditLog", entity!.GetTableName());
|
||||||
|
|
||||||
|
var pk = entity.FindPrimaryKey();
|
||||||
|
Assert.NotNull(pk);
|
||||||
|
var pkProperty = Assert.Single(pk!.Properties);
|
||||||
|
Assert.Equal(nameof(AuditEvent.EventId), pkProperty.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configure_HasExpectedPropertyCount()
|
||||||
|
{
|
||||||
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
|
||||||
|
var properties = entity!.GetProperties()
|
||||||
|
.Where(p => !p.IsShadowProperty())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// AuditEvent record exposes 21 init-only properties (alog.md §4).
|
||||||
|
Assert.Equal(21, properties.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configure_FiveExpectedIndexes_WithCorrectNames()
|
||||||
|
{
|
||||||
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
|
||||||
|
var indexNames = entity!.GetIndexes()
|
||||||
|
.Select(i => i.GetDatabaseName())
|
||||||
|
.OrderBy(n => n, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var expected = new[]
|
||||||
|
{
|
||||||
|
"IX_AuditLog_Channel_Status_Occurred",
|
||||||
|
"IX_AuditLog_CorrelationId",
|
||||||
|
"IX_AuditLog_OccurredAtUtc",
|
||||||
|
"IX_AuditLog_Site_Occurred",
|
||||||
|
"IX_AuditLog_Target_Occurred",
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal(expected, indexNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(nameof(AuditEvent.Channel))]
|
||||||
|
[InlineData(nameof(AuditEvent.Kind))]
|
||||||
|
[InlineData(nameof(AuditEvent.Status))]
|
||||||
|
[InlineData(nameof(AuditEvent.ForwardState))]
|
||||||
|
public void Configure_EnumColumns_StoredAsVarchar32(string propertyName)
|
||||||
|
{
|
||||||
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
|
||||||
|
var property = entity!.FindProperty(propertyName);
|
||||||
|
Assert.NotNull(property);
|
||||||
|
|
||||||
|
// Enums are converted to strings (varchar(32) IsUnicode=false on SQL Server).
|
||||||
|
Assert.Equal(typeof(string), property!.GetProviderClrType() ?? property.ClrType);
|
||||||
|
Assert.Equal(32, property.GetMaxLength());
|
||||||
|
Assert.False(property.IsUnicode() ?? true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Configure_FilteredIndexes_HaveExpectedFilters()
|
||||||
|
{
|
||||||
|
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||||
|
Assert.NotNull(entity);
|
||||||
|
|
||||||
|
var correlationIdx = entity!.GetIndexes()
|
||||||
|
.Single(i => i.GetDatabaseName() == "IX_AuditLog_CorrelationId");
|
||||||
|
Assert.Equal("[CorrelationId] IS NOT NULL", correlationIdx.GetFilter());
|
||||||
|
|
||||||
|
var targetIdx = entity.GetIndexes()
|
||||||
|
.Single(i => i.GetDatabaseName() == "IX_AuditLog_Target_Occurred");
|
||||||
|
Assert.Equal("[Target] IS NOT NULL", targetIdx.GetFilter());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user