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) { } } } }