using EonaCat.LogStack.Core; using EonaCat.LogStack.Flows; using Xunit; namespace EonaCat.LogStack.FileDeletionTests; public sealed class FileDeletionTests : IDisposable { private readonly string _directory = Path.Combine(Path.GetTempPath(), "EonaCat.FileDeletionTests", Guid.NewGuid().ToString("N")); public FileDeletionTests() { Directory.CreateDirectory(_directory); } [Fact] public async Task EncryptedLogCanBeDeletedAndRecreatedWithAValidNewHeader() { await using var flow = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites(); await WriteEncryptedAsync(flow, "before-deletion"); string path = Assert.Single(Directory.GetFiles(_directory, "*.eona")); byte[] originalHeader = ReadHeader(path); Assert.Throws(() => File.Open(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete)); File.Delete(path); Assert.False(File.Exists(path)); await WriteEncryptedAsync(flow, "after-deletion"); Assert.True(File.Exists(path)); Assert.False(originalHeader.SequenceEqual(ReadHeader(path))); string outputPath = Path.Combine(_directory, "decrypted.txt"); Assert.True(EncryptedFileFlow.DecryptToFile(path, outputPath, "test-password")); string contents = File.ReadAllText(outputPath); Assert.Contains("after-deletion", contents); Assert.DoesNotContain("before-deletion", contents); Assert.Equal(0, flow.GetTotalErrors()); } [Fact] public async Task EncryptedLogCanResumeAnExistingFile() { byte[] originalHeader; string path; await using (var first = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites()) { await WriteEncryptedAsync(first, "first-session"); path = Assert.Single(Directory.GetFiles(_directory, "*.eona")); originalHeader = ReadHeader(path); } await using var second = new EncryptedFileFlow(_directory, "test-password", template: "{message}").WithDurableWrites(); await WriteEncryptedAsync(second, "second-session"); Assert.Equal(originalHeader, ReadHeader(path)); string outputPath = Path.Combine(_directory, "decrypted.txt"); Assert.True(EncryptedFileFlow.DecryptToFile(path, outputPath, "test-password")); string contents = File.ReadAllText(outputPath); Assert.Contains("first-session", contents); Assert.Contains("second-session", contents); } [Fact] public async Task RotatingLogCanBeDeletedAndRecreated() { await using var flow = new RotatingFileFlow(_directory); Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("before-deletion"))); string path = Assert.Single(Directory.GetFiles(_directory)); File.Delete(path); Assert.False(File.Exists(path)); Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("after-deletion"))); string contents = ReadSharedText(path); Assert.Contains("after-deletion", contents); Assert.DoesNotContain("before-deletion", contents); } [Fact] public async Task AuditLogCanBeDeletedAndRecreatedWithAValidHashChain() { var flow = new AuditFlow(_directory); try { await flow.BlastAsync(CreateEvent("before-deletion")); Assert.Throws(() => File.Open(flow.FilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete)); File.Delete(flow.FilePath); Assert.False(File.Exists(flow.FilePath)); await flow.BlastAsync(CreateEvent("after-deletion")); await flow.BlastAsync(CreateEvent("next-entry")); var liveVerification = AuditFlow.Verify(flow.FilePath); Assert.True(liveVerification.ok, liveVerification.reason); } finally { await flow.DisposeAsync(); } var verification = AuditFlow.Verify(flow.FilePath); Assert.True(verification.ok, verification.reason); string contents = File.ReadAllText(flow.FilePath); Assert.Contains("after-deletion", contents); Assert.Contains("next-entry", contents); Assert.DoesNotContain("before-deletion", contents); } [Fact] public async Task PlainLogCanBeDeletedWhileOpen() { await using var flow = new FileFlow(_directory); Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent("before-deletion"))); Assert.True(SpinWait.SpinUntil(() => flow.GetStats().BytesWritten > 0 || flow.GetTotalErrors() > 0, TimeSpan.FromSeconds(10)), "Plain writer did not complete."); Assert.Equal(0, flow.GetTotalErrors()); await flow.FlushAsync(); string path = Assert.Single(Directory.GetFiles(_directory, "*.log")); File.Delete(path); Assert.False(File.Exists(path)); } private static LogEvent CreateEvent(string message) => new LogEventBuilder().WithMessage(message).Build(); private static async Task WriteEncryptedAsync(EncryptedFileFlow flow, string message) { long previousBytes = flow.GetStats().BytesWritten; Assert.Equal(WriteResult.Success, await flow.BlastAsync(CreateEvent(message))); Assert.True(SpinWait.SpinUntil(() => flow.GetStats().BytesWritten > previousBytes || flow.GetTotalErrors() > 0, TimeSpan.FromSeconds(10)), "Encrypted writer did not complete."); Assert.Equal(0, flow.GetTotalErrors()); await flow.FlushAsync(); } private static byte[] ReadHeader(string path) { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); byte[] header = new byte[52]; stream.ReadExactly(header); return header; } private static string ReadSharedText(string path) { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using var reader = new StreamReader(stream); return reader.ReadToEnd(); } public void Dispose() { Directory.Delete(_directory, recursive: true); } }