using Microsoft.EntityFrameworkCore; using ScadaLink.Commons.Entities.Templates; using ScadaLink.ConfigurationDatabase; using ScadaLink.ConfigurationDatabase.Repositories; namespace ScadaLink.ConfigurationDatabase.Tests; public class TemplateEngineRepositoryTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly TemplateEngineRepository _repository; public TemplateEngineRepositoryTests() { var options = new DbContextOptionsBuilder() .UseSqlite("DataSource=:memory:") .Options; _context = new ScadaLinkDbContext(options); _context.Database.OpenConnection(); _context.Database.EnsureCreated(); _repository = new TemplateEngineRepository(_context); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task GetTemplateWithChildrenAsync_ReturnsTemplateWithAllMemberCollectionsPopulated() { // Arrange: a template with one attribute, one alarm, one script and one composition. var composed = new Template("ComposedTemplate"); _context.Templates.Add(composed); await _context.SaveChangesAsync(); var template = new Template("ParentTemplate"); template.Attributes.Add(new TemplateAttribute("Attr1")); template.Alarms.Add(new TemplateAlarm("Alarm1")); template.Scripts.Add(new TemplateScript("Script1", "return 1;")); template.Compositions.Add(new TemplateComposition("Slot1") { ComposedTemplateId = composed.Id }); _context.Templates.Add(template); await _context.SaveChangesAsync(); // Act var loaded = await _repository.GetTemplateWithChildrenAsync(template.Id); // Assert: the method must deliver the template's child members to the caller, // not silently drop them. Regression guard for ConfigurationDatabase-001. Assert.NotNull(loaded); Assert.Equal(template.Id, loaded!.Id); Assert.Single(loaded.Attributes); Assert.Equal("Attr1", loaded.Attributes.First().Name); Assert.Single(loaded.Alarms); Assert.Equal("Alarm1", loaded.Alarms.First().Name); Assert.Single(loaded.Scripts); Assert.Equal("Script1", loaded.Scripts.First().Name); Assert.Single(loaded.Compositions); Assert.Equal("Slot1", loaded.Compositions.First().InstanceName); } [Fact] public async Task GetTemplateWithChildrenAsync_ReturnsNull_WhenTemplateDoesNotExist() { var loaded = await _repository.GetTemplateWithChildrenAsync(9999); Assert.Null(loaded); } [Fact] public async Task GetTemplatesWithChildrenAsync_BulkVariant_FetchesEveryMatchingNameInOneQuery() { // Transport-008 regression: BundleImporter.PreviewAsync previously // called GetTemplateWithChildrenAsync(stub.Id) per matching template // name. The bulk variant returns every match in a single query. var a = new Template("Alpha"); a.Attributes.Add(new TemplateAttribute("A1")); a.Scripts.Add(new TemplateScript("AS1", "return 1;")); var b = new Template("Beta"); b.Alarms.Add(new TemplateAlarm("BAlarm")); var c = new Template("Gamma"); _context.Templates.AddRange(a, b, c); await _context.SaveChangesAsync(); var result = await _repository.GetTemplatesWithChildrenAsync(new[] { "Alpha", "Beta", "DoesNotExist" }); Assert.Equal(2, result.Count); var loadedA = Assert.Single(result, t => t.Name == "Alpha"); var loadedB = Assert.Single(result, t => t.Name == "Beta"); Assert.Single(loadedA.Attributes); Assert.Equal("A1", loadedA.Attributes.First().Name); Assert.Single(loadedA.Scripts); Assert.Single(loadedB.Alarms); Assert.Equal("BAlarm", loadedB.Alarms.First().Name); Assert.DoesNotContain(result, t => t.Name == "Gamma"); } [Fact] public async Task GetTemplatesWithChildrenAsync_EmptyNames_ReturnsEmpty() { var result = await _repository.GetTemplatesWithChildrenAsync(Array.Empty()); Assert.Empty(result); } [Fact] public async Task GetTemplatesWithChildrenAsync_NullEnumerable_ReturnsEmpty() { var result = await _repository.GetTemplatesWithChildrenAsync(null!); Assert.Empty(result); } [Fact] public async Task GetTemplatesWithChildrenAsync_FiltersOutDuplicatesAndEmptyStrings() { var a = new Template("Alpha"); _context.Templates.Add(a); await _context.SaveChangesAsync(); // Duplicate names + empty / null entries should not throw; the helper // deduplicates and filters them out before the SQL IN clause. var result = await _repository.GetTemplatesWithChildrenAsync( new[] { "Alpha", "Alpha", "", null!, "Alpha" }); Assert.Single(result); Assert.Equal("Alpha", result[0].Name); } [Fact] public async Task GetTemplateWithChildrenAsync_PreservesParentTemplateId_ForInheritanceChainWalk() { // FlatteningPipeline.BuildTemplateChainAsync walks ParentTemplateId upward. // The result of GetTemplateWithChildrenAsync must carry that link intact. var baseTemplate = new Template("BaseTemplate"); _context.Templates.Add(baseTemplate); await _context.SaveChangesAsync(); var derived = new Template("DerivedTemplate") { ParentTemplateId = baseTemplate.Id }; _context.Templates.Add(derived); await _context.SaveChangesAsync(); var loaded = await _repository.GetTemplateWithChildrenAsync(derived.Id); Assert.NotNull(loaded); Assert.Equal(baseTemplate.Id, loaded!.ParentTemplateId); } }