205 lines
9.0 KiB
C#
205 lines
9.0 KiB
C#
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.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;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
|
|
|
|
/// <summary>
|
|
/// C3 — the import apply path must wire template inheritance edges
|
|
/// (<c>ParentTemplateId</c>) from the bundle's name-keyed <c>BaseTemplateName</c>.
|
|
/// Before the fix a derived template imported as a root (null parent) — silent
|
|
/// data loss. Mirrors <c>CompositionImportTests</c>: full export → load →
|
|
/// preview → apply, so the wire-level DTO carries the reference to resolve.
|
|
/// </summary>
|
|
public sealed class InheritanceImportTests : IDisposable
|
|
{
|
|
private readonly ServiceProvider _provider;
|
|
|
|
public InheritanceImportTests()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton<IConfiguration>(
|
|
new ConfigurationBuilder().AddInMemoryCollection().Build());
|
|
|
|
var dbName = $"InheritanceImportTests_{Guid.NewGuid()}";
|
|
services.AddDbContext<ScadaBridgeDbContext>(opts => opts
|
|
.UseInMemoryDatabase(dbName)
|
|
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
|
|
|
|
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
|
|
services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
|
|
services.AddScoped<INotificationRepository, NotificationRepository>();
|
|
services.AddScoped<IInboundApiRepository, InboundApiRepository>();
|
|
services.AddScoped<ISiteRepository, SiteRepository>();
|
|
services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
|
|
services.AddScoped<IAuditService, AuditService>();
|
|
services.AddTransport();
|
|
|
|
_provider = services.BuildServiceProvider();
|
|
}
|
|
|
|
public void Dispose() => _provider.Dispose();
|
|
|
|
private async Task<byte[]> ExportAllTemplatesAsync()
|
|
{
|
|
await using var scope = _provider.CreateAsyncScope();
|
|
var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var templateIds = await ctx.Templates.Select(t => t.Id).ToListAsync();
|
|
|
|
var selection = new ExportSelection(
|
|
TemplateIds: templateIds,
|
|
SharedScriptIds: Array.Empty<int>(),
|
|
ExternalSystemIds: Array.Empty<int>(),
|
|
DatabaseConnectionIds: Array.Empty<int>(),
|
|
NotificationListIds: Array.Empty<int>(),
|
|
SmtpConfigurationIds: Array.Empty<int>(),
|
|
ApiMethodIds: Array.Empty<int>(),
|
|
IncludeDependencies: false);
|
|
|
|
var stream = await exporter.ExportAsync(selection,
|
|
user: "alice", sourceEnvironment: "dev",
|
|
passphrase: null, cancellationToken: CancellationToken.None);
|
|
using var ms = new MemoryStream();
|
|
await stream.CopyToAsync(ms);
|
|
return ms.ToArray();
|
|
}
|
|
|
|
private async Task WipeTemplatesAsync()
|
|
{
|
|
await using var scope = _provider.CreateAsyncScope();
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
ctx.Templates.RemoveRange(ctx.Templates);
|
|
ctx.AuditLogEntries.RemoveRange(ctx.AuditLogEntries);
|
|
await ctx.SaveChangesAsync();
|
|
}
|
|
|
|
/// <summary>Seed base "Pump" + derived "WaterPump" (ParentTemplateId -> Pump).</summary>
|
|
private async Task SeedBaseAndDerivedAsync()
|
|
{
|
|
await using var scope = _provider.CreateAsyncScope();
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var pump = new Template("Pump") { Description = "base" };
|
|
ctx.Templates.Add(pump);
|
|
await ctx.SaveChangesAsync();
|
|
ctx.Templates.Add(new Template("WaterPump") { Description = "derived", ParentTemplateId = pump.Id });
|
|
await ctx.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task<Guid> LoadAsync(byte[] bundleBytes)
|
|
{
|
|
await using var scope = _provider.CreateAsyncScope();
|
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
using var ms = new MemoryStream(bundleBytes, writable: false);
|
|
var session = await importer.LoadAsync(ms, passphrase: null);
|
|
return session.SessionId;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Import_DerivedTemplate_WiresParentTemplateId()
|
|
{
|
|
await SeedBaseAndDerivedAsync();
|
|
var bundle = await ExportAllTemplatesAsync();
|
|
await WipeTemplatesAsync();
|
|
|
|
var sessionId = await LoadAsync(bundle);
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
var preview = await importer.PreviewAsync(sessionId);
|
|
var resolutions = preview.Items
|
|
.Select(i => new ImportResolution(i.EntityType, i.Name, ResolutionAction.Add, null))
|
|
.ToList();
|
|
await importer.ApplyAsync(sessionId, resolutions, user: "bob");
|
|
}
|
|
|
|
await using (var assert = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var basePump = await ctx.Templates.SingleAsync(t => t.Name == "Pump");
|
|
var derived = await ctx.Templates.SingleAsync(t => t.Name == "WaterPump");
|
|
Assert.Equal(basePump.Id, derived.ParentTemplateId); // FAILS today: null
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Import_Overwrite_UpdatesParentEdge_AndRenamedBaseIsHonoured()
|
|
{
|
|
// Bundle base renamed to "Pump2" via ResolutionAction.Rename; derived's
|
|
// BaseTemplateName ("Pump") must resolve through the rename map to Pump2.
|
|
await SeedBaseAndDerivedAsync();
|
|
var bundle = await ExportAllTemplatesAsync();
|
|
await WipeTemplatesAsync();
|
|
|
|
var sessionId = await LoadAsync(bundle);
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
var resolutions = new List<ImportResolution>
|
|
{
|
|
new("Template", "Pump", ResolutionAction.Rename, "Pump2"),
|
|
new("Template", "WaterPump", ResolutionAction.Add, null),
|
|
};
|
|
await importer.ApplyAsync(sessionId, resolutions, user: "bob");
|
|
}
|
|
|
|
await using (var assert = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var pump2 = await ctx.Templates.SingleAsync(t => t.Name == "Pump2");
|
|
var derived = await ctx.Templates.SingleAsync(t => t.Name == "WaterPump");
|
|
Assert.Equal(pump2.Id, derived.ParentTemplateId);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Import_DerivedWithSkippedBase_LeavesNullAndAuditsUnresolved()
|
|
{
|
|
// Base "Pump" is Skip-resolved and absent from the wiped target, so the
|
|
// derived's BaseTemplateName cannot resolve — the edge stays null and a
|
|
// BundleImportBaseTemplateUnresolved audit row is emitted (never throws).
|
|
await SeedBaseAndDerivedAsync();
|
|
var bundle = await ExportAllTemplatesAsync();
|
|
await WipeTemplatesAsync();
|
|
|
|
var sessionId = await LoadAsync(bundle);
|
|
ImportResult result;
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
var resolutions = new List<ImportResolution>
|
|
{
|
|
new("Template", "Pump", ResolutionAction.Skip, null),
|
|
new("Template", "WaterPump", ResolutionAction.Add, null),
|
|
};
|
|
result = await importer.ApplyAsync(sessionId, resolutions, user: "bob");
|
|
}
|
|
|
|
await using (var assert = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = assert.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var derived = await ctx.Templates.SingleAsync(t => t.Name == "WaterPump");
|
|
Assert.Null(derived.ParentTemplateId);
|
|
Assert.False(await ctx.Templates.AnyAsync(t => t.Name == "Pump"));
|
|
|
|
var row = Assert.Single(await ctx.AuditLogEntries
|
|
.Where(e => e.Action == "BundleImportBaseTemplateUnresolved")
|
|
.ToListAsync());
|
|
Assert.Equal(result.BundleImportId, row.BundleImportId);
|
|
Assert.Equal("Template", row.EntityType);
|
|
Assert.Equal("WaterPump", row.EntityName);
|
|
}
|
|
}
|
|
}
|