refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
+229
@@ -0,0 +1,229 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Configurations;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.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 ScadaBridgeDbContext _context;
|
||||
|
||||
public AuditLogEntityTypeConfigurationTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_MapsToAuditLogTable_WithCompositePrimaryKey()
|
||||
{
|
||||
// Composite PK {EventId, OccurredAtUtc} is required by the partitioned
|
||||
// AuditLog table — the clustered key must include the partition column
|
||||
// (OccurredAtUtc) so each row can be located in its partition (#23 Bundle C).
|
||||
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||
|
||||
Assert.NotNull(entity);
|
||||
Assert.Equal("AuditLog", entity!.GetTableName());
|
||||
|
||||
var pk = entity.FindPrimaryKey();
|
||||
Assert.NotNull(pk);
|
||||
|
||||
var pkPropertyNames = pk!.Properties.Select(p => p.Name).ToArray();
|
||||
Assert.Equal(new[] { nameof(AuditEvent.EventId), nameof(AuditEvent.OccurredAtUtc) }, pkPropertyNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_DeclaresUniqueIndex_OnEventIdAlone_ForIdempotencyLookups()
|
||||
{
|
||||
// EventId remains globally unique (the idempotency key for
|
||||
// InsertIfNotExistsAsync, per M1-T8) via a dedicated unique index that
|
||||
// is independent of the composite PK.
|
||||
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
var eventIdIndex = entity!.GetIndexes()
|
||||
.SingleOrDefault(i => i.GetDatabaseName() == "UX_AuditLog_EventId");
|
||||
|
||||
Assert.NotNull(eventIdIndex);
|
||||
Assert.True(eventIdIndex!.IsUnique);
|
||||
|
||||
var indexedProperty = Assert.Single(eventIdIndex.Properties);
|
||||
Assert.Equal(nameof(AuditEvent.EventId), indexedProperty.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 24 init-only properties (alog.md §4 plus the
|
||||
// additive ExecutionId universal correlation column, its ParentExecutionId
|
||||
// sibling, and the SourceNode-stamping column).
|
||||
Assert.Equal(24, properties.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_ExpectedIndexes_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();
|
||||
|
||||
// Five reconciliation/query indexes from alog.md §4, plus the EventId unique
|
||||
// index introduced alongside the composite PK (Bundle C), plus the additive
|
||||
// IX_AuditLog_Execution index supporting ExecutionId lookups, the
|
||||
// IX_AuditLog_ParentExecution index supporting ParentExecutionId lookups,
|
||||
// and the IX_AuditLog_Node_Occurred composite supporting per-node queries
|
||||
// (SourceNode-stamping).
|
||||
var expected = new[]
|
||||
{
|
||||
"IX_AuditLog_Channel_Status_Occurred",
|
||||
"IX_AuditLog_CorrelationId",
|
||||
"IX_AuditLog_Execution",
|
||||
"IX_AuditLog_Node_Occurred",
|
||||
"IX_AuditLog_OccurredAtUtc",
|
||||
"IX_AuditLog_ParentExecution",
|
||||
"IX_AuditLog_Site_Occurred",
|
||||
"IX_AuditLog_Target_Occurred",
|
||||
"UX_AuditLog_EventId",
|
||||
};
|
||||
|
||||
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 async Task Configure_UtcConverter_HydratesOccurredAtUtcAsKindUtc()
|
||||
{
|
||||
// Insert an AuditEvent with an Unspecified-Kind DateTime, then re-read
|
||||
// it in a fresh context. The UtcConverter on the OccurredAtUtc /
|
||||
// IngestedAtUtc columns must re-tag the round-tripped value as
|
||||
// DateTimeKind.Utc. Without the converter the SQLite (and on production
|
||||
// SQL Server, datetime2) provider would yield Kind=Unspecified — see
|
||||
// ConfigurationDatabase-018/020 and Commons-019.
|
||||
var unspecifiedOccurred = new DateTime(2026, 5, 28, 10, 30, 0, DateTimeKind.Unspecified);
|
||||
var unspecifiedIngested = new DateTime(2026, 5, 28, 10, 31, 0, DateTimeKind.Unspecified);
|
||||
var eventId = Guid.NewGuid();
|
||||
var siteId = "test-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
|
||||
var evt = new AuditEvent
|
||||
{
|
||||
EventId = eventId,
|
||||
// The AuditEvent record's init-setter (Commons-019 resolution)
|
||||
// re-tags Unspecified values as Utc on assignment, so the value EF
|
||||
// ultimately writes already has Kind=Utc. The converter's job is
|
||||
// to keep the Kind tag on the READ path, which the assertions
|
||||
// below exercise.
|
||||
OccurredAtUtc = unspecifiedOccurred,
|
||||
IngestedAtUtc = unspecifiedIngested,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
Status = AuditStatus.Delivered,
|
||||
SourceSiteId = siteId,
|
||||
};
|
||||
|
||||
_context.Set<AuditEvent>().Add(evt);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Detach the tracked entity and re-read in a fresh query so we exercise
|
||||
// the actual hydrate path, not the change-tracker cache.
|
||||
_context.ChangeTracker.Clear();
|
||||
|
||||
var loaded = await _context.Set<AuditEvent>()
|
||||
.AsNoTracking()
|
||||
.Where(e => e.SourceSiteId == siteId)
|
||||
.SingleAsync();
|
||||
|
||||
Assert.Equal(DateTimeKind.Utc, loaded.OccurredAtUtc.Kind);
|
||||
Assert.NotNull(loaded.IngestedAtUtc);
|
||||
Assert.Equal(DateTimeKind.Utc, loaded.IngestedAtUtc!.Value.Kind);
|
||||
|
||||
// The timestamp ticks must round-trip unchanged — the converter only
|
||||
// touches the Kind flag, not the wall-clock value.
|
||||
Assert.Equal(unspecifiedOccurred.Ticks, loaded.OccurredAtUtc.Ticks);
|
||||
Assert.Equal(unspecifiedIngested.Ticks, loaded.IngestedAtUtc.Value.Ticks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_OccurredAtUtcAndIngestedAtUtc_HaveUtcValueConverters()
|
||||
{
|
||||
// Model-metadata cross-check on the converter wiring — guards against a
|
||||
// future config refactor accidentally removing the HasConversion calls.
|
||||
// The converter type itself is internal to the configuration, so we
|
||||
// just assert SOME converter is present on each *Utc DateTime column.
|
||||
var entity = _context.Model.FindEntityType(typeof(AuditEvent));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
var occurredAt = entity!.FindProperty(nameof(AuditEvent.OccurredAtUtc));
|
||||
Assert.NotNull(occurredAt);
|
||||
Assert.NotNull(occurredAt!.GetValueConverter());
|
||||
|
||||
var ingestedAt = entity.FindProperty(nameof(AuditEvent.IngestedAtUtc));
|
||||
Assert.NotNull(ingestedAt);
|
||||
Assert.NotNull(ingestedAt!.GetValueConverter());
|
||||
}
|
||||
|
||||
[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());
|
||||
|
||||
var executionIdx = entity.GetIndexes()
|
||||
.Single(i => i.GetDatabaseName() == "IX_AuditLog_Execution");
|
||||
Assert.Equal("[ExecutionId] IS NOT NULL", executionIdx.GetFilter());
|
||||
|
||||
var parentExecutionIdx = entity.GetIndexes()
|
||||
.Single(i => i.GetDatabaseName() == "IX_AuditLog_ParentExecution");
|
||||
Assert.Equal("[ParentExecutionId] IS NOT NULL", parentExecutionIdx.GetFilter());
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Configurations;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Schema-level tests for <see cref="SiteCallEntityTypeConfiguration"/> (#22 / #23 M3 Bundle B).
|
||||
/// Verifies the <see cref="SiteCall"/> record maps to the <c>SiteCalls</c> table with the
|
||||
/// expected primary key, value conversion on <c>TrackedOperationId</c>, and the two named
|
||||
/// indexes that back the "calls from this site" and "calls in this status" Central UI queries.
|
||||
/// Mirrors the AuditLog Bundle B test pattern — inspects EF model metadata via the existing
|
||||
/// in-memory SQLite test context, no database round-trips required.
|
||||
/// </summary>
|
||||
public class SiteCallEntityTypeConfigurationTests : IDisposable
|
||||
{
|
||||
private readonly ScadaBridgeDbContext _context;
|
||||
|
||||
public SiteCallEntityTypeConfigurationTests()
|
||||
{
|
||||
_context = SqliteTestHelper.CreateInMemoryContext();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Database.CloseConnection();
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_MapsToSiteCallsTable()
|
||||
{
|
||||
var entity = _context.Model.FindEntityType(typeof(SiteCall));
|
||||
|
||||
Assert.NotNull(entity);
|
||||
Assert.Equal("SiteCalls", entity!.GetTableName());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_PrimaryKey_TrackedOperationId()
|
||||
{
|
||||
var entity = _context.Model.FindEntityType(typeof(SiteCall));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
var pk = entity!.FindPrimaryKey();
|
||||
Assert.NotNull(pk);
|
||||
|
||||
var pkPropertyNames = pk!.Properties.Select(p => p.Name).ToArray();
|
||||
Assert.Equal(new[] { nameof(SiteCall.TrackedOperationId) }, pkPropertyNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_HasIndexes_NamedAndOrdered()
|
||||
{
|
||||
var entity = _context.Model.FindEntityType(typeof(SiteCall));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
var indexes = entity!.GetIndexes().ToList();
|
||||
|
||||
// IX_SiteCalls_Source_Created: (SourceSite ASC, CreatedAtUtc DESC).
|
||||
var sourceCreated = indexes.SingleOrDefault(i => i.GetDatabaseName() == "IX_SiteCalls_Source_Created");
|
||||
Assert.NotNull(sourceCreated);
|
||||
var sourceCreatedProps = sourceCreated!.Properties.Select(p => p.Name).ToArray();
|
||||
Assert.Equal(new[] { nameof(SiteCall.SourceSite), nameof(SiteCall.CreatedAtUtc) }, sourceCreatedProps);
|
||||
|
||||
// IX_SiteCalls_Status_Updated: (Status ASC, UpdatedAtUtc DESC).
|
||||
var statusUpdated = indexes.SingleOrDefault(i => i.GetDatabaseName() == "IX_SiteCalls_Status_Updated");
|
||||
Assert.NotNull(statusUpdated);
|
||||
var statusUpdatedProps = statusUpdated!.Properties.Select(p => p.Name).ToArray();
|
||||
Assert.Equal(new[] { nameof(SiteCall.Status), nameof(SiteCall.UpdatedAtUtc) }, statusUpdatedProps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_TrackedOperationId_ConvertedToString_Length36()
|
||||
{
|
||||
var entity = _context.Model.FindEntityType(typeof(SiteCall));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
var property = entity!.FindProperty(nameof(SiteCall.TrackedOperationId));
|
||||
Assert.NotNull(property);
|
||||
|
||||
// Stored as varchar(36) (TrackedOperationId.ToString("D") is always 36 chars).
|
||||
// The value-conversion target type is exposed via GetProviderClrType when set, or
|
||||
// discovered indirectly through the configured converter; either way the on-wire
|
||||
// CLR type is string.
|
||||
var providerClrType = property!.GetProviderClrType() ?? property.GetValueConverter()?.ProviderClrType;
|
||||
Assert.Equal(typeof(string), providerClrType);
|
||||
Assert.Equal(36, property.GetMaxLength());
|
||||
Assert.False(property.IsUnicode() ?? true);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(SiteCall.Channel), 32)]
|
||||
[InlineData(nameof(SiteCall.SourceSite), 64)]
|
||||
[InlineData(nameof(SiteCall.Status), 32)]
|
||||
[InlineData(nameof(SiteCall.Target), 256)]
|
||||
public void Configure_AsciiBoundedColumns(string propertyName, int expectedMaxLength)
|
||||
{
|
||||
var entity = _context.Model.FindEntityType(typeof(SiteCall));
|
||||
Assert.NotNull(entity);
|
||||
|
||||
var property = entity!.FindProperty(propertyName);
|
||||
Assert.NotNull(property);
|
||||
|
||||
Assert.Equal(expectedMaxLength, property!.GetMaxLength());
|
||||
Assert.False(property.IsUnicode() ?? true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user