166 lines
7.4 KiB
C#
166 lines
7.4 KiB
C#
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);
|
|
}
|