using System.Collections.Generic; using System.Data.Common; using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading.Tasks; using API.Data; using API.Entities; using API.Entities.Enums; using API.Entities.Metadata; using API.Helpers; using API.Helpers.Builders; using API.Services; using AutoMapper; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; namespace API.Tests.Repository; #nullable enable public class SeriesRepositoryTests { private readonly IUnitOfWork _unitOfWork; 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 SeriesRepositoryTests() { 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; } 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); var lib = new LibraryBuilder("Manga") .WithFolderPath(new FolderPathBuilder("C:/data/").Build()) .Build(); _context.AppUser.Add(new AppUser() { UserName = "majora2007", Libraries = new List() { lib } }); return await _context.SaveChangesAsync() > 0; } private async Task ResetDb() { _context.Series.RemoveRange(_context.Series.ToList()); _context.AppUserRating.RemoveRange(_context.AppUserRating.ToList()); _context.Genre.RemoveRange(_context.Genre.ToList()); _context.CollectionTag.RemoveRange(_context.CollectionTag.ToList()); _context.Person.RemoveRange(_context.Person.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 private async Task SetupSeriesData() { var library = new LibraryBuilder("GetFullSeriesByAnyName Manga", LibraryType.Manga) .WithFolderPath(new FolderPathBuilder("C:/data/manga/").Build()) .WithSeries(new SeriesBuilder("The Idaten Deities Know Only Peace") .WithLocalizedName("Heion Sedai no Idaten-tachi") .WithFormat(MangaFormat.Archive) .Build()) .WithSeries(new SeriesBuilder("Hitomi-chan is Shy With Strangers") .WithLocalizedName("Hitomi-chan wa Hitomishiri") .WithFormat(MangaFormat.Archive) .Build()) .Build(); _unitOfWork.LibraryRepository.Add(library); await _unitOfWork.CommitAsync(); } [Theory] [InlineData("The Idaten Deities Know Only Peace", MangaFormat.Archive, "", "The Idaten Deities Know Only Peace")] // Matching on series name in DB [InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Archive, "The Idaten Deities Know Only Peace", "The Idaten Deities Know Only Peace")] // Matching on localized name in DB [InlineData("Heion Sedai no Idaten-tachi", MangaFormat.Pdf, "", null)] [InlineData("Hitomi-chan wa Hitomishiri", MangaFormat.Archive, "", "Hitomi-chan is Shy With Strangers")] public async Task GetFullSeriesByAnyName_Should(string seriesName, MangaFormat format, string localizedName, string? expected) { await ResetDb(); await SetupSeriesData(); var series = await _unitOfWork.SeriesRepository.GetFullSeriesByAnyName(seriesName, localizedName, 2, format, false); if (expected == null) { Assert.Null(series); } else { Assert.NotNull(series); Assert.Equal(expected, series.Name); } } [Theory] [InlineData(12345, null, 12345)] // Case 1: Prioritize existing ExternalSeries id [InlineData(0, "https://anilist.co/manga/100664/Ijiranaide-Nagatorosan/", 100664)] // Case 2: Extract from weblink if no external series id [InlineData(0, "", null)] // Case 3: Return null if neither exist public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int externalAniListId, string? webLinks, int? expectedAniListId) { // Arrange await ResetDb(); var series = new SeriesBuilder("Test Series") .WithFormat(MangaFormat.Archive) .Build(); var library = new LibraryBuilder("Test Library", LibraryType.Manga) .WithFolderPath(new FolderPathBuilder("C:/data/manga/").Build()) .WithSeries(series) .Build(); // Set up ExternalSeriesMetadata series.ExternalSeriesMetadata = new ExternalSeriesMetadata() { AniListId = externalAniListId, CbrId = 0, MalId = 0, GoogleBooksId = string.Empty }; // Set up SeriesMetadata with WebLinks series.Metadata = new SeriesMetadata() { WebLinks = webLinks, ReleaseYear = 2021 }; _unitOfWork.LibraryRepository.Add(library); _unitOfWork.SeriesRepository.Add(series); await _unitOfWork.CommitAsync(); // Act var result = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(series.Id); // Assert Assert.NotNull(result); Assert.Equal(expectedAniListId, result.AniListId); Assert.Equal("Test Series", result.SeriesName); } // TODO: GetSeriesDtoForLibraryIdV2Async Tests (On Deck) }