[FA-5] Adds image support with proper S3 upload and replacement after upload
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
using FictionArchive.Common.Enums;
|
||||
using FictionArchive.Service.FileService.IntegrationEvents;
|
||||
using FictionArchive.Service.NovelService.Models.Configuration;
|
||||
using FictionArchive.Service.NovelService.Models.Enums;
|
||||
using FictionArchive.Service.NovelService.Models.Images;
|
||||
using FictionArchive.Service.NovelService.Models.Localization;
|
||||
using FictionArchive.Service.NovelService.Models.Novels;
|
||||
using FictionArchive.Service.NovelService.Models.SourceAdapters;
|
||||
using FictionArchive.Service.NovelService.Services;
|
||||
using FictionArchive.Service.NovelService.Services.SourceAdapters;
|
||||
using FictionArchive.Service.Shared.Services.EventBus;
|
||||
using FluentAssertions;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace FictionArchive.Service.NovelService.Tests;
|
||||
|
||||
public class NovelUpdateServiceTests
|
||||
{
|
||||
private static NovelServiceDbContext CreateDbContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<NovelServiceDbContext>()
|
||||
.UseInMemoryDatabase($"NovelUpdateServiceTests-{Guid.NewGuid()}")
|
||||
.Options;
|
||||
|
||||
return new NovelServiceDbContext(options, NullLogger<NovelServiceDbContext>.Instance);
|
||||
}
|
||||
|
||||
private static NovelCreateResult CreateNovelWithSingleChapter(NovelServiceDbContext dbContext, Source source)
|
||||
{
|
||||
var chapter = new Chapter
|
||||
{
|
||||
Order = 1,
|
||||
Revision = 1,
|
||||
Url = "http://demo/chapter-1",
|
||||
Name = LocalizationKey.CreateFromText("Chapter 1", Language.En),
|
||||
Body = new LocalizationKey { Texts = new List<LocalizationText>() },
|
||||
Images = new List<Image>()
|
||||
};
|
||||
|
||||
var novel = new Novel
|
||||
{
|
||||
Url = "http://demo/novel",
|
||||
ExternalId = "demo-1",
|
||||
Author = new Person { Name = LocalizationKey.CreateFromText("Author", Language.En) },
|
||||
RawLanguage = Language.En,
|
||||
RawStatus = NovelStatus.InProgress,
|
||||
Source = source,
|
||||
Name = LocalizationKey.CreateFromText("Demo Novel", Language.En),
|
||||
Description = LocalizationKey.CreateFromText("Description", Language.En),
|
||||
Chapters = new List<Chapter> { chapter },
|
||||
Tags = new List<NovelTag>()
|
||||
};
|
||||
|
||||
dbContext.Novels.Add(novel);
|
||||
dbContext.SaveChanges();
|
||||
|
||||
return new NovelCreateResult(novel, chapter);
|
||||
}
|
||||
|
||||
private static NovelUpdateService CreateService(
|
||||
NovelServiceDbContext dbContext,
|
||||
ISourceAdapter adapter,
|
||||
IEventBus eventBus,
|
||||
string pendingImageUrl = "https://pending/placeholder.jpg")
|
||||
{
|
||||
var options = Options.Create(new NovelUpdateServiceConfiguration
|
||||
{
|
||||
PendingImageUrl = pendingImageUrl
|
||||
});
|
||||
|
||||
return new NovelUpdateService(dbContext, NullLogger<NovelUpdateService>.Instance, new[] { adapter }, eventBus, options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PullChapterContents_rewrites_images_and_publishes_requests()
|
||||
{
|
||||
using var dbContext = CreateDbContext();
|
||||
var source = new Source { Name = "Demo", Key = "demo", Url = "http://demo" };
|
||||
var (novel, chapter) = CreateNovelWithSingleChapter(dbContext, source);
|
||||
|
||||
var rawHtml = "<p>Hello</p><img src=\"http://img/x1.jpg\" alt=\"first\" /><img src=\"http://img/x2.jpg\" alt=\"second\" />";
|
||||
var image1 = new ImageData { Url = "http://img/x1.jpg", Data = new byte[] { 1, 2, 3 } };
|
||||
var image2 = new ImageData { Url = "http://img/x2.jpg", Data = new byte[] { 4, 5, 6 } };
|
||||
|
||||
var adapter = Substitute.For<ISourceAdapter>();
|
||||
adapter.SourceDescriptor.Returns(new SourceDescriptor { Key = "demo", Name = "Demo", Url = "http://demo" });
|
||||
adapter.GetRawChapter(chapter.Url).Returns(Task.FromResult(new ChapterFetchResult
|
||||
{
|
||||
Text = rawHtml,
|
||||
ImageData = new List<ImageData> { image1, image2 }
|
||||
}));
|
||||
|
||||
var publishedEvents = new List<FileUploadRequestCreatedEvent>();
|
||||
var eventBus = Substitute.For<IEventBus>();
|
||||
eventBus.Publish(Arg.Do<FileUploadRequestCreatedEvent>(publishedEvents.Add)).Returns(Task.CompletedTask);
|
||||
eventBus.Publish(Arg.Any<object>(), Arg.Any<string>()).Returns(Task.CompletedTask);
|
||||
|
||||
var pendingImageUrl = "https://pending/placeholder.jpg";
|
||||
var service = CreateService(dbContext, adapter, eventBus, pendingImageUrl);
|
||||
|
||||
var updatedChapter = await service.PullChapterContents(novel.Id, chapter.Order);
|
||||
|
||||
updatedChapter.Images.Should().HaveCount(2);
|
||||
updatedChapter.Images.Select(i => i.OriginalPath).Should().BeEquivalentTo(new[] { image1.Url, image2.Url });
|
||||
updatedChapter.Images.All(i => i.Id != Guid.Empty).Should().BeTrue();
|
||||
|
||||
var storedHtml = updatedChapter.Body.Texts.Single().Text;
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(storedHtml);
|
||||
var imgNodes = doc.DocumentNode.SelectNodes("//img");
|
||||
imgNodes.Should().NotBeNull();
|
||||
imgNodes!.Count.Should().Be(2);
|
||||
imgNodes.Should().OnlyContain(node => node.GetAttributeValue("src", string.Empty) == pendingImageUrl);
|
||||
imgNodes.Select(node => node.GetAttributeValue("alt", string.Empty))
|
||||
.Should()
|
||||
.BeEquivalentTo(updatedChapter.Images.Select(img => img.Id.ToString()));
|
||||
|
||||
publishedEvents.Should().HaveCount(2);
|
||||
publishedEvents.Select(e => e.RequestId).Should().BeEquivalentTo(updatedChapter.Images.Select(i => i.Id));
|
||||
publishedEvents.Select(e => e.FileData).Should().BeEquivalentTo(new[] { image1.Data, image2.Data });
|
||||
publishedEvents.Should().OnlyContain(e => e.FilePath.StartsWith($"{novel.Id}/Images/Chapter-{updatedChapter.Id}/"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PullChapterContents_adds_alt_when_missing()
|
||||
{
|
||||
using var dbContext = CreateDbContext();
|
||||
var source = new Source { Name = "Demo", Key = "demo", Url = "http://demo" };
|
||||
var (novel, chapter) = CreateNovelWithSingleChapter(dbContext, source);
|
||||
|
||||
var rawHtml = "<p>Hi</p><img src=\"http://img/x1.jpg\">";
|
||||
var image = new ImageData { Url = "http://img/x1.jpg", Data = new byte[] { 7, 8, 9 } };
|
||||
|
||||
var adapter = Substitute.For<ISourceAdapter>();
|
||||
adapter.SourceDescriptor.Returns(new SourceDescriptor { Key = "demo", Name = "Demo", Url = "http://demo" });
|
||||
adapter.GetRawChapter(chapter.Url).Returns(Task.FromResult(new ChapterFetchResult
|
||||
{
|
||||
Text = rawHtml,
|
||||
ImageData = new List<ImageData> { image }
|
||||
}));
|
||||
|
||||
var eventBus = Substitute.For<IEventBus>();
|
||||
eventBus.Publish(Arg.Any<FileUploadRequestCreatedEvent>()).Returns(Task.CompletedTask);
|
||||
eventBus.Publish(Arg.Any<object>(), Arg.Any<string>()).Returns(Task.CompletedTask);
|
||||
|
||||
var service = CreateService(dbContext, adapter, eventBus);
|
||||
|
||||
var updatedChapter = await service.PullChapterContents(novel.Id, chapter.Order);
|
||||
|
||||
var storedHtml = updatedChapter.Body.Texts.Single().Text;
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(storedHtml);
|
||||
var imgNode = doc.DocumentNode.SelectSingleNode("//img");
|
||||
imgNode.Should().NotBeNull();
|
||||
imgNode!.GetAttributeValue("alt", string.Empty).Should().Be(updatedChapter.Images.Single().Id.ToString());
|
||||
imgNode.GetAttributeValue("src", string.Empty).Should().Be("https://pending/placeholder.jpg");
|
||||
}
|
||||
|
||||
private record NovelCreateResult(Novel Novel, Chapter Chapter);
|
||||
}
|
||||
Reference in New Issue
Block a user