using System; 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.DTOs.Settings; using API.Entities; using API.Entities.Enums; using API.Helpers; using API.Helpers.Converters; using API.Services; using API.Services.Tasks; 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 CleanupServiceTests { private readonly ILogger _logger = Substitute.For>(); private readonly IUnitOfWork _unitOfWork; private readonly IEventHub _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 LogDirectory = "C:/kavita/config/logs/"; private const string BookmarkDirectory = "C:/kavita/config/bookmarks/"; public CleanupServiceTests() { 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; setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.BookmarkDirectory).SingleAsync(); setting.Value = BookmarkDirectory; setting = await _context.ServerSetting.Where(s => s.Key == ServerSettingKey.TotalLogs).SingleAsync(); setting.Value = "10"; _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()); _context.Users.RemoveRange(_context.Users.ToList()); _context.AppUserBookmark.RemoveRange(_context.AppUserBookmark.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(BookmarkDirectory); fileSystem.AddDirectory("C:/data/"); return fileSystem; } #endregion #region DeleteSeriesCoverImages [Fact] public async Task DeleteSeriesCoverImages_ShouldDeleteAll() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); var s = DbFactory.Series("Test 1"); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 2"); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 3"); s.CoverImage = $"{ImageService.GetSeriesFormat(1000)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteSeriesCoverImages(); Assert.Empty(ds.GetFiles(CoverImageDirectory)); } [Fact] public async Task DeleteSeriesCoverImages_ShouldNotDeleteLinkedFiles() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(3)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetSeriesFormat(1000)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); // Add 2 series with cover images var s = DbFactory.Series("Test 1"); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 2"); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteSeriesCoverImages(); Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); } #endregion #region DeleteChapterCoverImages [Fact] public async Task DeleteChapterCoverImages_ShouldNotDeleteLinkedFiles() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CoverImageDirectory}v01_c01.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}v01_c03.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}v01_c1000.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); // Add 2 series with cover images var s = DbFactory.Series("Test 1"); var v = DbFactory.Volume("1"); v.Chapters.Add(new Chapter() { CoverImage = "v01_c01.jpg" }); v.CoverImage = "v01_c01.jpg"; s.Volumes.Add(v); s.CoverImage = "series_01.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 2"); v = DbFactory.Volume("1"); v.Chapters.Add(new Chapter() { CoverImage = "v01_c03.jpg" }); v.CoverImage = "v01_c03jpg"; s.Volumes.Add(v); s.CoverImage = "series_03.jpg"; s.LibraryId = 1; _context.Series.Add(s); await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteChapterCoverImages(); Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); } #endregion #region DeleteTagCoverImages [Fact] public async Task DeleteTagCoverImages_ShouldNotDeleteLinkedFiles() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(2)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetCollectionTagFormat(1000)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); // Add 2 series with cover images var s = DbFactory.Series("Test 1"); s.Metadata.CollectionTags = new List(); s.Metadata.CollectionTags.Add(new CollectionTag() { Title = "Something", CoverImage = $"{ImageService.GetCollectionTagFormat(1)}.jpg" }); s.CoverImage = $"{ImageService.GetSeriesFormat(1)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); s = DbFactory.Series("Test 2"); s.Metadata.CollectionTags = new List(); s.Metadata.CollectionTags.Add(new CollectionTag() { Title = "Something 2", CoverImage = $"{ImageService.GetCollectionTagFormat(2)}.jpg" }); s.CoverImage = $"{ImageService.GetSeriesFormat(3)}.jpg"; s.LibraryId = 1; _context.Series.Add(s); await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteTagCoverImages(); Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); } #endregion #region DeleteReadingListCoverImages [Fact] public async Task DeleteReadingListCoverImages_ShouldNotDeleteLinkedFiles() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(1)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(2)}.jpg", new MockFileData("")); filesystem.AddFile($"{CoverImageDirectory}{ImageService.GetReadingListFormat(3)}.jpg", new MockFileData("")); // Delete all Series to reset state await ResetDB(); _context.Users.Add(new AppUser() { UserName = "Joe", ReadingLists = new List() { new ReadingList() { Title = "Something", NormalizedTitle = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something"), CoverImage = $"{ImageService.GetReadingListFormat(1)}.jpg" }, new ReadingList() { Title = "Something 2", NormalizedTitle = API.Services.Tasks.Scanner.Parser.Parser.Normalize("Something 2"), CoverImage = $"{ImageService.GetReadingListFormat(2)}.jpg" } } }); await _context.SaveChangesAsync(); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.DeleteReadingListCoverImages(); Assert.Equal(2, ds.GetFiles(CoverImageDirectory).Count()); } #endregion #region CleanupCacheDirectory [Fact] public void CleanupCacheDirectory_ClearAllFiles() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CacheDirectory}01.jpg", new MockFileData("")); filesystem.AddFile($"{CacheDirectory}02.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); cleanupService.CleanupCacheAndTempDirectories(); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] public void CleanupCacheDirectory_ClearAllFilesInSubDirectory() { var filesystem = CreateFileSystem(); filesystem.AddFile($"{CacheDirectory}01.jpg", new MockFileData("")); filesystem.AddFile($"{CacheDirectory}subdir/02.jpg", new MockFileData("")); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); cleanupService.CleanupCacheAndTempDirectories(); Assert.Empty(ds.GetFiles(CacheDirectory, searchOption: SearchOption.AllDirectories)); } #endregion #region CleanupBackups [Fact] public async Task CleanupBackups_LeaveOneFile_SinceAllAreExpired() { var filesystem = CreateFileSystem(); var filesystemFile = new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31)) }; filesystem.AddFile($"{BackupDirectory}kavita_backup_11_29_2021_12_00_13 AM.zip", filesystemFile); filesystem.AddFile($"{BackupDirectory}kavita_backup_12_3_2021_9_27_58 AM.zip", filesystemFile); filesystem.AddFile($"{BackupDirectory}randomfile.zip", filesystemFile); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupBackups(); Assert.Single(ds.GetFiles(BackupDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] public async Task CleanupBackups_LeaveLestExpired() { var filesystem = CreateFileSystem(); var filesystemFile = new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31)) }; filesystem.AddFile($"{BackupDirectory}kavita_backup_11_29_2021_12_00_13 AM.zip", filesystemFile); filesystem.AddFile($"{BackupDirectory}kavita_backup_12_3_2021_9_27_58 AM.zip", filesystemFile); filesystem.AddFile($"{BackupDirectory}randomfile.zip", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(14)) }); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupBackups(); Assert.True(filesystem.File.Exists($"{BackupDirectory}randomfile.zip")); } #endregion #region CleanupLogs [Fact] public async Task CleanupLogs_LeaveOneFile_SinceAllAreExpired() { var filesystem = CreateFileSystem(); foreach (var i in Enumerable.Range(1, 10)) { var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}"); filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31)) }); } var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupLogs(); Assert.Single(ds.GetFiles(LogDirectory, searchOption: SearchOption.AllDirectories)); } [Fact] public async Task CleanupLogs_LeaveLestExpired() { var filesystem = CreateFileSystem(); foreach (var i in Enumerable.Range(1, 9)) { var day = API.Services.Tasks.Scanner.Parser.Parser.PadZeros($"{i}"); filesystem.AddFile($"{LogDirectory}kavita202009{day}.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - i)) }); } filesystem.AddFile($"{LogDirectory}kavita20200910.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - 10)) }); filesystem.AddFile($"{LogDirectory}kavita20200911.log", new MockFileData("") { CreationTime = DateTimeOffset.Now.Subtract(TimeSpan.FromDays(31 - 11)) }); var ds = new DirectoryService(Substitute.For>(), filesystem); var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, ds); await cleanupService.CleanupLogs(); Assert.True(filesystem.File.Exists($"{LogDirectory}kavita20200911.log")); } #endregion // #region CleanupBookmarks // // [Fact] // public async Task CleanupBookmarks_LeaveAllFiles() // { // var filesystem = CreateFileSystem(); // filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("")); // filesystem.AddFile($"{BookmarkDirectory}1/1/1/0002.jpg", new MockFileData("")); // // // Delete all Series to reset state // 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() // { // // } // } // } // } // }); // // await _context.SaveChangesAsync(); // // _context.AppUser.Add(new AppUser() // { // Bookmarks = new List() // { // new AppUserBookmark() // { // AppUserId = 1, // ChapterId = 1, // Page = 1, // FileName = "1/1/1/0001.jpg", // SeriesId = 1, // VolumeId = 1 // }, // new AppUserBookmark() // { // AppUserId = 1, // ChapterId = 1, // Page = 2, // FileName = "1/1/1/0002.jpg", // SeriesId = 1, // VolumeId = 1 // } // } // }); // // await _context.SaveChangesAsync(); // // // var ds = new DirectoryService(Substitute.For>(), filesystem); // var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, // ds); // // await cleanupService.CleanupBookmarks(); // // Assert.Equal(2, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); // // } // // [Fact] // public async Task CleanupBookmarks_LeavesOneFiles() // { // var filesystem = CreateFileSystem(); // filesystem.AddFile($"{BookmarkDirectory}1/1/1/0001.jpg", new MockFileData("")); // filesystem.AddFile($"{BookmarkDirectory}1/1/2/0002.jpg", new MockFileData("")); // // // Delete all Series to reset state // 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() // { // // } // } // } // } // }); // // await _context.SaveChangesAsync(); // // _context.AppUser.Add(new AppUser() // { // Bookmarks = new List() // { // new AppUserBookmark() // { // AppUserId = 1, // ChapterId = 1, // Page = 1, // FileName = "1/1/1/0001.jpg", // SeriesId = 1, // VolumeId = 1 // } // } // }); // // await _context.SaveChangesAsync(); // // // var ds = new DirectoryService(Substitute.For>(), filesystem); // var cleanupService = new CleanupService(_logger, _unitOfWork, _messageHub, // ds); // // await cleanupService.CleanupBookmarks(); // // Assert.Equal(1, ds.GetFiles(BookmarkDirectory, searchOption:SearchOption.AllDirectories).Count()); // Assert.Equal(1, ds.FileSystem.Directory.GetDirectories($"{BookmarkDirectory}1/1/").Length); // } // // #endregion }