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

186 lines
7.9 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.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests;
/// <summary>
/// Regression guard: secret-bearing columns
/// (SMTP credentials, external-system auth config, database connection strings)
/// must be encrypted at rest, not persisted verbatim.
/// </summary>
public class SecretEncryptionTests : IDisposable
{
private readonly ScadaBridgeDbContext _context;
private readonly IDataProtectionProvider _protectionProvider;
/// <summary>Initializes a new instance of <c>SecretEncryptionTests</c>.</summary>
public SecretEncryptionTests()
{
_protectionProvider = new EphemeralDataProtectionProvider();
var options = new DbContextOptionsBuilder<ScadaBridgeDbContext>()
.UseSqlite("DataSource=:memory:")
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
.Options;
_context = new ScadaBridgeDbContext(options, _protectionProvider);
_context.Database.OpenConnection();
_context.Database.EnsureCreated();
}
/// <summary>Releases the resources held by this test class.</summary>
public void Dispose()
{
_context.Database.CloseConnection();
_context.Dispose();
}
/// <summary>Verifies that database connection definition connection string stored encrypted round trips.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task DatabaseConnectionDefinition_ConnectionString_StoredEncrypted_RoundTrips()
{
const string secret = "Server=db;Database=X;User Id=svc;Password=SuperSecret123!";
var def = new DatabaseConnectionDefinition("PrimaryDb", secret);
_context.DatabaseConnectionDefinitions.Add(def);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
// Raw column value must not be the plaintext secret.
var raw = await ReadRawColumnAsync("DatabaseConnectionDefinitions", "ConnectionString", def.Id);
Assert.NotNull(raw);
Assert.NotEqual(secret, raw);
Assert.DoesNotContain("SuperSecret123!", raw);
// Reading back through EF must transparently decrypt.
var loaded = await _context.DatabaseConnectionDefinitions.SingleAsync(d => d.Id == def.Id);
Assert.Equal(secret, loaded.ConnectionString);
}
/// <summary>Verifies that smtp configuration credentials stored encrypted round trips.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SmtpConfiguration_Credentials_StoredEncrypted_RoundTrips()
{
const string secret = "client_secret=oauth2-abc-very-secret";
var smtp = new SmtpConfiguration("smtp.example.com", "OAuth2", "noreply@example.com")
{
Credentials = secret
};
_context.SmtpConfigurations.Add(smtp);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var raw = await ReadRawColumnAsync("SmtpConfigurations", "Credentials", smtp.Id);
Assert.NotNull(raw);
Assert.NotEqual(secret, raw);
Assert.DoesNotContain("oauth2-abc-very-secret", raw);
var loaded = await _context.SmtpConfigurations.SingleAsync(s => s.Id == smtp.Id);
Assert.Equal(secret, loaded.Credentials);
}
/// <summary>Verifies that external system definition auth configuration stored encrypted round trips.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task ExternalSystemDefinition_AuthConfiguration_StoredEncrypted_RoundTrips()
{
const string secret = "{\"apiKey\":\"live-key-do-not-leak\"}";
var ext = new ExternalSystemDefinition("Erp", "https://erp.example.com", "ApiKey")
{
AuthConfiguration = secret
};
_context.ExternalSystemDefinitions.Add(ext);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var raw = await ReadRawColumnAsync("ExternalSystemDefinitions", "AuthConfiguration", ext.Id);
Assert.NotNull(raw);
Assert.NotEqual(secret, raw);
Assert.DoesNotContain("live-key-do-not-leak", raw);
var loaded = await _context.ExternalSystemDefinitions.SingleAsync(e => e.Id == ext.Id);
Assert.Equal(secret, loaded.AuthConfiguration);
}
/// <summary>Verifies that smtp configuration null credentials round trips as null.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SmtpConfiguration_NullCredentials_RoundTripsAsNull()
{
var smtp = new SmtpConfiguration("smtp.example.com", "None", "noreply@example.com")
{
Credentials = null
};
_context.SmtpConfigurations.Add(smtp);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var loaded = await _context.SmtpConfigurations.SingleAsync(s => s.Id == smtp.Id);
Assert.Null(loaded.Credentials);
}
/// <summary>Verifies that sms configuration auth token stored encrypted round trips.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SmsConfiguration_AuthToken_StoredEncrypted_RoundTrips()
{
// ConfigurationDatabase-NNN regression: the Twilio Auth Token is a secret and
// must be encrypted at rest exactly like SmtpConfiguration.Credentials. Guards
// against a future refactor dropping SmsConfiguration from ApplySecretColumnEncryption.
const string secret = "twilio-auth-token-do-not-leak";
var sms = new SmsConfiguration("AC0123456789", "+15550000000")
{
AuthToken = secret
};
_context.SmsConfigurations.Add(sms);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var raw = await ReadRawColumnAsync("SmsConfigurations", "AuthToken", sms.Id);
Assert.NotNull(raw);
Assert.NotEqual(secret, raw);
Assert.DoesNotContain("twilio-auth-token-do-not-leak", raw);
var loaded = await _context.SmsConfigurations.SingleAsync(s => s.Id == sms.Id);
Assert.Equal(secret, loaded.AuthToken);
// AccountSid is NOT a secret (it also rides in the Twilio URL path) — stored verbatim.
var rawSid = await ReadRawColumnAsync("SmsConfigurations", "AccountSid", sms.Id);
Assert.Equal("AC0123456789", rawSid);
}
/// <summary>Verifies that sms configuration null auth token round trips as null.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task SmsConfiguration_NullAuthToken_RoundTripsAsNull()
{
var sms = new SmsConfiguration("AC0123456789", "+15550000000")
{
AuthToken = null
};
_context.SmsConfigurations.Add(sms);
await _context.SaveChangesAsync();
_context.ChangeTracker.Clear();
var loaded = await _context.SmsConfigurations.SingleAsync(s => s.Id == sms.Id);
Assert.Null(loaded.AuthToken);
}
private async Task<string?> ReadRawColumnAsync(string table, string column, int id)
{
var connection = _context.Database.GetDbConnection();
await using var cmd = connection.CreateCommand();
cmd.CommandText = $"SELECT \"{column}\" FROM \"{table}\" WHERE \"Id\" = $id";
var p = cmd.CreateParameter();
p.ParameterName = "$id";
p.Value = id;
cmd.Parameters.Add(p);
var result = await cmd.ExecuteScalarAsync();
return result == null || result == DBNull.Value ? null : (string)result;
}
}