e7bf07c2ad
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>
53 lines
1.6 KiB
C#
53 lines
1.6 KiB
C#
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);
|
|
}
|
|
}
|