using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests; /// /// Tests for — the idempotent startup normalizer that /// rewrites already-persisted List attribute values from the old array-of-strings JSON /// form (["10","20"]) to the new native-typed form ([10,20]). /// public class ListValueNormalizerTests : IDisposable { private readonly ScadaBridgeDbContext _context; public ListValueNormalizerTests() { _context = SqliteTestHelper.CreateInMemoryContext(); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } // Seeds a Template parent (satisfies the TemplateAttribute -> Template FK) and returns its Id. private async Task SeedTemplateAsync() { var template = new Template("T1"); _context.Templates.Add(template); await _context.SaveChangesAsync(); return template.Id; } // Seeds Site + Template + Instance (satisfies the InstanceAttributeOverride -> Instance FK) // and returns the Instance Id. private async Task SeedInstanceAsync() { var site = new Site("Site1", "S-001"); var template = new Template("T1"); _context.Sites.Add(site); _context.Templates.Add(template); await _context.SaveChangesAsync(); var instance = new Instance("Inst1") { SiteId = site.Id, TemplateId = template.Id }; _context.Instances.Add(instance); await _context.SaveChangesAsync(); return instance.Id; } [Fact] public async Task TemplateAttribute_OldStringForm_RewrittenToNative() { var templateId = await SeedTemplateAsync(); _context.TemplateAttributes.Add(new TemplateAttribute("intList") { TemplateId = templateId, DataType = DataType.List, ElementDataType = DataType.Int32, Value = "[\"10\",\"20\"]", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); var reloaded = await _context.TemplateAttributes.AsNoTracking().SingleAsync(); Assert.Equal("[10,20]", reloaded.Value); } [Fact] public async Task TemplateAttribute_AlreadyNative_IsNotRewritten() { var templateId = await SeedTemplateAsync(); _context.TemplateAttributes.Add(new TemplateAttribute("intList") { TemplateId = templateId, DataType = DataType.List, ElementDataType = DataType.Int32, Value = "[10,20]", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); // No tracked entity should be marked Modified — idempotent no-op. var tracked = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .ToList(); Assert.Empty(tracked); var reloaded = await _context.TemplateAttributes.AsNoTracking().SingleAsync(); Assert.Equal("[10,20]", reloaded.Value); } [Fact] public async Task TemplateAttribute_StringList_IsUnchanged() { var templateId = await SeedTemplateAsync(); _context.TemplateAttributes.Add(new TemplateAttribute("stringList") { TemplateId = templateId, DataType = DataType.List, ElementDataType = DataType.String, Value = "[\"a\",\"b\"]", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); var reloaded = await _context.TemplateAttributes.AsNoTracking().SingleAsync(); Assert.Equal("[\"a\",\"b\"]", reloaded.Value); } [Fact] public async Task TemplateAttribute_Malformed_IsSkipped_AndSiblingStillNormalized() { var templateId = await SeedTemplateAsync(); _context.TemplateAttributes.Add(new TemplateAttribute("badList") { TemplateId = templateId, DataType = DataType.List, ElementDataType = DataType.Int32, Value = "[\"a\"", // malformed JSON }); _context.TemplateAttributes.Add(new TemplateAttribute("goodList") { TemplateId = templateId, DataType = DataType.List, ElementDataType = DataType.Int32, Value = "[\"5\"]", // old form, valid }); await _context.SaveChangesAsync(); // Must NOT throw despite the malformed row. await ListValueNormalizer.NormalizeAsync(_context); var bad = await _context.TemplateAttributes.AsNoTracking() .SingleAsync(a => a.Name == "badList"); var good = await _context.TemplateAttributes.AsNoTracking() .SingleAsync(a => a.Name == "goodList"); Assert.Equal("[\"a\"", bad.Value); // skipped, untouched Assert.Equal("[5]", good.Value); // normalized } [Fact] public async Task TemplateAttribute_NonListRow_IsUnchanged() { var templateId = await SeedTemplateAsync(); _context.TemplateAttributes.Add(new TemplateAttribute("scalar") { TemplateId = templateId, DataType = DataType.Int32, ElementDataType = null, Value = "42", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); var reloaded = await _context.TemplateAttributes.AsNoTracking().SingleAsync(); Assert.Equal("42", reloaded.Value); } [Fact] public async Task InstanceAttributeOverride_OldStringForm_RewrittenToNative() { var instanceId = await SeedInstanceAsync(); _context.InstanceAttributeOverrides.Add(new InstanceAttributeOverride("intList") { InstanceId = instanceId, ElementDataType = DataType.Int32, OverrideValue = "[\"5\"]", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); var reloaded = await _context.InstanceAttributeOverrides.AsNoTracking().SingleAsync(); Assert.Equal("[5]", reloaded.OverrideValue); } [Fact] public async Task InstanceAttributeOverride_NullElementType_IsUntouched() { var instanceId = await SeedInstanceAsync(); _context.InstanceAttributeOverrides.Add(new InstanceAttributeOverride("scalar") { InstanceId = instanceId, ElementDataType = null, OverrideValue = "42", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); var reloaded = await _context.InstanceAttributeOverrides.AsNoTracking().SingleAsync(); Assert.Equal("42", reloaded.OverrideValue); } [Fact] public async Task NormalizeAsync_IsIdempotent_SecondRunChangesNothing() { var templateId = await SeedTemplateAsync(); _context.TemplateAttributes.Add(new TemplateAttribute("intList") { TemplateId = templateId, DataType = DataType.List, ElementDataType = DataType.Int32, Value = "[\"10\",\"20\"]", }); await _context.SaveChangesAsync(); await ListValueNormalizer.NormalizeAsync(_context); var afterFirst = await _context.TemplateAttributes.AsNoTracking().SingleAsync(); Assert.Equal("[10,20]", afterFirst.Value); await ListValueNormalizer.NormalizeAsync(_context); var afterSecond = await _context.TemplateAttributes.AsNoTracking().SingleAsync(); Assert.Equal("[10,20]", afterSecond.Value); var tracked = _context.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .ToList(); Assert.Empty(tracked); } }