ASP.NET Core Blazor (.NET 10) + EF Core/SQLite + Identity. Features: - Wishlists & items with local image storage (upload, clipboard paste, or URL fetched and stored locally) - Owner-hidden claims (enforced at the query layer) to preserve surprises - Admin-invite-only onboarding with email-based password resets - All state under /data; ships as a single Docker image Includes Dockerfile, docker-compose, Gitea Actions CI (test + push image), Unraid template, and xUnit tests (claim privacy, invite lifecycle, image validation). Co-Authored-By: Claude Opus 4.8 <[email protected]>
85 lines
2.8 KiB
C#
85 lines
2.8 KiB
C#
using System.Text;
|
|
using Microsoft.Extensions.Options;
|
|
using WishNinja.Configuration;
|
|
using WishNinja.Services;
|
|
using Xunit;
|
|
|
|
namespace WishNinja.Tests;
|
|
|
|
public class ImageServiceTests : IDisposable
|
|
{
|
|
private readonly string _tempDir;
|
|
private readonly ImageService _svc;
|
|
|
|
public ImageServiceTests()
|
|
{
|
|
_tempDir = Path.Combine(Path.GetTempPath(), "wishninja-tests-" + Guid.NewGuid().ToString("N"));
|
|
var options = Options.Create(new WishNinjaOptions
|
|
{
|
|
DataPath = _tempDir,
|
|
Uploads = new UploadOptions { MaxBytes = 1024 }, // 1 KB cap for the test
|
|
});
|
|
_svc = new ImageService(options, new StubHttpClientFactory());
|
|
}
|
|
|
|
private sealed class StubHttpClientFactory : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => new();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("image/png", true)]
|
|
[InlineData("image/jpeg", true)]
|
|
[InlineData("image/webp", true)]
|
|
[InlineData("image/gif", true)]
|
|
[InlineData("application/pdf", false)]
|
|
[InlineData("text/html", false)]
|
|
[InlineData(null, false)]
|
|
public void IsAllowedContentType_enforces_allowlist(string? contentType, bool expected) =>
|
|
Assert.Equal(expected, _svc.IsAllowedContentType(contentType));
|
|
|
|
[Fact]
|
|
public async Task SaveAsync_rejects_disallowed_type()
|
|
{
|
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes("not an image"));
|
|
var result = await _svc.SaveAsync(stream, "application/pdf");
|
|
Assert.False(result.Succeeded);
|
|
Assert.Null(result.FileName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveAsync_rejects_oversize_file_and_cleans_up()
|
|
{
|
|
using var stream = new MemoryStream(new byte[2048]); // exceeds 1 KB cap
|
|
var result = await _svc.SaveAsync(stream, "image/png");
|
|
Assert.False(result.Succeeded);
|
|
Assert.Empty(Directory.GetFiles(_svc.UploadsDirectory)); // partial file removed
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("not-a-url")]
|
|
[InlineData("ftp://example.com/x.png")]
|
|
[InlineData("file:///etc/passwd")]
|
|
public async Task SaveFromUrlAsync_rejects_non_http_urls(string url)
|
|
{
|
|
var result = await _svc.SaveFromUrlAsync(url);
|
|
Assert.False(result.Succeeded);
|
|
Assert.Empty(Directory.GetFiles(_svc.UploadsDirectory));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SaveAsync_writes_allowed_file_with_guid_name()
|
|
{
|
|
using var stream = new MemoryStream(new byte[512]);
|
|
var result = await _svc.SaveAsync(stream, "image/png");
|
|
Assert.True(result.Succeeded);
|
|
Assert.EndsWith(".png", result.FileName);
|
|
Assert.True(File.Exists(Path.Combine(_svc.UploadsDirectory, result.FileName!)));
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, recursive: true);
|
|
}
|
|
}
|