Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests/ValidationFailureTests.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

183 lines
8.8 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
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.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.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests;
/// <summary>
/// T26 — integration validation-failure rollback test. Complements the unit
/// rollback covered by <c>BundleImporterApplyTests</c> by driving the full
/// export → load → apply path, so the failing validation runs against a
/// real export-side serialised bundle.
/// </summary>
public sealed class ValidationFailureTests : IDisposable
{
private readonly ServiceProvider _provider;
/// <summary>Initializes a new instance of the <c>ValidationFailureTests</c> class.</summary>
public ValidationFailureTests()
{
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(
new ConfigurationBuilder().AddInMemoryCollection().Build());
var dbName = $"ValidationFailureTests_{Guid.NewGuid()}";
services.AddDbContext<ScadaBridgeDbContext>(opts => opts
.UseInMemoryDatabase(dbName)
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
services.AddSingleton<ITemplateGraphWatermark, TemplateGraphWatermark>();
services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
services.AddScoped<INotificationRepository, NotificationRepository>();
services.AddScoped<IInboundApiRepository, InboundApiRepository>();
// DependencyResolver now injects ISiteRepository to walk the
// site/data-connection/instance closure; register it or activation fails.
services.AddScoped<ISiteRepository, SiteRepository>();
services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
services.AddScoped<IAuditService, AuditService>();
services.AddTransport();
_provider = services.BuildServiceProvider();
}
/// <summary>Releases the resources held by this test class.</summary>
public void Dispose() => _provider.Dispose();
/// <summary>Verifies that a semantic validation failure rolls back all writes.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Semantic_validation_failure_rolls_back_all_writes()
{
// Arrange:
// - Seed an ApiMethod whose script body references MissingHelper(),
// a SharedScript that is NOT in the bundle (we don't seed one) and
// also NOT in the target after the wipe. ApiMethod name-resolution
// findings are HARD errors (no downstream deploy gate re-validates
// them — unlike template-script findings, which are advisory), so this is the vehicle that still forces a rollback.
// - Export the ApiMethod only (no helper alongside).
// - Wipe the target so the apply runs through Add-paths.
// - Snapshot row counts so the rollback assertion is unambiguous.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
ctx.ApiMethods.Add(new ApiMethod("BrokenIngest", "var x = MissingHelper();"));
await ctx.SaveChangesAsync();
}
// Export only the broken ApiMethod. IncludeDependencies=false guarantees
// MissingHelper is NOT pulled in even if one happened to exist.
byte[] bundleBytes;
await using (var scope = _provider.CreateAsyncScope())
{
var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var ids = await ctx.ApiMethods.Select(m => m.Id).ToListAsync();
var selection = new ExportSelection(
TemplateIds: Array.Empty<int>(),
SharedScriptIds: Array.Empty<int>(),
ExternalSystemIds: Array.Empty<int>(),
DatabaseConnectionIds: Array.Empty<int>(),
NotificationListIds: Array.Empty<int>(),
SmtpConfigurationIds: Array.Empty<int>(),
ApiMethodIds: ids,
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);
bundleBytes = ms.ToArray();
}
// Wipe target so the apply attempts an Add (the validation failure must
// not let it land).
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
ctx.ApiMethods.RemoveRange(ctx.ApiMethods);
ctx.SharedScripts.RemoveRange(ctx.SharedScripts);
await ctx.SaveChangesAsync();
}
int apiMethodsBefore;
int sharedScriptsBefore;
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
apiMethodsBefore = await ctx.ApiMethods.CountAsync();
sharedScriptsBefore = await ctx.SharedScripts.CountAsync();
}
Assert.Equal(0, apiMethodsBefore);
Assert.Equal(0, sharedScriptsBefore);
// Load the bundle into a session.
Guid sessionId;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
using var input = new MemoryStream(bundleBytes, writable: false);
var session = await importer.LoadAsync(input, passphrase: null);
sessionId = session.SessionId;
}
// Act — apply with Add. Validation MUST throw.
SemanticValidationException ex = default!;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
ex = await Assert.ThrowsAsync<SemanticValidationException>(() =>
importer.ApplyAsync(sessionId,
new List<ImportResolution>
{
new("ApiMethod", "BrokenIngest", ResolutionAction.Add, null),
},
user: "bob"));
}
// Assert
// 1. Errors list calls out the missing dependency by name.
Assert.NotEmpty(ex.Errors);
Assert.Contains(ex.Errors,
err => err.Contains("MissingHelper", StringComparison.Ordinal));
// 2. No new ApiMethod or SharedScript row was committed.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
Assert.Equal(apiMethodsBefore, await ctx.ApiMethods.CountAsync());
Assert.Equal(sharedScriptsBefore, await ctx.SharedScripts.CountAsync());
// 3. A BundleImportFailed audit row exists. The BundleImporter
// contract (T17) is: failure row's BundleImportId is NULL (the
// rolled-back id is intentionally disowned) and its
// AfterStateJson.Reason calls out the validation failure.
var failure = await ctx.AuditLogEntries
.SingleAsync(a => a.Action == "BundleImportFailed");
Assert.Equal("Bundle", failure.EntityType);
Assert.Null(failure.BundleImportId);
Assert.NotNull(failure.AfterStateJson);
// The exception message lands in the Reason field of the payload
// (BundleImporter.ApplyAsync wires it through). Spot-check for
// "validation" so the row's correlation to the failure is visible.
Assert.Contains("validation", failure.AfterStateJson!,
StringComparison.OrdinalIgnoreCase);
}
}
}