Initial build: WishNinja self-hosted gift wishlist manager
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]>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user