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

167 lines
8.0 KiB
C#

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
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.Commons.Types.Templates;
namespace ZB.MOM.WW.ScadaBridge.Transport.IntegrationTests.Import;
/// <summary>
/// Regression guard for the create-missing site/connection apply path on a REAL
/// relational provider (SQLite). Every other importer integration test runs on the
/// EF in-memory provider, which assigns surrogate ids eagerly on <c>AddAsync</c> —
/// masking that a create-missing <c>Site</c> still has <c>Id == 0</c> until the
/// context is flushed. <c>ApplyDataConnectionsAsync</c> stamps
/// <c>DataConnection.SiteId</c> as a raw scalar (no EF navigation, so no fix-up),
/// so without a flush between the site pass and the connection pass the insert
/// fails the FK on a relational provider (live failure 2026-08-01: SQL Server
/// error 547, <c>FK_DataConnections_Sites_SiteId</c>, on a cross-environment
/// bundle import into an empty target).
/// </summary>
public sealed class CreateMissingSiteRelationalTests : IDisposable
{
private readonly SqliteConnection _connection;
private readonly ServiceProvider _provider;
/// <summary>Initializes the test class with a temporary database.</summary>
public CreateMissingSiteRelationalTests()
{
// One open in-memory SQLite connection for the fixture's lifetime — the
// database lives exactly as long as the connection.
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(
new ConfigurationBuilder().AddInMemoryCollection().Build());
services.AddDbContext<ScadaBridgeDbContext>(opts => opts.UseSqlite(_connection));
// Secret-bearing columns require the encrypting two-arg ctor — same wiring
// as RoundTripTests / BundleImporterRollbackFailureTests.
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<IAuditCorrelationContext, AuditCorrelationContext>();
services.AddScoped<IAuditService, AuditService>();
services.AddTransport();
_provider = services.BuildServiceProvider();
using var scope = _provider.CreateScope();
scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>().Database.EnsureCreated();
}
/// <summary>Releases the resources held by the test fixture.</summary>
public void Dispose()
{
_provider.Dispose();
_connection.Dispose();
}
/// <summary>Verifies that importing with create-missing for a site and connection wires the materialized site id.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Import_create_missing_site_and_connection_wires_the_materialised_site_id()
{
// ---- Seed a source site + connection, export the site closure. ----
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var site = new Site("Plant 1", "plant-1")
{
Description = "source plant",
NodeAAddress = "akka://site@10.0.0.1:2552",
NodeBAddress = "akka://site@10.0.0.2:2552",
GrpcNodeAAddress = "10.0.0.1:8083",
GrpcNodeBAddress = "10.0.0.2:8083",
};
ctx.Sites.Add(site);
await ctx.SaveChangesAsync();
ctx.DataConnections.Add(new DataConnection("OpcUaPrimary", "OpcUa", site.Id)
{
PrimaryConfiguration = "{\"endpoint\":\"opc.tcp://primary\"}",
FailoverRetryCount = 5,
});
await ctx.SaveChangesAsync();
}
Guid sessionId;
await using (var scope = _provider.CreateAsyncScope())
{
var exporter = scope.ServiceProvider.GetRequiredService<IBundleExporter>();
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var siteIds = await ctx.Sites.Select(s => s.Id).ToListAsync();
var bundleStream = await exporter.ExportAsync(
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: Array.Empty<int>(),
IncludeDependencies: true,
SiteIds: siteIds),
user: "alice", sourceEnvironment: "dev",
passphrase: null, cancellationToken: CancellationToken.None);
using var ms = new MemoryStream();
await bundleStream.CopyToAsync(ms);
ms.Position = 0;
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
var session = await importer.LoadAsync(ms, passphrase: null);
sessionId = session.SessionId;
}
// ---- Wipe the target so the apply exercises CreateNew on both. ----
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
ctx.DataConnections.RemoveRange(ctx.DataConnections);
ctx.Sites.RemoveRange(ctx.Sites);
await ctx.SaveChangesAsync();
}
// ---- Apply with create-missing mappings (the CLI's --create-missing-*). ----
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
var nameMap = new BundleNameMap(
Sites: [new SiteMapping("plant-1", MappingAction.CreateNew, null)],
Connections: [new ConnectionMapping("plant-1", "OpcUaPrimary", MappingAction.CreateNew, null)]);
await importer.ApplyAsync(
sessionId, Array.Empty<ImportResolution>(), user: "bob",
ct: CancellationToken.None, nameMap: nameMap);
}
// ---- The created connection must reference the created site's REAL id. ----
await using (var verify = _provider.CreateAsyncScope())
{
var ctx = verify.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var site = Assert.Single(await ctx.Sites.ToListAsync());
var conn = Assert.Single(await ctx.DataConnections.ToListAsync());
Assert.True(site.Id > 0, "created site id was never materialised");
Assert.Equal(site.Id, conn.SiteId);
}
}
}