Progress Overhaul + Profile Page and a LOT more! (#4262)

Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo
2025-12-09 10:00:11 -07:00
committed by GitHub
parent 4ac13f1f25
commit 9f29fa593d
645 changed files with 25585 additions and 4805 deletions
@@ -1,177 +0,0 @@
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.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;
namespace API.Tests.Repository;
public class CollectionTagRepositoryTests
{
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 CollectionTagRepositoryTests()
{
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<AutoMapperProfiles>());
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<bool> SeedDb()
{
await _context.Database.MigrateAsync();
var filesystem = CreateFileSystem();
await Seed.SeedSettings(_context,
new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), 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<Library>()
{
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
// #region RemoveTagsWithoutSeries
//
// [Fact]
// public async Task RemoveTagsWithoutSeries_ShouldRemoveTags()
// {
// var library = new LibraryBuilder("Test", LibraryType.Manga).Build();
// var series = new SeriesBuilder("Test 1").Build();
// var commonTag = new AppUserCollectionBuilder("Tag 1").Build();
// series.Metadata.CollectionTags.Add(commonTag);
// series.Metadata.CollectionTags.Add(new AppUserCollectionBuilder("Tag 2").Build());
//
// var series2 = new SeriesBuilder("Test 1").Build();
// series2.Metadata.CollectionTags.Add(commonTag);
// library.Series.Add(series);
// library.Series.Add(series2);
// _unitOfWork.LibraryRepository.Add(library);
// await _unitOfWork.CommitAsync();
//
// Assert.Equal(2, series.Metadata.CollectionTags.Count);
// Assert.Single(series2.Metadata.CollectionTags);
//
// // Delete both series
// _unitOfWork.SeriesRepository.Remove(series);
// _unitOfWork.SeriesRepository.Remove(series2);
//
// await _unitOfWork.CommitAsync();
//
// // Validate that both tags exist
// Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count());
//
// await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
//
// Assert.Empty(await _unitOfWork.CollectionTagRepository.GetAllTagsAsync());
// }
//
// [Fact]
// public async Task RemoveTagsWithoutSeries_ShouldNotRemoveTags()
// {
// var library = new LibraryBuilder("Test", LibraryType.Manga).Build();
// var series = new SeriesBuilder("Test 1").Build();
// var commonTag = new AppUserCollectionBuilder("Tag 1").Build();
// series.Metadata.CollectionTags.Add(commonTag);
// series.Metadata.CollectionTags.Add(new AppUserCollectionBuilder("Tag 2").Build());
//
// var series2 = new SeriesBuilder("Test 1").Build();
// series2.Metadata.CollectionTags.Add(commonTag);
// library.Series.Add(series);
// library.Series.Add(series2);
// _unitOfWork.LibraryRepository.Add(library);
// await _unitOfWork.CommitAsync();
//
// Assert.Equal(2, series.Metadata.CollectionTags.Count);
// Assert.Single(series2.Metadata.CollectionTags);
//
// await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
//
// // Validate that both tags exist
// Assert.Equal(2, (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).Count());
// }
//
// #endregion
}
+19 -21
View File
@@ -6,10 +6,8 @@ using API.Data;
using API.DTOs.Metadata.Browse;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Helpers;
using API.Helpers.Builders;
using Polly;
using Xunit;
using Xunit.Abstractions;
@@ -18,7 +16,7 @@ namespace API.Tests.Repository;
public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper)
{
private TestGenreSet CreateTestGenres()
private static TestGenreSet CreateTestGenres()
{
return new TestGenreSet
{
@@ -117,7 +115,7 @@ public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTes
await context.SaveChangesAsync();
}
private async Task AssignLibrariesToUsers(DataContext context, AppUser fullAccess, AppUser restrictedAccess, AppUser restrictedAgeAccess)
private static async Task AssignLibrariesToUsers(DataContext context, AppUser fullAccess, AppUser restrictedAccess, AppUser restrictedAgeAccess)
{
var lib0 = context.Library.First(l => l.Name == "lib0");
var lib1 = context.Library.First(l => l.Name == "lib1");
@@ -151,11 +149,11 @@ public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTes
}
[Fact]
public async Task GetBrowseableGenrefullAccess_ReturnsAllGenresWithCorrectCounts()
public async Task GetBrowseableGenreFullAccess_ReturnsAllGenresWithCorrectCounts()
{
var genres = CreateTestGenres();
var (unitOfWork, context, mapper) = await CreateDatabase();
var (fullAccess, restrictedAccess, restrictedAgeAccess) = await Setup(context, genres);
var (unitOfWork, context, _) = await CreateDatabase();
var (fullAccess, _, _) = await Setup(context, genres);
// Act
var fullAccessGenres = await unitOfWork.GenreRepository.GetBrowseableGenre(fullAccess.Id, new UserParams());
@@ -178,8 +176,8 @@ public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTes
public async Task GetBrowseableGenre_RestrictedAccess_ReturnsOnlyAccessibleGenres()
{
var genres = CreateTestGenres();
var (unitOfWork, context, mapper) = await CreateDatabase();
var (fullAccess, restrictedAccess, restrictedAgeAccess) = await Setup(context, genres);
var (unitOfWork, context, _) = await CreateDatabase();
var (_, restrictedAccess, _) = await Setup(context, genres);
// Act
var restrictedAccessGenres = await unitOfWork.GenreRepository.GetBrowseableGenre(restrictedAccess.Id, new UserParams());
@@ -213,8 +211,8 @@ public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTes
public async Task GetBrowseableGenre_RestrictedAgeAccess_FiltersAgeRestrictedContent()
{
var genres = CreateTestGenres();
var (unitOfWork, context, mapper) = await CreateDatabase();
var (fullAccess, restrictedAccess, restrictedAgeAccess) = await Setup(context, genres);
var (unitOfWork, context, _) = await CreateDatabase();
var (_, _, restrictedAgeAccess) = await Setup(context, genres);
// Act
var restrictedAgeAccessGenres = await unitOfWork.GenreRepository.GetBrowseableGenre(restrictedAgeAccess.Id, new UserParams());
@@ -245,16 +243,16 @@ public class GenreRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTes
private class TestGenreSet
{
public Genre SharedSeriesChaptersGenre { get; set; }
public Genre SharedSeriesGenre { get; set; }
public Genre SharedChaptersGenre { get; set; }
public Genre Lib0SeriesChaptersGenre { get; set; }
public Genre Lib0SeriesGenre { get; set; }
public Genre Lib0ChaptersGenre { get; set; }
public Genre Lib1SeriesChaptersGenre { get; set; }
public Genre Lib1SeriesGenre { get; set; }
public Genre Lib1ChaptersGenre { get; set; }
public Genre Lib1ChapterAgeGenre { get; set; }
public Genre SharedSeriesChaptersGenre { get; init; }
public Genre SharedSeriesGenre { get; init; }
public Genre SharedChaptersGenre { get; init; }
public Genre Lib0SeriesChaptersGenre { get; init; }
public Genre Lib0SeriesGenre { get; init; }
public Genre Lib0ChaptersGenre { get; init; }
public Genre Lib1SeriesChaptersGenre { get; init; }
public Genre Lib1SeriesGenre { get; init; }
public Genre Lib1ChaptersGenre { get; init; }
public Genre Lib1ChapterAgeGenre { get; init; }
public List<Genre> GetAllGenres()
{
@@ -10,7 +10,6 @@ using API.Entities.Enums;
using API.Entities.Person;
using API.Helpers;
using API.Helpers.Builders;
using Polly;
using Xunit;
using Xunit.Abstractions;
@@ -19,7 +18,7 @@ namespace API.Tests.Repository;
public class PersonRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper)
{
private async Task<(AppUser, AppUser, AppUser)> Setup(DataContext context)
private static async Task<(AppUser, AppUser, AppUser)> Setup(DataContext context)
{
var fullAccess = new AppUserBuilder("amelia", "amelia@example.com").Build();
var restrictedAccess = new AppUserBuilder("mila", "mila@example.com").Build();
+11 -22
View File
@@ -1,35 +1,20 @@
using System.Collections.Generic;
using System.Data.Common;
using System.IO.Abstractions.TestingHelpers;
using System.Linq;
using System.Threading.Tasks;
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;
using Xunit.Abstractions;
namespace API.Tests.Repository;
#nullable enable
public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper): AbstractDbTest(testOutputHelper)
public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper) : AbstractDbTest(testOutputHelper)
{
private async Task SetupSeriesData(IUnitOfWork unitOfWork)
private static async Task SetupSeriesData(IUnitOfWork unitOfWork)
{
var library = new LibraryBuilder("GetFullSeriesByAnyName Manga", LibraryType.Manga)
.WithFolderPath(new FolderPathBuilder(DataDirectory+"manga/").Build())
.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)
@@ -70,11 +55,15 @@ public class SeriesRepositoryTests(ITestOutputHelper testOutputHelper): Abstract
}
[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
// Case 1: Prioritize existing ExternalSeries id
[InlineData(12345, null, 12345)]
// Case 2: Extract from weblink if no external series id
[InlineData(0, "https://anilist.co/manga/100664/Ijiranaide-Nagatorosan/", 100664)]
// Case 3: Return null if neither exist
[InlineData(0, "", null)]
public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int externalAniListId, string? webLinks, int? expectedAniListId)
{
var (unitOfWork, _, _) = await CreateDatabase();
await SetupSeriesData(unitOfWork);
+1 -2
View File
@@ -9,7 +9,6 @@ using API.Entities.Enums;
using API.Entities.Metadata;
using API.Helpers;
using API.Helpers.Builders;
using Polly;
using Xunit;
using Xunit.Abstractions;
@@ -18,7 +17,7 @@ namespace API.Tests.Repository;
public class TagRepositoryTests(ITestOutputHelper outputHelper): AbstractDbTest(outputHelper)
{
private TestTagSet CreateTestTags()
private static TestTagSet CreateTestTags()
{
return new TestTagSet
{