using GmRelay.Web.Services.Portfolio.Covers; namespace GmRelay.Bot.Tests.Web; public sealed class LocalPortfolioCoverStorageTests : IDisposable { private readonly string _storagePath; private readonly LocalPortfolioCoverStorage _storage; public LocalPortfolioCoverStorageTests() { _storagePath = Path.Combine( Path.GetTempPath(), "gmrelay-portfolio-covers-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_storagePath); _storage = new LocalPortfolioCoverStorage(new PortfolioCoverStorageOptions { StoragePath = _storagePath }); } public void Dispose() { if (Directory.Exists(_storagePath)) { Directory.Delete(_storagePath, recursive: true); } GC.SuppressFinalize(this); } [Fact] public async Task SaveAsync_ShouldPersistPngWithRandomProviderNeutralKey() { await using var stream = new MemoryStream( [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00]); var result = await _storage.SaveAsync(stream, "image/png"); Assert.EndsWith(".png", result.StorageKey, StringComparison.Ordinal); Assert.StartsWith("/portfolio-covers/", _storage.GetPublicPath(result.StorageKey), StringComparison.Ordinal); Assert.True(File.Exists(Path.Combine(_storagePath, result.StorageKey))); } [Theory] [InlineData("image/jpeg")] [InlineData("image/png")] [InlineData("image/webp")] public async Task SaveAsync_ShouldRejectMismatchedSignature(string contentType) { await using var stream = new MemoryStream([0x00, 0x01, 0x02, 0x03]); await Assert.ThrowsAsync( () => _storage.SaveAsync(stream, contentType)); } [Fact] public async Task SaveAsync_ShouldPersistJpegWithCorrectSignature() { await using var stream = new MemoryStream( [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46]); var result = await _storage.SaveAsync(stream, "image/jpeg"); Assert.EndsWith(".jpg", result.StorageKey, StringComparison.Ordinal); Assert.Equal("image/jpeg", result.ContentType); Assert.True(File.Exists(Path.Combine(_storagePath, result.StorageKey))); } [Fact] public async Task SaveAsync_ShouldPersistWebpWithRiffWebpSignature() { await using var stream = new MemoryStream( [0x52, 0x49, 0x46, 0x46, 0x1A, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38]); var result = await _storage.SaveAsync(stream, "image/webp"); Assert.EndsWith(".webp", result.StorageKey, StringComparison.Ordinal); Assert.Equal("image/webp", result.ContentType); Assert.True(File.Exists(Path.Combine(_storagePath, result.StorageKey))); } [Fact] public async Task SaveAsync_ShouldRejectStreamLargerThanMaxBytes() { var oversized = new byte[LocalPortfolioCoverStorage.MaxBytes + 1]; await using var stream = new MemoryStream(oversized); await Assert.ThrowsAsync( () => _storage.SaveAsync(stream, "image/png")); } [Fact] public async Task SaveAsync_ShouldRejectUnknownContentType() { await using var stream = new MemoryStream([0x89, 0x50, 0x4E, 0x47]); await Assert.ThrowsAsync( () => _storage.SaveAsync(stream, "application/octet-stream")); } [Fact] public async Task DeleteIfExistsAsync_ShouldRemoveExistingKey() { await using var stream = new MemoryStream( [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); var result = await _storage.SaveAsync(stream, "image/png"); var path = Path.Combine(_storagePath, result.StorageKey); Assert.True(File.Exists(path)); await _storage.DeleteIfExistsAsync(result.StorageKey); Assert.False(File.Exists(path)); } [Fact] public async Task DeleteIfExistsAsync_ShouldBeNoOpForMissingKey() { var key = Guid.NewGuid().ToString("N") + ".png"; await _storage.DeleteIfExistsAsync(key); } [Fact] public async Task DeleteIfExistsAsync_ShouldRejectPathTraversal() { await Assert.ThrowsAsync( () => _storage.DeleteIfExistsAsync("../escape.png")); } [Fact] public async Task DeleteIfExistsAsync_ShouldRejectKeyWithInvalidExtension() { await Assert.ThrowsAsync( () => _storage.DeleteIfExistsAsync(Guid.NewGuid().ToString("N") + ".gif")); } [Fact] public void GetPublicPath_ShouldEscapeSpecialCharacters() { var key = "0123456789abcdef0123456789abcdef" + ".png"; var path = _storage.GetPublicPath(key); Assert.Equal("/portfolio-covers/" + Uri.EscapeDataString(key), path); } [Fact] public async Task SaveAsync_ShouldGenerateUniqueKeys() { await using var stream1 = new MemoryStream( [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); await using var stream2 = new MemoryStream( [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); var first = await _storage.SaveAsync(stream1, "image/png"); var second = await _storage.SaveAsync(stream2, "image/png"); Assert.NotEqual(first.StorageKey, second.StorageKey); } [Fact] public async Task SaveAsync_ShouldNotLeaveTempFileBehind() { await using var stream = new MemoryStream( [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); await _storage.SaveAsync(stream, "image/png"); var tempFiles = Directory.GetFiles(_storagePath, "*.tmp"); Assert.Empty(tempFiles); } }