using ClaudeDo.Data; namespace ClaudeDo.Data.Tests; public sealed class AttachmentStoreTests : IDisposable { private readonly string _root; private readonly AttachmentStore _store; public AttachmentStoreTests() { _root = Path.Combine(Path.GetTempPath(), $"claudedo_att_{Guid.NewGuid():N}"); Directory.CreateDirectory(_root); _store = new AttachmentStore(_root); } public void Dispose() { try { Directory.Delete(_root, recursive: true); } catch { } } [Fact] public async Task Save_then_readback_size_matches() { var bytes = new byte[1024]; new Random(42).NextBytes(bytes); using var ms = new MemoryStream(bytes); var written = await _store.SaveAsync("task1", "data.bin", ms); Assert.Equal(1024, written); var filePath = Path.Combine(_store.TaskDir("task1"), "data.bin"); Assert.Equal(1024, new FileInfo(filePath).Length); } [Fact] public async Task Rejects_fileName_with_dotdot() { using var ms = new MemoryStream(new byte[10]); await Assert.ThrowsAsync(() => _store.SaveAsync("task1", "../escape.txt", ms)); } [Fact] public async Task Rejects_fileName_with_directory_separator() { using var ms = new MemoryStream(new byte[10]); await Assert.ThrowsAsync(() => _store.SaveAsync("task1", "sub/file.txt", ms)); } [Fact] public async Task Rejects_content_over_5MB() { var over = new byte[5 * 1024 * 1024 + 1]; using var ms = new MemoryStream(over); await Assert.ThrowsAsync(() => _store.SaveAsync("task1", "big.bin", ms)); } [Fact] public void DeleteTaskDir_removes_directory() { var dir = _store.TaskDir("task2"); Directory.CreateDirectory(dir); File.WriteAllText(Path.Combine(dir, "a.txt"), "x"); _store.DeleteTaskDir("task2"); Assert.False(Directory.Exists(dir)); } [Fact] public void Deleting_missing_file_is_noop() { // Should not throw _store.DeleteFile("taskX", "nonexistent.txt"); } [Fact] public void DeleteFile_removes_file_and_ignores_missing_second_call() { var dir = _store.TaskDir("task3"); Directory.CreateDirectory(dir); var filePath = Path.Combine(dir, "remove.txt"); File.WriteAllText(filePath, "data"); _store.DeleteFile("task3", "remove.txt"); Assert.False(File.Exists(filePath)); // Second call must not throw _store.DeleteFile("task3", "remove.txt"); } }