using System.Collections.Generic; using System.Data.Common; using System.IO; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Data.Repositories; using API.DTOs; using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Services; using API.SignalR; using API.Tests.Helpers; using AutoMapper; using Microsoft.AspNetCore.SignalR; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; namespace API.Tests.Services; public class ReaderServiceTests { private readonly ILogger _logger = Substitute.For>(); private readonly IUnitOfWork _unitOfWork; private readonly IHubContext _messageHub = Substitute.For>(); private readonly DbConnection _connection; private readonly DataContext _context; private const string CacheDirectory = "C:/kavita/config/cache/"; private const string CoverImageDirectory = "C:/kavita/config/covers/"; private const string BackupDirectory = "C:/kavita/config/backups/"; private const string DataDirectory = "C:/data/"; public ReaderServiceTests() { var contextOptions = new DbContextOptionsBuilder().UseSqlite(CreateInMemoryDatabase()).Options; _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; _context = new DataContext(contextOptions); Task.Run(SeedDb).GetAwaiter().GetResult(); var config = new MapperConfiguration(cfg => cfg.AddProfile()); var mapper = config.CreateMapper(); _unitOfWork = new UnitOfWork(_context, mapper, null); } #region Setup private static DbConnection CreateInMemoryDatabase() { var connection = new SqliteConnection("Filename=:memory:"); connection.Open(); return connection; } public void Dispose() => _connection.Dispose(); private async Task SeedDb() { await _context.Database.MigrateAsync(); var filesystem = CreateFileSystem(); await Seed.SeedSettings(_context, new DirectoryService(Substitute.For>(), filesystem)); var setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync(); setting.Value = CacheDirectory; setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BackupDirectory).SingleAsync(); setting.Value = BackupDirectory; _context.ServerSetting.Update(setting); _context.Library.Add(new Library() { Name = "Manga", Folders = new List() {new FolderPath() {Path = "C:/data/"}} }); return await _context.SaveChangesAsync() > 0; } private async Task ResetDB() { _context.Series.RemoveRange(_context.Series.ToList()); await _context.SaveChangesAsync(); } private static MockFileSystem CreateFileSystem() { var fileSystem = new MockFileSystem(); fileSystem.Directory.SetCurrentDirectory("C:/kavita/"); fileSystem.AddDirectory("C:/kavita/config/"); fileSystem.AddDirectory(CacheDirectory); fileSystem.AddDirectory(CoverImageDirectory); fileSystem.AddDirectory(BackupDirectory); fileSystem.AddDirectory(DataDirectory); return fileSystem; } #endregion #region FormatBookmarkFolderPath [Theory] [InlineData("/manga/", 1, 1, 1, "/manga/1/1/1")] [InlineData("C:/manga/", 1, 1, 10001, "C:/manga/1/1/10001")] public void FormatBookmarkFolderPathTest(string baseDir, int userId, int seriesId, int chapterId, string expected) { Assert.Equal(expected, ReaderService.FormatBookmarkFolderPath(baseDir, userId, seriesId, chapterId)); } #endregion #region CapPageToChapter [Fact] public async Task CapPageToChapterTest() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { new Volume() { Chapters = new List() { new Chapter() { Pages = 1 } } } } }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); Assert.Equal(0, await readerService.CapPageToChapter(1, -1)); Assert.Equal(1, await readerService.CapPageToChapter(1, 10)); } #endregion #region SaveReadingProgress [Fact] public async Task SaveReadingProgress_ShouldCreateNewEntity() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { new Volume() { Chapters = new List() { new Chapter() { Pages = 1 } } } } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var successful = await readerService.SaveReadingProgress(new ProgressDto() { ChapterId = 1, PageNum = 1, SeriesId = 1, VolumeId = 1, BookScrollId = null }, 1); Assert.True(successful); Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); } [Fact] public async Task SaveReadingProgress_ShouldUpdateExisting() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { new Volume() { Chapters = new List() { new Chapter() { Pages = 1 } } } } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var successful = await readerService.SaveReadingProgress(new ProgressDto() { ChapterId = 1, PageNum = 1, SeriesId = 1, VolumeId = 1, BookScrollId = null }, 1); Assert.True(successful); Assert.NotNull(await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)); Assert.True(await readerService.SaveReadingProgress(new ProgressDto() { ChapterId = 1, PageNum = 1, SeriesId = 1, VolumeId = 1, BookScrollId = "/h1/" }, 1)); Assert.Equal("/h1/", (await _unitOfWork.AppUserProgressRepository.GetUserProgressAsync(1, 1)).BookScrollId); } #endregion #region MarkChaptersAsRead [Fact] public async Task MarkChaptersAsReadTest() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { new Volume() { Chapters = new List() { new Chapter() { Pages = 1 }, new Chapter() { Pages = 2 } } } } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var volumes = await _unitOfWork.VolumeRepository.GetVolumes(1); readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); await _context.SaveChangesAsync(); Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); } #endregion #region MarkChapterAsUnread [Fact] public async Task MarkChapterAsUnreadTest() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { new Volume() { Chapters = new List() { new Chapter() { Pages = 1 }, new Chapter() { Pages = 2 } } } } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var volumes = (await _unitOfWork.VolumeRepository.GetVolumes(1)).ToList(); readerService.MarkChaptersAsRead(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); await _context.SaveChangesAsync(); Assert.Equal(2, (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses.Count); readerService.MarkChaptersAsUnread(await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress), 1, volumes.First().Chapters); await _context.SaveChangesAsync(); var progresses = (await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress)).Progresses; Assert.Equal(0, progresses.Max(p => p.PagesRead)); Assert.Equal(2, progresses.Count); } #endregion #region GetNextChapterIdAsync [Fact] public async Task GetNextChapterIdAsync_ShouldGetNextVolume() { // V1 -> V2 await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("2", new List() { EntityFactory.CreateChapter("21", false, new List()), EntityFactory.CreateChapter("22", false, new List()), }), EntityFactory.CreateVolume("3", new List() { EntityFactory.CreateChapter("31", false, new List()), EntityFactory.CreateChapter("32", false, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 1, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("2", actualChapter.Range); } [Fact] public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("2", new List() { EntityFactory.CreateChapter("21", false, new List()), EntityFactory.CreateChapter("22", false, new List()), }), EntityFactory.CreateVolume("3", new List() { EntityFactory.CreateChapter("31", false, new List()), EntityFactory.CreateChapter("32", false, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("21", actualChapter.Range); } [Fact] public async Task GetNextChapterIdAsync_ShouldNotMoveFromVolumeToSpecial() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("0", new List() { EntityFactory.CreateChapter("A.cbz", true, new List()), EntityFactory.CreateChapter("B.cbz", true, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var nextChapter = await readerService.GetNextChapterIdAsync(1, 1, 2, 1); Assert.Equal(-1, nextChapter); } [Fact] public async Task GetNextChapterIdAsync_ShouldMoveFromSpecialToSpecial() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("0", new List() { EntityFactory.CreateChapter("A.cbz", true, new List()), EntityFactory.CreateChapter("B.cbz", true, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var nextChapter = await readerService.GetNextChapterIdAsync(1, 2, 3, 1); Assert.NotEqual(-1, nextChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter); Assert.Equal("B.cbz", actualChapter.Range); } #endregion #region GetPrevChapterIdAsync [Fact] public async Task GetPrevChapterIdAsync_ShouldGetPrevVolume() { // V1 -> V2 await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("2", new List() { EntityFactory.CreateChapter("21", false, new List()), EntityFactory.CreateChapter("22", false, new List()), }), EntityFactory.CreateVolume("3", new List() { EntityFactory.CreateChapter("31", false, new List()), EntityFactory.CreateChapter("32", false, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 2, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("1", actualChapter.Range); } [Fact] public async Task GetPrevChapterIdAsync_ShouldRollIntoPrevVolume() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("2", new List() { EntityFactory.CreateChapter("21", false, new List()), EntityFactory.CreateChapter("22", false, new List()), }), EntityFactory.CreateVolume("3", new List() { EntityFactory.CreateChapter("31", false, new List()), EntityFactory.CreateChapter("32", false, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 3, 1); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("2", actualChapter.Range); } [Fact] public async Task GetPrevChapterIdAsync_ShouldMoveFromVolumeToSpecial() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("0", new List() { EntityFactory.CreateChapter("A.cbz", true, new List()), EntityFactory.CreateChapter("B.cbz", true, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 1, 1, 1); Assert.NotEqual(-1, prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("B.cbz", actualChapter.Range); } [Fact] public async Task GetPrevChapterIdAsync_ShouldMoveFromSpecialToSpecial() { await ResetDB(); _context.Series.Add(new Series() { Name = "Test", Library = new Library() { Name = "Test LIb", Type = LibraryType.Manga, }, Volumes = new List() { EntityFactory.CreateVolume("1", new List() { EntityFactory.CreateChapter("1", false, new List()), EntityFactory.CreateChapter("2", false, new List()), }), EntityFactory.CreateVolume("0", new List() { EntityFactory.CreateChapter("A.cbz", true, new List()), EntityFactory.CreateChapter("B.cbz", true, new List()), }), } }); _context.AppUser.Add(new AppUser() { UserName = "majora2007" }); await _context.SaveChangesAsync(); var fileSystem = new MockFileSystem(); var ds = new DirectoryService(Substitute.For>(), fileSystem); var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); var prevChapter = await readerService.GetPrevChapterIdAsync(1, 2, 4, 1); Assert.NotEqual(-1, prevChapter); var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(prevChapter); Assert.Equal("A.cbz", actualChapter.Range); } #endregion // #region GetNumberOfPages // // [Fact] // public void GetNumberOfPages_EPUB() // { // const string testDirectory = "/manga/"; // var fileSystem = new MockFileSystem(); // // var actualFile = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService/EPUB"), "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub") // fileSystem.File.WriteAllBytes("${testDirectory}test.epub", File.ReadAllBytes(actualFile)); // // fileSystem.AddDirectory(CacheDirectory); // // var ds = new DirectoryService(Substitute.For>(), fileSystem); // var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); // var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); // // // } // // // #endregion }