feat(storage): add IFileStorage + local-disk implementation
Adds IFileStorage abstraction and LocalDiskFileStorage for receipt file storage with path-traversal protection, and registers it in DI. Includes 3 TDD-verified xUnit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using ROLAC.API.Services.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace ROLAC.API.Tests.Services;
|
||||
|
||||
public class LocalDiskFileStorageTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "rolac-test-" + Guid.NewGuid());
|
||||
|
||||
private LocalDiskFileStorage Build()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?> { ["Storage:LocalRoot"] = _root })
|
||||
.Build();
|
||||
return new LocalDiskFileStorage(config);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveThenOpen_RoundTrips()
|
||||
{
|
||||
var fs = Build();
|
||||
using var input = new MemoryStream(Encoding.UTF8.GetBytes("hello"));
|
||||
var path = await fs.SaveAsync(input, "finance/receipts/2026/5/1-r.txt");
|
||||
|
||||
await using var read = await fs.OpenReadAsync(path);
|
||||
Assert.NotNull(read);
|
||||
using var sr = new StreamReader(read!);
|
||||
Assert.Equal("hello", await sr.ReadToEndAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenRead_ReturnsNull_WhenMissing()
|
||||
{
|
||||
var fs = Build();
|
||||
Assert.Null(await fs.OpenReadAsync("finance/receipts/none.txt"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_RejectsPathTraversal()
|
||||
{
|
||||
var fs = Build();
|
||||
using var input = new MemoryStream(Encoding.UTF8.GetBytes("x"));
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => fs.SaveAsync(input, "../escape.txt"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user