Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/Import/TemplateScriptFidelityTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

268 lines
12 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;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
/// <summary>
/// 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;
/// <summary>Initializes the test class with a fresh service provider and database.</summary>
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.AddSingleton<ITemplateGraphWatermark, TemplateGraphWatermark>();
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();
}
/// <summary>Disposes the service provider.</summary>
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;
}
/// <summary>Verifies that adding a template script preserves its minimum time between runs and execution timeout.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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);
}
}
/// <summary>Verifies that overwriting updates the execution timeout, shows it as the only diff, and emits an audit entry.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[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"));
}
}
/// <summary>Verifies that adding preserves the locked-in-derived flag on attributes, alarms and scripts.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Add_preserves_LockedInDerived_on_attribute_alarm_script()
{
// LockedInDerived must survive export→import on all three child kinds.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("Pump") { Description = "fresh" };
t.Attributes.Add(new TemplateAttribute("Pressure")
{
DataType = DataType.Double,
LockedInDerived = true,
});
t.Alarms.Add(new TemplateAlarm("High")
{
TriggerType = AlarmTriggerType.RangeViolation,
LockedInDerived = true,
});
t.Scripts.Add(new TemplateScript("cadence", "return 1;")
{
LockedInDerived = true,
});
ctx.Templates.Add(t);
await ctx.SaveChangesAsync();
}
var sessionId = await ExportAndLoadAsync();
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
ctx.Templates.RemoveRange(ctx.Templates);
await ctx.SaveChangesAsync();
}
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");
}
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = await ctx.Templates
.Where(t => t.Name == "Pump")
.Include(t => t.Attributes)
.Include(t => t.Alarms)
.Include(t => t.Scripts)
.SingleAsync();
Assert.True(t.Attributes.Single(a => a.Name == "Pressure").LockedInDerived);
Assert.True(t.Alarms.Single(a => a.Name == "High").LockedInDerived);
Assert.True(t.Scripts.Single(s => s.Name == "cadence").LockedInDerived);
}
}
}