5c52b4c80b
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
201 lines
9.0 KiB
C#
201 lines
9.0 KiB
C#
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.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.DeploymentManager;
|
|
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
|
using ZB.MOM.WW.ScadaBridge.Transport;
|
|
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
|
|
|
|
/// <summary>
|
|
/// #05-T3: a template script's <see cref="TemplateScript.MinTimeBetweenRuns"/> and
|
|
/// <see cref="TemplateScript.ExecutionTimeoutSeconds"/> must survive an export→import
|
|
/// round-trip. The exporter serializes both fields (EntitySerializer) and the DTO
|
|
/// carries them; before this fix the importer's <c>BuildTemplate</c> (Add) and
|
|
/// <c>SyncTemplateScriptsAsync</c> (Overwrite) rebuild dropped them, silently resetting
|
|
/// the persisted script to schema defaults. Same in-memory host pattern as
|
|
/// <see cref="BundleImporterApplyTests"/>.
|
|
/// </summary>
|
|
public sealed class TemplateScriptFidelityTests : IDisposable
|
|
{
|
|
private readonly ServiceProvider _provider;
|
|
|
|
public TemplateScriptFidelityTests()
|
|
{
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton<IConfiguration>(
|
|
new ConfigurationBuilder().AddInMemoryCollection().Build());
|
|
|
|
var dbName = $"TemplateScriptFidelityTests_{Guid.NewGuid()}";
|
|
services.AddDbContext<ScadaBridgeDbContext>(opts => opts
|
|
.UseInMemoryDatabase(dbName)
|
|
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
|
|
|
|
services.AddSingleton<IDataProtectionProvider>(new EphemeralDataProtectionProvider());
|
|
services.AddScoped(sp => new ScadaBridgeDbContext(
|
|
sp.GetRequiredService<DbContextOptions<ScadaBridgeDbContext>>(),
|
|
sp.GetRequiredService<IDataProtectionProvider>()));
|
|
|
|
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
|
|
services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
|
|
services.AddScoped<INotificationRepository, NotificationRepository>();
|
|
services.AddScoped<IInboundApiRepository, InboundApiRepository>();
|
|
services.AddScoped<ISiteRepository, SiteRepository>();
|
|
services.AddScoped<IDeploymentManagerRepository, DeploymentManagerRepository>();
|
|
services.AddScoped<ISharedSchemaRepository, SharedSchemaRepository>();
|
|
services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
|
|
services.AddScoped<IAuditService, AuditService>();
|
|
services.AddTransport();
|
|
services.AddTemplateEngine();
|
|
services.AddDeploymentManager();
|
|
|
|
_provider = services.BuildServiceProvider();
|
|
}
|
|
|
|
public void Dispose() => _provider.Dispose();
|
|
|
|
private async Task<Guid> ExportAndLoadAsync()
|
|
{
|
|
Stream bundleStream;
|
|
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);
|
|
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;
|
|
|
|
await using var loadScope = _provider.CreateAsyncScope();
|
|
var importer = loadScope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
var session = await importer.LoadAsync(ms, passphrase: null);
|
|
return session.SessionId;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Add_preserves_MinTimeBetweenRuns_and_ExecutionTimeoutSeconds()
|
|
{
|
|
// Arrange: seed a template whose script carries non-default cadence + timeout.
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var t = new Template("Pump") { Description = "fresh" };
|
|
t.Scripts.Add(new TemplateScript("cadence", "return 1;")
|
|
{
|
|
MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
|
|
ExecutionTimeoutSeconds = 42,
|
|
});
|
|
ctx.Templates.Add(t);
|
|
await ctx.SaveChangesAsync();
|
|
}
|
|
var sessionId = await ExportAndLoadAsync();
|
|
|
|
// Wipe so the import exercises the Add path.
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
ctx.Templates.RemoveRange(ctx.Templates);
|
|
await ctx.SaveChangesAsync();
|
|
}
|
|
|
|
// Act
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
await importer.ApplyAsync(sessionId,
|
|
new List<ImportResolution> { new("Template", "Pump", ResolutionAction.Add, null) },
|
|
user: "bob");
|
|
}
|
|
|
|
// Assert: both fields survived the rebuild.
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var script = await ctx.Templates
|
|
.Where(t => t.Name == "Pump")
|
|
.SelectMany(t => t.Scripts)
|
|
.SingleAsync(s => s.Name == "cadence");
|
|
Assert.Equal(TimeSpan.FromSeconds(30), script.MinTimeBetweenRuns);
|
|
Assert.Equal(42, script.ExecutionTimeoutSeconds);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overwrite_updates_ExecutionTimeoutSeconds_only_diff_and_emits_audit()
|
|
{
|
|
// Arrange: seed script with ExecutionTimeoutSeconds=42; export; then mutate the
|
|
// target script so it differs from the bundle ONLY in ExecutionTimeoutSeconds.
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var t = new Template("Pump") { Description = "same" };
|
|
t.Scripts.Add(new TemplateScript("cadence", "return 1;")
|
|
{
|
|
MinTimeBetweenRuns = TimeSpan.FromSeconds(30),
|
|
ExecutionTimeoutSeconds = 42,
|
|
});
|
|
ctx.Templates.Add(t);
|
|
await ctx.SaveChangesAsync();
|
|
}
|
|
var sessionId = await ExportAndLoadAsync();
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var script = await ctx.Templates
|
|
.Where(t => t.Name == "Pump")
|
|
.SelectMany(t => t.Scripts)
|
|
.SingleAsync(s => s.Name == "cadence");
|
|
script.ExecutionTimeoutSeconds = 10; // now the only field that differs from the bundle
|
|
await ctx.SaveChangesAsync();
|
|
}
|
|
|
|
// Act
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
|
await importer.ApplyAsync(sessionId,
|
|
new List<ImportResolution> { new("Template", "Pump", ResolutionAction.Overwrite, null) },
|
|
user: "bob");
|
|
}
|
|
|
|
// Assert: field restored to the bundle value AND a TemplateScriptUpdated audit row emitted.
|
|
await using (var scope = _provider.CreateAsyncScope())
|
|
{
|
|
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
|
var script = await ctx.Templates
|
|
.Where(t => t.Name == "Pump")
|
|
.SelectMany(t => t.Scripts)
|
|
.SingleAsync(s => s.Name == "cadence");
|
|
Assert.Equal(42, script.ExecutionTimeoutSeconds);
|
|
|
|
Assert.True(await ctx.AuditLogEntries.AnyAsync(a =>
|
|
a.Action == "TemplateScriptUpdated" && a.EntityName == "Pump.cadence"));
|
|
}
|
|
}
|
|
}
|