using System; using System.IO; using System.Runtime.InteropServices; using PolyTrader.Core.Security; using Xunit; namespace PolyTrader.Tests { /// /// Sicherheitsnetz für D-5/D-10 (Master-Key-Erzeugung bei der Erstinstallation). Der wichtigste /// Fall ist der negative: ein vorhandener Schlüssel darf NIEMALS überschrieben werden - das /// würde jedes bereits verschlüsselte Secret in der Datenbank unwiederbringlich unlesbar machen. /// public class MasterKeyResolverTests : IDisposable { private readonly string _tempDir = Path.Combine(Path.GetTempPath(), "pt-masterkey-tests-" + Guid.NewGuid().ToString("N")); public MasterKeyResolverTests() => Directory.CreateDirectory(_tempDir); public void Dispose() { try { Directory.Delete(_tempDir, recursive: true); } catch { /* Best effort */ } } private string KeyFile => Path.Combine(_tempDir, "master.key"); [Fact] public void Resolve_prefers_environment_variable_and_never_touches_the_file() { string? result = MasterKeyResolver.Resolve(KeyFile, () => "env-key-value"); Assert.Equal("env-key-value", result); Assert.False(File.Exists(KeyFile)); } [Fact] public void Resolve_reads_existing_file_without_modifying_it() { File.WriteAllText(KeyFile, "existing-key-value"); var originalWriteTime = File.GetLastWriteTimeUtc(KeyFile); string? result = MasterKeyResolver.Resolve(KeyFile, () => null); Assert.Equal("existing-key-value", result); Assert.Equal(originalWriteTime, File.GetLastWriteTimeUtc(KeyFile)); } [Fact] public void Resolve_generates_a_new_key_when_neither_variable_nor_file_exist() { bool generatedCallbackFired = false; string? result = MasterKeyResolver.Resolve(KeyFile, () => null, onGenerated: _ => generatedCallbackFired = true); Assert.False(string.IsNullOrWhiteSpace(result)); Assert.True(File.Exists(KeyFile)); Assert.True(generatedCallbackFired); Assert.Equal(result, File.ReadAllText(KeyFile).Trim()); } [Fact] public void Resolve_generated_key_decodes_as_a_256_bit_key() { string? result = MasterKeyResolver.Resolve(KeyFile, () => null); byte[] bytes = Convert.FromBase64String(result!); Assert.Equal(32, bytes.Length); } [Fact] public void Resolve_never_regenerates_once_a_file_exists() { string? first = MasterKeyResolver.Resolve(KeyFile, () => null); string? second = MasterKeyResolver.Resolve(KeyFile, () => null); string? third = MasterKeyResolver.Resolve(KeyFile, () => null); Assert.Equal(first, second); Assert.Equal(first, third); } [Fact] public void Resolve_does_not_generate_when_environment_variable_is_set_even_without_a_file() { _ = MasterKeyResolver.Resolve(KeyFile, () => "from-env"); Assert.False(File.Exists(KeyFile)); } [Fact] public void Resolve_restricts_generated_file_to_owner_on_unix() { // Kein separates Skip-Paket eingebunden - unter Windows greift ohnehin die // NTFS-Vererbung des Profils (siehe FilePermissions), dort gibt es nichts zu prüfen. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; MasterKeyResolver.Resolve(KeyFile, () => null); var mode = File.GetUnixFileMode(KeyFile); Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, mode); } [Fact] public void Resolve_returns_null_and_warns_instead_of_throwing_when_generation_fails() { // Ein Pfad in einem nicht existierenden Verzeichnis lässt File.WriteAllText scheitern. string badPath = Path.Combine(_tempDir, "does-not-exist", "master.key"); string? warning = null; string? result = MasterKeyResolver.Resolve(badPath, () => null, onWarning: msg => warning = msg); Assert.Null(result); Assert.NotNull(warning); } } }