using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.ConfigurationDatabase; namespace ScadaLink.ConfigurationDatabase.Tests; /// /// Regression guard for ConfigurationDatabase-013: a /// 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 instead. /// public class EphemeralEncryptionFallbackTests { private static DbContextOptions SqliteOptions() => new DbContextOptionsBuilder() .UseSqlite("DataSource=:memory:") .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)) .Options; [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 ScadaLinkDbContext(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( () => context.SaveChangesAsync()); Assert.Contains("Data Protection", ex.Message); } [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 ScadaLinkDbContext(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); } [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 ScadaLinkDbContext(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); } }