using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using NexusReader.Data.Persistence; using NexusReader.Domain.Entities; using NexusReader.Application.Queries.Reader; using NexusReader.Infrastructure.Services; using Xunit; namespace NexusReader.Application.Tests.Services; public class EpubReaderServiceTests : IDisposable { private readonly SqliteConnection _connection; private readonly DbContextOptions _contextOptions; private readonly Mock> _dbContextFactoryMock; private readonly Mock> _loggerMock; public EpubReaderServiceTests() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); _contextOptions = new DbContextOptionsBuilder() .UseSqlite(_connection) .Options; // Seed initial database schema using var context = new AppDbContext(_contextOptions); context.Database.EnsureCreated(); _dbContextFactoryMock = new Mock>(); _dbContextFactoryMock.Setup(f => f.CreateDbContextAsync(It.IsAny())) .ReturnsAsync(() => new AppDbContext(_contextOptions)); _dbContextFactoryMock.Setup(f => f.CreateDbContext()) .Returns(() => new AppDbContext(_contextOptions)); _loggerMock = new Mock>(); } [Fact] public async Task GetEpubContentAsync_RewritesImageUrlsAndExtractsImages() { // Arrange var ebookId = Guid.NewGuid(); var userId = "test-user-id"; using (var context = new AppDbContext(_contextOptions)) { var user = new NexusUser { Id = userId, UserName = "testuser", Email = "test@nexus.com", TenantId = "tenant-123", SubscriptionPlanId = 1 }; context.Users.Add(user); var author = new Author { Id = 10, Name = "Giorgio Vasari" }; context.Authors.Add(author); var ebook = new Ebook { Id = ebookId, UserId = userId, Title = "Test Book", AuthorId = author.Id, FilePath = "assets/book.epub", AddedDate = DateTime.UtcNow, LastReadDate = DateTime.UtcNow, Progress = 0, LastChapter = "Introduction" }; context.Ebooks.Add(ebook); await context.SaveChangesAsync(); } var service = new EpubReaderService(_dbContextFactoryMock.Object, _loggerMock.Object); // Act var result = await service.GetEpubContentAsync(ebookId, 0, userId); // Assert result.IsSuccess.Should().BeTrue(); result.Value.Should().NotBeNull(); result.Value.Blocks.Should().NotBeEmpty(); // Check that any img tags extracted are preserved and rewritten var hasImages = false; foreach (var block in result.Value.Blocks) { if (block is TextSegmentBlock textBlock && textBlock.Content.Contains(""; // Act var result = (string)method.Invoke(null, new object[] { input }); // Assert result.Should().NotContain("onerror"); result.Should().NotContain("onload"); result.Should().NotContain("style"); result.Should().NotContain("class"); result.Should().Contain("src=\"images/cover.jpg\""); result.Should().Contain("alt=\"Cover Image\""); } [Fact] public void RewriteImageUrls_BlocksJavaScriptScheme() { // Arrange var method = typeof(EpubReaderService).GetMethod("RewriteImageUrls", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); method.Should().NotBeNull(); var input = ""; var ebookId = Guid.NewGuid(); // Act var result = (string)method.Invoke(null, new object[] { input, ebookId, "OEBPS/chapter1.xhtml" }); // Assert result.Should().NotContain("javascript:alert(1)"); } [Fact] public async Task GetEpubResourceAsync_RejectsInvalidResourcePaths() { // Arrange var ebookId = Guid.NewGuid(); var userId = "test-user-id"; using (var context = new AppDbContext(_contextOptions)) { var user = new NexusUser { Id = userId, UserName = "testuser", Email = "test@nexus.com", TenantId = "tenant-123", SubscriptionPlanId = 1 }; context.Users.Add(user); var author = new Author { Id = 10, Name = "Giorgio Vasari" }; context.Authors.Add(author); var ebook = new Ebook { Id = ebookId, UserId = userId, Title = "Test Book", AuthorId = author.Id, FilePath = "assets/book.epub", AddedDate = DateTime.UtcNow, LastReadDate = DateTime.UtcNow, Progress = 0, LastChapter = "Introduction" }; context.Ebooks.Add(ebook); await context.SaveChangesAsync(); } var service = new EpubReaderService(_dbContextFactoryMock.Object, _loggerMock.Object); // Act var traversalResult = await service.GetEpubResourceAsync(ebookId, "../../appsettings.json", userId); var colonResult = await service.GetEpubResourceAsync(ebookId, "C:\\windows\\win.ini", userId); // Assert traversalResult.IsSuccess.Should().BeFalse(); traversalResult.Errors.First().Message.Should().Contain("Invalid resource path"); colonResult.IsSuccess.Should().BeFalse(); colonResult.Errors.First().Message.Should().Contain("Invalid resource path"); } [Fact] public void RewriteImageUrls_PreservesImgPrefix() { // Arrange var method = typeof(EpubReaderService).GetMethod("RewriteImageUrls", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); method.Should().NotBeNull(); var input = ""; var ebookId = Guid.NewGuid(); // Act var result = (string)method.Invoke(null, new object[] { input, ebookId, "OEBPS/cover-page.xhtml" }); // Assert result.Should().StartWith(""; var inputHref = ""; var ebookId = Guid.NewGuid(); // Act var resultXlink = (string)method.Invoke(null, new object[] { inputXlink, ebookId, "OEBPS/chapter1.xhtml" }); var resultHref = (string)method.Invoke(null, new object[] { inputHref, ebookId, "OEBPS/chapter1.xhtml" }); // Assert resultXlink.Should().Contain("

")] [InlineData("

 

")] [InlineData("


")] [InlineData("
")] [InlineData(" ")] public void EmptyBlockRegex_MatchesEmptyBlocks(string input) { // Arrange var field = typeof(EpubReaderService).GetField("EmptyBlockRegex", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); field.Should().NotBeNull(); var regex = (System.Text.RegularExpressions.Regex)field.GetValue(null); regex.Should().NotBeNull(); var method = typeof(EpubReaderService).GetMethod("SanitizeParagraph", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); method.Should().NotBeNull(); // Act var sanitized = (string)method.Invoke(null, new object[] { input }); var isMatch = regex.IsMatch(sanitized); // Assert isMatch.Should().BeTrue(); } public void Dispose() { _connection.Close(); _connection.Dispose(); } }