Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/ServiceCollectionExtensionsTests.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

161 lines
7.6 KiB
C#

using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
public class ServiceCollectionExtensionsTests
{
/// <summary>Verifies that adding the configuration database with a connection string registers the repositories and services.</summary>
[Fact]
public void AddConfigurationDatabase_WithConnectionString_RegistersRepositoriesAndServices()
{
var services = new ServiceCollection();
services.AddConfigurationDatabase("Server=unused;Database=unused;");
Assert.Contains(services, d => d.ServiceType == typeof(ITemplateEngineRepository));
Assert.Contains(services, d => d.ServiceType == typeof(IAuditService));
Assert.Contains(services, d => d.ServiceType == typeof(IInstanceLocator));
Assert.Contains(services, d => d.ServiceType == typeof(IAuditLogRepository));
}
/// <summary>Verifies that the configuration database is configured with a retrying execution strategy.</summary>
[Fact]
public void AddConfigurationDatabase_Configures_RetryingExecutionStrategy()
{
// The central DbContext must survive transient
// SQL faults. EnableRetryOnFailure installs a non-null ExecutionStrategyFactory
// on the SqlServer options extension — covering every read-side path (KPI asks,
// outbox queries, Tracking.Status, execution trees) that previously surfaced
// transient faults raw.
var services = new ServiceCollection();
services.AddConfigurationDatabase("Server=unused;Database=unused;");
using var sp = services.BuildServiceProvider();
var options = sp.GetRequiredService<DbContextOptions<ScadaBridgeDbContext>>();
// EF1001: SqlServerOptionsExtension is an internal EF surface, but it is the
// only place the retrying-strategy factory is observable — asserting it is the
// point of this test. Scoped suppression, not a project-wide one.
#pragma warning disable EF1001
var sqlExt = options.Extensions
.OfType<Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal.SqlServerOptionsExtension>()
.Single();
Assert.NotNull(sqlExt.ExecutionStrategyFactory);
#pragma warning restore EF1001
}
/// <summary>
/// Deferred-work register row 60: <c>ArtifactDeploymentService</c> builds its per-site
/// deploy commands serially ("DbContext is not thread-safe") and then fans the SEND out
/// with <c>Task.WhenAll</c>, with EF-materialised <c>Site</c> entities captured by the
/// parallel lambda. That is safe for exactly one reason: with lazy loading off, reading
/// a property on a materialised entity is a plain field read that never re-enters the
/// DbContext. Turn lazy-loading proxies on and every one of those reads becomes a
/// database round-trip on the shared context from N threads at once — the row's named
/// trigger, and a failure mode that surfaces as sporadic "A second operation was
/// started on this context instance" rather than a clean break.
/// <para>
/// Pinned here rather than at the call site because the hazard is a DI/configuration
/// change made somewhere else entirely, by someone who will never read
/// <c>ArtifactDeploymentService</c>.
/// </para>
/// </summary>
[Fact]
public void AddConfigurationDatabase_DoesNotEnableLazyLoading()
{
var services = new ServiceCollection();
services.AddConfigurationDatabase("Server=unused;Database=unused;");
using var sp = services.BuildServiceProvider();
var options = sp.GetRequiredService<DbContextOptions<ScadaBridgeDbContext>>();
Assert.DoesNotContain(
options.Extensions,
e => e.GetType().Name.Contains("Proxies", StringComparison.Ordinal));
// Belt and braces, and the structurally stronger half: UseLazyLoadingProxies() is
// not even a compilable call without the Proxies package, so asserting the package
// is absent from the whole repo closes the hazard at the source rather than at one
// registration. (Mirrors Csproj_ShouldNotHaveForbiddenPackageReferences in
// Commons.Tests/ArchitecturalConstraintTests.)
var repoRoot = GetRepositoryRoot();
var projectFiles = Directory
.GetFiles(repoRoot, "*.csproj", SearchOption.AllDirectories)
.Concat(Directory.GetFiles(repoRoot, "Directory.Packages.props", SearchOption.AllDirectories))
.Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
StringComparison.Ordinal)
&& !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}",
StringComparison.Ordinal))
.ToList();
Assert.NotEmpty(projectFiles);
foreach (var file in projectFiles)
{
Assert.DoesNotContain(
"EntityFrameworkCore.Proxies",
File.ReadAllText(file),
StringComparison.Ordinal);
}
}
/// <summary>Walks up from the test output directory to the repository root.</summary>
private static string GetRepositoryRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
if (File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.ScadaBridge.slnx")))
{
return dir.FullName;
}
dir = dir.Parent;
}
throw new InvalidOperationException("Could not locate the repository root (ZB.MOM.WW.ScadaBridge.slnx).");
}
// The no-arg overload is [Obsolete(error: true)], so it cannot be referenced directly
// from source — that is the compile-time guard. Invoke it via reflection to verify the
// runtime defence-in-depth behaviour.
private static MethodInfo NoArgOverload =>
typeof(ServiceCollectionExtensions).GetMethod(
nameof(ServiceCollectionExtensions.AddConfigurationDatabase),
BindingFlags.Public | BindingFlags.Static,
binder: null,
types: new[] { typeof(IServiceCollection) },
modifiers: null)!;
/// <summary>Verifies that the parameterless overload fails fast with a clear message.</summary>
[Fact]
public void AddConfigurationDatabase_NoArgOverload_FailsFastWithClearMessage()
{
// Regression guard for the original no-op bug: the parameterless overload must not
// silently register nothing. Misuse must surface immediately at wire-up time with an
// actionable message — not later as an opaque DI resolution failure.
var services = new ServiceCollection();
var invocation = Assert.Throws<TargetInvocationException>(
() => NoArgOverload.Invoke(null, new object[] { services }));
var ex = Assert.IsType<InvalidOperationException>(invocation.InnerException);
Assert.Contains("connection string", ex.Message, StringComparison.OrdinalIgnoreCase);
}
/// <summary>Verifies that the parameterless overload is marked obsolete as a compile-time error.</summary>
[Fact]
public void AddConfigurationDatabase_NoArgOverload_IsMarkedObsoleteAsError()
{
// The no-op overload must be flagged so misuse is caught at compile time.
var obsolete = NoArgOverload.GetCustomAttribute<ObsoleteAttribute>();
Assert.NotNull(obsolete);
Assert.True(obsolete!.IsError);
}
}