using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; 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.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.Enums; 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.Transport; using ZB.MOM.WW.ScadaBridge.Transport.Import; using ZB.MOM.WW.ScadaBridge.Transport.Serialization; namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import; /// /// Integration tests for Area-by-name transport (Task 7): a source instance /// assigned to an under its site must carry that area's NAME /// through the exported bundle so the importer's ResolveOrCreateAreaIdAsync /// machinery can resolve-or-create the equivalent area under the target site. This /// closes the half-shipped Area transport — the importer's GetOrCreate path was /// already built + tested, but the exporter hardcoded AreaName: null. /// /// Reuses the in-memory host pattern from SiteInstanceImportTests: real /// repositories, real EF in-memory provider, real Transport pipeline. /// /// public sealed class AreaTransportTests : IDisposable { private readonly ServiceProvider _provider; public AreaTransportTests() { var services = new ServiceCollection(); services.AddSingleton( new ConfigurationBuilder().AddInMemoryCollection().Build()); var dbName = $"AreaTransportTests_{Guid.NewGuid()}"; services.AddDbContext(opts => opts .UseInMemoryDatabase(dbName) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddTransport(); _provider = services.BuildServiceProvider(); } public void Dispose() => _provider.Dispose(); [Fact] public async Task Instance_Area_travels_by_name_and_resolves_under_target_site() { // ---- Seed a site + instance assigned to an Area "Line-1" ---- await using (var scope = _provider.CreateAsyncScope()) { var ctx = scope.ServiceProvider.GetRequiredService(); var template = new Template("Pump") { Description = "pump tpl" }; ctx.Templates.Add(template); var site = new Site("Plant 1", "plant-1"); ctx.Sites.Add(site); await ctx.SaveChangesAsync(); var area = new Area("Line-1") { SiteId = site.Id }; ctx.Areas.Add(area); await ctx.SaveChangesAsync(); var instance = new Instance("Pump-01") { TemplateId = template.Id, SiteId = site.Id, AreaId = area.Id, State = InstanceState.Enabled, }; ctx.Instances.Add(instance); await ctx.SaveChangesAsync(); } // ---- Export the whole site closure ---- Stream bundleStream; await using (var scope = _provider.CreateAsyncScope()) { var exporter = scope.ServiceProvider.GetRequiredService(); var ctx = scope.ServiceProvider.GetRequiredService(); var siteIds = await ctx.Sites.Select(s => s.Id).ToListAsync(); var selection = new ExportSelection( TemplateIds: Array.Empty(), SharedScriptIds: Array.Empty(), ExternalSystemIds: Array.Empty(), DatabaseConnectionIds: Array.Empty(), NotificationListIds: Array.Empty(), SmtpConfigurationIds: Array.Empty(), ApiMethodIds: Array.Empty(), IncludeDependencies: true, SiteIds: siteIds); bundleStream = await exporter.ExportAsync(selection, user: "alice", sourceEnvironment: "dev", passphrase: null, cancellationToken: CancellationToken.None); } using var ms = new MemoryStream(); await bundleStream.CopyToAsync(ms); ms.Position = 0; // ---- Wipe the source rows so the import creates a fresh target ---- await using (var scope = _provider.CreateAsyncScope()) { var ctx = scope.ServiceProvider.GetRequiredService(); ctx.Instances.RemoveRange(ctx.Instances); ctx.Areas.RemoveRange(ctx.Areas); ctx.Sites.RemoveRange(ctx.Sites); ctx.Templates.RemoveRange(ctx.Templates); await ctx.SaveChangesAsync(); // The template must exist for the instance to resolve its TemplateId. ctx.Templates.Add(new Template("Pump") { Description = "pump tpl" }); await ctx.SaveChangesAsync(); } // ---- Import with a CreateNew site mapping ---- Guid sessionId; await using (var scope = _provider.CreateAsyncScope()) { var importer = scope.ServiceProvider.GetRequiredService(); var session = await importer.LoadAsync(ms, passphrase: null); sessionId = session.SessionId; } var nameMap = new BundleNameMap( Sites: new[] { new SiteMapping("plant-1", MappingAction.CreateNew, null) }, Connections: Array.Empty()); await using (var scope = _provider.CreateAsyncScope()) { var importer = scope.ServiceProvider.GetRequiredService(); await importer.ApplyAsync( sessionId, new List { new("Template", "Pump", ResolutionAction.Skip, null), new("Site", "plant-1", ResolutionAction.Add, null), new("Instance", "Pump-01", ResolutionAction.Add, null), }, user: "bob", ct: CancellationToken.None, nameMap: nameMap); } // ---- Assert: the imported instance's AreaId resolves to an Area named // "Line-1" under the target site (NOT null). ---- await using (var scope = _provider.CreateAsyncScope()) { var ctx = scope.ServiceProvider.GetRequiredService(); var site = await ctx.Sites.SingleAsync(s => s.SiteIdentifier == "plant-1"); var inst = await ctx.Instances.SingleAsync(i => i.UniqueName == "Pump-01"); Assert.NotNull(inst.AreaId); var area = await ctx.Areas.SingleAsync(a => a.Id == inst.AreaId); Assert.Equal("Line-1", area.Name); Assert.Equal(site.Id, area.SiteId); } } }