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

92 lines
4.3 KiB
C#

using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
/// <summary>
/// Regression guard: a <see cref="ScadaBridgeDbContext"/>
/// constructed without an explicit Data Protection provider (the single-argument
/// constructor) must NOT silently encrypt secret columns with a throwaway ephemeral
/// key — that would persist ciphertext that becomes permanently undecryptable on the
/// next process restart, with no error. Writing a secret column on such a context
/// must fail fast with a clear <see cref="InvalidOperationException"/> instead.
/// </summary>
public class EphemeralEncryptionFallbackTests
{
private static DbContextOptions<ScadaBridgeDbContext> SqliteOptions() =>
new DbContextOptionsBuilder<ScadaBridgeDbContext>()
.UseSqlite("DataSource=:memory:")
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
.Options;
/// <summary>Verifies that writing a secret column through the single-argument constructor fails fast and does not persist throwaway ciphertext.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SingleArgConstructor_WritingSecretColumn_FailsFast_DoesNotPersistThrowawayCiphertext()
{
// Single-argument constructor: no Data Protection provider supplied (the
// design-time / schema-only path). Schema creation must still succeed.
using var context = new ScadaBridgeDbContext(SqliteOptions());
context.Database.OpenConnection();
context.Database.EnsureCreated();
// AuthConfiguration is an encrypted secret column. Persisting it without a real
// key ring would produce undecryptable ciphertext; it must throw instead.
var ext = new ExternalSystemDefinition("Erp", "https://erp.example.com", "ApiKey")
{
AuthConfiguration = "{\"apiKey\":\"live-secret\"}"
};
context.ExternalSystemDefinitions.Add(ext);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => context.SaveChangesAsync());
Assert.Contains("Data Protection", ex.Message);
}
/// <summary>Verifies that writing a non-secret column through the single-argument constructor succeeds.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SingleArgConstructor_WritingNonSecretColumn_Succeeds()
{
// The schema-only / no-provider context must remain fully usable for entities
// that have no encrypted secret columns — only secret writes are gated.
using var context = new ScadaBridgeDbContext(SqliteOptions());
context.Database.OpenConnection();
context.Database.EnsureCreated();
var ext = new ExternalSystemDefinition("Erp", "https://erp.example.com", "None");
context.ExternalSystemDefinitions.Add(ext);
await context.SaveChangesAsync();
Assert.True(ext.Id > 0);
}
/// <summary>Verifies that writing a secret column through the provider constructor still succeeds.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ProviderConstructor_WritingSecretColumn_StillSucceeds()
{
// Sanity check: the gating must not regress the supported runtime path where a
// real Data Protection provider is supplied.
using var context = new ScadaBridgeDbContext(SqliteOptions(), new EphemeralDataProtectionProvider());
context.Database.OpenConnection();
context.Database.EnsureCreated();
var ext = new ExternalSystemDefinition("Erp", "https://erp.example.com", "ApiKey")
{
AuthConfiguration = "{\"apiKey\":\"live-secret\"}"
};
context.ExternalSystemDefinitions.Add(ext);
await context.SaveChangesAsync();
context.ChangeTracker.Clear();
var loaded = await context.ExternalSystemDefinitions.SingleAsync(e => e.Id == ext.Id);
Assert.Equal("{\"apiKey\":\"live-secret\"}", loaded.AuthConfiguration);
}
}