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:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -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());
}
}
@@ -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);
}
}