using Api.SeaHavenIndustries.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Moq; using Xunit; namespace Api.SeaHavenIndustries.Tests; public sealed class FileStorageAdapterTests : IDisposable { private readonly string _webRoot = Path.Combine(Path.GetTempPath(), $"shoc-storage-{Guid.NewGuid():N}"); private readonly FileStorageAdapter _adapter; public FileStorageAdapterTests() { Directory.CreateDirectory(_webRoot); var environment = new Mock(); environment.SetupGet(candidate => candidate.WebRootPath).Returns(_webRoot); environment.SetupGet(candidate => candidate.ContentRootPath).Returns(_webRoot); _adapter = new FileStorageAdapter(environment.Object, new HttpContextAccessor()); } [Fact] public void OpenRead_ValidStoredUrl_ReturnsReadableStream() { var directory = Path.Combine(_webRoot, "Assets", "Documents"); Directory.CreateDirectory(directory); File.WriteAllText(Path.Combine(directory, "report.pdf"), "stored"); using var stream = _adapter.OpenRead("https://example.test/Assets/Documents/report.pdf"); using var reader = new StreamReader(Assert.IsAssignableFrom(stream)); Assert.Equal("stored", reader.ReadToEnd()); } [Theory] [InlineData("https://example.test/Assets/Images/report.pdf")] [InlineData("https://example.test/Assets/Documents/../secret.txt")] [InlineData("https://example.test/Assets/Documents/%2e%2e/secret.txt")] public void OpenRead_UnsafeUrl_ReturnsNull(string fileUrl) { Assert.Null(_adapter.OpenRead(fileUrl)); } [Fact] public void OpenRead_MissingFile_ReturnsNull() { Assert.Null(_adapter.OpenRead("https://example.test/Assets/Documents/missing.pdf")); } public void Dispose() { if (Directory.Exists(_webRoot)) Directory.Delete(_webRoot, recursive: true); } }