173 lines
5.7 KiB
C#
173 lines
5.7 KiB
C#
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<InvalidOperationException>(
|
|
() => _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<InvalidOperationException>(
|
|
() => _storage.SaveAsync(stream, "image/png"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveAsync_ShouldRejectUnknownContentType()
|
|
{
|
|
await using var stream = new MemoryStream([0x89, 0x50, 0x4E, 0x47]);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => _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<InvalidOperationException>(
|
|
() => _storage.DeleteIfExistsAsync("../escape.png"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteIfExistsAsync_ShouldRejectKeyWithInvalidExtension()
|
|
{
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => _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);
|
|
}
|
|
}
|