using System.Text;
using w4c_workflows.Services.Nodes.Binary;
using Xunit;
namespace w4c_workflows.Tests;
///
/// Unit tests for the content-addressed filesystem binary store.
///
public class FileSystemBinaryStoreTests
{
private static async Task ReadAllAsync(Stream stream)
{
using var buffer = new MemoryStream();
await stream.CopyToAsync(buffer);
return buffer.ToArray();
}
[Fact]
public async Task Save_then_open_round_trips_bytes_and_metadata()
{
var store = BinaryTestData.Store();
var bytes = Encoding.UTF8.GetBytes("hello binary");
var attachment = await store.SaveAsync(
new MemoryStream(bytes), "greeting.txt", "text/plain");
Assert.StartsWith("sha256:", attachment.AssetId);
Assert.Equal("greeting.txt", attachment.FileName);
Assert.Equal("text/plain", attachment.MimeType);
Assert.Equal(bytes.Length, attachment.SizeBytes);
await using var opened = await store.OpenAsync(attachment.AssetId);
Assert.NotNull(opened);
Assert.Equal(bytes, await ReadAllAsync(opened!));
var described = await store.DescribeAsync(attachment.AssetId);
Assert.Equal(attachment, described);
}
[Fact]
public async Task Identical_content_is_content_addressed_to_one_asset()
{
var store = BinaryTestData.Store();
var bytes = Encoding.UTF8.GetBytes("same bytes");
var first = await store.SaveAsync(new MemoryStream(bytes), "a.bin", "application/octet-stream");
var second = await store.SaveAsync(new MemoryStream(bytes), "b.bin", "application/octet-stream");
Assert.Equal(first.AssetId, second.AssetId);
}
[Fact]
public async Task Different_content_yields_different_assets()
{
var store = BinaryTestData.Store();
var first = await store.SaveAsync(new MemoryStream([1, 2, 3]), "a.bin", null);
var second = await store.SaveAsync(new MemoryStream([4, 5, 6]), "b.bin", null);
Assert.NotEqual(first.AssetId, second.AssetId);
}
[Fact]
public async Task Open_can_be_called_more_than_once()
{
var store = BinaryTestData.Store();
var bytes = Encoding.UTF8.GetBytes("repeatable");
var attachment = await store.SaveAsync(new MemoryStream(bytes), null, null);
await using var first = await store.OpenAsync(attachment.AssetId);
await using var second = await store.OpenAsync(attachment.AssetId);
Assert.Equal(bytes, await ReadAllAsync(first!));
Assert.Equal(bytes, await ReadAllAsync(second!));
}
[Theory]
[InlineData("")]
[InlineData("not-a-sha")]
[InlineData("sha256:../../etc/passwd")]
[InlineData("sha256:zzzz")]
public async Task Malformed_ids_are_rejected(string assetId)
{
var store = BinaryTestData.Store();
Assert.Null(await store.OpenAsync(assetId));
Assert.Null(await store.DescribeAsync(assetId));
}
[Fact]
public async Task Valid_but_unknown_id_returns_null()
{
var store = BinaryTestData.Store();
var id = FileSystemBinaryStore.BuildAssetId(new string('a', 64));
Assert.Null(await store.OpenAsync(id));
Assert.Null(await store.DescribeAsync(id));
}
[Fact]
public async Task Save_rejects_payload_over_the_cap()
{
var store = BinaryTestData.Store(o => o.MaxBytes = 3);
await Assert.ThrowsAsync(() =>
store.SaveAsync(new MemoryStream(Encoding.UTF8.GetBytes("four")), "x", null));
}
[Fact]
public async Task Blank_name_and_mime_are_normalised_to_null()
{
var store = BinaryTestData.Store();
var attachment = await store.SaveAsync(new MemoryStream([9]), " ", " ");
Assert.Null(attachment.FileName);
Assert.Null(attachment.MimeType);
}
[Fact]
public void Constructor_sweeps_stale_temp_files_and_keeps_fresh_ones()
{
var root = Directory.CreateTempSubdirectory("w4c-binary-sweep-").FullName;
try
{
var stalePayload = Path.Combine(root, "tmp-stale");
var staleMeta = Path.Combine(root, "ab", "deadbeef.json.tmp-stale");
var freshPayload = Path.Combine(root, "tmp-fresh");
Directory.CreateDirectory(Path.Combine(root, "ab"));
File.WriteAllText(stalePayload, "x");
File.WriteAllText(staleMeta, "x");
File.WriteAllText(freshPayload, "x");
var old = DateTime.UtcNow.AddHours(-12);
File.SetLastWriteTimeUtc(stalePayload, old);
File.SetLastWriteTimeUtc(staleMeta, old);
// Constructing the store runs the startup sweep.
_ = new FileSystemBinaryStore(new BinaryStoreOptions { RootPath = root });
Assert.False(File.Exists(stalePayload));
Assert.False(File.Exists(staleMeta));
Assert.True(File.Exists(freshPayload));
}
finally
{
try { Directory.Delete(root, recursive: true); } catch (IOException) { }
}
}
}