Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/SchemaConfigurationTests.cs
T
Joseph Doherty 7b0b9c7365 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.
2026-05-28 09:37:45 -04:00

164 lines
6.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
public class SchemaConfigurationTests : IDisposable
{
private readonly ScadaBridgeDbContext _context;
public SchemaConfigurationTests()
{
_context = SqliteTestHelper.CreateInMemoryContext();
}
public void Dispose()
{
_context.Database.CloseConnection();
_context.Dispose();
}
// ConfigurationDatabase-006: the gRPC node-address columns must be length-bounded
// (HasMaxLength(500)) consistently with the sibling NodeAAddress/NodeBAddress columns,
// rather than being left to map to nvarchar(max).
[Theory]
[InlineData(nameof(Site.GrpcNodeAAddress))]
[InlineData(nameof(Site.GrpcNodeBAddress))]
public void GrpcNodeAddressColumns_AreLengthBoundedTo500(string propertyName)
{
var property = _context.Model
.FindEntityType(typeof(Site))!
.FindProperty(propertyName)!;
Assert.Equal(500, property.GetMaxLength());
}
[Theory]
[InlineData(nameof(Site.NodeAAddress))]
[InlineData(nameof(Site.NodeBAddress))]
public void GrpcNodeAddressColumns_MatchSiblingNodeAddressBounds(string siblingPropertyName)
{
var entity = _context.Model.FindEntityType(typeof(Site))!;
var siblingMaxLength = entity.FindProperty(siblingPropertyName)!.GetMaxLength();
Assert.Equal(siblingMaxLength, entity.FindProperty(nameof(Site.GrpcNodeAAddress))!.GetMaxLength());
Assert.Equal(siblingMaxLength, entity.FindProperty(nameof(Site.GrpcNodeBAddress))!.GetMaxLength());
}
// ConfigurationDatabase-014: the encrypting value converter must be applied
// uniformly to all three secret-bearing columns, including the non-nullable
// DatabaseConnectionDefinition.ConnectionString. A regression here (e.g. the
// converter dropped from one HasConversion call) would silently store a secret
// in plaintext.
[Theory]
[InlineData(typeof(SmtpConfiguration), nameof(SmtpConfiguration.Credentials))]
[InlineData(typeof(ExternalSystemDefinition), nameof(ExternalSystemDefinition.AuthConfiguration))]
[InlineData(typeof(DatabaseConnectionDefinition), nameof(DatabaseConnectionDefinition.ConnectionString))]
public void SecretColumns_AllHaveEncryptedStringConverterApplied(Type entityType, string propertyName)
{
var converter = _context.Model
.FindEntityType(entityType)!
.FindProperty(propertyName)!
.GetValueConverter();
Assert.IsType<EncryptedStringConverter>(converter);
}
}
public class SplitQueryBehaviourTests : IDisposable
{
private readonly ScadaBridgeDbContext _context;
private readonly TemplateEngineRepository _repository;
public SplitQueryBehaviourTests()
{
_context = SqliteTestHelper.CreateInMemoryContext();
_repository = new TemplateEngineRepository(_context);
}
public void Dispose()
{
_context.Database.CloseConnection();
_context.Dispose();
}
// ConfigurationDatabase-009: the multi-collection eager-load queries were switched to
// AsSplitQuery() to avoid cartesian-product joins. The result set must be unchanged —
// every member collection still fully populated, with no row duplication.
[Fact]
public async Task GetAllTemplatesAsync_WithMultipleMembersPerCollection_LoadsAllWithoutDuplication()
{
var template = new Template("MultiMember");
for (int i = 0; i < 3; i++)
template.Attributes.Add(new TemplateAttribute($"Attr{i}"));
for (int i = 0; i < 2; i++)
template.Alarms.Add(new TemplateAlarm($"Alarm{i}"));
for (int i = 0; i < 4; i++)
template.Scripts.Add(new TemplateScript($"Script{i}", "return 1;"));
_context.Templates.Add(template);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var all = await _repository.GetAllTemplatesAsync();
var loaded = Assert.Single(all);
// A cartesian-product single query would yield 3 x 2 x 4 = 24 joined rows; the
// collections must still contain exactly the inserted counts.
Assert.Equal(3, loaded.Attributes.Count);
Assert.Equal(2, loaded.Alarms.Count);
Assert.Equal(4, loaded.Scripts.Count);
}
[Fact]
public async Task GetTemplateByIdAsync_WithMultipleMembers_LoadsAllCollections()
{
var template = new Template("Single");
template.Attributes.Add(new TemplateAttribute("A1"));
template.Attributes.Add(new TemplateAttribute("A2"));
template.Scripts.Add(new TemplateScript("S1", "return 1;"));
_context.Templates.Add(template);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var loaded = await _repository.GetTemplateByIdAsync(template.Id);
Assert.NotNull(loaded);
Assert.Equal(2, loaded!.Attributes.Count);
Assert.Single(loaded.Scripts);
}
// ConfigurationDatabase-012: the ApiKey table must persist the bearer credential
// as a hash column (KeyHash) and must NOT carry a plaintext KeyValue column.
[Fact]
public void ApiKey_KeyHashColumn_IsMappedAndUniquelyIndexed()
{
var entityType = _context.Model.FindEntityType(typeof(ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi.ApiKey))!;
var keyHash = entityType.FindProperty("KeyHash");
Assert.NotNull(keyHash);
Assert.False(keyHash!.IsNullable);
var hashIndex = entityType.GetIndexes()
.FirstOrDefault(i => i.Properties.Any(p => p.Name == "KeyHash"));
Assert.NotNull(hashIndex);
Assert.True(hashIndex!.IsUnique);
}
[Fact]
public void ApiKey_HasNoPlaintextKeyValueColumn()
{
var entityType = _context.Model.FindEntityType(typeof(ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi.ApiKey))!;
Assert.Null(entityType.FindProperty("KeyValue"));
}
}