using NSubstitute; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Entities.Scripts; using ScadaLink.Commons.Entities.Templates; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Types.Transport; using ScadaLink.Transport.Export; namespace ScadaLink.Transport.Tests.Export; public sealed class DependencyResolverTests { private readonly ITemplateEngineRepository _templates = Substitute.For(); private readonly IExternalSystemRepository _externalSystems = Substitute.For(); private readonly INotificationRepository _notifications = Substitute.For(); private readonly IInboundApiRepository _inboundApi = Substitute.For(); private DependencyResolver Sut() => new(_templates, _externalSystems, _notifications, _inboundApi); private static ExportSelection SelectTemplates(params int[] ids) => new( TemplateIds: ids, SharedScriptIds: Array.Empty(), ExternalSystemIds: Array.Empty(), DatabaseConnectionIds: Array.Empty(), NotificationListIds: Array.Empty(), SmtpConfigurationIds: Array.Empty(), ApiKeyIds: Array.Empty(), ApiMethodIds: Array.Empty(), IncludeDependencies: true); private static ExportSelection SelectApiMethods(params int[] ids) => new( TemplateIds: Array.Empty(), SharedScriptIds: Array.Empty(), ExternalSystemIds: Array.Empty(), DatabaseConnectionIds: Array.Empty(), NotificationListIds: Array.Empty(), SmtpConfigurationIds: Array.Empty(), ApiKeyIds: Array.Empty(), ApiMethodIds: ids, IncludeDependencies: true); private void StubTemplate(Template t) { _templates.GetTemplateWithChildrenAsync(t.Id, Arg.Any()).Returns(t); } private void StubAllSharedScripts(params SharedScript[] scripts) { _templates.GetAllSharedScriptsAsync(Arg.Any()).Returns(scripts); } private void StubAllExternalSystems(params ExternalSystemDefinition[] systems) { _externalSystems.GetAllExternalSystemsAsync(Arg.Any()).Returns(systems); foreach (var es in systems) { _externalSystems .GetMethodsByExternalSystemIdAsync(es.Id, Arg.Any()) .Returns(Array.Empty()); } } private void StubAllFolders(params TemplateFolder[] folders) { _templates.GetAllFoldersAsync(Arg.Any()).Returns(folders); } [Fact] public async Task Resolve_includes_base_template_for_composed_template() { var baseT = new Template("Base") { Id = 10 }; var composing = new Template("Top") { Id = 11 }; composing.Compositions.Add(new TemplateComposition("slot") { TemplateId = 11, ComposedTemplateId = 10 }); StubTemplate(composing); StubTemplate(baseT); StubAllSharedScripts(); StubAllExternalSystems(); StubAllFolders(); var result = await Sut().ResolveAsync(SelectTemplates(11), CancellationToken.None); Assert.Equal(2, result.Templates.Count); Assert.Contains(result.Templates, t => t.Id == 10); Assert.Contains(result.Templates, t => t.Id == 11); } [Fact] public async Task Resolve_includes_shared_script_referenced_by_template() { var shared = new SharedScript("UtilHelper", "return 42;") { Id = 100 }; var t = new Template("UsesUtil") { Id = 1 }; t.Scripts.Add(new TemplateScript("body", "var x = UtilHelper(); return x;") { TemplateId = 1 }); StubTemplate(t); StubAllSharedScripts(shared, new SharedScript("OtherScript", "return 0;") { Id = 101 }); StubAllExternalSystems(); StubAllFolders(); var result = await Sut().ResolveAsync(SelectTemplates(1), CancellationToken.None); Assert.Single(result.SharedScripts); Assert.Equal("UtilHelper", result.SharedScripts[0].Name); } [Fact] public async Task Resolve_includes_external_system_referenced_by_template() { var es = new ExternalSystemDefinition("ErpSystem", "https://erp", "ApiKey") { Id = 7 }; var t = new Template("UsesErp") { Id = 2 }; t.Scripts.Add(new TemplateScript("call", "ErpSystem.Call(\"x\");") { TemplateId = 2 }); StubTemplate(t); StubAllSharedScripts(); StubAllExternalSystems(es, new ExternalSystemDefinition("Other", "https://o", "Basic") { Id = 8 }); StubAllFolders(); var result = await Sut().ResolveAsync(SelectTemplates(2), CancellationToken.None); Assert.Single(result.ExternalSystems); Assert.Equal("ErpSystem", result.ExternalSystems[0].Name); } [Fact] public async Task Resolve_includes_api_method_shared_script_dependency() { var shared = new SharedScript("Validator", "return true;") { Id = 50 }; var method = new ApiMethod("submit", "var ok = Validator(input); return ok;") { Id = 5 }; _inboundApi.GetApiMethodByIdAsync(5, Arg.Any()).Returns(method); StubAllSharedScripts(shared); StubAllExternalSystems(); StubAllFolders(); var result = await Sut().ResolveAsync(SelectApiMethods(5), CancellationToken.None); Assert.Single(result.ApiMethods); Assert.Single(result.SharedScripts); Assert.Equal("Validator", result.SharedScripts[0].Name); } [Fact] public async Task Resolve_handles_diamond_dependency_without_duplication() { // A composes B and C; both B and C compose D. Selection={A}. D must appear once. var d = new Template("D") { Id = 4 }; var b = new Template("B") { Id = 2 }; b.Compositions.Add(new TemplateComposition("d-in-b") { TemplateId = 2, ComposedTemplateId = 4 }); var c = new Template("C") { Id = 3 }; c.Compositions.Add(new TemplateComposition("d-in-c") { TemplateId = 3, ComposedTemplateId = 4 }); var a = new Template("A") { Id = 1 }; a.Compositions.Add(new TemplateComposition("b-in-a") { TemplateId = 1, ComposedTemplateId = 2 }); a.Compositions.Add(new TemplateComposition("c-in-a") { TemplateId = 1, ComposedTemplateId = 3 }); StubTemplate(a); StubTemplate(b); StubTemplate(c); StubTemplate(d); StubAllSharedScripts(); StubAllExternalSystems(); StubAllFolders(); var result = await Sut().ResolveAsync(SelectTemplates(1), CancellationToken.None); Assert.Equal(4, result.Templates.Count); Assert.Single(result.Templates, t => t.Id == 4); } [Fact] public async Task Resolve_includes_template_folder_for_each_selected_template() { var root = new TemplateFolder("Root") { Id = 1, ParentFolderId = null }; var child = new TemplateFolder("Child") { Id = 2, ParentFolderId = 1 }; var grand = new TemplateFolder("Grand") { Id = 3, ParentFolderId = 2 }; var t = new Template("T") { Id = 99, FolderId = 3 }; StubTemplate(t); StubAllSharedScripts(); StubAllExternalSystems(); StubAllFolders(root, child, grand, new TemplateFolder("Unrelated") { Id = 4, ParentFolderId = null }); var result = await Sut().ResolveAsync(SelectTemplates(99), CancellationToken.None); Assert.Equal(3, result.TemplateFolders.Count); // Root-first ordering: depth 0, 1, 2. Assert.Equal("Root", result.TemplateFolders[0].Name); Assert.Equal("Child", result.TemplateFolders[1].Name); Assert.Equal("Grand", result.TemplateFolders[2].Name); } [Fact] public async Task Resolve_returns_topological_order_base_before_derived() { // Top composes Middle, Middle composes Leaf. Order must be Leaf, Middle, Top. var leaf = new Template("Leaf") { Id = 30 }; var middle = new Template("Middle") { Id = 20 }; middle.Compositions.Add(new TemplateComposition("l") { TemplateId = 20, ComposedTemplateId = 30 }); var top = new Template("Top") { Id = 10 }; top.Compositions.Add(new TemplateComposition("m") { TemplateId = 10, ComposedTemplateId = 20 }); StubTemplate(top); StubTemplate(middle); StubTemplate(leaf); StubAllSharedScripts(); StubAllExternalSystems(); StubAllFolders(); var result = await Sut().ResolveAsync(SelectTemplates(10), CancellationToken.None); Assert.Equal(3, result.Templates.Count); Assert.Equal("Leaf", result.Templates[0].Name); Assert.Equal("Middle", result.Templates[1].Name); Assert.Equal("Top", result.Templates[2].Name); } }