148 lines
6.2 KiB
C#
148 lines
6.2 KiB
C#
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<IOException>(() => 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<IOException>(() => 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);
|
|
}
|
|
} |