Add media image handling and related endpoints
CI / Backend (build + test) (push) Failing after 1m35s
CI / Frontend (lint + typecheck + build) (push) Successful in 43s

- Introduced `MediaImage` entity to manage images for markdown in instructions and news.
- Updated `IAppDbContext` and `AppDbContext` to include `MediaImages` DbSet.
- Implemented `DeleteMediaImageFilesAsync` method in `FactoryResetCommandHandler` to remove media images during factory reset.
- Added new API endpoints for uploading and retrieving media images, enhancing markdown support.
- Updated frontend components to utilize the new `MarkdownEditor` for image uploads in instructions and news.
- Enhanced documentation to reflect the new media handling features and API specifications.
This commit is contained in:
Leonid Pershin
2026-07-30 04:05:01 +03:00
parent cc7e2a7f8f
commit c2ed3240bd
37 changed files with 2052 additions and 63 deletions
@@ -0,0 +1,95 @@
using NSubstitute;
using PnvPanel.Application.Admin.Media;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Application.Media;
using PnvPanel.Application.Tests.TestSupport;
using Xunit;
namespace PnvPanel.Application.Tests.Media;
public class UploadMediaImageCommandHandlerTests
{
private readonly IFileStorage _fileStorage = Substitute.For<IFileStorage>();
private static MediaImageUpload Upload(
string contentType = "image/png",
long sizeBytes = 1024,
string fileName = "screenshot.png"
) => new(new MemoryStream([1, 2, 3]), fileName, contentType, sizeBytes);
[Fact]
public async Task Handle_WithValidImage_SavesFileAndRow()
{
using var dbContext = InMemoryDbContextFactory.Create();
var adminId = Guid.NewGuid();
_fileStorage
.SaveAsync(Arg.Any<Stream>(), Arg.Any<CancellationToken>())
.Returns("stored-name");
var handler = new UploadMediaImageCommandHandler(
dbContext,
_fileStorage,
FakeCurrentUser.Authenticated(adminId, "admin")
);
var result = await handler.Handle(
new UploadMediaImageCommand(Upload()),
CancellationToken.None
);
await dbContext.SaveChangesAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal("screenshot.png", result.Value.FileName);
var image = Assert.Single(dbContext.MediaImages);
Assert.Equal("stored-name", image.StoredFileName);
Assert.Equal(adminId, image.UploadedBy);
}
[Fact]
public async Task Handle_WithUnsupportedContentType_ReturnsValidationErrorWithoutSaving()
{
using var dbContext = InMemoryDbContextFactory.Create();
var handler = new UploadMediaImageCommandHandler(
dbContext,
_fileStorage,
FakeCurrentUser.Authenticated(Guid.NewGuid())
);
var result = await handler.Handle(
new UploadMediaImageCommand(Upload(contentType: "image/svg+xml", fileName: "x.svg")),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(MediaErrors.UnsupportedImageType, result.Error);
Assert.Empty(dbContext.MediaImages);
await _fileStorage
.DidNotReceive()
.SaveAsync(Arg.Any<Stream>(), Arg.Any<CancellationToken>());
}
/// <summary>Лимит продублирован из MediaImageValidation — она internal (см. Application).</summary>
private const long MaxSizeBytes = 5 * 1024 * 1024;
[Fact]
public async Task Handle_WithTooLargeImage_ReturnsValidationError()
{
using var dbContext = InMemoryDbContextFactory.Create();
var handler = new UploadMediaImageCommandHandler(
dbContext,
_fileStorage,
FakeCurrentUser.Authenticated(Guid.NewGuid())
);
var result = await handler.Handle(
new UploadMediaImageCommand(Upload(sizeBytes: MaxSizeBytes + 1)),
CancellationToken.None
);
Assert.False(result.IsSuccess);
Assert.Equal(MediaErrors.ImageTooLarge, result.Error);
Assert.Empty(dbContext.MediaImages);
}
}