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.Metadata; using API.Entities; using API.Entities.Enums; using API.Parser; using API.Services; using API.SignalR; 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 { internal class MockReadingItemServiceForCacheService : IReadingItemService { private readonly DirectoryService _directoryService; public MockReadingItemServiceForCacheService(DirectoryService directoryService) { _directoryService = directoryService; } public ComicInfo GetComicInfo(string filePath) { return null; } public int GetNumberOfPages(string filePath, MangaFormat format) { return 1; } public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format) { return string.Empty; } public void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1) { throw new System.NotImplementedException(); } public ParserInfo Parse(string path, string rootPath, LibraryType type) { throw new System.NotImplementedException(); } } public class CacheServiceTests { 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 CacheServiceTests() { var contextOptions = new DbContextOptionsBuilder() .UseSqlite(CreateInMemoryDatabase()) .Options; _connection = RelationalOptionsExtension.Extract(contextOptions).Connection; _context = new DataContext(contextOptions); Task.Run(SeedDb).GetAwaiter().GetResult(); _unitOfWork = new UnitOfWork(_context, Substitute.For(), 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 Ensure [Fact] public async Task Ensure_DirectoryAlreadyExists_DontExtractAnything() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{DataDirectory}Test v1.zip", new MockFileData("")); filesystem.AddDirectory($"{CacheDirectory}1/"); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); await ResetDB(); var s = DbFactory.Series("Test"); var v = DbFactory.Volume("1"); var c = new Chapter() { Number = "1", Files = new List() { new MangaFile() { Format = MangaFormat.Archive, FilePath = $"{DataDirectory}Test v1.zip", } } }; v.Chapters.Add(c); s.Volumes.Add(v); s.LibraryId = 1; _context.Series.Add(s); await _context.SaveChangesAsync(); await cleanupService.Ensure(1); Assert.Empty(ds.GetFiles(filesystem.Path.Join(CacheDirectory, "1"), searchOption:SearchOption.AllDirectories)); } // [Fact] // public async Task Ensure_DirectoryAlreadyExists_ExtractsImages() // { // // TODO: Figure out a way to test this // var filesystem = CreateFileSystem(); // filesystem.AddFile($"{DataDirectory}Test v1.zip", new MockFileData("")); // filesystem.AddDirectory($"{CacheDirectory}1/"); // var ds = new DirectoryService(Substitute.For>(), filesystem); // var archiveService = Substitute.For(); // archiveService.ExtractArchive($"{DataDirectory}Test v1.zip", // filesystem.Path.Join(CacheDirectory, "1")); // var cleanupService = new CacheService(_logger, _unitOfWork, ds, // new ReadingItemService(archiveService, Substitute.For(), Substitute.For(), ds)); // // await ResetDB(); // var s = DbFactory.Series("Test"); // var v = DbFactory.Volume("1"); // var c = new Chapter() // { // Number = "1", // Files = new List() // { // new MangaFile() // { // Format = MangaFormat.Archive, // FilePath = $"{DataDirectory}Test v1.zip", // } // } // }; // v.Chapters.Add(c); // s.Volumes.Add(v); // s.LibraryId = 1; // _context.Series.Add(s); // // await _context.SaveChangesAsync(); // // await cleanupService.Ensure(1); // Assert.Empty(ds.GetFiles(filesystem.Path.Join(CacheDirectory, "1"), searchOption:SearchOption.AllDirectories)); // } #endregion #region CleanupChapters [Fact] public void CleanupChapters_AllFilesShouldBeDeleted() { var filesystem = CreateFileSystem(); filesystem.AddDirectory($"{CacheDirectory}1/"); filesystem.AddFile($"{CacheDirectory}1/001.jpg", new MockFileData("")); filesystem.AddFile($"{CacheDirectory}1/002.jpg", new MockFileData("")); filesystem.AddFile($"{CacheDirectory}3/003.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); cleanupService.CleanupChapters(new []{1, 3}); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption:SearchOption.AllDirectories)); } #endregion #region GetCachedEpubFile [Fact] public void GetCachedEpubFile_ShouldReturnFirstEpub() { var filesystem = CreateFileSystem(); filesystem.AddDirectory($"{CacheDirectory}1/"); filesystem.AddFile($"{DataDirectory}1.epub", new MockFileData("")); filesystem.AddFile($"{DataDirectory}2.epub", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); var c = new Chapter() { Files = new List() { new MangaFile() { FilePath = $"{DataDirectory}1.epub" }, new MangaFile() { FilePath = $"{DataDirectory}2.epub" } } }; cs.GetCachedFile(c); Assert.Same($"{DataDirectory}1.epub", cs.GetCachedFile(c)); } #endregion #region GetCachedPagePath [Fact] public void GetCachedPagePath_ReturnNullIfNoFiles() { var filesystem = CreateFileSystem(); filesystem.AddDirectory($"{CacheDirectory}1/"); filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData("")); filesystem.AddFile($"{DataDirectory}2.zip", new MockFileData("")); var c = new Chapter() { Id = 1, Files = new List() }; var fileIndex = 0; foreach (var file in c.Files) { for (var i = 0; i < file.Pages - 1; i++) { filesystem.AddFile($"{CacheDirectory}1/{fileIndex}/{i+1}.jpg", new MockFileData("")); } fileIndex++; } var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); var path = cs.GetCachedPagePath(c, 11); Assert.Equal(string.Empty, path); } [Fact] public void GetCachedPagePath_GetFileFromFirstFile() { var filesystem = CreateFileSystem(); filesystem.AddDirectory($"{CacheDirectory}1/"); filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData("")); filesystem.AddFile($"{DataDirectory}2.zip", new MockFileData("")); var c = new Chapter() { Id = 1, Files = new List() { new MangaFile() { Id = 1, FilePath = $"{DataDirectory}1.zip", Pages = 10 }, new MangaFile() { Id = 2, FilePath = $"{DataDirectory}2.zip", Pages = 5 } } }; var fileIndex = 0; foreach (var file in c.Files) { for (var i = 0; i < file.Pages; i++) { filesystem.AddFile($"{CacheDirectory}1/00{fileIndex}_00{i+1}.jpg", new MockFileData("")); } fileIndex++; } var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/000_001.jpg"), ds.FileSystem.Path.GetFullPath(cs.GetCachedPagePath(c, 0))); } [Fact] public void GetCachedPagePath_GetLastPageFromSingleFile() { var filesystem = CreateFileSystem(); filesystem.AddDirectory($"{CacheDirectory}1/"); filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData("")); var c = new Chapter() { Id = 1, Files = new List() { new MangaFile() { Id = 1, FilePath = $"{DataDirectory}1.zip", Pages = 10 } } }; c.Pages = c.Files.Sum(f => f.Pages); var fileIndex = 0; foreach (var file in c.Files) { for (var i = 0; i < file.Pages; i++) { filesystem.AddFile($"{CacheDirectory}1/{fileIndex}/{i+1}.jpg", new MockFileData("")); } fileIndex++; } var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); // Remember that we start at 0, so this is the 10th file var path = cs.GetCachedPagePath(c, c.Pages); Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/000_0{c.Pages}.jpg"), ds.FileSystem.Path.GetFullPath(path)); } [Fact] public void GetCachedPagePath_GetFileFromSecondFile() { var filesystem = CreateFileSystem(); filesystem.AddDirectory($"{CacheDirectory}1/"); filesystem.AddFile($"{DataDirectory}1.zip", new MockFileData("")); filesystem.AddFile($"{DataDirectory}2.zip", new MockFileData("")); var c = new Chapter() { Id = 1, Files = new List() { new MangaFile() { Id = 1, FilePath = $"{DataDirectory}1.zip", Pages = 10 }, new MangaFile() { Id = 2, FilePath = $"{DataDirectory}2.zip", Pages = 5 } } }; var fileIndex = 0; foreach (var file in c.Files) { for (var i = 0; i < file.Pages; i++) { filesystem.AddFile($"{CacheDirectory}1/{fileIndex}/{i+1}.jpg", new MockFileData("")); } fileIndex++; } var ds = new DirectoryService(Substitute.For>(), filesystem); var cs = new CacheService(_logger, _unitOfWork, ds, new ReadingItemService(Substitute.For(), Substitute.For(), Substitute.For(), ds), Substitute.For()); // Flatten to prepare for how GetFullPath expects ds.Flatten($"{CacheDirectory}1/"); // Remember that we start at 0, so this is the page + 1 file var path = cs.GetCachedPagePath(c, 10); Assert.Equal(ds.FileSystem.Path.GetFullPath($"{CacheDirectory}/1/001_001.jpg"), ds.FileSystem.Path.GetFullPath(path)); } #endregion #region ExtractChapterFiles // [Fact] // public void ExtractChapterFiles_ShouldExtractOnlyImages() // { // const string testDirectory = "/manga/"; // var fileSystem = new MockFileSystem(); // for (var i = 0; i < 10; i++) // { // fileSystem.AddFile($"{testDirectory}file_{i}.zip", new MockFileData("")); // } // // fileSystem.AddDirectory(CacheDirectory); // // var ds = new DirectoryService(Substitute.For>(), fileSystem); // var cs = new CacheService(_logger, _unitOfWork, ds, // new MockReadingItemServiceForCacheService(ds)); // // // cs.ExtractChapterFiles(CacheDirectory, new List() // { // new MangaFile() // { // ChapterId = 1, // Format = MangaFormat.Archive, // Pages = 2, // FilePath = // } // }) // } #endregion } }