using System.IO.Compression; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport; using ZB.MOM.WW.ScadaBridge.Commons.Types; using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services; using ZB.MOM.WW.ScadaBridge.TemplateEngine; using ZB.MOM.WW.ScadaBridge.Transport.Import; using ZB.MOM.WW.ScadaBridge.Transport.Serialization; namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import; /// /// ScadaBridge#118 — derived (composition-slot) templates travel as derived templates: the /// bundle carries each one's owner path, base path and own rows, and the importer rebuilds /// the slot through the TemplateService compose flow instead of creating a base template /// named after the slot. Driven through the real export → load → preview → apply pipeline. /// public sealed class DerivedTemplateTransportTests : IDisposable { private readonly ServiceProvider _provider; /// Initializes the test class with an in-memory database and the Transport services. public DerivedTemplateTransportTests() => _provider = BuildProvider(new Dictionary()); /// Releases the resources held by this test class. public void Dispose() => _provider.Dispose(); private static ServiceProvider BuildProvider(IDictionary settings) { var services = new ServiceCollection(); services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection(settings).Build()); var dbName = $"DerivedTemplateTransportTests_{Guid.NewGuid()}"; services.AddDbContext(opts => opts .UseInMemoryDatabase(dbName) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))); services.AddSingleton(new EphemeralDataProtectionProvider()); services.AddScoped(sp => new ScadaBridgeDbContext( sp.GetRequiredService>(), sp.GetRequiredService())); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddTransport(); return services.BuildServiceProvider(); } // ── seeding through the real compose flow ─────────────────────────────────── private static async Task WithServiceAsync(ServiceProvider provider, Func> body) { await using var scope = provider.CreateAsyncScope(); var svc = new TemplateService( scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService()); return await body(svc, scope.ServiceProvider.GetRequiredService()); } private static T Ok(Result result) { Assert.True(result.IsSuccess, result.IsSuccess ? null : result.Error); return result.Value; } /// /// Motor (Speed=1, Mode="a", script Run, alarm High → Run) and Pump composing Motor as /// slot M1; on the slot, Speed is overridden to 5 and an own attribute Extra is added. /// Returns the derived slot template's id. /// private static Task SeedPumpWithDerivedMotorAsync(ServiceProvider provider) => WithServiceAsync(provider, async (svc, ctx) => { var motor = Ok(await svc.CreateTemplateAsync("Motor", "motor base", null, "seed")); Ok(await svc.AddAttributeAsync(motor.Id, new TemplateAttribute("Speed") { DataType = DataType.Int32, Value = "1" }, "seed")); Ok(await svc.AddAttributeAsync(motor.Id, new TemplateAttribute("Mode") { DataType = DataType.String, Value = "a" }, "seed")); var run = Ok(await svc.AddScriptAsync(motor.Id, new TemplateScript("Run", "return 1;") { TriggerType = "Startup" }, "seed")); Ok(await svc.AddAlarmAsync(motor.Id, new TemplateAlarm("High") { PriorityLevel = 500, TriggerType = AlarmTriggerType.ValueMatch, TriggerConfiguration = "{\"attribute\":\"Speed\",\"matchValue\":\"9\"}", OnTriggerScriptId = run.Id, }, "seed")); var pump = Ok(await svc.CreateTemplateAsync("Pump", "pump", null, "seed")); var slot = Ok(await svc.AddCompositionAsync(pump.Id, motor.Id, "M1", "seed")); var derivedId = slot.ComposedTemplateId; Ok(await svc.AddAttributeAsync(derivedId, new TemplateAttribute("Extra") { DataType = DataType.String, Value = "x" }, "seed")); var speed = await ctx.TemplateAttributes.AsNoTracking().SingleAsync(a => a.TemplateId == derivedId && a.Name == "Speed"); Ok(await svc.UpdateAttributeAsync(speed.Id, new TemplateAttribute("Speed") { DataType = DataType.Int32, Value = "5", IsInherited = false }, "seed")); return derivedId; }); // ── export / load / preview / apply ───────────────────────────────────────── private static async Task ExportAsync(ServiceProvider provider, string? passphrase = null, params string[] baseTemplateNames) { await using var scope = provider.CreateAsyncScope(); var ctx = scope.ServiceProvider.GetRequiredService(); var ids = await ctx.Templates.Where(t => !t.IsDerived && (baseTemplateNames.Length == 0 || baseTemplateNames.Contains(t.Name))) .Select(t => t.Id).ToListAsync(); var selection = new ExportSelection( TemplateIds: ids, SharedScriptIds: Array.Empty(), ExternalSystemIds: Array.Empty(), DatabaseConnectionIds: Array.Empty(), NotificationListIds: Array.Empty(), SmtpConfigurationIds: Array.Empty(), ApiMethodIds: Array.Empty(), IncludeDependencies: true); var stream = await scope.ServiceProvider.GetRequiredService() .ExportAsync(selection, "alice", "dev", passphrase); using var ms = new MemoryStream(); await stream.CopyToAsync(ms); return ms.ToArray(); } private static async Task WipeTemplatesAsync(ServiceProvider provider) { await using var scope = provider.CreateAsyncScope(); var ctx = scope.ServiceProvider.GetRequiredService(); ctx.TemplateCompositions.RemoveRange(ctx.TemplateCompositions); ctx.TemplateAttributes.RemoveRange(ctx.TemplateAttributes); ctx.TemplateAlarms.RemoveRange(ctx.TemplateAlarms); ctx.TemplateScripts.RemoveRange(ctx.TemplateScripts); ctx.Templates.RemoveRange(ctx.Templates); ctx.AuditLogEntries.RemoveRange(ctx.AuditLogEntries); await ctx.SaveChangesAsync(); } private static async Task LoadAsync(ServiceProvider provider, byte[] bundle, string? passphrase = null) { await using var scope = provider.CreateAsyncScope(); return await scope.ServiceProvider.GetRequiredService() .LoadAsync(new MemoryStream(bundle, writable: false), passphrase); } private static async Task PreviewAsync(ServiceProvider provider, Guid sessionId) { await using var scope = provider.CreateAsyncScope(); return await scope.ServiceProvider.GetRequiredService().PreviewAsync(sessionId); } /// Applies with the Central UI's default mapping (New→Add, Modified→Overwrite, Identical→Skip). private static async Task ApplyDefaultsAsync( ServiceProvider provider, Guid sessionId, ImportPreview preview, params ImportResolution[] overrides) { var map = new Dictionary<(string, string), ImportResolution>(); foreach (var item in preview.Items) { var action = item.Kind switch { ConflictKind.New => ResolutionAction.Add, ConflictKind.Modified => ResolutionAction.Overwrite, _ => ResolutionAction.Skip, }; map[(item.EntityType, item.Name)] = new ImportResolution(item.EntityType, item.Name, action, null); } foreach (var o in overrides) map[(o.EntityType, o.Name)] = o; await using var scope = provider.CreateAsyncScope(); return await scope.ServiceProvider.GetRequiredService() .ApplyAsync(sessionId, map.Values.ToList(), user: "bob"); } private static async Task RoundTripAsync(ServiceProvider provider, byte[] bundle, params ImportResolution[] overrides) { var session = await LoadAsync(provider, bundle); var preview = await PreviewAsync(provider, session.SessionId); Assert.DoesNotContain(preview.Items, i => i.Kind == ConflictKind.Blocker); return await ApplyDefaultsAsync(provider, session.SessionId, preview, overrides); } private static async Task