Merged develop in
This commit is contained in:
commit
7c692a1b46
580 changed files with 21233 additions and 9031 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
# Editor configuration, see https://editorconfig.org
|
# Editor configuration, see https://editorconfig.org
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
@ -22,3 +23,7 @@ indent_size = 2
|
||||||
|
|
||||||
[*.csproj]
|
[*.csproj]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
# Disable SonarLint warning S1075 (Don't use hardcoded url)
|
||||||
|
dotnet_diagnostic.S1075.severity = none
|
||||||
|
|
|
||||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -25,10 +25,10 @@ body:
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Kavita Version Number - If you don not see your version number listed, please update Kavita and see if your issue still persists.
|
label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists.
|
||||||
multiple: false
|
multiple: false
|
||||||
options:
|
options:
|
||||||
- 0.8.5.3 - Stable
|
- 0.8.5.11 - Stable
|
||||||
- Nightly Testing Branch
|
- Nightly Testing Branch
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.11" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.11" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.13" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
|
|
@ -12,7 +10,6 @@ using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
@ -21,24 +18,13 @@ using NSubstitute;
|
||||||
|
|
||||||
namespace API.Tests;
|
namespace API.Tests;
|
||||||
|
|
||||||
public abstract class AbstractDbTest : IDisposable
|
public abstract class AbstractDbTest : AbstractFsTest , IDisposable
|
||||||
{
|
{
|
||||||
protected readonly DbConnection _connection;
|
protected readonly DbConnection _connection;
|
||||||
protected readonly DataContext _context;
|
protected readonly DataContext _context;
|
||||||
protected readonly IUnitOfWork _unitOfWork;
|
protected readonly IUnitOfWork _unitOfWork;
|
||||||
protected readonly IMapper _mapper;
|
protected readonly IMapper _mapper;
|
||||||
|
|
||||||
|
|
||||||
protected const string CacheDirectory = "C:/kavita/config/cache/";
|
|
||||||
protected const string CacheLongDirectory = "C:/kavita/config/cache-long/";
|
|
||||||
protected const string CoverImageDirectory = "C:/kavita/config/covers/";
|
|
||||||
protected const string BackupDirectory = "C:/kavita/config/backups/";
|
|
||||||
protected const string LogDirectory = "C:/kavita/config/logs/";
|
|
||||||
protected const string BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
|
||||||
protected const string SiteThemeDirectory = "C:/kavita/config/themes/";
|
|
||||||
protected const string TempDirectory = "C:/kavita/config/temp/";
|
|
||||||
protected const string DataDirectory = "C:/data/";
|
|
||||||
|
|
||||||
protected AbstractDbTest()
|
protected AbstractDbTest()
|
||||||
{
|
{
|
||||||
var contextOptions = new DbContextOptionsBuilder<DataContext>()
|
var contextOptions = new DbContextOptionsBuilder<DataContext>()
|
||||||
|
|
@ -113,27 +99,24 @@ public abstract class AbstractDbTest : IDisposable
|
||||||
|
|
||||||
protected abstract Task ResetDb();
|
protected abstract Task ResetDb();
|
||||||
|
|
||||||
protected static MockFileSystem CreateFileSystem()
|
|
||||||
{
|
|
||||||
var fileSystem = new MockFileSystem();
|
|
||||||
fileSystem.Directory.SetCurrentDirectory("C:/kavita/");
|
|
||||||
fileSystem.AddDirectory("C:/kavita/config/");
|
|
||||||
fileSystem.AddDirectory(CacheDirectory);
|
|
||||||
fileSystem.AddDirectory(CacheLongDirectory);
|
|
||||||
fileSystem.AddDirectory(CoverImageDirectory);
|
|
||||||
fileSystem.AddDirectory(BackupDirectory);
|
|
||||||
fileSystem.AddDirectory(BookmarkDirectory);
|
|
||||||
fileSystem.AddDirectory(SiteThemeDirectory);
|
|
||||||
fileSystem.AddDirectory(LogDirectory);
|
|
||||||
fileSystem.AddDirectory(TempDirectory);
|
|
||||||
fileSystem.AddDirectory(DataDirectory);
|
|
||||||
|
|
||||||
return fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_context.Dispose();
|
_context.Dispose();
|
||||||
_connection.Dispose();
|
_connection.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a role to an existing User. Commits.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <param name="roleName"></param>
|
||||||
|
protected async Task AddUserWithRole(int userId, string roleName)
|
||||||
|
{
|
||||||
|
var role = new AppRole { Id = userId, Name = roleName, NormalizedName = roleName.ToUpper() };
|
||||||
|
|
||||||
|
await _context.Roles.AddAsync(role);
|
||||||
|
await _context.UserRoles.AddAsync(new AppUserRole { UserId = userId, RoleId = userId });
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
API.Tests/AbstractFsTest.cs
Normal file
43
API.Tests/AbstractFsTest.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
|
namespace API.Tests;
|
||||||
|
|
||||||
|
public abstract class AbstractFsTest
|
||||||
|
{
|
||||||
|
|
||||||
|
protected static readonly string Root = Parser.NormalizePath(Path.GetPathRoot(Directory.GetCurrentDirectory()));
|
||||||
|
protected static readonly string ConfigDirectory = Root + "kavita/config/";
|
||||||
|
protected static readonly string CacheDirectory = ConfigDirectory + "cache/";
|
||||||
|
protected static readonly string CacheLongDirectory = ConfigDirectory + "cache-long/";
|
||||||
|
protected static readonly string CoverImageDirectory = ConfigDirectory + "covers/";
|
||||||
|
protected static readonly string BackupDirectory = ConfigDirectory + "backups/";
|
||||||
|
protected static readonly string LogDirectory = ConfigDirectory + "logs/";
|
||||||
|
protected static readonly string BookmarkDirectory = ConfigDirectory + "bookmarks/";
|
||||||
|
protected static readonly string SiteThemeDirectory = ConfigDirectory + "themes/";
|
||||||
|
protected static readonly string TempDirectory = ConfigDirectory + "temp/";
|
||||||
|
protected static readonly string ThemesDirectory = ConfigDirectory + "theme";
|
||||||
|
protected static readonly string DataDirectory = Root + "data/";
|
||||||
|
|
||||||
|
protected static MockFileSystem CreateFileSystem()
|
||||||
|
{
|
||||||
|
var fileSystem = new MockFileSystem();
|
||||||
|
fileSystem.Directory.SetCurrentDirectory(Root + "kavita/");
|
||||||
|
fileSystem.AddDirectory(Root + "kavita/config/");
|
||||||
|
fileSystem.AddDirectory(CacheDirectory);
|
||||||
|
fileSystem.AddDirectory(CacheLongDirectory);
|
||||||
|
fileSystem.AddDirectory(CoverImageDirectory);
|
||||||
|
fileSystem.AddDirectory(BackupDirectory);
|
||||||
|
fileSystem.AddDirectory(BookmarkDirectory);
|
||||||
|
fileSystem.AddDirectory(SiteThemeDirectory);
|
||||||
|
fileSystem.AddDirectory(LogDirectory);
|
||||||
|
fileSystem.AddDirectory(TempDirectory);
|
||||||
|
fileSystem.AddDirectory(DataDirectory);
|
||||||
|
fileSystem.AddDirectory(ThemesDirectory);
|
||||||
|
|
||||||
|
return fileSystem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
using Hangfire;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Converters;
|
namespace API.Tests.Converters;
|
||||||
|
|
|
||||||
31
API.Tests/Extensions/EncodeFormatExtensionsTests.cs
Normal file
31
API.Tests/Extensions/EncodeFormatExtensionsTests.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class EncodeFormatExtensionsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetExtension_ShouldReturnCorrectExtensionForAllValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedExtensions = new Dictionary<EncodeFormat, string>
|
||||||
|
{
|
||||||
|
{ EncodeFormat.PNG, ".png" },
|
||||||
|
{ EncodeFormat.WEBP, ".webp" },
|
||||||
|
{ EncodeFormat.AVIF, ".avif" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
foreach (var format in Enum.GetValues(typeof(EncodeFormat)).Cast<EncodeFormat>())
|
||||||
|
{
|
||||||
|
var extension = format.GetExtension();
|
||||||
|
Assert.Equal(expectedExtensions[format], extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.Tests.Helpers;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Data;
|
|
||||||
using API.Data.Misc;
|
using API.Data.Misc;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Person;
|
||||||
using API.Extensions;
|
|
||||||
using API.Extensions.QueryExtensions;
|
using API.Extensions.QueryExtensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
|
|
@ -932,7 +932,8 @@ public class SeriesFilterTests : AbstractDbTest
|
||||||
|
|
||||||
var seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
var seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
||||||
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
|
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
|
||||||
Substitute.For<IScrobblingService>(), Substitute.For<ILocalizationService>());
|
Substitute.For<IScrobblingService>(), Substitute.For<ILocalizationService>(),
|
||||||
|
Substitute.For<IReadingListService>());
|
||||||
|
|
||||||
// Select 0 Rating
|
// Select 0 Rating
|
||||||
var zeroRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2);
|
var zeroRating = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2);
|
||||||
|
|
|
||||||
81
API.Tests/Extensions/VersionExtensionTests.cs
Normal file
81
API.Tests/Extensions/VersionExtensionTests.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
using System;
|
||||||
|
using API.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Extensions;
|
||||||
|
|
||||||
|
public class VersionHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CompareWithoutRevision_ShouldReturnTrue_WhenMajorMinorBuildMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var v1 = new Version(1, 2, 3, 4);
|
||||||
|
var v2 = new Version(1, 2, 3, 5);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = v1.CompareWithoutRevision(v2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CompareWithoutRevision_ShouldHandleBuildlessVersions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var v1 = new Version(1, 2);
|
||||||
|
var v2 = new Version(1, 2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = v1.CompareWithoutRevision(v2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1, 2, 3, 1, 2, 4)]
|
||||||
|
[InlineData(1, 2, 3, 1, 2, 0)]
|
||||||
|
public void CompareWithoutRevision_ShouldReturnFalse_WhenBuildDiffers(
|
||||||
|
int major1, int minor1, int build1,
|
||||||
|
int major2, int minor2, int build2)
|
||||||
|
{
|
||||||
|
var v1 = new Version(major1, minor1, build1);
|
||||||
|
var v2 = new Version(major2, minor2, build2);
|
||||||
|
|
||||||
|
var result = v1.CompareWithoutRevision(v2);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1, 2, 3, 1, 3, 3)]
|
||||||
|
[InlineData(1, 2, 3, 1, 0, 3)]
|
||||||
|
public void CompareWithoutRevision_ShouldReturnFalse_WhenMinorDiffers(
|
||||||
|
int major1, int minor1, int build1,
|
||||||
|
int major2, int minor2, int build2)
|
||||||
|
{
|
||||||
|
var v1 = new Version(major1, minor1, build1);
|
||||||
|
var v2 = new Version(major2, minor2, build2);
|
||||||
|
|
||||||
|
var result = v1.CompareWithoutRevision(v2);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1, 2, 3, 2, 2, 3)]
|
||||||
|
[InlineData(1, 2, 3, 0, 2, 3)]
|
||||||
|
public void CompareWithoutRevision_ShouldReturnFalse_WhenMajorDiffers(
|
||||||
|
int major1, int minor1, int build1,
|
||||||
|
int major2, int minor2, int build2)
|
||||||
|
{
|
||||||
|
var v1 = new Version(major1, minor1, build1);
|
||||||
|
var v2 = new Version(major2, minor2, build2);
|
||||||
|
|
||||||
|
var result = v1.CompareWithoutRevision(v2);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Tests.Helpers;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Extensions;
|
namespace API.Tests.Extensions;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
|
|
@ -11,9 +10,9 @@ using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Helpers;
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
public class CacheHelperTests
|
public class CacheHelperTests: AbstractFsTest
|
||||||
{
|
{
|
||||||
private const string TestCoverImageDirectory = @"c:\";
|
private static readonly string TestCoverImageDirectory = Root;
|
||||||
private const string TestCoverImageFile = "thumbnail.jpg";
|
private const string TestCoverImageFile = "thumbnail.jpg";
|
||||||
private readonly string _testCoverPath = Path.Join(TestCoverImageDirectory, TestCoverImageFile);
|
private readonly string _testCoverPath = Path.Join(TestCoverImageDirectory, TestCoverImageFile);
|
||||||
private const string TestCoverArchive = @"file in folder.zip";
|
private const string TestCoverArchive = @"file in folder.zip";
|
||||||
|
|
@ -37,24 +36,29 @@ public class CacheHelperTests
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("", false)]
|
[InlineData("", false)]
|
||||||
[InlineData("C:/", false)]
|
|
||||||
[InlineData(null, false)]
|
[InlineData(null, false)]
|
||||||
public void CoverImageExists_DoesFileExist(string coverImage, bool exists)
|
public void CoverImageExists_DoesFileExist(string coverImage, bool exists)
|
||||||
{
|
{
|
||||||
Assert.Equal(exists, _cacheHelper.CoverImageExists(coverImage));
|
Assert.Equal(exists, _cacheHelper.CoverImageExists(coverImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CoverImageExists_DoesFileExistRoot()
|
||||||
|
{
|
||||||
|
Assert.False(_cacheHelper.CoverImageExists(Root));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CoverImageExists_FileExists()
|
public void CoverImageExists_FileExists()
|
||||||
{
|
{
|
||||||
Assert.True(_cacheHelper.CoverImageExists(TestCoverArchive));
|
Assert.True(_cacheHelper.CoverImageExists(Path.Join(TestCoverImageDirectory, TestCoverArchive)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShouldUpdateCoverImage_OnFirstRun()
|
public void ShouldUpdateCoverImage_OnFirstRun()
|
||||||
{
|
{
|
||||||
|
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(DateTime.Now)
|
.WithLastModified(DateTime.Now)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.True(_cacheHelper.ShouldUpdateCoverImage(null, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
Assert.True(_cacheHelper.ShouldUpdateCoverImage(null, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||||
|
|
@ -65,7 +69,7 @@ public class CacheHelperTests
|
||||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked()
|
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked()
|
||||||
{
|
{
|
||||||
// Represents first run
|
// Represents first run
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(DateTime.Now)
|
.WithLastModified(DateTime.Now)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||||
|
|
@ -76,7 +80,7 @@ public class CacheHelperTests
|
||||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked_2()
|
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetNotLocked_2()
|
||||||
{
|
{
|
||||||
// Represents first run
|
// Represents first run
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(DateTime.Now)
|
.WithLastModified(DateTime.Now)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now,
|
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now,
|
||||||
|
|
@ -87,7 +91,7 @@ public class CacheHelperTests
|
||||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked()
|
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked()
|
||||||
{
|
{
|
||||||
// Represents first run
|
// Represents first run
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(DateTime.Now)
|
.WithLastModified(DateTime.Now)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||||
|
|
@ -98,7 +102,7 @@ public class CacheHelperTests
|
||||||
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked_Modified()
|
public void ShouldUpdateCoverImage_ShouldNotUpdateOnSecondRunWithCoverImageSetLocked_Modified()
|
||||||
{
|
{
|
||||||
// Represents first run
|
// Represents first run
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(DateTime.Now)
|
.WithLastModified(DateTime.Now)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
Assert.False(_cacheHelper.ShouldUpdateCoverImage(_testCoverPath, file, DateTime.Now.Subtract(TimeSpan.FromMinutes(1)),
|
||||||
|
|
@ -122,7 +126,7 @@ public class CacheHelperTests
|
||||||
var cacheHelper = new CacheHelper(fileService);
|
var cacheHelper = new CacheHelper(fileService);
|
||||||
|
|
||||||
var created = DateTime.Now.Subtract(TimeSpan.FromHours(1));
|
var created = DateTime.Now.Subtract(TimeSpan.FromHours(1));
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(DateTime.Now.Subtract(TimeSpan.FromMinutes(1)))
|
.WithLastModified(DateTime.Now.Subtract(TimeSpan.FromMinutes(1)))
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|
@ -133,9 +137,10 @@ public class CacheHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceCreated()
|
public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceCreated()
|
||||||
{
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
var filesystemFile = new MockFileData("")
|
var filesystemFile = new MockFileData("")
|
||||||
{
|
{
|
||||||
LastWriteTime = DateTimeOffset.Now
|
LastWriteTime =now,
|
||||||
};
|
};
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
|
|
@ -147,12 +152,12 @@ public class CacheHelperTests
|
||||||
var cacheHelper = new CacheHelper(fileService);
|
var cacheHelper = new CacheHelper(fileService);
|
||||||
|
|
||||||
var chapter = new ChapterBuilder("1")
|
var chapter = new ChapterBuilder("1")
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.WithCreated(filesystemFile.LastWriteTime.DateTime)
|
.WithCreated(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
@ -160,9 +165,10 @@ public class CacheHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceLastModified()
|
public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceLastModified()
|
||||||
{
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
var filesystemFile = new MockFileData("")
|
var filesystemFile = new MockFileData("")
|
||||||
{
|
{
|
||||||
LastWriteTime = DateTimeOffset.Now
|
LastWriteTime = now,
|
||||||
};
|
};
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
|
|
@ -174,12 +180,12 @@ public class CacheHelperTests
|
||||||
var cacheHelper = new CacheHelper(fileService);
|
var cacheHelper = new CacheHelper(fileService);
|
||||||
|
|
||||||
var chapter = new ChapterBuilder("1")
|
var chapter = new ChapterBuilder("1")
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.WithCreated(filesystemFile.LastWriteTime.DateTime)
|
.WithCreated(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
Assert.True(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
|
|
@ -188,9 +194,10 @@ public class CacheHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceLastModified_ForceUpdate()
|
public void HasFileNotChangedSinceCreationOrLastScan_NotChangedSinceLastModified_ForceUpdate()
|
||||||
{
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
var filesystemFile = new MockFileData("")
|
var filesystemFile = new MockFileData("")
|
||||||
{
|
{
|
||||||
LastWriteTime = DateTimeOffset.Now
|
LastWriteTime = now.DateTime,
|
||||||
};
|
};
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
|
|
@ -202,12 +209,12 @@ public class CacheHelperTests
|
||||||
var cacheHelper = new CacheHelper(fileService);
|
var cacheHelper = new CacheHelper(fileService);
|
||||||
|
|
||||||
var chapter = new ChapterBuilder("1")
|
var chapter = new ChapterBuilder("1")
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.WithCreated(filesystemFile.LastWriteTime.DateTime)
|
.WithCreated(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, true, file));
|
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, true, file));
|
||||||
}
|
}
|
||||||
|
|
@ -215,10 +222,11 @@ public class CacheHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsFileUnmodifiedSinceCreationOrLastScan_ModifiedSinceLastScan()
|
public void IsFileUnmodifiedSinceCreationOrLastScan_ModifiedSinceLastScan()
|
||||||
{
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
var filesystemFile = new MockFileData("")
|
var filesystemFile = new MockFileData("")
|
||||||
{
|
{
|
||||||
LastWriteTime = DateTimeOffset.Now,
|
LastWriteTime = now.DateTime,
|
||||||
CreationTime = DateTimeOffset.Now
|
CreationTime = now.DateTime
|
||||||
};
|
};
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
|
|
@ -234,8 +242,8 @@ public class CacheHelperTests
|
||||||
.WithCreated(DateTime.Now.Subtract(TimeSpan.FromMinutes(10)))
|
.WithCreated(DateTime.Now.Subtract(TimeSpan.FromMinutes(10)))
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var file = new MangaFileBuilder(TestCoverArchive, MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
}
|
}
|
||||||
|
|
@ -243,9 +251,10 @@ public class CacheHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HasFileNotChangedSinceCreationOrLastScan_ModifiedSinceLastScan_ButLastModifiedSame()
|
public void HasFileNotChangedSinceCreationOrLastScan_ModifiedSinceLastScan_ButLastModifiedSame()
|
||||||
{
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
var filesystemFile = new MockFileData("")
|
var filesystemFile = new MockFileData("")
|
||||||
{
|
{
|
||||||
LastWriteTime = DateTimeOffset.Now
|
LastWriteTime =now.DateTime
|
||||||
};
|
};
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
|
|
@ -262,7 +271,7 @@ public class CacheHelperTests
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
var file = new MangaFileBuilder(Path.Join(TestCoverImageDirectory, TestCoverArchive), MangaFormat.Archive)
|
||||||
.WithLastModified(filesystemFile.LastWriteTime.DateTime)
|
.WithLastModified(now.DateTime)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
Assert.False(cacheHelper.IsFileUnmodifiedSinceCreationOrLastScan(chapter, false, file));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
|
|
@ -49,17 +50,14 @@ public class OrderableHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReorderItems_InvalidPosition_NoChange()
|
public void ReorderItems_InvalidPosition_NoChange()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var items = new List<AppUserSideNavStream>
|
var items = new List<AppUserSideNavStream>
|
||||||
{
|
{
|
||||||
new AppUserSideNavStream { Id = 1, Order = 0, Name = "A" },
|
new AppUserSideNavStream { Id = 1, Order = 0, Name = "A" },
|
||||||
new AppUserSideNavStream { Id = 2, Order = 1, Name = "A" },
|
new AppUserSideNavStream { Id = 2, Order = 1, Name = "A" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
|
||||||
OrderableHelper.ReorderItems(items, 2, 3); // Position 3 is out of range
|
OrderableHelper.ReorderItems(items, 2, 3); // Position 3 is out of range
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(1, items[0].Id); // Item 1 should remain at position 0
|
Assert.Equal(1, items[0].Id); // Item 1 should remain at position 0
|
||||||
Assert.Equal(2, items[1].Id); // Item 2 should remain at position 1
|
Assert.Equal(2, items[1].Id); // Item 2 should remain at position 1
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +78,6 @@ public class OrderableHelperTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReorderItems_DoubleMove()
|
public void ReorderItems_DoubleMove()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var items = new List<AppUserSideNavStream>
|
var items = new List<AppUserSideNavStream>
|
||||||
{
|
{
|
||||||
new AppUserSideNavStream { Id = 1, Order = 0, Name = "0" },
|
new AppUserSideNavStream { Id = 1, Order = 0, Name = "0" },
|
||||||
|
|
@ -94,7 +91,6 @@ public class OrderableHelperTests
|
||||||
// Move 4 -> 1
|
// Move 4 -> 1
|
||||||
OrderableHelper.ReorderItems(items, 5, 1);
|
OrderableHelper.ReorderItems(items, 5, 1);
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(1, items[0].Id);
|
Assert.Equal(1, items[0].Id);
|
||||||
Assert.Equal(0, items[0].Order);
|
Assert.Equal(0, items[0].Order);
|
||||||
Assert.Equal(5, items[1].Id);
|
Assert.Equal(5, items[1].Id);
|
||||||
|
|
@ -109,4 +105,98 @@ public class OrderableHelperTests
|
||||||
|
|
||||||
Assert.Equal("034125", string.Join("", items.Select(s => s.Name)));
|
Assert.Equal("034125", string.Join("", items.Select(s => s.Name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ReadingListItem> CreateTestReadingListItems(int count = 4)
|
||||||
|
{
|
||||||
|
var items = new List<ReadingListItem>();
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
items.Add(new ReadingListItem() { Id = i + 1, Order = count, ReadingListId = i + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReorderItems_MoveItemToBeginning_CorrectOrder()
|
||||||
|
{
|
||||||
|
var items = CreateTestReadingListItems();
|
||||||
|
|
||||||
|
OrderableHelper.ReorderItems(items, 3, 0);
|
||||||
|
|
||||||
|
Assert.Equal(3, items[0].Id);
|
||||||
|
Assert.Equal(1, items[1].Id);
|
||||||
|
Assert.Equal(2, items[2].Id);
|
||||||
|
Assert.Equal(4, items[3].Id);
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(i, items[i].Order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReorderItems_MoveItemToEnd_CorrectOrder()
|
||||||
|
{
|
||||||
|
var items = CreateTestReadingListItems();
|
||||||
|
|
||||||
|
OrderableHelper.ReorderItems(items, 1, 3);
|
||||||
|
|
||||||
|
Assert.Equal(2, items[0].Id);
|
||||||
|
Assert.Equal(3, items[1].Id);
|
||||||
|
Assert.Equal(4, items[2].Id);
|
||||||
|
Assert.Equal(1, items[3].Id);
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(i, items[i].Order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReorderItems_MoveItemToMiddle_CorrectOrder()
|
||||||
|
{
|
||||||
|
var items = CreateTestReadingListItems();
|
||||||
|
|
||||||
|
OrderableHelper.ReorderItems(items, 4, 2);
|
||||||
|
|
||||||
|
Assert.Equal(1, items[0].Id);
|
||||||
|
Assert.Equal(2, items[1].Id);
|
||||||
|
Assert.Equal(4, items[2].Id);
|
||||||
|
Assert.Equal(3, items[3].Id);
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(i, items[i].Order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReorderItems_MoveItemToOutOfBoundsPosition_MovesToEnd()
|
||||||
|
{
|
||||||
|
var items = CreateTestReadingListItems();
|
||||||
|
|
||||||
|
OrderableHelper.ReorderItems(items, 2, 10);
|
||||||
|
|
||||||
|
Assert.Equal(1, items[0].Id);
|
||||||
|
Assert.Equal(3, items[1].Id);
|
||||||
|
Assert.Equal(4, items[2].Id);
|
||||||
|
Assert.Equal(2, items[3].Id);
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(i, items[i].Order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReorderItems_NegativePosition_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
var items = CreateTestReadingListItems();
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() =>
|
||||||
|
OrderableHelper.ReorderItems(items, 2, -1)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
|
||||||
using API.Extensions;
|
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,5 @@
|
||||||
using System;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
|
||||||
using API.DTOs;
|
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
|
||||||
using API.Helpers;
|
|
||||||
using API.Helpers.Builders;
|
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace API.Tests.Helpers;
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
|
|
|
||||||
258
API.Tests/Helpers/ReviewHelperTests.cs
Normal file
258
API.Tests/Helpers/ReviewHelperTests.cs
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
using API.Helpers;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
using API.DTOs.SeriesDetail;
|
||||||
|
|
||||||
|
namespace API.Tests.Helpers;
|
||||||
|
|
||||||
|
public class ReviewHelperTests
|
||||||
|
{
|
||||||
|
#region SelectSpectrumOfReviews Tests
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSpectrumOfReviews_WhenLessThan10Reviews_ReturnsAllReviews()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var reviews = CreateReviewList(8);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(8, result.Count);
|
||||||
|
Assert.Equal(reviews, result.OrderByDescending(r => r.Score));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSpectrumOfReviews_WhenMoreThan10Reviews_Returns10Reviews()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var reviews = CreateReviewList(20);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(10, result.Count);
|
||||||
|
Assert.Equal(reviews[0], result.First());
|
||||||
|
Assert.Equal(reviews[19], result.Last());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSpectrumOfReviews_WithExactly10Reviews_ReturnsAllReviews()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var reviews = CreateReviewList(10);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(10, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSpectrumOfReviews_WithLargeNumberOfReviews_ReturnsCorrectSpectrum()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var reviews = CreateReviewList(100);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(10, result.Count);
|
||||||
|
Assert.Contains(reviews[0], result);
|
||||||
|
Assert.Contains(reviews[1], result);
|
||||||
|
Assert.Contains(reviews[98], result);
|
||||||
|
Assert.Contains(reviews[99], result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSpectrumOfReviews_WithEmptyList_ReturnsEmptyList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var reviews = new List<UserReviewDto>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectSpectrumOfReviews_ResultsOrderedByScoreDescending()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var reviews = new List<UserReviewDto>
|
||||||
|
{
|
||||||
|
new UserReviewDto { Tagline = "1", Score = 3 },
|
||||||
|
new UserReviewDto { Tagline = "2", Score = 5 },
|
||||||
|
new UserReviewDto { Tagline = "3", Score = 1 },
|
||||||
|
new UserReviewDto { Tagline = "4", Score = 4 },
|
||||||
|
new UserReviewDto { Tagline = "5", Score = 2 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.SelectSpectrumOfReviews(reviews).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(5, result.Count);
|
||||||
|
Assert.Equal(5, result[0].Score);
|
||||||
|
Assert.Equal(4, result[1].Score);
|
||||||
|
Assert.Equal(3, result[2].Score);
|
||||||
|
Assert.Equal(2, result[3].Score);
|
||||||
|
Assert.Equal(1, result[4].Score);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region GetCharacters Tests
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_WithNullBody_ReturnsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
string body = null;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_WithEmptyBody_ReturnsEmptyString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var body = string.Empty;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(string.Empty, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_WithNoTextNodes_ReturnsEmptyString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string body = "<div></div>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(string.Empty, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_WithLessCharactersThanLimit_ReturnsFullText()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var body = "<p>This is a short review.</p>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("This is a short review.…", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_WithMoreCharactersThanLimit_TruncatesText()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var body = "<p>" + new string('a', 200) + "</p>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(new string('a', 175) + "…", result);
|
||||||
|
Assert.Equal(176, result.Length); // 175 characters + ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_IgnoresScriptTags()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string body = "<p>Visible text</p><script>console.log('hidden');</script>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Visible text…", result);
|
||||||
|
Assert.DoesNotContain("hidden", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_RemovesMarkdownSymbols()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string body = "<p>This is **bold** and _italic_ text with [link](url).</p>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("This is bold and italic text with link.…", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCharacters_HandlesComplexMarkdownAndHtml()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string body = """
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1># Header</h1>
|
||||||
|
<p>This is ~~strikethrough~~ and __underlined__ text</p>
|
||||||
|
<p>~~~code block~~~</p>
|
||||||
|
<p>+++highlighted+++</p>
|
||||||
|
<p>img123(image.jpg)</p>
|
||||||
|
</div>
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = ReviewHelper.GetCharacters(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.DoesNotContain("~~", result);
|
||||||
|
Assert.DoesNotContain("__", result);
|
||||||
|
Assert.DoesNotContain("~~~", result);
|
||||||
|
Assert.DoesNotContain("+++", result);
|
||||||
|
Assert.DoesNotContain("img123(", result);
|
||||||
|
Assert.Contains("Header", result);
|
||||||
|
Assert.Contains("strikethrough", result);
|
||||||
|
Assert.Contains("underlined", result);
|
||||||
|
Assert.Contains("code block", result);
|
||||||
|
Assert.Contains("highlighted", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
private static List<UserReviewDto> CreateReviewList(int count)
|
||||||
|
{
|
||||||
|
var reviews = new List<UserReviewDto>();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
reviews.Add(new UserReviewDto
|
||||||
|
{
|
||||||
|
Tagline = $"{i + 1}",
|
||||||
|
Score = count - i // This makes them ordered by score descending initially
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return reviews;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -26,6 +26,7 @@ using NSubstitute;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace API.Tests.Helpers;
|
namespace API.Tests.Helpers;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class ScannerHelper
|
public class ScannerHelper
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using API.Data;
|
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using API.Helpers;
|
||||||
using API.Helpers;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Helpers;
|
namespace API.Tests.Helpers;
|
||||||
|
|
@ -11,6 +10,10 @@ public class StringHelperTests
|
||||||
"<p>A Perfect Marriage Becomes a Perfect Affair!<br /> <br><br><br /> Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?</p>",
|
"<p>A Perfect Marriage Becomes a Perfect Affair!<br /> <br><br><br /> Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?</p>",
|
||||||
"<p>A Perfect Marriage Becomes a Perfect Affair!<br /> Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?</p>"
|
"<p>A Perfect Marriage Becomes a Perfect Affair!<br /> Every woman wishes for that happily ever after, but when time flies by and you've become a neglected housewife, what's a woman to do?</p>"
|
||||||
)]
|
)]
|
||||||
|
[InlineData(
|
||||||
|
"<p><a href=\"https://blog.goo.ne.jp/tamakiya_web\">Blog</a> | <a href=\"https://twitter.com/tamakinozomu\">Twitter</a> | <a href=\"https://www.pixiv.net/member.php?id=68961\">Pixiv</a> | <a href=\"https://pawoo.net/&#64;tamakiya\">Pawoo</a></p>",
|
||||||
|
"<p><a href=\"https://blog.goo.ne.jp/tamakiya_web\">Blog</a> | <a href=\"https://twitter.com/tamakinozomu\">Twitter</a> | <a href=\"https://www.pixiv.net/member.php?id=68961\">Pixiv</a> | <a href=\"https://pawoo.net/&#64;tamakiya\">Pawoo</a></p>"
|
||||||
|
)]
|
||||||
public void TestSquashBreaklines(string input, string expected)
|
public void TestSquashBreaklines(string input, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, StringHelper.SquashBreaklines(input));
|
Assert.Equal(expected, StringHelper.SquashBreaklines(input));
|
||||||
|
|
@ -29,4 +32,15 @@ public class StringHelperTests
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, StringHelper.RemoveSourceInDescription(input));
|
Assert.Equal(expected, StringHelper.RemoveSourceInDescription(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(
|
||||||
|
"""<a href=\"https://pawoo.net/&#64;tamakiya\">Pawoo</a></p>""",
|
||||||
|
"""<a href=\"https://pawoo.net/@tamakiya\">Pawoo</a></p>"""
|
||||||
|
)]
|
||||||
|
public void TestCorrectUrls(string input, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, StringHelper.CorrectUrls(input));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO;
|
||||||
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
@ -8,59 +9,54 @@ using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Parsers;
|
namespace API.Tests.Parsers;
|
||||||
|
|
||||||
public class BasicParserTests
|
public class BasicParserTests : AbstractFsTest
|
||||||
{
|
{
|
||||||
private readonly BasicParser _parser;
|
private readonly BasicParser _parser;
|
||||||
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
private readonly ILogger<DirectoryService> _dsLogger = Substitute.For<ILogger<DirectoryService>>();
|
||||||
private const string RootDirectory = "C:/Books/";
|
private readonly string _rootDirectory;
|
||||||
|
|
||||||
public BasicParserTests()
|
public BasicParserTests()
|
||||||
{
|
{
|
||||||
var fileSystem = new MockFileSystem();
|
var fileSystem = CreateFileSystem();
|
||||||
fileSystem.AddDirectory("C:/Books/");
|
_rootDirectory = Path.Join(DataDirectory, "Books/");
|
||||||
fileSystem.AddFile("C:/Books/Harry Potter/Harry Potter - Vol 1.epub", new MockFileData(""));
|
fileSystem.AddDirectory(_rootDirectory);
|
||||||
|
fileSystem.AddFile($"{_rootDirectory}Harry Potter/Harry Potter - Vol 1.epub", new MockFileData(""));
|
||||||
|
|
||||||
fileSystem.AddFile("C:/Books/Accel World/Accel World - Volume 1.cbz", new MockFileData(""));
|
fileSystem.AddFile($"{_rootDirectory}Accel World/Accel World - Volume 1.cbz", new MockFileData(""));
|
||||||
fileSystem.AddFile("C:/Books/Accel World/Accel World - Volume 1 Chapter 2.cbz", new MockFileData(""));
|
fileSystem.AddFile($"{_rootDirectory}Accel World/Accel World - Volume 1 Chapter 2.cbz", new MockFileData(""));
|
||||||
fileSystem.AddFile("C:/Books/Accel World/Accel World - Chapter 3.cbz", new MockFileData(""));
|
fileSystem.AddFile($"{_rootDirectory}Accel World/Accel World - Chapter 3.cbz", new MockFileData(""));
|
||||||
fileSystem.AddFile("C:/Books/Accel World/Accel World Gaiden SP01.cbz", new MockFileData(""));
|
fileSystem.AddFile("$\"{RootDirectory}Accel World/Accel World Gaiden SP01.cbz", new MockFileData(""));
|
||||||
|
|
||||||
|
|
||||||
fileSystem.AddFile("C:/Books/Accel World/cover.png", new MockFileData(""));
|
fileSystem.AddFile($"{_rootDirectory}Accel World/cover.png", new MockFileData(""));
|
||||||
|
|
||||||
fileSystem.AddFile("C:/Books/Batman/Batman #1.cbz", new MockFileData(""));
|
fileSystem.AddFile($"{_rootDirectory}Batman/Batman #1.cbz", new MockFileData(""));
|
||||||
|
|
||||||
var ds = new DirectoryService(_dsLogger, fileSystem);
|
var ds = new DirectoryService(_dsLogger, fileSystem);
|
||||||
_parser = new BasicParser(ds, new ImageParser(ds));
|
_parser = new BasicParser(ds, new ImageParser(ds));
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Parse_Books
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Parse_Manga
|
#region Parse_Manga
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests that when there is a loose leaf cover in the manga library, that it is ignored
|
/// Tests that when there is a loose-leaf cover in the manga library, that it is ignored
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
|
public void Parse_MangaLibrary_JustCover_ShouldReturnNull()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse(@"C:/Books/Accel World/cover.png", "C:/Books/Accel World/",
|
var actual = _parser.Parse($"{_rootDirectory}Accel World/cover.png", $"{_rootDirectory}Accel World/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.Null(actual);
|
Assert.Null(actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests that when there is a loose leaf cover in the manga library, that it is ignored
|
/// Tests that when there is a loose-leaf cover in the manga library, that it is ignored
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_OtherImage_ShouldReturnNull()
|
public void Parse_MangaLibrary_OtherImage_ShouldReturnNull()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse(@"C:/Books/Accel World/page 01.png", "C:/Books/Accel World/",
|
var actual = _parser.Parse($"{_rootDirectory}Accel World/page 01.png", $"{_rootDirectory}Accel World/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,8 +66,8 @@ public class BasicParserTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_VolumeAndChapterInFilename()
|
public void Parse_MangaLibrary_VolumeAndChapterInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz", "C:/Books/Mujaki no Rakuen/",
|
var actual = _parser.Parse($"{_rootDirectory}Mujaki no Rakuen/Mujaki no Rakuen Vol12 ch76.cbz", $"{_rootDirectory}Mujaki no Rakuen/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Mujaki no Rakuen", actual.Series);
|
Assert.Equal("Mujaki no Rakuen", actual.Series);
|
||||||
|
|
@ -86,9 +82,9 @@ public class BasicParserTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_JustVolumeInFilename()
|
public void Parse_MangaLibrary_JustVolumeInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz",
|
var actual = _parser.Parse($"{_rootDirectory}Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/Vol 1.cbz",
|
||||||
"C:/Books/Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/",
|
$"{_rootDirectory}Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", actual.Series);
|
Assert.Equal("Shimoneta to Iu Gainen ga Sonzai Shinai Taikutsu na Sekai Man-hen", actual.Series);
|
||||||
|
|
@ -103,9 +99,9 @@ public class BasicParserTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_JustChapterInFilename()
|
public void Parse_MangaLibrary_JustChapterInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Beelzebub/Beelzebub_01_[Noodles].zip",
|
var actual = _parser.Parse($"{_rootDirectory}Beelzebub/Beelzebub_01_[Noodles].zip",
|
||||||
"C:/Books/Beelzebub/",
|
$"{_rootDirectory}Beelzebub/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Beelzebub", actual.Series);
|
Assert.Equal("Beelzebub", actual.Series);
|
||||||
|
|
@ -120,9 +116,9 @@ public class BasicParserTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_SpecialMarkerInFilename()
|
public void Parse_MangaLibrary_SpecialMarkerInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr",
|
var actual = _parser.Parse($"{_rootDirectory}Summer Time Rendering/Specials/Record 014 (between chapter 083 and ch084) SP11.cbr",
|
||||||
"C:/Books/Summer Time Rendering/",
|
$"{_rootDirectory}Summer Time Rendering/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Summer Time Rendering", actual.Series);
|
Assert.Equal("Summer Time Rendering", actual.Series);
|
||||||
|
|
@ -133,36 +129,54 @@ public class BasicParserTests
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests that when the filename parses as a speical, it appropriately parses
|
/// Tests that when the filename parses as a special, it appropriately parses
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_SpecialInFilename()
|
public void Parse_MangaLibrary_SpecialInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Summer Time Rendering/Volume SP01.cbr",
|
var actual = _parser.Parse($"{_rootDirectory}Summer Time Rendering/Volume SP01.cbr",
|
||||||
"C:/Books/Summer Time Rendering/",
|
$"{_rootDirectory}Summer Time Rendering/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Summer Time Rendering", actual.Series);
|
Assert.Equal("Summer Time Rendering", actual.Series);
|
||||||
Assert.Equal("Volume SP01", actual.Title);
|
Assert.Equal("Volume", actual.Title);
|
||||||
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
||||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||||
Assert.True(actual.IsSpecial);
|
Assert.True(actual.IsSpecial);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests that when the filename parses as a speical, it appropriately parses
|
/// Tests that when the filename parses as a special, it appropriately parses
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_SpecialInFilename2()
|
public void Parse_MangaLibrary_SpecialInFilename2()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("M:/Kimi wa Midara na Boku no Joou/Specials/[Renzokusei] Special 1 SP02.zip",
|
var actual = _parser.Parse("M:/Kimi wa Midara na Boku no Joou/Specials/[Renzokusei] Special 1 SP02.zip",
|
||||||
"M:/Kimi wa Midara na Boku no Joou/",
|
"M:/Kimi wa Midara na Boku no Joou/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Kimi wa Midara na Boku no Joou", actual.Series);
|
Assert.Equal("Kimi wa Midara na Boku no Joou", actual.Series);
|
||||||
Assert.Equal("[Renzokusei] Special 1 SP02", actual.Title);
|
Assert.Equal("[Renzokusei] Special 1", actual.Title);
|
||||||
|
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
||||||
|
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||||
|
Assert.True(actual.IsSpecial);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when the filename parses as a special, it appropriately parses
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Parse_MangaLibrary_SpecialInFilename_StrangeNaming()
|
||||||
|
{
|
||||||
|
var actual = _parser.Parse($"{_rootDirectory}My Dress-Up Darling/SP01 1. Special Name.cbz",
|
||||||
|
_rootDirectory,
|
||||||
|
_rootDirectory, LibraryType.Manga);
|
||||||
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
|
Assert.Equal("My Dress-Up Darling", actual.Series);
|
||||||
|
Assert.Equal("1. Special Name", actual.Title);
|
||||||
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
Assert.Equal(Parser.SpecialVolume, actual.Volumes);
|
||||||
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
Assert.Equal(Parser.DefaultChapter, actual.Chapters);
|
||||||
Assert.True(actual.IsSpecial);
|
Assert.True(actual.IsSpecial);
|
||||||
|
|
@ -174,9 +188,9 @@ public class BasicParserTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaLibrary_EditionInFilename()
|
public void Parse_MangaLibrary_EditionInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Air Gear/Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz",
|
var actual = _parser.Parse($"{_rootDirectory}Air Gear/Air Gear Omnibus v01 (2016) (Digital) (Shadowcat-Empire).cbz",
|
||||||
"C:/Books/Air Gear/",
|
$"{_rootDirectory}Air Gear/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Air Gear", actual.Series);
|
Assert.Equal("Air Gear", actual.Series);
|
||||||
|
|
@ -195,9 +209,9 @@ public class BasicParserTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Parse_MangaBooks_JustVolumeInFilename()
|
public void Parse_MangaBooks_JustVolumeInFilename()
|
||||||
{
|
{
|
||||||
var actual = _parser.Parse("C:/Books/Epubs/Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub",
|
var actual = _parser.Parse($"{_rootDirectory}Epubs/Harrison, Kim - The Good, The Bad, and the Undead - Hollows Vol 2.5.epub",
|
||||||
"C:/Books/Epubs/",
|
$"{_rootDirectory}Epubs/",
|
||||||
RootDirectory, LibraryType.Manga, null);
|
_rootDirectory, LibraryType.Manga);
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
|
|
||||||
Assert.Equal("Harrison, Kim - The Good, The Bad, and the Undead - Hollows", actual.Series);
|
Assert.Equal("Harrison, Kim - The Good, The Bad, and the Undead - Hollows", actual.Series);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using API.Data.Metadata;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace API.Tests.Parsing;
|
namespace API.Tests.Parsing;
|
||||||
|
|
||||||
public class MangaParsingTests
|
public class MangaParsingTests
|
||||||
{
|
{
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
|
||||||
|
|
||||||
public MangaParsingTests(ITestOutputHelper testOutputHelper)
|
|
||||||
{
|
|
||||||
_testOutputHelper = testOutputHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
|
[InlineData("Killing Bites Vol. 0001 Ch. 0001 - Galactica Scanlations (gb)", "1")]
|
||||||
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "1")]
|
[InlineData("My Girlfriend Is Shobitch v01 - ch. 09 - pg. 008.png", "1")]
|
||||||
|
|
@ -79,11 +71,13 @@ public class MangaParsingTests
|
||||||
[InlineData("죽음 13회", "13")]
|
[InlineData("죽음 13회", "13")]
|
||||||
[InlineData("동의보감 13장", "13")]
|
[InlineData("동의보감 13장", "13")]
|
||||||
[InlineData("몰?루 아카이브 7.5권", "7.5")]
|
[InlineData("몰?루 아카이브 7.5권", "7.5")]
|
||||||
|
[InlineData("주술회전 1.5권", "1.5")]
|
||||||
[InlineData("63권#200", "63")]
|
[InlineData("63권#200", "63")]
|
||||||
[InlineData("시즌34삽화2", "34")]
|
[InlineData("시즌34삽화2", "34")]
|
||||||
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
[InlineData("Accel World Chapter 001 Volume 002", "2")]
|
||||||
[InlineData("Accel World Volume 2", "2")]
|
[InlineData("Accel World Volume 2", "2")]
|
||||||
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
|
[InlineData("Nagasarete Airantou - Vol. 30 Ch. 187.5 - Vol.31 Omake", "30")]
|
||||||
|
[InlineData("Zom 100 - Bucket List of the Dead v01", "1")]
|
||||||
public void ParseVolumeTest(string filename, string expected)
|
public void ParseVolumeTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseVolume(filename, LibraryType.Manga));
|
||||||
|
|
@ -212,6 +206,8 @@ public class MangaParsingTests
|
||||||
[InlineData("不安の種\uff0b - 01", "不安の種\uff0b")]
|
[InlineData("不安の種\uff0b - 01", "不安の種\uff0b")]
|
||||||
[InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")]
|
[InlineData("Giant Ojou-sama - Ch. 33.5 - Volume 04 Bonus Chapter", "Giant Ojou-sama")]
|
||||||
[InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")]
|
[InlineData("[218565]-(C92) [BRIO (Puyocha)] Mika-nee no Tanryoku Shidou - Mika s Guide to Self-Confidence (THE IDOLM@STE", "")]
|
||||||
|
[InlineData("Monster #8 Ch. 001", "Monster #8")]
|
||||||
|
[InlineData("Zom 100 - Bucket List of the Dead v01", "Zom 100 - Bucket List of the Dead")]
|
||||||
public void ParseSeriesTest(string filename, string expected)
|
public void ParseSeriesTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseSeries(filename, LibraryType.Manga));
|
||||||
|
|
@ -304,6 +300,7 @@ public class MangaParsingTests
|
||||||
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
|
[InlineData("เด็กคนนี้ขอลาออกจากการเป็นเจ้าของปราสาท เล่ม 1 ตอนที่ 3", "3")]
|
||||||
[InlineData("Max Level Returner ตอนที่ 5", "5")]
|
[InlineData("Max Level Returner ตอนที่ 5", "5")]
|
||||||
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
|
[InlineData("หนึ่งความคิด นิจนิรันดร์ บทที่ 112", "112")]
|
||||||
|
[InlineData("Monster #8 Ch. 001", "1")]
|
||||||
public void ParseChaptersTest(string filename, string expected)
|
public void ParseChaptersTest(string filename, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga));
|
Assert.Equal(expected, API.Services.Tasks.Scanner.Parser.Parser.ParseChapter(filename, LibraryType.Manga));
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@ public class ParserInfoTests
|
||||||
{
|
{
|
||||||
var p1 = new ParserInfo()
|
var p1 = new ParserInfo()
|
||||||
{
|
{
|
||||||
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
Chapters = Parser.DefaultChapter,
|
||||||
Edition = "",
|
Edition = "",
|
||||||
Format = MangaFormat.Archive,
|
Format = MangaFormat.Archive,
|
||||||
FullFilePath = "/manga/darker than black.cbz",
|
FullFilePath = "/manga/darker than black.cbz",
|
||||||
IsSpecial = false,
|
IsSpecial = false,
|
||||||
Series = "darker than black",
|
Series = "darker than black",
|
||||||
Title = "darker than black",
|
Title = "darker than black",
|
||||||
Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume
|
Volumes = Parser.LooseLeafVolume
|
||||||
};
|
};
|
||||||
|
|
||||||
var p2 = new ParserInfo()
|
var p2 = new ParserInfo()
|
||||||
|
|
@ -30,7 +30,7 @@ public class ParserInfoTests
|
||||||
IsSpecial = false,
|
IsSpecial = false,
|
||||||
Series = "darker than black",
|
Series = "darker than black",
|
||||||
Title = "Darker Than Black",
|
Title = "Darker Than Black",
|
||||||
Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume
|
Volumes = Parser.LooseLeafVolume
|
||||||
};
|
};
|
||||||
|
|
||||||
var expected = new ParserInfo()
|
var expected = new ParserInfo()
|
||||||
|
|
@ -42,7 +42,7 @@ public class ParserInfoTests
|
||||||
IsSpecial = false,
|
IsSpecial = false,
|
||||||
Series = "darker than black",
|
Series = "darker than black",
|
||||||
Title = "darker than black",
|
Title = "darker than black",
|
||||||
Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume
|
Volumes = Parser.LooseLeafVolume
|
||||||
};
|
};
|
||||||
p1.Merge(p2);
|
p1.Merge(p2);
|
||||||
|
|
||||||
|
|
@ -62,12 +62,12 @@ public class ParserInfoTests
|
||||||
IsSpecial = true,
|
IsSpecial = true,
|
||||||
Series = "darker than black",
|
Series = "darker than black",
|
||||||
Title = "darker than black",
|
Title = "darker than black",
|
||||||
Volumes = API.Services.Tasks.Scanner.Parser.Parser.LooseLeafVolume
|
Volumes = Parser.LooseLeafVolume
|
||||||
};
|
};
|
||||||
|
|
||||||
var p2 = new ParserInfo()
|
var p2 = new ParserInfo()
|
||||||
{
|
{
|
||||||
Chapters = API.Services.Tasks.Scanner.Parser.Parser.DefaultChapter,
|
Chapters = Parser.DefaultChapter,
|
||||||
Edition = "",
|
Edition = "",
|
||||||
Format = MangaFormat.Archive,
|
Format = MangaFormat.Archive,
|
||||||
FullFilePath = "/manga/darker than black.cbz",
|
FullFilePath = "/manga/darker than black.cbz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static API.Services.Tasks.Scanner.Parser.Parser;
|
using static API.Services.Tasks.Scanner.Parser.Parser;
|
||||||
|
|
||||||
|
|
@ -11,9 +10,13 @@ public class ParsingTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShouldWork()
|
public void ShouldWork()
|
||||||
{
|
{
|
||||||
var s = 6.5f + "";
|
var s = 6.5f.ToString(CultureInfo.InvariantCulture);
|
||||||
var a = float.Parse(s, CultureInfo.InvariantCulture);
|
var a = float.Parse(s, CultureInfo.InvariantCulture);
|
||||||
Assert.Equal(6.5f, a);
|
Assert.Equal(6.5f, a);
|
||||||
|
|
||||||
|
s = 6.5f + "";
|
||||||
|
a = float.Parse(s, CultureInfo.CurrentCulture);
|
||||||
|
Assert.Equal(6.5f, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Theory]
|
// [Theory]
|
||||||
|
|
@ -40,6 +43,7 @@ public class ParsingTests
|
||||||
[InlineData("DEAD Tube Prologue", "DEAD Tube Prologue")]
|
[InlineData("DEAD Tube Prologue", "DEAD Tube Prologue")]
|
||||||
[InlineData("DEAD Tube Prologue SP01", "DEAD Tube Prologue")]
|
[InlineData("DEAD Tube Prologue SP01", "DEAD Tube Prologue")]
|
||||||
[InlineData("DEAD_Tube_Prologue SP01", "DEAD Tube Prologue")]
|
[InlineData("DEAD_Tube_Prologue SP01", "DEAD Tube Prologue")]
|
||||||
|
[InlineData("SP01 1. DEAD Tube Prologue", "1. DEAD Tube Prologue")]
|
||||||
public void CleanSpecialTitleTest(string input, string expected)
|
public void CleanSpecialTitleTest(string input, string expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, CleanSpecialTitle(input));
|
Assert.Equal(expected, CleanSpecialTitle(input));
|
||||||
|
|
@ -247,6 +251,7 @@ public class ParsingTests
|
||||||
[InlineData("ch1/backcover.png", false)]
|
[InlineData("ch1/backcover.png", false)]
|
||||||
[InlineData("backcover.png", false)]
|
[InlineData("backcover.png", false)]
|
||||||
[InlineData("back_cover.png", false)]
|
[InlineData("back_cover.png", false)]
|
||||||
|
[InlineData("LD Blacklands #1 35 (back cover).png", false)]
|
||||||
public void IsCoverImageTest(string inputPath, bool expected)
|
public void IsCoverImageTest(string inputPath, bool expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, IsCoverImage(inputPath));
|
Assert.Equal(expected, IsCoverImage(inputPath));
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace API.Tests.Repository;
|
namespace API.Tests.Repository;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
||||||
using API.Archive;
|
using API.Archive;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using EasyCaching.Core;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Data.Common;
|
||||||
using System.Data.Common;
|
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
@ -21,7 +19,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class BackupServiceTests
|
public class BackupServiceTests: AbstractFsTest
|
||||||
{
|
{
|
||||||
private readonly ILogger<BackupService> _logger = Substitute.For<ILogger<BackupService>>();
|
private readonly ILogger<BackupService> _logger = Substitute.For<ILogger<BackupService>>();
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
@ -31,13 +29,6 @@ public class BackupServiceTests
|
||||||
private readonly DbConnection _connection;
|
private readonly DbConnection _connection;
|
||||||
private readonly DataContext _context;
|
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 ConfigDirectory = "C:/kavita/config/";
|
|
||||||
private const string BookmarkDirectory = "C:/kavita/config/bookmarks";
|
|
||||||
private const string ThemesDirectory = "C:/kavita/config/theme";
|
|
||||||
|
|
||||||
public BackupServiceTests()
|
public BackupServiceTests()
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +73,7 @@ public class BackupServiceTests
|
||||||
|
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
_context.Library.Add(new LibraryBuilder("Manga")
|
_context.Library.Add(new LibraryBuilder("Manga")
|
||||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
.WithFolderPath(new FolderPathBuilder(Root + "data/").Build())
|
||||||
.Build());
|
.Build());
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
@ -94,22 +85,6 @@ public class BackupServiceTests
|
||||||
await _context.SaveChangesAsync();
|
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(LogDirectory);
|
|
||||||
fileSystem.AddDirectory(ThemesDirectory);
|
|
||||||
fileSystem.AddDirectory(BookmarkDirectory);
|
|
||||||
fileSystem.AddDirectory("C:/data/");
|
|
||||||
|
|
||||||
return fileSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using EasyCaching.Core;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -92,18 +93,17 @@ public class BookServiceTests
|
||||||
Assert.Equal("Georges Bizet \\(1838-1875\\)", comicInfo.Writer);
|
Assert.Equal("Georges Bizet \\(1838-1875\\)", comicInfo.Writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get the file from microtherion
|
|
||||||
//[Fact]
|
//[Fact]
|
||||||
// public void ShouldUsePdfInfoDict()
|
public void ShouldUsePdfInfoDict()
|
||||||
// {
|
{
|
||||||
// var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs");
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/Library/Books/PDFs");
|
||||||
// var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf");
|
var document = Path.Join(testDirectory, "Rollo at Work SP01.pdf");
|
||||||
// var comicInfo = _bookService.GetComicInfo(document);
|
var comicInfo = _bookService.GetComicInfo(document);
|
||||||
// Assert.NotNull(comicInfo);
|
Assert.NotNull(comicInfo);
|
||||||
// Assert.Equal("Rollo at Work", comicInfo.Title);
|
Assert.Equal("Rollo at Work", comicInfo.Title);
|
||||||
// Assert.Equal("Jacob Abbott", comicInfo.Writer);
|
Assert.Equal("Jacob Abbott", comicInfo.Writer);
|
||||||
// Assert.Equal(2008, comicInfo.Year);
|
Assert.Equal(2008, comicInfo.Year);
|
||||||
// }
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ShouldHandleIndirectPdfObjects()
|
public void ShouldHandleIndirectPdfObjects()
|
||||||
|
|
@ -124,4 +124,22 @@ public class BookServiceTests
|
||||||
var comicInfo = _bookService.GetComicInfo(document);
|
var comicInfo = _bookService.GetComicInfo(document);
|
||||||
Assert.Null(comicInfo);
|
Assert.Null(comicInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeriesFallBackToMetadataTitle()
|
||||||
|
{
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());
|
||||||
|
var pdfParser = new PdfParser(ds);
|
||||||
|
|
||||||
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService");
|
||||||
|
var filePath = Path.Join(testDirectory, "Bizet-Variations_Chromatiques_de_concert_Theme_A4.pdf");
|
||||||
|
|
||||||
|
var comicInfo = _bookService.GetComicInfo(filePath);
|
||||||
|
Assert.NotNull(comicInfo);
|
||||||
|
|
||||||
|
var parserInfo = pdfParser.Parse(filePath, testDirectory, ds.GetParentDirectoryName(testDirectory), LibraryType.Book, comicInfo);
|
||||||
|
Assert.NotNull(parserInfo);
|
||||||
|
Assert.Equal(parserInfo.Title, comicInfo.Title);
|
||||||
|
Assert.Equal(parserInfo.Series, comicInfo.Title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,9 @@ using API.Data.Repositories;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
|
||||||
using API.Extensions;
|
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -25,17 +22,12 @@ using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class BookmarkServiceTests
|
public class BookmarkServiceTests: AbstractFsTest
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly DbConnection _connection;
|
private readonly DbConnection _connection;
|
||||||
private readonly DataContext _context;
|
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 BookmarkDirectory = "C:/kavita/config/bookmarks/";
|
|
||||||
|
|
||||||
|
|
||||||
public BookmarkServiceTests()
|
public BookmarkServiceTests()
|
||||||
{
|
{
|
||||||
|
|
@ -88,7 +80,7 @@ Substitute.For<IMediaConversionService>());
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
_context.Library.Add(new LibraryBuilder("Manga")
|
_context.Library.Add(new LibraryBuilder("Manga")
|
||||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
.WithFolderPath(new FolderPathBuilder(Root + "data/").Build())
|
||||||
.Build());
|
.Build());
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
@ -102,20 +94,6 @@ Substitute.For<IMediaConversionService>());
|
||||||
await _context.SaveChangesAsync();
|
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
|
#endregion
|
||||||
|
|
||||||
#region BookmarkPage
|
#region BookmarkPage
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Data.Common;
|
||||||
using System.Data.Common;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Metadata;
|
using API.Data.Metadata;
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
@ -62,7 +60,7 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public class CacheServiceTests
|
public class CacheServiceTests: AbstractFsTest
|
||||||
{
|
{
|
||||||
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
private readonly ILogger<CacheService> _logger = Substitute.For<ILogger<CacheService>>();
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
|
@ -71,11 +69,6 @@ public class CacheServiceTests
|
||||||
private readonly DbConnection _connection;
|
private readonly DbConnection _connection;
|
||||||
private readonly DataContext _context;
|
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()
|
public CacheServiceTests()
|
||||||
{
|
{
|
||||||
var contextOptions = new DbContextOptionsBuilder()
|
var contextOptions = new DbContextOptionsBuilder()
|
||||||
|
|
@ -118,7 +111,7 @@ public class CacheServiceTests
|
||||||
_context.ServerSetting.Update(setting);
|
_context.ServerSetting.Update(setting);
|
||||||
|
|
||||||
_context.Library.Add(new LibraryBuilder("Manga")
|
_context.Library.Add(new LibraryBuilder("Manga")
|
||||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
.WithFolderPath(new FolderPathBuilder(Root + "data/").Build())
|
||||||
.Build());
|
.Build());
|
||||||
return await _context.SaveChangesAsync() > 0;
|
return await _context.SaveChangesAsync() > 0;
|
||||||
}
|
}
|
||||||
|
|
@ -130,19 +123,6 @@ public class CacheServiceTests
|
||||||
await _context.SaveChangesAsync();
|
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
|
#endregion
|
||||||
|
|
||||||
#region Ensure
|
#region Ensure
|
||||||
|
|
@ -263,7 +243,7 @@ public class CacheServiceTests
|
||||||
.WithFile(new MangaFileBuilder($"{DataDirectory}2.epub", MangaFormat.Epub).Build())
|
.WithFile(new MangaFileBuilder($"{DataDirectory}2.epub", MangaFormat.Epub).Build())
|
||||||
.Build();
|
.Build();
|
||||||
cs.GetCachedFile(c);
|
cs.GetCachedFile(c);
|
||||||
Assert.Same($"{DataDirectory}1.epub", cs.GetCachedFile(c));
|
Assert.Equal($"{DataDirectory}1.epub", cs.GetCachedFile(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
|
|
@ -30,11 +27,10 @@ public class CleanupServiceTests : AbstractDbTest
|
||||||
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
|
||||||
private readonly IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
|
|
||||||
|
|
||||||
public CleanupServiceTests() : base()
|
public CleanupServiceTests() : base()
|
||||||
{
|
{
|
||||||
_context.Library.Add(new LibraryBuilder("Manga")
|
_context.Library.Add(new LibraryBuilder("Manga")
|
||||||
.WithFolderPath(new FolderPathBuilder("C:/data/").Build())
|
.WithFolderPath(new FolderPathBuilder(Root + "data/").Build())
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>(),
|
_readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs.Collection;
|
using API.DTOs.Collection;
|
||||||
|
|
@ -10,6 +12,7 @@ using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
|
using Kavita.Common;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -53,6 +56,64 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region DeleteTag
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteTag_ShouldDeleteTag_WhenTagExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.DeleteTag(1, user);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
var deletedTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.Null(deletedTag);
|
||||||
|
Assert.Single(user.Collections); // Only one collection should remain
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteTag_ShouldReturnTrue_WhenTagDoesNotExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Act - Try to delete a non-existent tag
|
||||||
|
var result = await _service.DeleteTag(999, user);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result); // Should return true because the tag is already "deleted"
|
||||||
|
Assert.Equal(2, user.Collections.Count); // Both collections should remain
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteTag_ShouldNotAffectOtherTags()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.DeleteTag(1, user);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
var remainingTag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2);
|
||||||
|
Assert.NotNull(remainingTag);
|
||||||
|
Assert.Equal("Tag 2", remainingTag.Title);
|
||||||
|
Assert.True(remainingTag.Promoted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region UpdateTag
|
#region UpdateTag
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -111,6 +172,189 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||||
Assert.Equal("UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource", tag.Title);
|
Assert.Equal("UpdateTag_ShouldNotChangeTitle_WhenNotKavitaSource", tag.Title);
|
||||||
Assert.False(string.IsNullOrEmpty(tag.Summary));
|
Assert.False(string.IsNullOrEmpty(tag.Summary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldThrowException_WhenTagDoesNotExist()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<KavitaException>(() => _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Non-existent Tag",
|
||||||
|
Id = 999, // Non-existent ID
|
||||||
|
Promoted = false
|
||||||
|
}, 1));
|
||||||
|
|
||||||
|
Assert.Equal("collection-doesnt-exist", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldThrowException_WhenUserDoesNotOwnTag()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Create a second user
|
||||||
|
var user2 = new AppUserBuilder("user2", "user2", Seed.DefaultThemes.First()).Build();
|
||||||
|
_unitOfWork.UserRepository.Add(user2);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<KavitaException>(() => _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 1",
|
||||||
|
Id = 1, // This belongs to user1
|
||||||
|
Promoted = false
|
||||||
|
}, 2)); // User with ID 2
|
||||||
|
|
||||||
|
Assert.Equal("access-denied", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldThrowException_WhenTitleIsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<KavitaException>(() => _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = " ", // Empty after trimming
|
||||||
|
Id = 1,
|
||||||
|
Promoted = false
|
||||||
|
}, 1));
|
||||||
|
|
||||||
|
Assert.Equal("collection-tag-title-required", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldThrowException_WhenTitleAlreadyExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<KavitaException>(() => _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 2", // Already exists
|
||||||
|
Id = 1, // Trying to rename Tag 1 to Tag 2
|
||||||
|
Promoted = false
|
||||||
|
}, 1));
|
||||||
|
|
||||||
|
Assert.Equal("collection-tag-duplicate", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldUpdateCoverImageSettings()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 1",
|
||||||
|
Id = 1,
|
||||||
|
CoverImageLocked = true
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.True(tag.CoverImageLocked);
|
||||||
|
|
||||||
|
// Now test unlocking the cover image
|
||||||
|
await _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 1",
|
||||||
|
Id = 1,
|
||||||
|
CoverImageLocked = false
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.False(tag.CoverImageLocked);
|
||||||
|
Assert.Equal(string.Empty, tag.CoverImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldAllowPromoteForAdminRole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Setup a user with admin role
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
await AddUserWithRole(user.Id, PolicyConstants.AdminRole);
|
||||||
|
|
||||||
|
|
||||||
|
// Act - Try to promote a tag that wasn't previously promoted
|
||||||
|
await _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 1",
|
||||||
|
Id = 1,
|
||||||
|
Promoted = true
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.True(tag.Promoted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldAllowPromoteForPromoteRole()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Setup a user with promote role
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Mock to return promote role for the user
|
||||||
|
await AddUserWithRole(user.Id, PolicyConstants.PromoteRole);
|
||||||
|
|
||||||
|
// Act - Try to promote a tag that wasn't previously promoted
|
||||||
|
await _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 1",
|
||||||
|
Id = 1,
|
||||||
|
Promoted = true
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.True(tag.Promoted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTag_ShouldNotChangePromotion_WhenUserHasNoPermission()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Setup a user with no special roles
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
// Act - Try to promote a tag without proper role
|
||||||
|
await _service.UpdateTag(new AppUserCollectionDto()
|
||||||
|
{
|
||||||
|
Title = "Tag 1",
|
||||||
|
Id = 1,
|
||||||
|
Promoted = true
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.False(tag.Promoted); // Should remain unpromoted
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -131,7 +375,7 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||||
await _service.RemoveTagFromSeries(tag, new[] {1});
|
await _service.RemoveTagFromSeries(tag, new[] {1});
|
||||||
var userCollections = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
var userCollections = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Collections);
|
||||||
Assert.Equal(2, userCollections!.Collections.Count);
|
Assert.Equal(2, userCollections!.Collections.Count);
|
||||||
Assert.Equal(1, tag.Items.Count);
|
Assert.Single(tag.Items);
|
||||||
Assert.Equal(2, tag.Items.First().Id);
|
Assert.Equal(2, tag.Items.First().Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,6 +419,111 @@ public class CollectionTagServiceTests : AbstractDbTest
|
||||||
Assert.Null(tag2);
|
Assert.Null(tag2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveTagFromSeries_ShouldReturnFalse_WhenTagIsNull()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var result = await _service.RemoveTagFromSeries(null, [1]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveTagFromSeries_ShouldHandleEmptySeriesIdsList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
var initialItemCount = tag.Items.Count;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.RemoveTagFromSeries(tag, Array.Empty<int>());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.Equal(initialItemCount, tag.Items.Count); // No items should be removed
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveTagFromSeries_ShouldHandleNonExistentSeriesIds()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
var initialItemCount = tag.Items.Count;
|
||||||
|
|
||||||
|
// Act - Try to remove a series that doesn't exist in the tag
|
||||||
|
var result = await _service.RemoveTagFromSeries(tag, [999]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.Equal(initialItemCount, tag.Items.Count); // No items should be removed
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveTagFromSeries_ShouldHandleNullItemsList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
|
||||||
|
// Force null items list
|
||||||
|
tag.Items = null;
|
||||||
|
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.RemoveTagFromSeries(tag, [1]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
// The tag should not be removed since the items list was null, not empty
|
||||||
|
var tagAfter = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(1);
|
||||||
|
Assert.Null(tagAfter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveTagFromSeries_ShouldUpdateAgeRating_WhenMultipleSeriesRemain()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await SeedSeries();
|
||||||
|
|
||||||
|
// Add a third series with a different age rating
|
||||||
|
var s3 = new SeriesBuilder("Series 3").WithMetadata(new SeriesMetadataBuilder().WithAgeRating(AgeRating.PG).Build()).Build();
|
||||||
|
_context.Library.First().Series.Add(s3);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Add series 3 to tag 2
|
||||||
|
var tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
tag.Items.Add(s3);
|
||||||
|
_unitOfWork.CollectionTagRepository.Update(tag);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Act - Remove the series with Mature rating
|
||||||
|
await _service.RemoveTagFromSeries(tag, new[] {1});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
tag = await _unitOfWork.CollectionTagRepository.GetCollectionAsync(2);
|
||||||
|
Assert.NotNull(tag);
|
||||||
|
Assert.Equal(2, tag.Items.Count);
|
||||||
|
|
||||||
|
// The age rating should be updated to the highest remaining rating (PG)
|
||||||
|
Assert.Equal(AgeRating.PG, tag.AgeRating);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
@ -10,12 +12,19 @@ using Kavita.Common.Helpers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class DirectoryServiceTests
|
public class DirectoryServiceTests: AbstractFsTest
|
||||||
{
|
{
|
||||||
private readonly ILogger<DirectoryService> _logger = Substitute.For<ILogger<DirectoryService>>();
|
private readonly ILogger<DirectoryService> _logger = Substitute.For<ILogger<DirectoryService>>();
|
||||||
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
|
|
||||||
|
public DirectoryServiceTests(ITestOutputHelper testOutputHelper)
|
||||||
|
{
|
||||||
|
_testOutputHelper = testOutputHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#region TraverseTreeParallelForEach
|
#region TraverseTreeParallelForEach
|
||||||
|
|
@ -373,9 +382,16 @@ public class DirectoryServiceTests
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IsDriveMounted
|
#region IsDriveMounted
|
||||||
|
// The root directory (/) is always mounted on non windows
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsDriveMounted_DriveIsNotMounted()
|
public void IsDriveMounted_DriveIsNotMounted()
|
||||||
{
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
_testOutputHelper.WriteLine("Skipping test on non Windows platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const string testDirectory = "c:/manga/";
|
const string testDirectory = "c:/manga/";
|
||||||
var fileSystem = new MockFileSystem();
|
var fileSystem = new MockFileSystem();
|
||||||
fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc"));
|
fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc"));
|
||||||
|
|
@ -387,6 +403,12 @@ public class DirectoryServiceTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsDriveMounted_DriveIsMounted()
|
public void IsDriveMounted_DriveIsMounted()
|
||||||
{
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
_testOutputHelper.WriteLine("Skipping test on non Windows platform");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const string testDirectory = "c:/manga/";
|
const string testDirectory = "c:/manga/";
|
||||||
var fileSystem = new MockFileSystem();
|
var fileSystem = new MockFileSystem();
|
||||||
fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc"));
|
fileSystem.AddFile($"{testDirectory}data-0.txt", new MockFileData("abc"));
|
||||||
|
|
@ -900,12 +922,14 @@ public class DirectoryServiceTests
|
||||||
#region GetHumanReadableBytes
|
#region GetHumanReadableBytes
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(1200, "1.17 KB")]
|
[InlineData(1200, 1.17, " KB")]
|
||||||
[InlineData(1, "1 B")]
|
[InlineData(1, 1, " B")]
|
||||||
[InlineData(10000000, "9.54 MB")]
|
[InlineData(10000000, 9.54, " MB")]
|
||||||
[InlineData(10000000000, "9.31 GB")]
|
[InlineData(10000000000, 9.31, " GB")]
|
||||||
public void GetHumanReadableBytesTest(long bytes, string expected)
|
public void GetHumanReadableBytesTest(long bytes, float number, string suffix)
|
||||||
{
|
{
|
||||||
|
// GetHumanReadableBytes is user facing, should be in CultureInfo.CurrentCulture
|
||||||
|
var expected = number.ToString(CultureInfo.CurrentCulture) + suffix;
|
||||||
Assert.Equal(expected, DirectoryService.GetHumanReadableBytes(bytes));
|
Assert.Equal(expected, DirectoryService.GetHumanReadableBytes(bytes));
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -1041,11 +1065,14 @@ public class DirectoryServiceTests
|
||||||
#region GetParentDirectory
|
#region GetParentDirectory
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(@"C:/file.txt", "C:/")]
|
[InlineData(@"file.txt", "")]
|
||||||
[InlineData(@"C:/folder/file.txt", "C:/folder")]
|
[InlineData(@"folder/file.txt", "folder")]
|
||||||
[InlineData(@"C:/folder/subfolder/file.txt", "C:/folder/subfolder")]
|
[InlineData(@"folder/subfolder/file.txt", "folder/subfolder")]
|
||||||
public void GetParentDirectoryName_ShouldFindParentOfFiles(string path, string expected)
|
public void GetParentDirectoryName_ShouldFindParentOfFiles(string path, string expected)
|
||||||
{
|
{
|
||||||
|
path = Root + path;
|
||||||
|
expected = Root + expected;
|
||||||
|
|
||||||
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
|
||||||
{
|
{
|
||||||
{ path, new MockFileData(string.Empty)}
|
{ path, new MockFileData(string.Empty)}
|
||||||
|
|
@ -1055,11 +1082,14 @@ public class DirectoryServiceTests
|
||||||
Assert.Equal(expected, ds.GetParentDirectoryName(path));
|
Assert.Equal(expected, ds.GetParentDirectoryName(path));
|
||||||
}
|
}
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(@"C:/folder", "C:/")]
|
[InlineData(@"folder", "")]
|
||||||
[InlineData(@"C:/folder/subfolder", "C:/folder")]
|
[InlineData(@"folder/subfolder", "folder")]
|
||||||
[InlineData(@"C:/folder/subfolder/another", "C:/folder/subfolder")]
|
[InlineData(@"folder/subfolder/another", "folder/subfolder")]
|
||||||
public void GetParentDirectoryName_ShouldFindParentOfDirectories(string path, string expected)
|
public void GetParentDirectoryName_ShouldFindParentOfDirectories(string path, string expected)
|
||||||
{
|
{
|
||||||
|
path = Root + path;
|
||||||
|
expected = Root + expected;
|
||||||
|
|
||||||
var fileSystem = new MockFileSystem();
|
var fileSystem = new MockFileSystem();
|
||||||
fileSystem.AddDirectory(path);
|
fileSystem.AddDirectory(path);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
|
|
@ -11,6 +10,8 @@ using API.DTOs.Scrobbling;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
|
using API.Entities.MetadataMatching;
|
||||||
|
using API.Entities.Person;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.Services.Tasks.Metadata;
|
using API.Services.Tasks.Metadata;
|
||||||
|
|
@ -20,8 +21,6 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
|
@ -30,17 +29,14 @@ namespace API.Tests.Services;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ExternalMetadataServiceTests : AbstractDbTest
|
public class ExternalMetadataServiceTests : AbstractDbTest
|
||||||
{
|
{
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
|
||||||
private readonly ExternalMetadataService _externalMetadataService;
|
private readonly ExternalMetadataService _externalMetadataService;
|
||||||
private readonly Dictionary<string, Genre> _genreLookup = new Dictionary<string, Genre>();
|
private readonly Dictionary<string, Genre> _genreLookup = new Dictionary<string, Genre>();
|
||||||
private readonly Dictionary<string, Tag> _tagLookup = new Dictionary<string, Tag>();
|
private readonly Dictionary<string, Tag> _tagLookup = new Dictionary<string, Tag>();
|
||||||
private readonly Dictionary<string, Person> _personLookup = new Dictionary<string, Person>();
|
private readonly Dictionary<string, Person> _personLookup = new Dictionary<string, Person>();
|
||||||
|
|
||||||
|
|
||||||
public ExternalMetadataServiceTests(ITestOutputHelper testOutputHelper)
|
public ExternalMetadataServiceTests()
|
||||||
{
|
{
|
||||||
_testOutputHelper = testOutputHelper;
|
|
||||||
|
|
||||||
// Set up Hangfire to use in-memory storage for testing
|
// Set up Hangfire to use in-memory storage for testing
|
||||||
GlobalConfiguration.Configuration.UseInMemoryStorage();
|
GlobalConfiguration.Configuration.UseInMemoryStorage();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
using System.Drawing;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using System.IO.Abstractions;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using EasyCaching.Core;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NetVips;
|
using NetVips;
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Image = NetVips.Image;
|
using Image = NetVips.Image;
|
||||||
|
|
||||||
|
|
@ -28,6 +23,7 @@ public class ImageServiceTests
|
||||||
public void GenerateBaseline()
|
public void GenerateBaseline()
|
||||||
{
|
{
|
||||||
GenerateFiles(BaselinePattern);
|
GenerateFiles(BaselinePattern);
|
||||||
|
Assert.True(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -38,6 +34,7 @@ public class ImageServiceTests
|
||||||
{
|
{
|
||||||
GenerateFiles(OutputPattern);
|
GenerateFiles(OutputPattern);
|
||||||
GenerateHtmlFile();
|
GenerateHtmlFile();
|
||||||
|
Assert.True(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateFiles(string outputExtension)
|
private void GenerateFiles(string outputExtension)
|
||||||
|
|
@ -159,7 +156,7 @@ public class ImageServiceTests
|
||||||
|
|
||||||
// Step 4: Generate HTML file
|
// Step 4: Generate HTML file
|
||||||
GenerateHtmlFileForColorScape();
|
GenerateHtmlFileForColorScape();
|
||||||
|
Assert.True(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void GenerateColorImage(string hexColor, string outputPath)
|
private static void GenerateColorImage(string hexColor, string outputPath)
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,19 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data.Common;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
|
||||||
using API.Data.Metadata;
|
using API.Data.Metadata;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
|
||||||
using API.Helpers.Builders;
|
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner;
|
using API.Services.Tasks.Scanner;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using API.Tests.Helpers;
|
using API.Tests.Helpers;
|
||||||
using AutoMapper;
|
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -204,11 +194,11 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
public async Task ScanLibrariesForSeries_ShouldFindFiles()
|
public async Task ScanLibrariesForSeries_ShouldFindFiles()
|
||||||
{
|
{
|
||||||
var fileSystem = new MockFileSystem();
|
var fileSystem = new MockFileSystem();
|
||||||
fileSystem.AddDirectory("C:/Data/");
|
fileSystem.AddDirectory(Root + "Data/");
|
||||||
fileSystem.AddFile("C:/Data/Accel World v1.cbz", new MockFileData(string.Empty));
|
fileSystem.AddFile(Root + "Data/Accel World v1.cbz", new MockFileData(string.Empty));
|
||||||
fileSystem.AddFile("C:/Data/Accel World v2.cbz", new MockFileData(string.Empty));
|
fileSystem.AddFile(Root + "Data/Accel World v2.cbz", new MockFileData(string.Empty));
|
||||||
fileSystem.AddFile("C:/Data/Accel World v2.pdf", new MockFileData(string.Empty));
|
fileSystem.AddFile(Root + "Data/Accel World v2.pdf", new MockFileData(string.Empty));
|
||||||
fileSystem.AddFile("C:/Data/Nothing.pdf", new MockFileData(string.Empty));
|
fileSystem.AddFile(Root + "Data/Nothing.pdf", new MockFileData(string.Empty));
|
||||||
|
|
||||||
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
|
||||||
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
var psf = new ParseScannedFiles(Substitute.For<ILogger<ParseScannedFiles>>(), ds,
|
||||||
|
|
@ -221,7 +211,7 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
Assert.NotNull(library);
|
Assert.NotNull(library);
|
||||||
|
|
||||||
library.Type = LibraryType.Manga;
|
library.Type = LibraryType.Manga;
|
||||||
var parsedSeries = await psf.ScanLibrariesForSeries(library, new List<string>() {"C:/Data/"}, false,
|
var parsedSeries = await psf.ScanLibrariesForSeries(library, new List<string>() {Root + "Data/"}, false,
|
||||||
await _unitOfWork.SeriesRepository.GetFolderPathMap(1));
|
await _unitOfWork.SeriesRepository.GetFolderPathMap(1));
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -358,7 +348,8 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
[Fact]
|
// TODO: Add back in (removed for Hotfix v0.8.5.x)
|
||||||
|
//[Fact]
|
||||||
public async Task HasSeriesFolderNotChangedSinceLastScan_AllSeriesFoldersHaveChanges()
|
public async Task HasSeriesFolderNotChangedSinceLastScan_AllSeriesFoldersHaveChanges()
|
||||||
{
|
{
|
||||||
const string testcase = "Subfolders always scanning all series changes - Manga.json";
|
const string testcase = "Subfolders always scanning all series changes - Manga.json";
|
||||||
|
|
@ -390,7 +381,7 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
var executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life");
|
var executionerAndHerWayOfLife = postLib.Series.First(x => x.Name == "The Executioner and Her Way of Life");
|
||||||
Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count);
|
Assert.Equal(2, executionerAndHerWayOfLife.Volumes.Count);
|
||||||
|
|
||||||
Thread.Sleep(1100); // Ensure at least one second has passed since library scan
|
await Task.Delay(1100); // Ensure at least one second has passed since library scan
|
||||||
|
|
||||||
// Add a new chapter to a volume of the series, and scan. Validate that only, and all directories of this
|
// Add a new chapter to a volume of the series, and scan. Validate that only, and all directories of this
|
||||||
// series are marked as HasChanged
|
// series are marked as HasChanged
|
||||||
|
|
@ -439,7 +430,7 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End");
|
var frieren = postLib.Series.First(x => x.Name == "Frieren - Beyond Journey's End");
|
||||||
Assert.Equal(2, frieren.Volumes.Count);
|
Assert.Equal(2, frieren.Volumes.Count);
|
||||||
|
|
||||||
Thread.Sleep(1100); // Ensure at least one second has passed since library scan
|
await Task.Delay(1100); // Ensure at least one second has passed since library scan
|
||||||
|
|
||||||
// Add a volume to a series, and scan. Ensure only this series is marked as HasChanged
|
// Add a volume to a series, and scan. Ensure only this series is marked as HasChanged
|
||||||
var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "YenPress"), "The Executioner and Her Way of Life");
|
var executionerCopyDir = Path.Join(Path.Join(testDirectoryPath, "YenPress"), "The Executioner and Her Way of Life");
|
||||||
|
|
@ -452,7 +443,8 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
Assert.Equal(1, changes);
|
Assert.Equal(1, changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
// TODO: Add back in (removed for Hotfix v0.8.5.x)
|
||||||
|
//[Fact]
|
||||||
public async Task SubFoldersNoSubFolders_SkipAll()
|
public async Task SubFoldersNoSubFolders_SkipAll()
|
||||||
{
|
{
|
||||||
const string testcase = "Subfolders and files at root - Manga.json";
|
const string testcase = "Subfolders and files at root - Manga.json";
|
||||||
|
|
@ -481,7 +473,7 @@ public class ParseScannedFilesTests : AbstractDbTest
|
||||||
|
|
||||||
// Needs to be actual time as the write time is now, so if we set LastFolderChecked in the past
|
// Needs to be actual time as the write time is now, so if we set LastFolderChecked in the past
|
||||||
// it'll always a scan as it was changed since the last scan.
|
// it'll always a scan as it was changed since the last scan.
|
||||||
Thread.Sleep(1100); // Ensure at least one second has passed since library scan
|
await Task.Delay(1100); // Ensure at least one second has passed since library scan
|
||||||
|
|
||||||
var res = await psf.ScanFiles(testDirectoryPath, true,
|
var res = await psf.ScanFiles(testDirectoryPath, true,
|
||||||
await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib);
|
await _unitOfWork.SeriesRepository.GetFolderPathMap(postLib.Id), postLib);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,4 @@
|
||||||
using System.IO;
|
namespace API.Tests.Services;
|
||||||
using API.Data;
|
|
||||||
using API.Data.Metadata;
|
|
||||||
using API.Entities;
|
|
||||||
using API.Entities.Enums;
|
|
||||||
using API.Helpers;
|
|
||||||
using API.Helpers.Builders;
|
|
||||||
using API.Services;
|
|
||||||
using API.Services.Tasks.Metadata;
|
|
||||||
using API.Services.Tasks.Scanner;
|
|
||||||
using API.SignalR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
|
||||||
|
|
||||||
public class ProcessSeriesTests
|
public class ProcessSeriesTests
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,20 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
|
||||||
using API.DTOs.Progress;
|
using API.DTOs.Progress;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.Services.Tasks;
|
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using API.Tests.Helpers;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Hangfire.InMemory;
|
using Hangfire.InMemory;
|
||||||
|
|
@ -32,18 +27,13 @@ using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
public class ReaderServiceTests
|
public class ReaderServiceTests: AbstractFsTest
|
||||||
{
|
{
|
||||||
private readonly ITestOutputHelper _testOutputHelper;
|
private readonly ITestOutputHelper _testOutputHelper;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly DataContext _context;
|
private readonly DataContext _context;
|
||||||
private readonly ReaderService _readerService;
|
private readonly ReaderService _readerService;
|
||||||
|
|
||||||
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(ITestOutputHelper testOutputHelper)
|
public ReaderServiceTests(ITestOutputHelper testOutputHelper)
|
||||||
{
|
{
|
||||||
_testOutputHelper = testOutputHelper;
|
_testOutputHelper = testOutputHelper;
|
||||||
|
|
@ -101,19 +91,6 @@ public class ReaderServiceTests
|
||||||
await _context.SaveChangesAsync();
|
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
|
#endregion
|
||||||
|
|
||||||
#region FormatBookmarkFolderPath
|
#region FormatBookmarkFolderPath
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,11 @@ using API.DTOs.ReadingLists;
|
||||||
using API.DTOs.ReadingLists.CBL;
|
using API.DTOs.ReadingLists.CBL;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
|
||||||
using API.Extensions;
|
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.Services.Tasks;
|
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using API.Tests.Helpers;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -583,6 +579,93 @@ public class ReadingListServiceTests
|
||||||
Assert.Equal(AgeRating.G, readingList.AgeRating);
|
Assert.Equal(AgeRating.G, readingList.AgeRating);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateReadingListAgeRatingForSeries()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var spiceAndWolf = new SeriesBuilder("Spice and Wolf")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder().Build())
|
||||||
|
.WithVolumes([
|
||||||
|
new VolumeBuilder("1")
|
||||||
|
.WithChapters([
|
||||||
|
new ChapterBuilder("1").Build(),
|
||||||
|
new ChapterBuilder("2").Build(),
|
||||||
|
]).Build()
|
||||||
|
]).Build();
|
||||||
|
spiceAndWolf.Metadata.AgeRating = AgeRating.Everyone;
|
||||||
|
|
||||||
|
var othersidePicnic = new SeriesBuilder("Otherside Picnic ")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder().Build())
|
||||||
|
.WithVolumes([
|
||||||
|
new VolumeBuilder("1")
|
||||||
|
.WithChapters([
|
||||||
|
new ChapterBuilder("1").Build(),
|
||||||
|
new ChapterBuilder("2").Build(),
|
||||||
|
]).Build()
|
||||||
|
]).Build();
|
||||||
|
othersidePicnic.Metadata.AgeRating = AgeRating.Everyone;
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "Amelia",
|
||||||
|
ReadingLists = new List<ReadingList>(),
|
||||||
|
Libraries = new List<Library>
|
||||||
|
{
|
||||||
|
new LibraryBuilder("Test Library", LibraryType.LightNovel)
|
||||||
|
.WithSeries(spiceAndWolf)
|
||||||
|
.WithSeries(othersidePicnic)
|
||||||
|
.Build(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync("Amelia", AppUserIncludes.ReadingLists);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
|
||||||
|
var myTestReadingList = new ReadingListBuilder("MyReadingList").Build();
|
||||||
|
var mySecondTestReadingList = new ReadingListBuilder("MySecondReadingList").Build();
|
||||||
|
var myThirdTestReadingList = new ReadingListBuilder("MyThirdReadingList").Build();
|
||||||
|
user.ReadingLists = new List<ReadingList>()
|
||||||
|
{
|
||||||
|
myTestReadingList,
|
||||||
|
mySecondTestReadingList,
|
||||||
|
myThirdTestReadingList,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
await _readingListService.AddChaptersToReadingList(spiceAndWolf.Id, new List<int> {1, 2}, myTestReadingList);
|
||||||
|
await _readingListService.AddChaptersToReadingList(othersidePicnic.Id, new List<int> {3, 4}, myTestReadingList);
|
||||||
|
await _readingListService.AddChaptersToReadingList(spiceAndWolf.Id, new List<int> {1, 2}, myThirdTestReadingList);
|
||||||
|
await _readingListService.AddChaptersToReadingList(othersidePicnic.Id, new List<int> {3, 4}, mySecondTestReadingList);
|
||||||
|
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
await _readingListService.CalculateReadingListAgeRating(myTestReadingList);
|
||||||
|
await _readingListService.CalculateReadingListAgeRating(mySecondTestReadingList);
|
||||||
|
Assert.Equal(AgeRating.Everyone, myTestReadingList.AgeRating);
|
||||||
|
Assert.Equal(AgeRating.Everyone, mySecondTestReadingList.AgeRating);
|
||||||
|
Assert.Equal(AgeRating.Everyone, myThirdTestReadingList.AgeRating);
|
||||||
|
|
||||||
|
await _readingListService.UpdateReadingListAgeRatingForSeries(othersidePicnic.Id, AgeRating.Mature);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
// Reading lists containing Otherside Picnic are updated
|
||||||
|
myTestReadingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(1);
|
||||||
|
Assert.NotNull(myTestReadingList);
|
||||||
|
Assert.Equal(AgeRating.Mature, myTestReadingList.AgeRating);
|
||||||
|
|
||||||
|
mySecondTestReadingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(2);
|
||||||
|
Assert.NotNull(mySecondTestReadingList);
|
||||||
|
Assert.Equal(AgeRating.Mature, mySecondTestReadingList.AgeRating);
|
||||||
|
|
||||||
|
// Unrelated reading list is not updated
|
||||||
|
myThirdTestReadingList = await _unitOfWork.ReadingListRepository.GetReadingListByIdAsync(3);
|
||||||
|
Assert.NotNull(myThirdTestReadingList);
|
||||||
|
Assert.Equal(AgeRating.Everyone, myThirdTestReadingList.AgeRating);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region CalculateStartAndEndDates
|
#region CalculateStartAndEndDates
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
|
||||||
using System.Xml.Serialization;
|
|
||||||
using API.Data;
|
|
||||||
using API.Data.Metadata;
|
using API.Data.Metadata;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
|
||||||
using API.Helpers.Builders;
|
|
||||||
using API.Services;
|
|
||||||
using API.Services.Plus;
|
|
||||||
using API.Services.Tasks;
|
|
||||||
using API.Services.Tasks.Metadata;
|
|
||||||
using API.Services.Tasks.Scanner;
|
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using API.SignalR;
|
|
||||||
using API.Tests.Helpers;
|
using API.Tests.Helpers;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
|
@ -108,7 +90,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ScanLibrary_FlatSeries()
|
public async Task ScanLibrary_FlatSeries()
|
||||||
{
|
{
|
||||||
var testcase = "Flat Series - Manga.json";
|
const string testcase = "Flat Series - Manga.json";
|
||||||
var library = await _scannerHelper.GenerateScannerData(testcase);
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
var scanner = _scannerHelper.CreateServices();
|
var scanner = _scannerHelper.CreateServices();
|
||||||
await scanner.ScanLibrary(library.Id);
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
|
@ -124,7 +106,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ScanLibrary_FlatSeriesWithSpecialFolder()
|
public async Task ScanLibrary_FlatSeriesWithSpecialFolder()
|
||||||
{
|
{
|
||||||
var testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
||||||
var library = await _scannerHelper.GenerateScannerData(testcase);
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
var scanner = _scannerHelper.CreateServices();
|
var scanner = _scannerHelper.CreateServices();
|
||||||
await scanner.ScanLibrary(library.Id);
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
|
@ -139,7 +121,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ScanLibrary_FlatSeriesWithSpecialFolder_AlternativeNaming()
|
public async Task ScanLibrary_FlatSeriesWithSpecialFolder_AlternativeNaming()
|
||||||
{
|
{
|
||||||
var testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
const string testcase = "Flat Series with Specials Folder Alt Naming - Manga.json";
|
||||||
var library = await _scannerHelper.GenerateScannerData(testcase);
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
var scanner = _scannerHelper.CreateServices();
|
var scanner = _scannerHelper.CreateServices();
|
||||||
await scanner.ScanLibrary(library.Id);
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
|
@ -167,7 +149,6 @@ public class ScannerServiceTests : AbstractDbTest
|
||||||
Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null));
|
Assert.NotNull(postLib.Series.First().Volumes.FirstOrDefault(v => v.Chapters.FirstOrDefault(c => c.IsSpecial) != null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ScanLibrary_SeriesWithUnbalancedParenthesis()
|
public async Task ScanLibrary_SeriesWithUnbalancedParenthesis()
|
||||||
{
|
{
|
||||||
|
|
@ -927,4 +908,34 @@ public class ScannerServiceTests : AbstractDbTest
|
||||||
Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count));
|
Assert.Equal(6, spiceAndWolf.Volumes.Sum(v => v.Chapters.Count));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure when Kavita scans, the sort order of chapters is correct
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_SortOrderWorks()
|
||||||
|
{
|
||||||
|
const string testcase = "Sort Order - Manga.json";
|
||||||
|
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase);
|
||||||
|
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
|
||||||
|
// Get the loose leaf volume and confirm each chapter aligns with expectation of Sort Order
|
||||||
|
var series = postLib.Series.First();
|
||||||
|
Assert.NotNull(series);
|
||||||
|
|
||||||
|
var volume = series.Volumes.FirstOrDefault();
|
||||||
|
Assert.NotNull(volume);
|
||||||
|
|
||||||
|
var sortedChapters = volume.Chapters.OrderBy(c => c.SortOrder).ToList();
|
||||||
|
Assert.True(sortedChapters[0].SortOrder.Is(1f));
|
||||||
|
Assert.True(sortedChapters[1].SortOrder.Is(4f));
|
||||||
|
Assert.True(sortedChapters[2].SortOrder.Is(5f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO.Abstractions;
|
using System.IO.Abstractions;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -11,6 +12,7 @@ using API.DTOs.SeriesDetail;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
|
using API.Entities.Person;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
@ -56,8 +58,9 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
|
|
||||||
_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
_seriesService = new SeriesService(_unitOfWork, Substitute.For<IEventHub>(),
|
||||||
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
|
Substitute.For<ITaskScheduler>(), Substitute.For<ILogger<SeriesService>>(),
|
||||||
Substitute.For<IScrobblingService>(), locService);
|
Substitute.For<IScrobblingService>(), locService, Substitute.For<IReadingListService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Setup
|
#region Setup
|
||||||
|
|
||||||
protected override async Task ResetDb()
|
protected override async Task ResetDb()
|
||||||
|
|
@ -807,6 +810,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase()));
|
Assert.True(series.Metadata.Genres.Select(g1 => g1.Title).All(g2 => g2 == "New Genre".SentenceCase()));
|
||||||
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
|
Assert.False(series.Metadata.GenresLocked); // GenreLocked is false unless the UI Explicitly says it should be locked
|
||||||
|
|
@ -845,6 +849,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person"));
|
Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person"));
|
||||||
Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked
|
Assert.False(series.Metadata.PublisherLocked); // PublisherLocked is false unless the UI Explicitly says it should be locked
|
||||||
|
|
@ -885,6 +890,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person"));
|
Assert.True(series.Metadata.People.Select(g => g.Person.Name).All(personName => personName == "Existing Person"));
|
||||||
Assert.True(series.Metadata.PublisherLocked);
|
Assert.True(series.Metadata.PublisherLocked);
|
||||||
|
|
@ -974,10 +980,64 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.False(series.Metadata.People.Any());
|
Assert.False(series.Metadata.People.Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This emulates the UI operations wrt to locking
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateSeriesMetadata_ShouldRemoveExistingPerson_AfterAdding()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var s = new SeriesBuilder("Test")
|
||||||
|
.WithMetadata(new SeriesMetadataBuilder().Build())
|
||||||
|
.Build();
|
||||||
|
s.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build();
|
||||||
|
var g = new PersonBuilder("Existing Person").Build();
|
||||||
|
_context.Series.Add(s);
|
||||||
|
|
||||||
|
_context.Person.Add(g);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto
|
||||||
|
{
|
||||||
|
SeriesMetadata = new SeriesMetadataDto
|
||||||
|
{
|
||||||
|
SeriesId = 1,
|
||||||
|
Publishers = new List<PersonDto>() {new PersonDto() {Name = "Test"}},
|
||||||
|
PublisherLocked = true
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(success);
|
||||||
|
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
|
Assert.NotNull(series.Metadata);
|
||||||
|
Assert.True(series.Metadata.People.Count != 0);
|
||||||
|
Assert.True(series.Metadata.PublisherLocked);
|
||||||
|
|
||||||
|
|
||||||
|
success = await _seriesService.UpdateSeriesMetadata(new UpdateSeriesMetadataDto
|
||||||
|
{
|
||||||
|
SeriesMetadata = new SeriesMetadataDto
|
||||||
|
{
|
||||||
|
SeriesId = 1,
|
||||||
|
Publishers = new List<PersonDto>(),
|
||||||
|
PublisherLocked = false
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Empty(series.Metadata.People);
|
||||||
|
Assert.False(series.Metadata.PublisherLocked);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateSeriesMetadata_ShouldLockIfTold()
|
public async Task UpdateSeriesMetadata_ShouldLockIfTold()
|
||||||
{
|
{
|
||||||
|
|
@ -1008,6 +1068,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase()));
|
Assert.True(series.Metadata.Genres.Select(g => g.Title).All(g => g == "Existing Genre".SentenceCase()));
|
||||||
Assert.True(series.Metadata.GenresLocked);
|
Assert.True(series.Metadata.GenresLocked);
|
||||||
|
|
@ -1037,6 +1098,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.Equal(0, series.Metadata.ReleaseYear);
|
Assert.Equal(0, series.Metadata.ReleaseYear);
|
||||||
Assert.False(series.Metadata.ReleaseYearLocked);
|
Assert.False(series.Metadata.ReleaseYearLocked);
|
||||||
|
|
@ -1069,6 +1131,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title));
|
Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title));
|
||||||
Assert.False(series.Metadata.GenresLocked); // Ensure the lock is not activated unless specified.
|
Assert.False(series.Metadata.GenresLocked); // Ensure the lock is not activated unless specified.
|
||||||
|
|
@ -1102,6 +1165,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.DoesNotContain("Existing Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title));
|
Assert.DoesNotContain("Existing Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title));
|
||||||
Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title));
|
Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title));
|
||||||
|
|
@ -1135,6 +1199,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.Empty(series.Metadata.Genres);
|
Assert.Empty(series.Metadata.Genres);
|
||||||
}
|
}
|
||||||
|
|
@ -1166,6 +1231,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title));
|
Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title));
|
||||||
}
|
}
|
||||||
|
|
@ -1198,6 +1264,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.DoesNotContain("Existing Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title));
|
Assert.DoesNotContain("Existing Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title));
|
||||||
Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title));
|
Assert.Contains("New Tag".SentenceCase(), series.Metadata.Tags.Select(t => t.Title));
|
||||||
|
|
@ -1231,6 +1298,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
Assert.True(success);
|
Assert.True(success);
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(s.Id);
|
||||||
|
Assert.NotNull(series);
|
||||||
Assert.NotNull(series.Metadata);
|
Assert.NotNull(series.Metadata);
|
||||||
Assert.Empty(series.Metadata.Tags);
|
Assert.Empty(series.Metadata.Tags);
|
||||||
}
|
}
|
||||||
|
|
@ -1429,6 +1497,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
addRelationDto.Sequels.Add(2);
|
addRelationDto.Sequels.Add(2);
|
||||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
Assert.NotNull(series1);
|
Assert.NotNull(series1);
|
||||||
|
Assert.NotNull(series2);
|
||||||
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
||||||
Assert.Equal(1, series2.Relations.Single(s => s.TargetSeriesId == 1).TargetSeriesId);
|
Assert.Equal(1, series2.Relations.Single(s => s.TargetSeriesId == 1).TargetSeriesId);
|
||||||
}
|
}
|
||||||
|
|
@ -1471,8 +1540,9 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
// Remove relations
|
// Remove relations
|
||||||
var removeRelationDto = CreateRelationsDto(series1);
|
var removeRelationDto = CreateRelationsDto(series1);
|
||||||
await _seriesService.UpdateRelatedSeries(removeRelationDto);
|
await _seriesService.UpdateRelatedSeries(removeRelationDto);
|
||||||
Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 1));
|
Assert.NotNull(series1);
|
||||||
Assert.Empty(series1.Relations.Where(s => s.TargetSeriesId == 2));
|
Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 1);
|
||||||
|
Assert.DoesNotContain(series1.Relations, s => s.TargetSeriesId == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1505,6 +1575,8 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
var addRelationDto = CreateRelationsDto(series1);
|
var addRelationDto = CreateRelationsDto(series1);
|
||||||
addRelationDto.Adaptations.Add(2);
|
addRelationDto.Adaptations.Add(2);
|
||||||
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
await _seriesService.UpdateRelatedSeries(addRelationDto);
|
||||||
|
|
||||||
|
Assert.NotNull(series1);
|
||||||
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
|
||||||
|
|
||||||
_context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2));
|
_context.Series.Remove(await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2));
|
||||||
|
|
@ -2080,7 +2152,7 @@ public class SeriesServiceTests : AbstractDbTest
|
||||||
public async Task GetEstimatedChapterCreationDate_NextChapter_ChaptersMonthApart()
|
public async Task GetEstimatedChapterCreationDate_NextChapter_ChaptersMonthApart()
|
||||||
{
|
{
|
||||||
await ResetDb();
|
await ResetDb();
|
||||||
var now = DateTime.Parse("2021-01-01"); // 10/31/2024 can trigger an edge case bug
|
var now = DateTime.Parse("2021-01-01", CultureInfo.InvariantCulture); // 10/31/2024 can trigger an edge case bug
|
||||||
|
|
||||||
_context.Library.Add(new LibraryBuilder("Test LIb")
|
_context.Library.Add(new LibraryBuilder("Test LIb")
|
||||||
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
|
.WithAppUser(new AppUserBuilder("majora2007", string.Empty).Build())
|
||||||
|
|
|
||||||
292
API.Tests/Services/SettingsServiceTests.cs
Normal file
292
API.Tests/Services/SettingsServiceTests.cs
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs.KavitaPlus.Metadata;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.MetadataMatching;
|
||||||
|
using API.Services;
|
||||||
|
using API.Services.Tasks.Scanner;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace API.Tests.Services;
|
||||||
|
|
||||||
|
public class SettingsServiceTests
|
||||||
|
{
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
private readonly IUnitOfWork _mockUnitOfWork;
|
||||||
|
|
||||||
|
public SettingsServiceTests()
|
||||||
|
{
|
||||||
|
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), new FileSystem());
|
||||||
|
|
||||||
|
_mockUnitOfWork = Substitute.For<IUnitOfWork>();
|
||||||
|
_settingsService = new SettingsService(_mockUnitOfWork, ds,
|
||||||
|
Substitute.For<ILibraryWatcher>(), Substitute.For<ITaskScheduler>(),
|
||||||
|
Substitute.For<ILogger<SettingsService>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
#region UpdateMetadataSettings
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateMetadataSettings_ShouldUpdateExistingSettings()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var existingSettings = new MetadataSettings
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Enabled = false,
|
||||||
|
EnableSummary = false,
|
||||||
|
EnableLocalizedName = false,
|
||||||
|
EnablePublicationStatus = false,
|
||||||
|
EnableRelationships = false,
|
||||||
|
EnablePeople = false,
|
||||||
|
EnableStartDate = false,
|
||||||
|
EnableGenres = false,
|
||||||
|
EnableTags = false,
|
||||||
|
FirstLastPeopleNaming = false,
|
||||||
|
EnableCoverImage = false,
|
||||||
|
AgeRatingMappings = new Dictionary<string, AgeRating>(),
|
||||||
|
Blacklist = [],
|
||||||
|
Whitelist = [],
|
||||||
|
Overrides = [],
|
||||||
|
PersonRoles = [],
|
||||||
|
FieldMappings = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var settingsRepo = Substitute.For<ISettingsRepository>();
|
||||||
|
settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
|
||||||
|
settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
|
||||||
|
_mockUnitOfWork.SettingsRepository.Returns(settingsRepo);
|
||||||
|
|
||||||
|
var updateDto = new MetadataSettingsDto
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
EnableSummary = true,
|
||||||
|
EnableLocalizedName = true,
|
||||||
|
EnablePublicationStatus = true,
|
||||||
|
EnableRelationships = true,
|
||||||
|
EnablePeople = true,
|
||||||
|
EnableStartDate = true,
|
||||||
|
EnableGenres = true,
|
||||||
|
EnableTags = true,
|
||||||
|
FirstLastPeopleNaming = true,
|
||||||
|
EnableCoverImage = true,
|
||||||
|
AgeRatingMappings = new Dictionary<string, AgeRating> { { "Adult", AgeRating.R18Plus } },
|
||||||
|
Blacklist = ["blacklisted-tag"],
|
||||||
|
Whitelist = ["whitelisted-tag"],
|
||||||
|
Overrides = [MetadataSettingField.Summary],
|
||||||
|
PersonRoles = [PersonRole.Writer],
|
||||||
|
FieldMappings =
|
||||||
|
[
|
||||||
|
new MetadataFieldMappingDto
|
||||||
|
{
|
||||||
|
SourceType = MetadataFieldType.Genre,
|
||||||
|
DestinationType = MetadataFieldType.Tag,
|
||||||
|
SourceValue = "Action",
|
||||||
|
DestinationValue = "Fight",
|
||||||
|
ExcludeFromSource = true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _settingsService.UpdateMetadataSettings(updateDto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _mockUnitOfWork.Received(1).CommitAsync();
|
||||||
|
|
||||||
|
// Verify properties were updated
|
||||||
|
Assert.True(existingSettings.Enabled);
|
||||||
|
Assert.True(existingSettings.EnableSummary);
|
||||||
|
Assert.True(existingSettings.EnableLocalizedName);
|
||||||
|
Assert.True(existingSettings.EnablePublicationStatus);
|
||||||
|
Assert.True(existingSettings.EnableRelationships);
|
||||||
|
Assert.True(existingSettings.EnablePeople);
|
||||||
|
Assert.True(existingSettings.EnableStartDate);
|
||||||
|
Assert.True(existingSettings.EnableGenres);
|
||||||
|
Assert.True(existingSettings.EnableTags);
|
||||||
|
Assert.True(existingSettings.FirstLastPeopleNaming);
|
||||||
|
Assert.True(existingSettings.EnableCoverImage);
|
||||||
|
|
||||||
|
// Verify collections were updated
|
||||||
|
Assert.Single(existingSettings.AgeRatingMappings);
|
||||||
|
Assert.Equal(AgeRating.R18Plus, existingSettings.AgeRatingMappings["Adult"]);
|
||||||
|
|
||||||
|
Assert.Single(existingSettings.Blacklist);
|
||||||
|
Assert.Equal("blacklisted-tag", existingSettings.Blacklist[0]);
|
||||||
|
|
||||||
|
Assert.Single(existingSettings.Whitelist);
|
||||||
|
Assert.Equal("whitelisted-tag", existingSettings.Whitelist[0]);
|
||||||
|
|
||||||
|
Assert.Single(existingSettings.Overrides);
|
||||||
|
Assert.Equal(MetadataSettingField.Summary, existingSettings.Overrides[0]);
|
||||||
|
|
||||||
|
Assert.Single(existingSettings.PersonRoles);
|
||||||
|
Assert.Equal(PersonRole.Writer, existingSettings.PersonRoles[0]);
|
||||||
|
|
||||||
|
Assert.Single(existingSettings.FieldMappings);
|
||||||
|
Assert.Equal(MetadataFieldType.Genre, existingSettings.FieldMappings[0].SourceType);
|
||||||
|
Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[0].DestinationType);
|
||||||
|
Assert.Equal("Action", existingSettings.FieldMappings[0].SourceValue);
|
||||||
|
Assert.Equal("Fight", existingSettings.FieldMappings[0].DestinationValue);
|
||||||
|
Assert.True(existingSettings.FieldMappings[0].ExcludeFromSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateMetadataSettings_WithNullCollections_ShouldUseEmptyCollections()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var existingSettings = new MetadataSettings
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
FieldMappings = [new MetadataFieldMapping {Id = 1, SourceValue = "OldValue"}]
|
||||||
|
};
|
||||||
|
|
||||||
|
var settingsRepo = Substitute.For<ISettingsRepository>();
|
||||||
|
settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
|
||||||
|
settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
|
||||||
|
_mockUnitOfWork.SettingsRepository.Returns(settingsRepo);
|
||||||
|
|
||||||
|
var updateDto = new MetadataSettingsDto
|
||||||
|
{
|
||||||
|
AgeRatingMappings = null,
|
||||||
|
Blacklist = null,
|
||||||
|
Whitelist = null,
|
||||||
|
Overrides = null,
|
||||||
|
PersonRoles = null,
|
||||||
|
FieldMappings = null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _settingsService.UpdateMetadataSettings(updateDto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _mockUnitOfWork.Received(1).CommitAsync();
|
||||||
|
|
||||||
|
Assert.Empty(existingSettings.AgeRatingMappings);
|
||||||
|
Assert.Empty(existingSettings.Blacklist);
|
||||||
|
Assert.Empty(existingSettings.Whitelist);
|
||||||
|
Assert.Empty(existingSettings.Overrides);
|
||||||
|
Assert.Empty(existingSettings.PersonRoles);
|
||||||
|
|
||||||
|
// Verify existing field mappings were cleared
|
||||||
|
settingsRepo.Received(1).RemoveRange(Arg.Any<List<MetadataFieldMapping>>());
|
||||||
|
Assert.Empty(existingSettings.FieldMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateMetadataSettings_WithFieldMappings_ShouldReplaceExistingMappings()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var existingSettings = new MetadataSettings
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
FieldMappings =
|
||||||
|
[
|
||||||
|
new MetadataFieldMapping
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
SourceType = MetadataFieldType.Genre,
|
||||||
|
DestinationType = MetadataFieldType.Genre,
|
||||||
|
SourceValue = "OldValue",
|
||||||
|
DestinationValue = "OldDestination",
|
||||||
|
ExcludeFromSource = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var settingsRepo = Substitute.For<ISettingsRepository>();
|
||||||
|
settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
|
||||||
|
settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
|
||||||
|
_mockUnitOfWork.SettingsRepository.Returns(settingsRepo);
|
||||||
|
|
||||||
|
var updateDto = new MetadataSettingsDto
|
||||||
|
{
|
||||||
|
FieldMappings =
|
||||||
|
[
|
||||||
|
new MetadataFieldMappingDto
|
||||||
|
{
|
||||||
|
SourceType = MetadataFieldType.Tag,
|
||||||
|
DestinationType = MetadataFieldType.Genre,
|
||||||
|
SourceValue = "NewValue",
|
||||||
|
DestinationValue = "NewDestination",
|
||||||
|
ExcludeFromSource = true
|
||||||
|
},
|
||||||
|
|
||||||
|
new MetadataFieldMappingDto
|
||||||
|
{
|
||||||
|
SourceType = MetadataFieldType.Tag,
|
||||||
|
DestinationType = MetadataFieldType.Tag,
|
||||||
|
SourceValue = "AnotherValue",
|
||||||
|
DestinationValue = "AnotherDestination",
|
||||||
|
ExcludeFromSource = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _settingsService.UpdateMetadataSettings(updateDto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _mockUnitOfWork.Received(1).CommitAsync();
|
||||||
|
|
||||||
|
// Verify existing field mappings were cleared and new ones added
|
||||||
|
settingsRepo.Received(1).RemoveRange(Arg.Any<List<MetadataFieldMapping>>());
|
||||||
|
Assert.Equal(2, existingSettings.FieldMappings.Count);
|
||||||
|
|
||||||
|
// Verify first mapping
|
||||||
|
Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[0].SourceType);
|
||||||
|
Assert.Equal(MetadataFieldType.Genre, existingSettings.FieldMappings[0].DestinationType);
|
||||||
|
Assert.Equal("NewValue", existingSettings.FieldMappings[0].SourceValue);
|
||||||
|
Assert.Equal("NewDestination", existingSettings.FieldMappings[0].DestinationValue);
|
||||||
|
Assert.True(existingSettings.FieldMappings[0].ExcludeFromSource);
|
||||||
|
|
||||||
|
// Verify second mapping
|
||||||
|
Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[1].SourceType);
|
||||||
|
Assert.Equal(MetadataFieldType.Tag, existingSettings.FieldMappings[1].DestinationType);
|
||||||
|
Assert.Equal("AnotherValue", existingSettings.FieldMappings[1].SourceValue);
|
||||||
|
Assert.Equal("AnotherDestination", existingSettings.FieldMappings[1].DestinationValue);
|
||||||
|
Assert.False(existingSettings.FieldMappings[1].ExcludeFromSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateMetadataSettings_WithBlacklistWhitelist_ShouldNormalizeAndDeduplicateEntries()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var existingSettings = new MetadataSettings
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Blacklist = [],
|
||||||
|
Whitelist = []
|
||||||
|
};
|
||||||
|
|
||||||
|
// We need to mock the repository and provide a custom implementation for ToNormalized
|
||||||
|
var settingsRepo = Substitute.For<ISettingsRepository>();
|
||||||
|
settingsRepo.GetMetadataSettings().Returns(Task.FromResult(existingSettings));
|
||||||
|
settingsRepo.GetMetadataSettingDto().Returns(Task.FromResult(new MetadataSettingsDto()));
|
||||||
|
_mockUnitOfWork.SettingsRepository.Returns(settingsRepo);
|
||||||
|
|
||||||
|
var updateDto = new MetadataSettingsDto
|
||||||
|
{
|
||||||
|
// Include duplicates with different casing and whitespace
|
||||||
|
Blacklist = ["tag1", "Tag1", " tag2 ", "", " ", "tag3"],
|
||||||
|
Whitelist = ["allowed1", "Allowed1", " allowed2 ", "", "allowed3"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _settingsService.UpdateMetadataSettings(updateDto);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _mockUnitOfWork.Received(1).CommitAsync();
|
||||||
|
|
||||||
|
Assert.Equal(3, existingSettings.Blacklist.Count);
|
||||||
|
Assert.Equal(3, existingSettings.Whitelist.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using API.Extensions;
|
using API.Helpers.Builders;
|
||||||
using API.Helpers.Builders;
|
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.Services.Tasks;
|
|
||||||
|
|
||||||
namespace API.Tests.Services;
|
namespace API.Tests.Services;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
@ -16,7 +14,6 @@ using API.Entities.Enums;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using SignalR;
|
using SignalR;
|
||||||
using Helpers;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
|
||||||
Binary file not shown.
BIN
API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf
Normal file
BIN
API.Tests/Services/Test Data/BookService/Rollo at Work SP01.pdf
Normal file
Binary file not shown.
|
|
@ -1,5 +1,5 @@
|
||||||
[
|
[
|
||||||
"Uzaki-chan Wants to Hang Out!\\Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz",
|
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! - 2022 New Years Special SP01.cbz",
|
||||||
"Uzaki-chan Wants to Hang Out!\\Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz",
|
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! - Ch. 103 - Kouhai and Control.cbz",
|
||||||
"Uzaki-chan Wants to Hang Out!\\Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz"
|
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! v01 (2019) (Digital) (danke-Empire).cbz"
|
||||||
]
|
]
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 1-3.cbz",
|
||||||
|
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 4.cbz",
|
||||||
|
"Uzaki-chan Wants to Hang Out!/Uzaki-chan Wants to Hang Out! Ch 5.cbz"
|
||||||
|
]
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs.Update;
|
using API.DTOs.Update;
|
||||||
using API.Extensions;
|
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Flurl.Http;
|
|
||||||
using Flurl.Http.Testing;
|
using Flurl.Http.Testing;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -65,13 +60,13 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckForUpdate_ShouldReturnNull_WhenGithubApiReturnsNull()
|
public async Task CheckForUpdate_ShouldReturnNull_WhenGithubApiReturnsNull()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
_httpTest.RespondWith("null");
|
_httpTest.RespondWith("null");
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.CheckForUpdate();
|
var result = await _service.CheckForUpdate();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Null(result);
|
Assert.Null(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +74,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
//[Fact]
|
//[Fact]
|
||||||
public async Task CheckForUpdate_ShouldReturnUpdateNotification_WhenNewVersionIsAvailable()
|
public async Task CheckForUpdate_ShouldReturnUpdateNotification_WhenNewVersionIsAvailable()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var githubResponse = new
|
var githubResponse = new
|
||||||
{
|
{
|
||||||
tag_name = "v0.6.0",
|
tag_name = "v0.6.0",
|
||||||
|
|
@ -91,10 +86,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(githubResponse);
|
_httpTest.RespondWithJson(githubResponse);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.CheckForUpdate();
|
var result = await _service.CheckForUpdate();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Equal("0.6.0", result.UpdateVersion);
|
Assert.Equal("0.6.0", result.UpdateVersion);
|
||||||
Assert.Equal("0.5.0.0", result.CurrentVersion);
|
Assert.Equal("0.5.0.0", result.CurrentVersion);
|
||||||
|
|
@ -121,10 +116,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(githubResponse);
|
_httpTest.RespondWithJson(githubResponse);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.CheckForUpdate();
|
var result = await _service.CheckForUpdate();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.True(result.IsReleaseEqual);
|
Assert.True(result.IsReleaseEqual);
|
||||||
Assert.False(result.IsReleaseNewer);
|
Assert.False(result.IsReleaseNewer);
|
||||||
|
|
@ -134,7 +129,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
//[Fact]
|
//[Fact]
|
||||||
public async Task PushUpdate_ShouldSendUpdateEvent_WhenNewerVersionAvailable()
|
public async Task PushUpdate_ShouldSendUpdateEvent_WhenNewerVersionAvailable()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var update = new UpdateNotificationDto
|
var update = new UpdateNotificationDto
|
||||||
{
|
{
|
||||||
UpdateVersion = "0.6.0",
|
UpdateVersion = "0.6.0",
|
||||||
|
|
@ -145,10 +140,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
PublishDate = null
|
PublishDate = null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
|
||||||
await _service.PushUpdate(update);
|
await _service.PushUpdate(update);
|
||||||
|
|
||||||
// Assert
|
|
||||||
await _eventHub.Received(1).SendMessageAsync(
|
await _eventHub.Received(1).SendMessageAsync(
|
||||||
Arg.Is(MessageFactory.UpdateAvailable),
|
Arg.Is(MessageFactory.UpdateAvailable),
|
||||||
Arg.Any<SignalRMessage>(),
|
Arg.Any<SignalRMessage>(),
|
||||||
|
|
@ -159,7 +154,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PushUpdate_ShouldNotSendUpdateEvent_WhenVersionIsEqual()
|
public async Task PushUpdate_ShouldNotSendUpdateEvent_WhenVersionIsEqual()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var update = new UpdateNotificationDto
|
var update = new UpdateNotificationDto
|
||||||
{
|
{
|
||||||
UpdateVersion = "0.5.0.0",
|
UpdateVersion = "0.5.0.0",
|
||||||
|
|
@ -170,10 +165,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
PublishDate = null
|
PublishDate = null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
|
||||||
await _service.PushUpdate(update);
|
await _service.PushUpdate(update);
|
||||||
|
|
||||||
// Assert
|
|
||||||
await _eventHub.DidNotReceive().SendMessageAsync(
|
await _eventHub.DidNotReceive().SendMessageAsync(
|
||||||
Arg.Any<string>(),
|
Arg.Any<string>(),
|
||||||
Arg.Any<SignalRMessage>(),
|
Arg.Any<SignalRMessage>(),
|
||||||
|
|
@ -184,7 +179,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAllReleases_ShouldReturnReleases_LimitedByCount()
|
public async Task GetAllReleases_ShouldReturnReleases_LimitedByCount()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var releases = new List<object>
|
var releases = new List<object>
|
||||||
{
|
{
|
||||||
new
|
new
|
||||||
|
|
@ -215,10 +210,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(releases);
|
_httpTest.RespondWithJson(releases);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.GetAllReleases(2);
|
var result = await _service.GetAllReleases(2);
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(2, result.Count);
|
Assert.Equal(2, result.Count);
|
||||||
Assert.Equal("0.7.0.0", result[0].UpdateVersion);
|
Assert.Equal("0.7.0.0", result[0].UpdateVersion);
|
||||||
Assert.Equal("0.6.0", result[1].UpdateVersion);
|
Assert.Equal("0.6.0", result[1].UpdateVersion);
|
||||||
|
|
@ -227,7 +222,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAllReleases_ShouldUseCachedData_WhenCacheIsValid()
|
public async Task GetAllReleases_ShouldUseCachedData_WhenCacheIsValid()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var releases = new List<UpdateNotificationDto>
|
var releases = new List<UpdateNotificationDto>
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
|
|
@ -257,10 +252,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
await File.WriteAllTextAsync(cacheFilePath, System.Text.Json.JsonSerializer.Serialize(releases));
|
await File.WriteAllTextAsync(cacheFilePath, System.Text.Json.JsonSerializer.Serialize(releases));
|
||||||
File.SetLastWriteTimeUtc(cacheFilePath, DateTime.UtcNow); // Ensure it's fresh
|
File.SetLastWriteTimeUtc(cacheFilePath, DateTime.UtcNow); // Ensure it's fresh
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.GetAllReleases();
|
var result = await _service.GetAllReleases();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(2, result.Count);
|
Assert.Equal(2, result.Count);
|
||||||
Assert.Empty(_httpTest.CallLog); // No HTTP calls made
|
Assert.Empty(_httpTest.CallLog); // No HTTP calls made
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +263,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAllReleases_ShouldFetchNewData_WhenCacheIsExpired()
|
public async Task GetAllReleases_ShouldFetchNewData_WhenCacheIsExpired()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var releases = new List<UpdateNotificationDto>
|
var releases = new List<UpdateNotificationDto>
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
|
|
@ -303,10 +298,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(newReleases);
|
_httpTest.RespondWithJson(newReleases);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.GetAllReleases();
|
var result = await _service.GetAllReleases();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(1, result.Count);
|
Assert.Equal(1, result.Count);
|
||||||
Assert.Equal("0.7.0.0", result[0].UpdateVersion);
|
Assert.Equal("0.7.0.0", result[0].UpdateVersion);
|
||||||
Assert.NotEmpty(_httpTest.CallLog); // HTTP call was made
|
Assert.NotEmpty(_httpTest.CallLog); // HTTP call was made
|
||||||
|
|
@ -314,7 +309,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
public async Task GetNumberOfReleasesBehind_ShouldReturnCorrectCount()
|
public async Task GetNumberOfReleasesBehind_ShouldReturnCorrectCount()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var releases = new List<object>
|
var releases = new List<object>
|
||||||
{
|
{
|
||||||
new
|
new
|
||||||
|
|
@ -345,16 +340,16 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(releases);
|
_httpTest.RespondWithJson(releases);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.GetNumberOfReleasesBehind();
|
var result = await _service.GetNumberOfReleasesBehind();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(2 + 1, result); // Behind 0.7.0 and 0.6.0 - We have to add 1 because the current release is > 0.7.0
|
Assert.Equal(2 + 1, result); // Behind 0.7.0 and 0.6.0 - We have to add 1 because the current release is > 0.7.0
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task GetNumberOfReleasesBehind_ShouldReturnCorrectCount_WithNightlies()
|
public async Task GetNumberOfReleasesBehind_ShouldReturnCorrectCount_WithNightlies()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var releases = new List<object>
|
var releases = new List<object>
|
||||||
{
|
{
|
||||||
new
|
new
|
||||||
|
|
@ -377,17 +372,17 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(releases);
|
_httpTest.RespondWithJson(releases);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.GetNumberOfReleasesBehind();
|
var result = await _service.GetNumberOfReleasesBehind();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(2, result); // We have to add 1 because the current release is > 0.7.0
|
Assert.Equal(2, result); // We have to add 1 because the current release is > 0.7.0
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ParseReleaseBody_ShouldExtractSections()
|
public async Task ParseReleaseBody_ShouldExtractSections()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
var githubResponse = new
|
var githubResponse = new
|
||||||
{
|
{
|
||||||
tag_name = "v0.6.0",
|
tag_name = "v0.6.0",
|
||||||
|
|
@ -399,10 +394,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
|
|
||||||
_httpTest.RespondWithJson(githubResponse);
|
_httpTest.RespondWithJson(githubResponse);
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.CheckForUpdate();
|
var result = await _service.CheckForUpdate();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Equal(2, result.Added.Count);
|
Assert.Equal(2, result.Added.Count);
|
||||||
Assert.Equal(2, result.Fixed.Count);
|
Assert.Equal(2, result.Fixed.Count);
|
||||||
|
|
@ -414,7 +409,7 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAllReleases_ShouldHandleNightlyBuilds()
|
public async Task GetAllReleases_ShouldHandleNightlyBuilds()
|
||||||
{
|
{
|
||||||
// Arrange
|
|
||||||
// Set BuildInfo.Version to a nightly build version
|
// Set BuildInfo.Version to a nightly build version
|
||||||
typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, new Version("0.7.1.0"));
|
typeof(BuildInfo).GetProperty(nameof(BuildInfo.Version))?.SetValue(null, new Version("0.7.1.0"));
|
||||||
|
|
||||||
|
|
@ -444,10 +439,10 @@ public class VersionUpdaterServiceTests : IDisposable
|
||||||
// Mock commit info for develop branch
|
// Mock commit info for develop branch
|
||||||
_httpTest.RespondWithJson(new List<object>());
|
_httpTest.RespondWithJson(new List<object>());
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await _service.GetAllReleases();
|
var result = await _service.GetAllReleases();
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.True(result[0].IsOnNightlyInRelease);
|
Assert.True(result[0].IsOnNightlyInRelease);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Abstractions;
|
|
||||||
using System.IO.Abstractions.TestingHelpers;
|
using System.IO.Abstractions.TestingHelpers;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -11,10 +10,8 @@ using API.Helpers;
|
||||||
using API.Helpers.Builders;
|
using API.Helpers.Builders;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using API.Services.Tasks;
|
|
||||||
using API.Services.Tasks.Metadata;
|
using API.Services.Tasks.Metadata;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using API.Tests.Helpers;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||||
<PackageReference Include="MailKit" Version="4.10.0" />
|
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
@ -66,20 +66,20 @@
|
||||||
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
|
<PackageReference Include="Hangfire.InMemory" Version="1.0.0" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.74" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.12.0" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
|
||||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||||
<PackageReference Include="NetVips" Version="3.0.0" />
|
<PackageReference Include="NetVips" Version="3.0.0" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.16.0" />
|
<PackageReference Include="NetVips.Native" Version="8.16.1" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
<PackageReference Include="NReco.Logging.File" Version="1.2.2" />
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
|
|
@ -92,16 +92,16 @@
|
||||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445">
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.8.0.113526">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.6.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="22.0.11" />
|
<PackageReference Include="System.IO.Abstractions" Version="22.0.13" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="9.0.2" />
|
<PackageReference Include="System.Drawing.Common" Version="9.0.4" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.2" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.3" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -137,6 +138,12 @@ public class AccountController : BaseApiController
|
||||||
return BadRequest(usernameValidation);
|
return BadRequest(usernameValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Email is empty, default to the username
|
||||||
|
if (string.IsNullOrEmpty(registerDto.Email))
|
||||||
|
{
|
||||||
|
registerDto.Email = registerDto.Username;
|
||||||
|
}
|
||||||
|
|
||||||
var user = new AppUserBuilder(registerDto.Username, registerDto.Email,
|
var user = new AppUserBuilder(registerDto.Username, registerDto.Email,
|
||||||
await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
await _unitOfWork.SiteThemeRepository.GetDefaultTheme()).Build();
|
||||||
|
|
||||||
|
|
@ -351,10 +358,11 @@ public class AccountController : BaseApiController
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns>Returns just if the email was sent or server isn't reachable</returns>
|
/// <returns>Returns just if the email was sent or server isn't reachable</returns>
|
||||||
[HttpPost("update/email")]
|
[HttpPost("update/email")]
|
||||||
public async Task<ActionResult> UpdateEmail(UpdateEmailDto? dto)
|
public async Task<ActionResult<InviteUserResponse>> UpdateEmail(UpdateEmailDto? dto)
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole)) return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
if (user == null || User.IsInRole(PolicyConstants.ReadOnlyRole))
|
||||||
|
return Unauthorized(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
|
|
||||||
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
if (dto == null || string.IsNullOrEmpty(dto.Email) || string.IsNullOrEmpty(dto.Password))
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "invalid-payload"));
|
||||||
|
|
@ -363,12 +371,13 @@ public class AccountController : BaseApiController
|
||||||
// Validate this user's password
|
// Validate this user's password
|
||||||
if (! await _userManager.CheckPasswordAsync(user, dto.Password))
|
if (! await _userManager.CheckPasswordAsync(user, dto.Password))
|
||||||
{
|
{
|
||||||
_logger.LogCritical("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
|
_logger.LogWarning("A user tried to change {UserName}'s email, but password didn't validate", user.UserName);
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate no other users exist with this email
|
// Validate no other users exist with this email
|
||||||
if (user.Email!.Equals(dto.Email)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
if (user.Email!.Equals(dto.Email))
|
||||||
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||||
|
|
||||||
// Check if email is used by another user
|
// Check if email is used by another user
|
||||||
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
var existingUserEmail = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
|
||||||
|
|
@ -385,8 +394,10 @@ public class AccountController : BaseApiController
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generate-token"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isValidEmailAddress = _emailService.IsValidEmail(user.Email);
|
||||||
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
var shouldEmailUser = serverSettings.IsEmailSetup() || !_emailService.IsValidEmail(user.Email);
|
var shouldEmailUser = serverSettings.IsEmailSetup() || !isValidEmailAddress;
|
||||||
|
|
||||||
user.EmailConfirmed = !shouldEmailUser;
|
user.EmailConfirmed = !shouldEmailUser;
|
||||||
user.ConfirmationToken = token;
|
user.ConfirmationToken = token;
|
||||||
await _userManager.UpdateAsync(user);
|
await _userManager.UpdateAsync(user);
|
||||||
|
|
@ -400,7 +411,8 @@ public class AccountController : BaseApiController
|
||||||
return Ok(new InviteUserResponse
|
return Ok(new InviteUserResponse
|
||||||
{
|
{
|
||||||
EmailLink = string.Empty,
|
EmailLink = string.Empty,
|
||||||
EmailSent = false
|
EmailSent = false,
|
||||||
|
InvalidEmail = !isValidEmailAddress
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,7 +420,7 @@ public class AccountController : BaseApiController
|
||||||
// Send a confirmation email
|
// Send a confirmation email
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_emailService.IsValidEmail(user.Email))
|
if (!isValidEmailAddress)
|
||||||
{
|
{
|
||||||
_logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email);
|
_logger.LogCritical("[Update Email]: User is trying to update their email, but their existing email ({Email}) isn't valid. No email will be send", user.Email);
|
||||||
return Ok(new InviteUserResponse
|
return Ok(new InviteUserResponse
|
||||||
|
|
@ -440,7 +452,8 @@ public class AccountController : BaseApiController
|
||||||
return Ok(new InviteUserResponse
|
return Ok(new InviteUserResponse
|
||||||
{
|
{
|
||||||
EmailLink = string.Empty,
|
EmailLink = string.Empty,
|
||||||
EmailSent = true
|
EmailSent = true,
|
||||||
|
InvalidEmail = !isValidEmailAddress
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.ManualMigrations;
|
using API.Data.ManualMigrations;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
|
@ -16,12 +17,10 @@ namespace API.Controllers;
|
||||||
public class AdminController : BaseApiController
|
public class AdminController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
|
||||||
|
|
||||||
public AdminController(UserManager<AppUser> userManager, IUnitOfWork unitOfWork)
|
public AdminController(UserManager<AppUser> userManager)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_unitOfWork = unitOfWork;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -32,18 +31,7 @@ public class AdminController : BaseApiController
|
||||||
[HttpGet("exists")]
|
[HttpGet("exists")]
|
||||||
public async Task<ActionResult<bool>> AdminExists()
|
public async Task<ActionResult<bool>> AdminExists()
|
||||||
{
|
{
|
||||||
var users = await _userManager.GetUsersInRoleAsync("Admin");
|
var users = await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||||
return users.Count > 0;
|
return users.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the progress information for a particular user
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize("RequireAdminRole")]
|
|
||||||
[HttpPost("update-chapter-progress")]
|
|
||||||
public async Task<ActionResult<bool>> UpdateChapterProgress(UpdateUserProgressDto dto)
|
|
||||||
{
|
|
||||||
return Ok(await Task.FromResult(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ public class BookController : BaseApiController
|
||||||
case MangaFormat.Epub:
|
case MangaFormat.Epub:
|
||||||
{
|
{
|
||||||
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0];
|
var mangaFile = (await _unitOfWork.ChapterRepository.GetFilesForChapterAsync(chapterId))[0];
|
||||||
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.BookReaderOptions);
|
using var book = await EpubReader.OpenBookAsync(mangaFile.FilePath, BookService.LenientBookReaderOptions);
|
||||||
bookTitle = book.Title;
|
bookTitle = book.Title;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ public class BookController : BaseApiController
|
||||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||||
if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist"));
|
if (chapter == null) return BadRequest(await _localizationService.Get("en", "chapter-doesnt-exist"));
|
||||||
|
|
||||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.BookReaderOptions);
|
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath, BookService.LenientBookReaderOptions);
|
||||||
var key = BookService.CoalesceKeyForAnyFile(book, file);
|
var key = BookService.CoalesceKeyForAnyFile(book, file);
|
||||||
|
|
||||||
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing"));
|
if (!book.Content.AllFiles.ContainsLocalFileRefWithKey(key)) return BadRequest(await _localizationService.Get("en", "file-missing"));
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Person;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
@ -234,26 +235,17 @@ public class ChapterController : BaseApiController
|
||||||
|
|
||||||
|
|
||||||
#region Genres
|
#region Genres
|
||||||
if (dto.Genres is {Count: > 0})
|
chapter.Genres ??= [];
|
||||||
{
|
|
||||||
chapter.Genres ??= new List<Genre>();
|
|
||||||
await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), _unitOfWork);
|
await GenreHelper.UpdateChapterGenres(chapter, dto.Genres.Select(t => t.Title), _unitOfWork);
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Tags
|
#region Tags
|
||||||
if (dto.Tags is {Count: > 0})
|
chapter.Tags ??= [];
|
||||||
{
|
|
||||||
chapter.Tags ??= new List<Tag>();
|
|
||||||
await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), _unitOfWork);
|
await TagHelper.UpdateChapterTags(chapter, dto.Tags.Select(t => t.Title), _unitOfWork);
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region People
|
#region People
|
||||||
if (PersonHelper.HasAnyPeople(dto))
|
chapter.People ??= [];
|
||||||
{
|
|
||||||
chapter.People ??= new List<ChapterPeople>();
|
|
||||||
|
|
||||||
|
|
||||||
// Update writers
|
// Update writers
|
||||||
await PersonHelper.UpdateChapterPeopleAsync(
|
await PersonHelper.UpdateChapterPeopleAsync(
|
||||||
|
|
@ -358,7 +350,6 @@ public class ChapterController : BaseApiController
|
||||||
PersonRole.Location,
|
PersonRole.Location,
|
||||||
_unitOfWork
|
_unitOfWork
|
||||||
);
|
);
|
||||||
}
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Locks
|
#region Locks
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ public class DownloadController : BaseApiController
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.DownloadProgressEvent(username,
|
MessageFactory.DownloadProgressEvent(username,
|
||||||
filename, $"Downloading {filename}", 0F, "started"));
|
filename, $"Downloading {filename}", 0F, "started"));
|
||||||
|
|
||||||
if (files.Count == 1 && files.First().Format != MangaFormat.Image)
|
if (files.Count == 1 && files.First().Format != MangaFormat.Image)
|
||||||
{
|
{
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
|
|
@ -167,15 +168,17 @@ public class DownloadController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback);
|
var filePath = _archiveService.CreateZipFromFoldersForDownload(files.Select(c => c.FilePath).ToList(), tempFolder, ProgressCallback);
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.DownloadProgressEvent(username,
|
MessageFactory.DownloadProgressEvent(username,
|
||||||
filename, "Download Complete", 1F, "ended"));
|
filename, "Download Complete", 1F, "ended"));
|
||||||
|
|
||||||
return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true);
|
return PhysicalFile(filePath, DefaultContentType, Uri.EscapeDataString(downloadName), true);
|
||||||
|
|
||||||
async Task ProgressCallback(Tuple<string, float> progressInfo)
|
async Task ProgressCallback(Tuple<string, float> progressInfo)
|
||||||
{
|
{
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.DownloadProgressEvent(username, filename, $"Extracting {Path.GetFileNameWithoutExtension(progressInfo.Item1)}",
|
MessageFactory.DownloadProgressEvent(username, filename, $"Processing {Path.GetFileNameWithoutExtension(progressInfo.Item1)}",
|
||||||
Math.Clamp(progressInfo.Item2, 0F, 1F)));
|
Math.Clamp(progressInfo.Item2, 0F, 1F)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,8 +196,10 @@ public class DownloadController : BaseApiController
|
||||||
public async Task<ActionResult> DownloadSeries(int seriesId)
|
public async Task<ActionResult> DownloadSeries(int seriesId)
|
||||||
{
|
{
|
||||||
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
if (!await HasDownloadPermission()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
|
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
if (series == null) return BadRequest("Invalid Series");
|
if (series == null) return BadRequest("Invalid Series");
|
||||||
|
|
||||||
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
var files = await _unitOfWork.SeriesRepository.GetFilesForSeries(seriesId);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
|
@ -24,11 +25,16 @@ public class FilterController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly IStreamService _streamService;
|
||||||
|
private readonly ILogger<FilterController> _logger;
|
||||||
|
|
||||||
public FilterController(IUnitOfWork unitOfWork, ILocalizationService localizationService)
|
public FilterController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IStreamService streamService,
|
||||||
|
ILogger<FilterController> logger)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
|
_streamService = streamService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -120,4 +126,57 @@ public class FilterController : BaseApiController
|
||||||
{
|
{
|
||||||
return Ok(SmartFilterHelper.Decode(dto.EncodedFilter));
|
return Ok(SmartFilterHelper.Decode(dto.EncodedFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rename a Smart Filter given the filterId and new name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filterId"></param>
|
||||||
|
/// <param name="name"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("rename")]
|
||||||
|
public async Task<ActionResult> RenameFilter([FromQuery] int filterId, [FromQuery] string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(),
|
||||||
|
AppUserIncludes.SmartFilters);
|
||||||
|
if (user == null) return Unauthorized();
|
||||||
|
|
||||||
|
name = name.Trim();
|
||||||
|
|
||||||
|
if (User.IsInRole(PolicyConstants.ReadOnlyRole))
|
||||||
|
{
|
||||||
|
return BadRequest(await _localizationService.Translate(user.Id, "permission-denied"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-name-required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Seed.DefaultStreams.Any(s => s.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
|
||||||
|
{
|
||||||
|
return BadRequest(await _localizationService.Translate(user.Id, "smart-filter-system-name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter = user.SmartFilters.FirstOrDefault(f => f.Id == filterId);
|
||||||
|
if (filter == null)
|
||||||
|
{
|
||||||
|
return BadRequest(await _localizationService.Translate(user.Id, "filter-not-found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.Name = name;
|
||||||
|
_unitOfWork.AppUserSmartFilterRepository.Update(filter);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
await _streamService.RenameSmartFilterStreams(filter);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an exception when renaming smart filter: {FilterId}", filterId);
|
||||||
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,6 @@ public class LibraryController : BaseApiController
|
||||||
|
|
||||||
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username);
|
var ret = _unitOfWork.LibraryRepository.GetLibraryDtosForUsernameAsync(username);
|
||||||
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
|
await _libraryCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromHours(24));
|
||||||
_logger.LogDebug("Caching libraries for {Key}", cacheKey);
|
|
||||||
|
|
||||||
return Ok(ret);
|
return Ok(ret);
|
||||||
}
|
}
|
||||||
|
|
@ -351,27 +350,6 @@ public class LibraryController : BaseApiController
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
[HttpPost("analyze")]
|
|
||||||
public ActionResult Analyze(int libraryId)
|
|
||||||
{
|
|
||||||
_taskScheduler.AnalyzeFilesForLibrary(libraryId, true);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
|
||||||
[HttpPost("analyze-multiple")]
|
|
||||||
public ActionResult AnalyzeMultiple(BulkActionDto dto)
|
|
||||||
{
|
|
||||||
foreach (var libraryId in dto.Ids)
|
|
||||||
{
|
|
||||||
_taskScheduler.AnalyzeFilesForLibrary(libraryId, dto.Force ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the library settings (adv tab + optional type) to a set of other libraries.
|
/// Copy the library settings (adv tab + optional type) to a set of other libraries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -440,8 +418,7 @@ public class LibraryController : BaseApiController
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
.Select(Services.Tasks.Scanner.Parser.Parser.NormalizePath);
|
||||||
|
|
||||||
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder,
|
var seriesFolder = _directoryService.FindHighestDirectoriesFromFiles(libraryFolder, [dto.FolderPath]);
|
||||||
new List<string>() {dto.FolderPath});
|
|
||||||
|
|
||||||
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
|
_taskScheduler.ScanFolder(seriesFolder.Keys.Count == 1 ? seriesFolder.Keys.First() : dto.FolderPath);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
|
using API.DTOs;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using EasyCaching.Core;
|
using EasyCaching.Core;
|
||||||
|
|
@ -45,8 +46,8 @@ public class LocaleController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f);
|
var ret = _localizationService.GetLocales().Where(l => l.TranslationCompletion > 0f);
|
||||||
await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(7));
|
await _localeCacheProvider.SetAsync(CacheKey, ret, TimeSpan.FromDays(1));
|
||||||
|
|
||||||
return Ok();
|
return Ok(ret);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ using API.DTOs.Recommendation;
|
||||||
using API.DTOs.SeriesDetail;
|
using API.DTOs.SeriesDetail;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
using Kavita.Common.Extensions;
|
using Kavita.Common.Extensions;
|
||||||
|
|
@ -225,7 +226,7 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
||||||
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
var isAdmin = User.IsInRole(PolicyConstants.AdminRole);
|
||||||
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!;
|
var user = await unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId())!;
|
||||||
|
|
||||||
userReviews.AddRange(ReviewService.SelectSpectrumOfReviews(ret.Reviews.ToList()));
|
userReviews.AddRange(ReviewHelper.SelectSpectrumOfReviews(ret.Reviews.ToList()));
|
||||||
ret.Reviews = userReviews;
|
ret.Reviews = userReviews;
|
||||||
|
|
||||||
if (!isAdmin && ret.Recommendations != null && user != null)
|
if (!isAdmin && ret.Recommendations != null && user != null)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ using AutoMapper;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using MimeTypes;
|
using MimeTypes;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
|
@ -36,6 +37,7 @@ namespace API.Controllers;
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public class OpdsController : BaseApiController
|
public class OpdsController : BaseApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<OpdsController> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
|
@ -82,7 +84,7 @@ public class OpdsController : BaseApiController
|
||||||
IDirectoryService directoryService, ICacheService cacheService,
|
IDirectoryService directoryService, ICacheService cacheService,
|
||||||
IReaderService readerService, ISeriesService seriesService,
|
IReaderService readerService, ISeriesService seriesService,
|
||||||
IAccountService accountService, ILocalizationService localizationService,
|
IAccountService accountService, ILocalizationService localizationService,
|
||||||
IMapper mapper)
|
IMapper mapper, ILogger<OpdsController> logger)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_downloadService = downloadService;
|
_downloadService = downloadService;
|
||||||
|
|
@ -93,6 +95,7 @@ public class OpdsController : BaseApiController
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
_xmlSerializer = new XmlSerializer(typeof(Feed));
|
||||||
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
_xmlOpenSearchSerializer = new XmlSerializer(typeof(OpenSearchDescription));
|
||||||
|
|
@ -580,19 +583,25 @@ public class OpdsController : BaseApiController
|
||||||
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = 0)
|
public async Task<IActionResult> GetReadingListItems(int readingListId, string apiKey, [FromQuery] int pageNumber = 0)
|
||||||
{
|
{
|
||||||
var userId = await GetUser(apiKey);
|
var userId = await GetUser(apiKey);
|
||||||
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
|
||||||
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
|
||||||
var (baseUrl, prefix) = await GetPrefix();
|
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
|
||||||
|
|
||||||
var userWithLists = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user!.UserName!, AppUserIncludes.ReadingListsWithItems);
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds)
|
||||||
if (userWithLists == null) return Unauthorized();
|
{
|
||||||
var readingList = userWithLists.ReadingLists.SingleOrDefault(t => t.Id == readingListId);
|
return BadRequest(await _localizationService.Translate(userId, "opds-disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, user.Id);
|
||||||
if (readingList == null)
|
if (readingList == null)
|
||||||
{
|
{
|
||||||
return BadRequest(await _localizationService.Translate(userId, "reading-list-restricted"));
|
return BadRequest(await _localizationService.Translate(userId, "reading-list-restricted"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (baseUrl, prefix) = await GetPrefix();
|
||||||
var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix);
|
var feed = CreateFeed(readingList.Title + " " + await _localizationService.Translate(userId, "reading-list"), $"{apiKey}/reading-list/{readingListId}", apiKey, prefix);
|
||||||
SetFeedId(feed, $"reading-list-{readingListId}");
|
SetFeedId(feed, $"reading-list-{readingListId}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public class PersonController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of authors & artists for browsing
|
/// Returns a list of authors and artists for browsing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userParams"></param>
|
/// <param name="userParams"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|
|
||||||
|
|
@ -803,7 +803,7 @@ public class ReaderController : BaseApiController
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("time-left")]
|
[HttpGet("time-left")]
|
||||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = ["seriesId"])]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["seriesId"])]
|
||||||
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
public async Task<ActionResult<HourEstimateRangeDto>> GetEstimateToCompletion(int seriesId)
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
var userId = User.GetUserId();
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.ReadingLists;
|
using API.DTOs.ReadingLists;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -24,13 +24,15 @@ public class ReadingListController : BaseApiController
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly IReadingListService _readingListService;
|
private readonly IReadingListService _readingListService;
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly IReaderService _readerService;
|
||||||
|
|
||||||
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
|
public ReadingListController(IUnitOfWork unitOfWork, IReadingListService readingListService,
|
||||||
ILocalizationService localizationService)
|
ILocalizationService localizationService, IReaderService readerService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_readingListService = readingListService;
|
_readingListService = readingListService;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
|
_readerService = readerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -39,9 +41,15 @@ public class ReadingListController : BaseApiController
|
||||||
/// <param name="readingListId"></param>
|
/// <param name="readingListId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<IEnumerable<ReadingListDto>>> GetList(int readingListId)
|
public async Task<ActionResult<ReadingListDto>> GetList(int readingListId)
|
||||||
{
|
{
|
||||||
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId()));
|
var readingList = await _unitOfWork.ReadingListRepository.GetReadingListDtoByIdAsync(readingListId, User.GetUserId());
|
||||||
|
if (readingList == null)
|
||||||
|
{
|
||||||
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-restricted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(readingList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -123,7 +131,7 @@ public class ReadingListController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a list item from the list. Will reorder all item positions afterwards
|
/// Deletes a list item from the list. Item orders will update as a result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|
@ -262,7 +270,7 @@ public class ReadingListController : BaseApiController
|
||||||
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
var readingList = user.ReadingLists.SingleOrDefault(l => l.Id == dto.ReadingListId);
|
||||||
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
if (readingList == null) return BadRequest(await _localizationService.Translate(User.GetUserId(), "reading-list-doesnt-exist"));
|
||||||
var chapterIdsForSeries =
|
var chapterIdsForSeries =
|
||||||
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new [] {dto.SeriesId});
|
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync([dto.SeriesId]);
|
||||||
|
|
||||||
// If there are adds, tell tracking this has been modified
|
// If there are adds, tell tracking this has been modified
|
||||||
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
|
if (await _readingListService.AddChaptersToReadingList(dto.SeriesId, chapterIdsForSeries, readingList))
|
||||||
|
|
@ -447,26 +455,38 @@ public class ReadingListController : BaseApiController
|
||||||
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
return Ok(await _localizationService.Translate(User.GetUserId(), "nothing-to-do"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a list of characters associated with the reading list
|
/// Returns a list of a given role associated with the reading list
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="readingListId"></param>
|
||||||
|
/// <param name="role">PersonRole</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("people")]
|
||||||
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId", "role"])]
|
||||||
|
public ActionResult<IEnumerable<PersonDto>> GetPeopleByRoleForList(int readingListId, PersonRole role)
|
||||||
|
{
|
||||||
|
return Ok(_unitOfWork.ReadingListRepository.GetReadingListPeopleAsync(readingListId, role));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all people in given roles for a reading list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="readingListId"></param>
|
/// <param name="readingListId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("characters")]
|
[HttpGet("all-people")]
|
||||||
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute)]
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.TenMinute, VaryByQueryKeys = ["readingListId"])]
|
||||||
public ActionResult<IEnumerable<PersonDto>> GetCharactersForList(int readingListId)
|
public async Task<ActionResult<IEnumerable<PersonDto>>> GetAllPeopleForList(int readingListId)
|
||||||
{
|
{
|
||||||
return Ok(_unitOfWork.ReadingListRepository.GetReadingListCharactersAsync(readingListId));
|
return Ok(await _unitOfWork.ReadingListRepository.GetReadingListAllPeopleAsync(readingListId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the next chapter within the reading list
|
/// Returns the next chapter within the reading list
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="currentChapterId"></param>
|
/// <param name="currentChapterId"></param>
|
||||||
/// <param name="readingListId"></param>
|
/// <param name="readingListId"></param>
|
||||||
/// <returns>Chapter Id for next item, -1 if nothing exists</returns>
|
/// <returns>Chapter ID for next item, -1 if nothing exists</returns>
|
||||||
[HttpGet("next-chapter")]
|
[HttpGet("next-chapter")]
|
||||||
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
|
public async Task<ActionResult<int>> GetNextChapter(int currentChapterId, int readingListId)
|
||||||
{
|
{
|
||||||
|
|
@ -572,4 +592,26 @@ public class ReadingListController : BaseApiController
|
||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns random information about a Reading List
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="readingListId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("info")]
|
||||||
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.Hour, VaryByQueryKeys = ["readingListId"])]
|
||||||
|
public async Task<ActionResult<ReadingListInfoDto?>> GetReadingListInfo(int readingListId)
|
||||||
|
{
|
||||||
|
var result = await _unitOfWork.ReadingListRepository.GetReadingListInfoAsync(readingListId);
|
||||||
|
|
||||||
|
if (result == null) return Ok(null);
|
||||||
|
|
||||||
|
var timeEstimate = _readerService.GetTimeEstimate(result.WordCount, result.Pages, result.IsAllEpub);
|
||||||
|
|
||||||
|
result.MinHoursToRead = timeEstimate.MinHours;
|
||||||
|
result.AvgHoursToRead = timeEstimate.AvgHours;
|
||||||
|
result.MaxHoursToRead = timeEstimate.MaxHours;
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ public class ScrobblingController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the current user's MAL token & username
|
/// Get the current user's MAL token and username
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("mal-token")]
|
[HttpGet("mal-token")]
|
||||||
|
|
@ -270,4 +270,15 @@ public class ScrobblingController : BaseApiController
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the logged in user ran scrobble generation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet("has-ran-scrobble-gen")]
|
||||||
|
public async Task<ActionResult<bool>> HasRanScrobbleGen()
|
||||||
|
{
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId());
|
||||||
|
return Ok(user is {HasRunScrobbleEventGeneration: true});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,8 @@ public class SeriesController : BaseApiController
|
||||||
// Trigger a refresh when we are moving from a locked image to a non-locked
|
// Trigger a refresh when we are moving from a locked image to a non-locked
|
||||||
needsRefreshMetadata = true;
|
needsRefreshMetadata = true;
|
||||||
series.CoverImage = null;
|
series.CoverImage = null;
|
||||||
series.CoverImageLocked = updateSeries.CoverImageLocked;
|
series.CoverImageLocked = false;
|
||||||
|
_logger.LogDebug("[SeriesCoverImageBug] Setting Series Cover Image to null: {SeriesId}", series.Id);
|
||||||
series.ResetColorScape();
|
series.ResetColorScape();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,10 +203,11 @@ public class ServerController : BaseApiController
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns how many versions out of date this install is
|
/// Returns how many versions out of date this install is
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="stableOnly">Only count Stable releases</param>
|
||||||
[HttpGet("check-out-of-date")]
|
[HttpGet("check-out-of-date")]
|
||||||
public async Task<ActionResult<int>> CheckHowOutOfDate()
|
public async Task<ActionResult<int>> CheckHowOutOfDate(bool stableOnly = true)
|
||||||
{
|
{
|
||||||
return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind());
|
return Ok(await _versionUpdaterService.GetNumberOfReleasesBehind(stableOnly));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,27 +34,26 @@ public class SettingsController : BaseApiController
|
||||||
{
|
{
|
||||||
private readonly ILogger<SettingsController> _logger;
|
private readonly ILogger<SettingsController> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITaskScheduler _taskScheduler;
|
|
||||||
private readonly IDirectoryService _directoryService;
|
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
private readonly ILibraryWatcher _libraryWatcher;
|
|
||||||
private readonly ILocalizationService _localizationService;
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
|
||||||
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler,
|
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, IMapper mapper,
|
||||||
IDirectoryService directoryService, IMapper mapper, IEmailService emailService, ILibraryWatcher libraryWatcher,
|
IEmailService emailService, ILocalizationService localizationService, ISettingsService settingsService)
|
||||||
ILocalizationService localizationService)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_taskScheduler = taskScheduler;
|
|
||||||
_directoryService = directoryService;
|
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_libraryWatcher = libraryWatcher;
|
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
|
_settingsService = settingsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the base url for this instance (if set)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("base-url")]
|
[HttpGet("base-url")]
|
||||||
public async Task<ActionResult<string>> GetBaseUrl()
|
public async Task<ActionResult<string>> GetBaseUrl()
|
||||||
{
|
{
|
||||||
|
|
@ -139,346 +138,33 @@ public class SettingsController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update Server settings
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="updateSettingsDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
|
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
|
||||||
|
|
||||||
// We do not allow CacheDirectory changes, so we will ignore.
|
|
||||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
|
||||||
var updateBookmarks = false;
|
|
||||||
var originalBookmarkDirectory = _directoryService.BookmarkDirectory;
|
|
||||||
|
|
||||||
var bookmarkDirectory = updateSettingsDto.BookmarksDirectory;
|
|
||||||
if (!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks") &&
|
|
||||||
!updateSettingsDto.BookmarksDirectory.EndsWith("bookmarks/"))
|
|
||||||
{
|
|
||||||
bookmarkDirectory =
|
|
||||||
_directoryService.FileSystem.Path.Join(updateSettingsDto.BookmarksDirectory, "bookmarks");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(updateSettingsDto.BookmarksDirectory))
|
|
||||||
{
|
|
||||||
bookmarkDirectory = _directoryService.BookmarkDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateTask = false;
|
|
||||||
foreach (var setting in currentSettings)
|
|
||||||
{
|
|
||||||
if (setting.Key == ServerSettingKey.OnDeckProgressDays &&
|
|
||||||
updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.OnDeckProgressDays + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.OnDeckUpdateDays &&
|
|
||||||
updateSettingsDto.OnDeckUpdateDays + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.OnDeckUpdateDays + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
if (OsInfo.IsDocker) continue;
|
|
||||||
setting.Value = updateSettingsDto.Port + string.Empty;
|
|
||||||
// Port is managed in appSetting.json
|
|
||||||
Configuration.Port = updateSettingsDto.Port;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.CacheSize &&
|
|
||||||
updateSettingsDto.CacheSize + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.CacheSize + string.Empty;
|
|
||||||
// CacheSize is managed in appSetting.json
|
|
||||||
Configuration.CacheSize = updateSettingsDto.CacheSize;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTask = updateTask || UpdateSchedulingSettings(setting, updateSettingsDto);
|
|
||||||
|
|
||||||
UpdateEmailSettings(setting, updateSettingsDto);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.IpAddresses && updateSettingsDto.IpAddresses != setting.Value)
|
|
||||||
{
|
|
||||||
if (OsInfo.IsDocker) continue;
|
|
||||||
// Validate IP addresses
|
|
||||||
foreach (var ipAddress in updateSettingsDto.IpAddresses.Split(',',
|
|
||||||
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
if (!IPAddress.TryParse(ipAddress.Trim(), out _))
|
|
||||||
{
|
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "ip-address-invalid",
|
|
||||||
ipAddress));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setting.Value = updateSettingsDto.IpAddresses;
|
|
||||||
// IpAddresses is managed in appSetting.json
|
|
||||||
Configuration.IpAddresses = updateSettingsDto.IpAddresses;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
var path = !updateSettingsDto.BaseUrl.StartsWith('/')
|
|
||||||
? $"/{updateSettingsDto.BaseUrl}"
|
|
||||||
: updateSettingsDto.BaseUrl;
|
|
||||||
path = !path.EndsWith('/')
|
|
||||||
? $"{path}/"
|
|
||||||
: path;
|
|
||||||
setting.Value = path;
|
|
||||||
Configuration.BaseUrl = updateSettingsDto.BaseUrl;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.LoggingLevel &&
|
|
||||||
updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
|
||||||
LogLevelOptions.SwitchLogLevel(updateSettingsDto.LoggingLevel);
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EnableOpds &&
|
|
||||||
updateSettingsDto.EnableOpds + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.EnableOpds + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EncodeMediaAs &&
|
|
||||||
((int)updateSettingsDto.EncodeMediaAs).ToString() != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = ((int)updateSettingsDto.EncodeMediaAs).ToString();
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.CoverImageSize &&
|
|
||||||
((int)updateSettingsDto.CoverImageSize).ToString() != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = ((int)updateSettingsDto.CoverImageSize).ToString();
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = (updateSettingsDto.HostName + string.Empty).Trim();
|
|
||||||
setting.Value = UrlHelper.RemoveEndingSlash(setting.Value);
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
|
||||||
{
|
|
||||||
// Validate new directory can be used
|
|
||||||
if (!await _directoryService.CheckWriteAccess(bookmarkDirectory))
|
|
||||||
{
|
|
||||||
return BadRequest(
|
|
||||||
await _localizationService.Translate(User.GetUserId(), "bookmark-dir-permissions"));
|
|
||||||
}
|
|
||||||
|
|
||||||
originalBookmarkDirectory = setting.Value;
|
|
||||||
// Normalize the path deliminators. Just to look nice in DB, no functionality
|
|
||||||
setting.Value = _directoryService.FileSystem.Path.GetFullPath(bookmarkDirectory);
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
updateBookmarks = true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.AllowStatCollection &&
|
|
||||||
updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.TotalBackups &&
|
|
||||||
updateSettingsDto.TotalBackups + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
if (updateSettingsDto.TotalBackups > 30 || updateSettingsDto.TotalBackups < 1)
|
|
||||||
{
|
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-backups"));
|
|
||||||
}
|
|
||||||
|
|
||||||
setting.Value = updateSettingsDto.TotalBackups + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.TotalLogs &&
|
|
||||||
updateSettingsDto.TotalLogs + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
if (updateSettingsDto.TotalLogs > 30 || updateSettingsDto.TotalLogs < 1)
|
|
||||||
{
|
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "total-logs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
setting.Value = updateSettingsDto.TotalLogs + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EnableFolderWatching &&
|
|
||||||
updateSettingsDto.EnableFolderWatching + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.EnableFolderWatching + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges()) return Ok(updateSettingsDto);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _unitOfWork.CommitAsync();
|
var d = await _settingsService.UpdateSettings(updateSettingsDto);
|
||||||
|
return Ok(d);
|
||||||
if (!updateSettingsDto.AllowStatCollection)
|
|
||||||
{
|
|
||||||
_taskScheduler.CancelStatsTasks();
|
|
||||||
}
|
}
|
||||||
else
|
catch (KavitaException ex)
|
||||||
{
|
{
|
||||||
await _taskScheduler.ScheduleStatsTasks();
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), ex.Message));
|
||||||
}
|
|
||||||
|
|
||||||
if (updateBookmarks)
|
|
||||||
{
|
|
||||||
UpdateBookmarkDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateTask)
|
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateSettingsDto.EnableFolderWatching)
|
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _libraryWatcher.StartWatching());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BackgroundJob.Enqueue(() => _libraryWatcher.StopWatching());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "There was an exception when updating server settings");
|
_logger.LogError(ex, "There was an exception when updating server settings");
|
||||||
await _unitOfWork.RollbackAsync();
|
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "generic-error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_logger.LogInformation("Server Settings updated");
|
|
||||||
BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks());
|
|
||||||
|
|
||||||
return Ok(updateSettingsDto);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory)
|
|
||||||
{
|
|
||||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
|
||||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
|
||||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
|
||||||
{
|
|
||||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.TaskBackup;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.TaskScan;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.TaskCleanup && updateSettingsDto.TaskCleanup != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.TaskCleanup;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateEmailSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
|
||||||
{
|
|
||||||
if (setting.Key == ServerSettingKey.EmailHost &&
|
|
||||||
updateSettingsDto.SmtpConfig.Host + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.Host + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailPort &&
|
|
||||||
updateSettingsDto.SmtpConfig.Port + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.Port + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailAuthPassword &&
|
|
||||||
updateSettingsDto.SmtpConfig.Password + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.Password + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailAuthUserName &&
|
|
||||||
updateSettingsDto.SmtpConfig.UserName + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.UserName + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailSenderAddress &&
|
|
||||||
updateSettingsDto.SmtpConfig.SenderAddress + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.SenderAddress + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailSenderDisplayName &&
|
|
||||||
updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.SenderDisplayName + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailSizeLimit &&
|
|
||||||
updateSettingsDto.SmtpConfig.SizeLimit + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.SizeLimit + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailEnableSsl &&
|
|
||||||
updateSettingsDto.SmtpConfig.EnableSsl + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.EnableSsl + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.EmailCustomizedTemplates &&
|
|
||||||
updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty != setting.Value)
|
|
||||||
{
|
|
||||||
setting.Value = updateSettingsDto.SmtpConfig.CustomizedTemplates + string.Empty;
|
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup.
|
/// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -549,7 +235,7 @@ public class SettingsController : BaseApiController
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the metadata settings for Kavita+ users
|
/// Update the metadata settings for Kavita+ Metadata feature
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dto"></param>
|
/// <param name="dto"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
|
@ -557,54 +243,14 @@ public class SettingsController : BaseApiController
|
||||||
[HttpPost("metadata-settings")]
|
[HttpPost("metadata-settings")]
|
||||||
public async Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto)
|
public async Task<ActionResult<MetadataSettingsDto>> UpdateMetadataSettings(MetadataSettingsDto dto)
|
||||||
{
|
{
|
||||||
var existingMetadataSetting = await _unitOfWork.SettingsRepository.GetMetadataSettings();
|
try
|
||||||
existingMetadataSetting.Enabled = dto.Enabled;
|
|
||||||
existingMetadataSetting.EnableSummary = dto.EnableSummary;
|
|
||||||
existingMetadataSetting.EnableLocalizedName = dto.EnableLocalizedName;
|
|
||||||
existingMetadataSetting.EnablePublicationStatus = dto.EnablePublicationStatus;
|
|
||||||
existingMetadataSetting.EnableRelationships = dto.EnableRelationships;
|
|
||||||
existingMetadataSetting.EnablePeople = dto.EnablePeople;
|
|
||||||
existingMetadataSetting.EnableStartDate = dto.EnableStartDate;
|
|
||||||
existingMetadataSetting.EnableGenres = dto.EnableGenres;
|
|
||||||
existingMetadataSetting.EnableTags = dto.EnableTags;
|
|
||||||
existingMetadataSetting.FirstLastPeopleNaming = dto.FirstLastPeopleNaming;
|
|
||||||
existingMetadataSetting.EnableCoverImage = dto.EnableCoverImage;
|
|
||||||
|
|
||||||
existingMetadataSetting.AgeRatingMappings = dto.AgeRatingMappings ?? [];
|
|
||||||
|
|
||||||
existingMetadataSetting.Blacklist = dto.Blacklist.Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? [];
|
|
||||||
existingMetadataSetting.Whitelist = dto.Whitelist.Where(s => !string.IsNullOrWhiteSpace(s)).DistinctBy(d => d.ToNormalized()).ToList() ?? [];
|
|
||||||
existingMetadataSetting.Overrides = dto.Overrides.ToList() ?? [];
|
|
||||||
existingMetadataSetting.PersonRoles = dto.PersonRoles ?? [];
|
|
||||||
|
|
||||||
// Handle Field Mappings
|
|
||||||
if (dto.FieldMappings != null)
|
|
||||||
{
|
{
|
||||||
// Clear existing mappings
|
return Ok(await _settingsService.UpdateMetadataSettings(dto));
|
||||||
existingMetadataSetting.FieldMappings ??= [];
|
}
|
||||||
_unitOfWork.SettingsRepository.RemoveRange(existingMetadataSetting.FieldMappings);
|
catch (Exception ex)
|
||||||
|
|
||||||
existingMetadataSetting.FieldMappings.Clear();
|
|
||||||
|
|
||||||
|
|
||||||
// Add new mappings
|
|
||||||
foreach (var mappingDto in dto.FieldMappings)
|
|
||||||
{
|
{
|
||||||
existingMetadataSetting.FieldMappings.Add(new MetadataFieldMapping
|
_logger.LogError(ex, "There was an issue when updating metadata settings");
|
||||||
{
|
return BadRequest(ex.Message);
|
||||||
SourceType = mappingDto.SourceType,
|
|
||||||
DestinationType = mappingDto.DestinationType,
|
|
||||||
SourceValue = mappingDto.SourceValue,
|
|
||||||
DestinationValue = mappingDto.DestinationValue,
|
|
||||||
ExcludeFromSource = mappingDto.ExcludeFromSource
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save changes
|
|
||||||
await _unitOfWork.CommitAsync();
|
|
||||||
|
|
||||||
// Return updated settings
|
|
||||||
return Ok(await _unitOfWork.SettingsRepository.GetMetadataSettingDto());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -204,4 +204,30 @@ public class StreamController : BaseApiController
|
||||||
await _streamService.UpdateSideNavStreamBulk(User.GetUserId(), dto);
|
await _streamService.UpdateSideNavStreamBulk(User.GetUserId(), dto);
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a Smart Filter from a user's SideNav Streams
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sideNavStreamId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete("smart-filter-side-nav-stream")]
|
||||||
|
public async Task<ActionResult> DeleteSmartFilterSideNavStream([FromQuery] int sideNavStreamId)
|
||||||
|
{
|
||||||
|
if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
|
await _streamService.DeleteSideNavSmartFilterStream(User.GetUserId(), sideNavStreamId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a Smart Filter from a user's Dashboard Streams
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dashboardStreamId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpDelete("smart-filter-dashboard-stream")]
|
||||||
|
public async Task<ActionResult> DeleteSmartFilterDashboardStream([FromQuery] int dashboardStreamId)
|
||||||
|
{
|
||||||
|
if (User.IsInRole(PolicyConstants.ReadOnlyRole)) return BadRequest(await _localizationService.Translate(User.GetUserId(), "permission-denied"));
|
||||||
|
await _streamService.DeleteDashboardSmartFilterStream(User.GetUserId(), dashboardStreamId);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers;
|
namespace API.Controllers;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class VolumeController : BaseApiController
|
public class VolumeController : BaseApiController
|
||||||
{
|
{
|
||||||
|
|
@ -23,13 +24,15 @@ public class VolumeController : BaseApiController
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the appropriate Volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="volumeId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<VolumeDto>> GetVolume(int volumeId)
|
public async Task<ActionResult<VolumeDto?>> GetVolume(int volumeId)
|
||||||
{
|
{
|
||||||
var volume =
|
return Ok(await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId()));
|
||||||
await _unitOfWork.VolumeRepository.GetVolumeDtoAsync(volumeId, User.GetUserId());
|
|
||||||
|
|
||||||
return Ok(volume);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(Policy = "RequireAdminRole")]
|
[Authorize(Policy = "RequireAdminRole")]
|
||||||
|
|
@ -39,7 +42,7 @@ public class VolumeController : BaseApiController
|
||||||
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId,
|
var volume = await _unitOfWork.VolumeRepository.GetVolumeAsync(volumeId,
|
||||||
VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags);
|
VolumeIncludes.Chapters | VolumeIncludes.People | VolumeIncludes.Tags);
|
||||||
if (volume == null)
|
if (volume == null)
|
||||||
return BadRequest(_localizationService.Translate(User.GetUserId(), "chapter-doesnt-exist"));
|
return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist"));
|
||||||
|
|
||||||
_unitOfWork.VolumeRepository.Remove(volume);
|
_unitOfWork.VolumeRepository.Remove(volume);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace API.DTOs.Account;
|
namespace API.DTOs.Account;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public record UpdateUserDto
|
public record UpdateUserDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using API.Entities.Enums;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
/// A Chapter is the lowest grouping of a reading medium. A Chapter contains a set of MangaFiles which represents the underlying
|
||||||
|
|
@ -188,8 +189,8 @@ public class ChapterDto : IHasReadTimeEstimate, IHasCoverImage
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public string CoverImage { get; set; }
|
public string CoverImage { get; set; }
|
||||||
public string PrimaryColor { get; set; }
|
public string PrimaryColor { get; set; } = string.Empty;
|
||||||
public string SecondaryColor { get; set; }
|
public string SecondaryColor { get; set; } = string.Empty;
|
||||||
|
|
||||||
public void ResetColorScape()
|
public void ResetColorScape()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ public class AppUserCollectionDto : IHasCoverImage
|
||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string Title { get; set; } = default!;
|
public string Title { get; set; } = default!;
|
||||||
public string Summary { get; set; } = default!;
|
public string? Summary { get; set; } = default!;
|
||||||
public bool Promoted { get; set; }
|
public bool Promoted { get; set; }
|
||||||
public AgeRating AgeRating { get; set; }
|
public AgeRating AgeRating { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs.Collection;
|
namespace API.DTOs.Collection;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an Interest Stack from MAL
|
/// Represents an Interest Stack from MAL
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A primary and secondary color
|
/// A primary and secondary color
|
||||||
|
|
|
||||||
10
API/DTOs/KavitaLocale.cs
Normal file
10
API/DTOs/KavitaLocale.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
public class KavitaLocale
|
||||||
|
{
|
||||||
|
public string FileName { get; set; } // Key
|
||||||
|
public string RenderName { get; set; }
|
||||||
|
public float TranslationCompletion { get; set; }
|
||||||
|
public bool IsRtL { get; set; }
|
||||||
|
public string Hash { get; set; } // ETAG hash so I can run my own localization busting implementation
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
|
|
||||||
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used for matching and fetching metadata on a series
|
/// Used for matching and fetching metadata on a series
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using API.DTOs.Scrobbling;
|
using API.DTOs.Scrobbling;
|
||||||
|
|
||||||
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
namespace API.DTOs.KavitaPlus.ExternalMetadata;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
internal class MatchSeriesRequestDto
|
internal class MatchSeriesRequestDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs.KavitaPlus.License;
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class EncryptLicenseDto
|
public class EncryptLicenseDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs.KavitaPlus.License;
|
namespace API.DTOs.KavitaPlus.License;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class UpdateLicenseDto
|
public class UpdateLicenseDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.MetadataMatching;
|
||||||
using NotImplementedException = System.NotImplementedException;
|
using NotImplementedException = System.NotImplementedException;
|
||||||
|
|
||||||
namespace API.DTOs.KavitaPlus.Metadata;
|
namespace API.DTOs.KavitaPlus.Metadata;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs.KavitaPlus.Metadata;
|
namespace API.DTOs.KavitaPlus.Metadata;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public enum CharacterRole
|
public enum CharacterRole
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class MangaFileDto
|
public class MangaFileDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class PersonDto
|
public class PersonDto
|
||||||
{
|
{
|
||||||
|
|
@ -6,12 +9,12 @@ public class PersonDto
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
public bool CoverImageLocked { get; set; }
|
public bool CoverImageLocked { get; set; }
|
||||||
public string PrimaryColor { get; set; }
|
public string? PrimaryColor { get; set; }
|
||||||
public string SecondaryColor { get; set; }
|
public string? SecondaryColor { get; set; }
|
||||||
|
|
||||||
public string? CoverImage { get; set; }
|
public string? CoverImage { get; set; }
|
||||||
|
|
||||||
public string Description { get; set; }
|
public string? Description { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ASIN for person
|
/// ASIN for person
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class UpdatePersonDto
|
public class UpdatePersonDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace API.DTOs.Progress;
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
public class UpdateUserProgressDto
|
|
||||||
{
|
|
||||||
public int PageNum { get; set; }
|
|
||||||
public DateTime LastModifiedUtc { get; set; }
|
|
||||||
public DateTime CreatedUtc { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs.Reader;
|
namespace API.DTOs.Reader;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class CreatePersonalToCDto
|
public class CreatePersonalToCDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
20
API/DTOs/ReadingLists/ReadingListCast.cs
Normal file
20
API/DTOs/ReadingLists/ReadingListCast.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.DTOs.ReadingLists;
|
||||||
|
|
||||||
|
public class ReadingListCast
|
||||||
|
{
|
||||||
|
public ICollection<PersonDto> Writers { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> CoverArtists { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Publishers { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Characters { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Pencillers { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Inkers { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Imprints { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Colorists { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Letterers { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Editors { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Translators { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Teams { get; set; } = [];
|
||||||
|
public ICollection<PersonDto> Locations { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.DTOs.ReadingLists;
|
namespace API.DTOs.ReadingLists;
|
||||||
|
|
@ -43,6 +44,10 @@ public class ReadingListDto : IHasCoverImage
|
||||||
/// Maximum Month the Reading List starts
|
/// Maximum Month the Reading List starts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EndingMonth { get; set; }
|
public int EndingMonth { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The highest age rating from all Series within the reading list
|
||||||
|
/// </summary>
|
||||||
|
public required AgeRating AgeRating { get; set; } = AgeRating.Unknown;
|
||||||
|
|
||||||
public void ResetColorScape()
|
public void ResetColorScape()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
26
API/DTOs/ReadingLists/ReadingListInfoDto.cs
Normal file
26
API/DTOs/ReadingLists/ReadingListInfoDto.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
using API.DTOs.Reader;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
|
namespace API.DTOs.ReadingLists;
|
||||||
|
|
||||||
|
public class ReadingListInfoDto : IHasReadTimeEstimate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Total Pages across all Reading List Items
|
||||||
|
/// </summary>
|
||||||
|
public int Pages { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Total Word count across all Reading List Items
|
||||||
|
/// </summary>
|
||||||
|
public long WordCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Are ALL Reading List Items epub
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAllEpub { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MinHoursToRead"/>
|
||||||
|
public int MinHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.MaxHoursToRead"/>
|
||||||
|
public int MaxHoursToRead { get; set; }
|
||||||
|
/// <inheritdoc cref="IHasReadTimeEstimate.AvgHoursToRead"/>
|
||||||
|
public float AvgHoursToRead { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ public class ReadingListItemDto
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Release Date from Chapter
|
/// Release Date from Chapter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime ReleaseDate { get; set; }
|
public DateTime? ReleaseDate { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used internally only
|
/// Used internally only
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -33,7 +33,7 @@ public class ReadingListItemDto
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The last time a reading list item (underlying chapter) was read by current authenticated user
|
/// The last time a reading list item (underlying chapter) was read by current authenticated user
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastReadingProgressUtc { get; set; }
|
public DateTime? LastReadingProgressUtc { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// File size of underlying item
|
/// File size of underlying item
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class RegisterDto
|
public class RegisterDto
|
||||||
{
|
{
|
||||||
|
|
@ -9,7 +10,7 @@ public class RegisterDto
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An email to register with. Optional. Provides Forgot Password functionality
|
/// An email to register with. Optional. Provides Forgot Password functionality
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Email { get; init; } = default!;
|
public string? Email { get; set; } = default!;
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(256, MinimumLength = 6)]
|
[StringLength(256, MinimumLength = 6)]
|
||||||
public string Password { get; set; } = default!;
|
public string Password { get; set; } = default!;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using API.Services.Plus;
|
using API.Services.Plus;
|
||||||
|
|
||||||
namespace API.DTOs.Scrobbling;
|
namespace API.DTOs.Scrobbling;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public record MediaRecommendationDto
|
public record MediaRecommendationDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
namespace API.DTOs.Scrobbling;
|
namespace API.DTOs.Scrobbling;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents information about a potential Series for Kavita+
|
/// Represents information about a potential Series for Kavita+
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace API.DTOs.Scrobbling;
|
namespace API.DTOs.Scrobbling;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class ScrobbleEventDto
|
public class ScrobbleEventDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using API.DTOs.Recommendation;
|
using API.DTOs.Recommendation;
|
||||||
|
|
||||||
namespace API.DTOs.SeriesDetail;
|
namespace API.DTOs.SeriesDetail;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All the data from Kavita+ for Series Detail
|
/// All the data from Kavita+ for Series Detail
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,8 @@ public class SeriesDto : IHasReadTimeEstimate, IHasCoverImage
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public string? CoverImage { get; set; }
|
public string? CoverImage { get; set; }
|
||||||
public string PrimaryColor { get; set; }
|
public string PrimaryColor { get; set; } = string.Empty;
|
||||||
public string SecondaryColor { get; set; }
|
public string SecondaryColor { get; set; } = string.Empty;
|
||||||
|
|
||||||
public void ResetColorScape()
|
public void ResetColorScape()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
|
|
||||||
namespace API.DTOs.Settings;
|
namespace API.DTOs.Settings;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class ServerSettingDto
|
public class ServerSettingDto
|
||||||
{
|
{
|
||||||
|
|
@ -44,6 +46,7 @@ public class ServerSettingDto
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs.
|
/// Represents a unique Id to this Kavita installation. Only used in Stats to identify unique installs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
||||||
public string InstallId { get; set; } = default!;
|
public string InstallId { get; set; } = default!;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The format that should be used when saving media for Kavita
|
/// The format that should be used when saving media for Kavita
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
||||||
namespace API.DTOs.SideNav;
|
namespace API.DTOs.SideNav;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
public class SideNavStreamDto
|
public class SideNavStreamDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue