refactor: Convert processors/updaters to pipelines

pull/201/head
Robert Dailey 1 year ago
parent e3d6d4f79a
commit cffb8d783a

@ -340,6 +340,7 @@ resharper_csharp_space_after_unary_operator = false
resharper_csharp_stick_comment = false
resharper_csharp_wrap_after_declaration_lpar = true
resharper_csharp_wrap_lines = true
resharper_csharp_wrap_parameters_style = chop_if_long
resharper_cxxcli_property_declaration_braces = next_line
resharper_default_exception_variable_name = e
resharper_default_value_when_type_evident = default_literal
@ -442,8 +443,8 @@ resharper_line_break_before_requires_clause = do_not_change
resharper_linkage_specification_braces = end_of_line
resharper_linkage_specification_indentation = none
resharper_local_function_body = block_body
resharper_macro_block_begin =
resharper_macro_block_end =
resharper_macro_block_begin =
resharper_macro_block_end =
resharper_max_array_initializer_elements_on_line = 10000
resharper_max_attribute_length_for_same_line = 60
resharper_max_enum_members_on_line = 3
@ -510,7 +511,7 @@ resharper_remove_blank_lines_near_braces_in_declarations = true
resharper_remove_this_qualifier = true
resharper_requires_expression_braces = next_line
resharper_resx_attribute_indent = single_indent
resharper_resx_linebreak_before_elements =
resharper_resx_linebreak_before_elements =
resharper_resx_max_blank_lines_between_tags = 0
resharper_resx_max_line_length = 2147483647
resharper_resx_pi_attribute_style = do_not_touch
@ -741,7 +742,7 @@ resharper_xmldoc_wrap_lines = true
resharper_xmldoc_wrap_tags_and_pi = true
resharper_xmldoc_wrap_text = true
resharper_xml_attribute_indent = align_by_first_attribute
resharper_xml_linebreak_before_elements =
resharper_xml_linebreak_before_elements =
resharper_xml_max_blank_lines_between_tags = 2
resharper_xml_max_line_length = 120
resharper_xml_pi_attribute_style = do_not_touch

@ -67,6 +67,11 @@
"description": "If enabled, custom formats that you remove from your YAML configuration OR that are removed from the guide will be deleted from your Radarr instance.",
"default": false
},
"replace_existing_custom_formats": {
"type": "boolean",
"description": "If disabled, custom formats that Recyclarr didn't explicitly create or know about will not be replaced.",
"default": true
},
"custom_formats": {
"type": "array",
"minItems": 1,
@ -136,7 +141,7 @@
"api_key": {
"type": "string",
"minLength": 1,
"description": "The API key from Radarr."
"description": "The API key from Radarr"
},
"quality_definition": {
"$ref": "#/$defs/quality_definition"
@ -144,6 +149,9 @@
"delete_old_custom_formats": {
"$ref": "#/$defs/delete_old_custom_formats"
},
"replace_existing_custom_formats": {
"$ref": "#/$defs/replace_existing_custom_formats"
},
"custom_formats": {
"$ref": "#/$defs/custom_formats"
}
@ -173,6 +181,9 @@
"delete_old_custom_formats": {
"$ref": "#/$defs/delete_old_custom_formats"
},
"replace_existing_custom_formats": {
"$ref": "#/$defs/replace_existing_custom_formats"
},
"custom_formats": {
"$ref": "#/$defs/custom_formats"
},

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<attachedFolders>
<Path>../../../code</Path>
</attachedFolders>
<explicitIncludes />
<explicitExcludes />
</component>

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="list release-profiles --terms" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="list release-profiles --terms EBC725268D687D588A20CBC5F97E538B" />
<option name="PROGRAM_PARAMETERS" value="list release-profiles --terms 76e060895c5b8a765c310933da0a5357" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="radarr --list-custom-formats" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="radarr --list-custom-formats" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="radarr --list-qualities" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="radarr --list-qualities" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="radarr --preview" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="radarr --preview" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sonarr" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="sonarr" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sonarr --list-custom-formats" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="sonarr --list-custom-formats" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sonarr --list-qualities" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="sonarr --list-qualities" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sonarr --list-release-profiles" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="sonarr --list-release-profiles" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,20 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sonarr --preview" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="sonarr --preview" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,23 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sonarr (custom config dir)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="sonarr" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="RECYCLARR_APP_DATA" value="$PROJECT_DIR$/../docker/config" />
</envs>
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net7.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>

@ -1,7 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="radarr" type="DotNetProject" factoryName=".NET Project">
<configuration default="false" name="sync --preview" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
<option name="PROGRAM_PARAMETERS" value="radarr" />
<option name="PROGRAM_PARAMETERS" value="sync --preview" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />

@ -49,6 +49,7 @@
<!-- TEST ONLY PACKAGES -->
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests')) Or $(ProjectName.EndsWith('TestLibrary'))">
<PackageReference Include="AgileObjects.ReadableExpressions" PrivateAssets="All" />
<PackageReference Include="AutofacContrib.NSubstitute" PrivateAssets="All" />
<PackageReference Include="AutoFixture" PrivateAssets="All" />
<PackageReference Include="AutoFixture.AutoNSubstitute" PrivateAssets="All" />
@ -70,6 +71,14 @@
<PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests')) Or $(ProjectName.EndsWith('TestLibrary'))">
<Using Include="NUnit.Framework" />
<Using Include="NSubstitute" />
<Using Include="FluentAssertions" />
<Using Include="AutoFixture.NUnit3" />
<Using Include="System.IO.Abstractions.TestingHelpers" />
</ItemGroup>
<ItemGroup Condition="$(ProjectName.EndsWith('.Tests')) Or $(ProjectName.EndsWith('TestLibrary'))">
<EmbeddedResource Include="**\Data\*" />
</ItemGroup>

@ -7,6 +7,7 @@
<PackageVersion Include="Autofac.Extras.Ordering" Version="4.0.0" />
<PackageVersion Include="AutofacSerilogIntegration" Version="5.0.0" />
<PackageVersion Include="AutoMapper" Version="12.0.1" />
<PackageVersion Include="AutoMapper.Collection" Version="9.0.0" />
<PackageVersion Include="AutoMapper.Contrib.Autofac.DependencyInjection" Version="7.1.0" />
<PackageVersion Include="CliWrap" Version="3.6.0" />
<PackageVersion Include="FluentValidation" Version="11.4.0" />
@ -36,6 +37,7 @@
</ItemGroup>
<!-- Unit Test Packages -->
<ItemGroup>
<PackageVersion Include="AgileObjects.ReadableExpressions" Version="4.0.0" />
<PackageVersion Include="AutofacContrib.NSubstitute" Version="7.0.0" />
<PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="AutoFixture.AutoNSubstitute" Version="4.17.0" />

@ -1,18 +1,14 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using Autofac;
using Autofac.Features.ResolveAnything;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Common;
using Recyclarr.Common.TestLibrary;
using Recyclarr.TestLibrary;
using Recyclarr.TrashLib;
using Recyclarr.TrashLib.ApiServices.System;
using Recyclarr.TrashLib.Repo.VersionControl;
using Recyclarr.TrashLib.Services.System;
using Recyclarr.TrashLib.Startup;
using Serilog;
using Serilog.Events;
using Spectre.Console;
using Spectre.Console.Testing;
@ -74,7 +70,7 @@ public abstract class IntegrationFixture : IDisposable
private void SetupMetadataJson()
{
var metadataFile = Paths.RepoDirectory.File("metadata.json");
Fs.AddFileFromResource(metadataFile, "metadata.json");
Fs.AddFileFromEmbeddedResource(metadataFile, typeof(IntegrationFixture), "Data.metadata.json");
}
// ReSharper disable MemberCanBePrivate.Global

@ -1,5 +1,4 @@
using AutoMapper;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
namespace Recyclarr.Cli.Tests;

@ -1,7 +1,4 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config.Settings;

@ -2,8 +2,6 @@ using System.Collections;
using System.Diagnostics.CodeAnalysis;
using Autofac;
using Autofac.Core;
using FluentAssertions;
using NUnit.Framework;
using NUnit.Framework.Internal;
using Recyclarr.Cli.TestLibrary;

@ -1,6 +1,3 @@
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config.Services;

@ -1,9 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using MoreLinq.Extensions;
using NUnit.Framework;
using Recyclarr.Cli.Logging;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.TestLibrary;

@ -1,6 +1,3 @@
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Cli.Migration;
using Recyclarr.Cli.Migration.Steps;
using Recyclarr.Cli.TestLibrary;

@ -1,8 +1,4 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Cli.Migration.Steps;
using Recyclarr.TestLibrary;
using Recyclarr.TestLibrary.AutoFixture;

@ -1,8 +1,4 @@
using System.IO.Abstractions.TestingHelpers;
using System.Text.RegularExpressions;
using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Cli.Migration.Steps;
using Recyclarr.TestLibrary.AutoFixture;

@ -1,6 +1,3 @@
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config.Settings;

@ -3,24 +3,27 @@ using System.Reflection;
using Autofac;
using Autofac.Extras.Ordering;
using AutoMapper.Contrib.Autofac.DependencyInjection;
using AutoMapper.EquivalencyExpression;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging;
using Recyclarr.Cli.Migration;
using Recyclarr.Common;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.ApiServices;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Compatibility;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Http;
using Recyclarr.TrashLib.Pipelines;
using Recyclarr.TrashLib.Pipelines.CustomFormat;
using Recyclarr.TrashLib.Pipelines.QualityProfile;
using Recyclarr.TrashLib.Pipelines.QualitySize;
using Recyclarr.TrashLib.Pipelines.ReleaseProfile;
using Recyclarr.TrashLib.Pipelines.Tags;
using Recyclarr.TrashLib.Processors;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Repo.VersionControl;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.Processors;
using Recyclarr.TrashLib.Services.QualitySize;
using Recyclarr.TrashLib.Services.Radarr;
using Recyclarr.TrashLib.Services.ReleaseProfile;
using Recyclarr.TrashLib.Services.Sonarr;
using Recyclarr.TrashLib.Services.System;
using Recyclarr.TrashLib.Startup;
using Spectre.Console.Cli;
@ -37,15 +40,11 @@ public static class CompositionRoot
RegisterAppPaths(builder);
RegisterLogger(builder);
builder.RegisterModule<SonarrAutofacModule>();
builder.RegisterModule<RadarrAutofacModule>();
builder.RegisterModule<QualitySizeAutofacModule>();
builder.RegisterModule<CustomFormatAutofacModule>();
builder.RegisterModule<ReleaseProfileAutofacModule>();
builder.RegisterModule<VersionControlAutofacModule>();
builder.RegisterModule<MigrationAutofacModule>();
builder.RegisterModule<RepoAutofacModule>();
builder.RegisterModule<SystemServiceAutofacModule>();
builder.RegisterModule<CompatibilityAutofacModule>();
builder.RegisterModule<ApiServicesAutofacModule>();
builder.RegisterModule(new ConfigAutofacModule(assemblies));
builder.RegisterModule<ServiceProcessorsAutofacModule>();
builder.RegisterModule(new CommonAutofacModule(Assembly.GetExecutingAssembly()));
@ -58,12 +57,35 @@ public static class CompositionRoot
builder.RegisterType<ServiceRequestBuilder>().As<IServiceRequestBuilder>();
CommandRegistrations(builder);
PipelineRegistrations(builder);
builder.RegisterAutoMapper(false, assemblies);
builder.RegisterAutoMapper(c =>
{
c.AddCollectionMappers();
},
false, assemblies);
builder.RegisterType<FlurlClientFactory>().As<IFlurlClientFactory>().SingleInstance();
}
private static void PipelineRegistrations(ContainerBuilder builder)
{
builder.RegisterModule<TagsAutofacModule>();
builder.RegisterModule<CustomFormatAutofacModule>();
builder.RegisterModule<QualityProfileAutofacModule>();
builder.RegisterModule<QualitySizeAutofacModule>();
builder.RegisterModule<ReleaseProfileAutofacModule>();
builder.RegisterTypes(
typeof(TagSyncPipeline),
typeof(CustomFormatSyncPipeline),
typeof(QualityProfileSyncPipeline),
typeof(QualitySizeSyncPipeline),
typeof(ReleaseProfileSyncPipeline))
.As<ISyncPipeline>()
.OrderByRegistration();
}
private static void RegisterLogger(ContainerBuilder builder)
{
builder.RegisterType<LogJanitor>().As<ILogJanitor>();

@ -2,7 +2,7 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Recyclarr.TrashLib.ExceptionTypes;
using Recyclarr.TrashLib.Services.Processors;
using Recyclarr.TrashLib.Processors;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;

@ -3,8 +3,8 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Spectre.Console.Cli;
#pragma warning disable CS8765

@ -3,8 +3,8 @@ using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Pipelines.QualitySize.Guide;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Services.QualitySize.Guide;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;

@ -1,8 +1,8 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations;
using Recyclarr.TrashLib.Pipelines.ReleaseProfile.Guide;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Services.ReleaseProfile.Guide;
using Spectre.Console.Cli;
#pragma warning disable CS8765

@ -5,10 +5,10 @@ using JetBrains.Annotations;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Migration;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.QualitySize.Guide;
using Recyclarr.TrashLib.Processors;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Recyclarr.TrashLib.Services.Processors;
using Recyclarr.TrashLib.Services.QualitySize.Guide;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;

@ -5,11 +5,11 @@ using JetBrains.Annotations;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Migration;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.QualitySize.Guide;
using Recyclarr.TrashLib.Pipelines.ReleaseProfile.Guide;
using Recyclarr.TrashLib.Processors;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Recyclarr.TrashLib.Services.Processors;
using Recyclarr.TrashLib.Services.QualitySize.Guide;
using Recyclarr.TrashLib.Services.ReleaseProfile.Guide;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;

@ -5,8 +5,8 @@ using JetBrains.Annotations;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.Migration;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Processors;
using Recyclarr.TrashLib.Repo;
using Recyclarr.TrashLib.Services.Processors;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;

@ -1,35 +1,35 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Reflection;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedMember.Global
namespace Recyclarr.Common.TestLibrary;
public static class CommonMockFileSystemExtensions
{
public static void AddFileFromResource(this MockFileSystem fs, string resourceFilename)
{
fs.AddFileFromResource(resourceFilename, resourceFilename, Assembly.GetCallingAssembly());
}
public static void AddFileFromResource(this MockFileSystem fs, IFileInfo file, string resourceFilename,
string resourceDir = "Data")
public static void AddFileFromEmbeddedResource(
this MockFileSystem fs,
IFileInfo path,
Assembly resourceAssembly,
string embeddedResourcePath)
{
fs.AddFileFromResource(file.FullName, resourceFilename, Assembly.GetCallingAssembly(), resourceDir);
fs.AddFileFromEmbeddedResource(path.FullName, resourceAssembly, embeddedResourcePath);
}
public static void AddFileFromResource(this MockFileSystem fs, string file, string resourceFilename,
string resourceDir = "Data")
public static void AddSameFileFromEmbeddedResource(
this MockFileSystem fs,
IFileInfo path,
Type typeInAssembly,
string resourceSubPath = "Data")
{
fs.AddFileFromResource(file, resourceFilename, Assembly.GetCallingAssembly(), resourceDir);
fs.AddFileFromEmbeddedResource(path, typeInAssembly, $"{resourceSubPath}.{path.Name}");
}
public static void AddFileFromResource(this MockFileSystem fs, string file, string resourceFilename,
Assembly assembly, string resourceDir = "Data")
public static void AddFileFromEmbeddedResource(
this MockFileSystem fs,
IFileInfo path,
Type typeInAssembly,
string embeddedResourcePath)
{
var resourceReader = new ResourceDataReader(assembly, resourceDir);
fs.AddFile(file, new MockFileData(resourceReader.ReadData(resourceFilename)));
var resourcePath = $"{typeInAssembly.Namespace}.{embeddedResourcePath}";
fs.AddFileFromEmbeddedResource(path, typeInAssembly.Assembly, resourcePath);
}
}

@ -1,5 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Common.Extensions;
namespace Recyclarr.Common.Tests.Extensions;

@ -1,8 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using System.Text.RegularExpressions;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Common.Extensions;
using Recyclarr.TestLibrary;
@ -12,8 +9,11 @@ namespace Recyclarr.Common.Tests.Extensions;
[Parallelizable(ParallelScope.All)]
public class FileSystemExtensionsTest
{
private static IEnumerable<string> ReRootFiles(IFileSystem fs, IEnumerable<string> files,
string oldRoot, string newRoot)
private static IEnumerable<string> ReRootFiles(
IFileSystem fs,
IEnumerable<string> files,
string oldRoot,
string newRoot)
{
return files.Select(x =>
{

@ -1,5 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Common.Extensions;
namespace Recyclarr.Common.Tests.Extensions;

@ -1,10 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.TestCorrelator;

@ -1,6 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
namespace Recyclarr.Common.Tests;
[TestFixture]

@ -1,21 +0,0 @@
using Autofac;
namespace Recyclarr.Common.Autofac;
public sealed class LifetimeScopedValue<T> : IDisposable
{
private readonly ILifetimeScope _scope;
public LifetimeScopedValue(ILifetimeScope scope, T value)
{
_scope = scope;
Value = value;
}
public T Value { get; }
public void Dispose()
{
_scope.Dispose();
}
}

@ -46,10 +46,16 @@ public static class CollectionExtensions
}
}
public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> observable)
public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> source)
where T : class
{
return observable.Where(x => x is not null).Select(x => x!);
return source.Where(x => x is not null).Select(x => x!);
}
public static IEnumerable<T> NotNull<T>(this IEnumerable<T?> source)
where T : struct
{
return source.Where(x => x is not null).Select(x => x!.Value);
}
public static bool IsEmpty<T>(this ICollection<T>? collection)
@ -66,4 +72,10 @@ public static class CollectionExtensions
{
return collection is {Count: > 0};
}
public static IList<T>? ToListOrNull<T>(this IEnumerable<T> source)
{
var list = source.ToList();
return list.Any() ? list : null;
}
}

@ -21,7 +21,10 @@ public static class FileSystemExtensions
}
}
public static void MergeDirectory(this IFileSystem fs, IDirectoryInfo targetDir, IDirectoryInfo destDir,
public static void MergeDirectory(
this IFileSystem fs,
IDirectoryInfo targetDir,
IDirectoryInfo destDir,
IAnsiConsole? console = null)
{
var directories = targetDir

@ -1,3 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Recyclarr.Common.Extensions;
@ -21,4 +22,10 @@ public static class JsonNetExtensions
return value;
}
public static T Clone<T>(this T source) where T : notnull
{
return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source))
?? throw new ArgumentException("Could not deep clone", nameof(source));
}
}

@ -45,4 +45,9 @@ public static class StringExtensions
{
return value.Trim('\r', '\n');
}
public static string ToCamelCase(this string value)
{
return char.ToLowerInvariant(value[0]) + value[1..];
}
}

@ -9,7 +9,9 @@ public static class FluentValidationExtensions
// From: https://github.com/FluentValidation/FluentValidation/issues/1648
// ReSharper disable once UnusedMethodReturnValue.Global
public static IRuleBuilderOptions<T, TProperty?> SetNonNullableValidator<T, TProperty>(
this IRuleBuilder<T, TProperty?> ruleBuilder, IValidator<TProperty> validator, params string[] ruleSets)
this IRuleBuilder<T, TProperty?> ruleBuilder,
IValidator<TProperty> validator,
params string[] ruleSets)
{
var adapter = new NullableChildValidatorAdaptor<T, TProperty>(validator, validator.GetType())
{
@ -32,7 +34,9 @@ public static class FluentValidationExtensions
return base.IsValid(context, value!);
}
public override Task<bool> IsValidAsync(ValidationContext<T> context, TProperty? value,
public override Task<bool> IsValidAsync(
ValidationContext<T> context,
TProperty? value,
CancellationToken cancellation)
{
return base.IsValidAsync(context, value!, cancellation);
@ -40,7 +44,8 @@ public static class FluentValidationExtensions
}
public static IEnumerable<TSource> IsValid<TSource, TValidator>(
this IEnumerable<TSource> source, TValidator validator,
this IEnumerable<TSource> source,
TValidator validator,
Action<List<ValidationFailure>, TSource>? handleInvalid = null)
where TValidator : IValidator<TSource>
{

@ -11,7 +11,9 @@ internal sealed class NullableChildValidatorAdaptor<T, TProperty> : ChildValidat
{
}
public override Task<bool> IsValidAsync(ValidationContext<T> context, TProperty? value,
public override Task<bool> IsValidAsync(
ValidationContext<T> context,
TProperty? value,
CancellationToken cancellation)
{
return base.IsValidAsync(context, value!, cancellation);

@ -0,0 +1,43 @@
namespace Recyclarr.Common;
public sealed class GenericEqualityComparer<T> : IEqualityComparer<T>
{
private readonly Func<T, T, bool> _equalsPredicate;
private readonly Func<T, int> _hashPredicate;
public GenericEqualityComparer(Func<T, T, bool> equalsPredicate, Func<T, int> hashPredicate)
{
_equalsPredicate = equalsPredicate;
_hashPredicate = hashPredicate;
}
public bool Equals(T? x, T? y)
{
if (ReferenceEquals(x, y))
{
return true;
}
if (ReferenceEquals(x, null))
{
return false;
}
if (ReferenceEquals(y, null))
{
return false;
}
if (x.GetType() != y.GetType())
{
return false;
}
return _equalsPredicate(x, y);
}
public int GetHashCode(T obj)
{
return _hashPredicate(obj);
}
}

@ -15,8 +15,11 @@ public sealed class ForceEmptySequences : INodeDeserializer
_objectFactory = objectFactory;
}
bool INodeDeserializer.Deserialize(IParser reader, Type expectedType,
Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
bool INodeDeserializer.Deserialize(
IParser reader,
Type expectedType,
Func<IParser, Type, object?> nestedObjectDeserializer,
out object? value)
{
value = null;

@ -13,8 +13,11 @@ internal class ValidatingDeserializer : INodeDeserializer
_nodeDeserializer = nodeDeserializer;
}
public bool Deserialize(IParser reader, Type expectedType,
Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
public bool Deserialize(
IParser reader,
Type expectedType,
Func<IParser, Type, object?> nestedObjectDeserializer,
out object? value)
{
if (!_nodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value) ||
value == null)

@ -1,6 +1,8 @@
@page
@model Recyclarr.Gui.Pages.ErrorModel
@* ReSharper disable Html.PathError *@
<!DOCTYPE html>
<html>

@ -1,6 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
namespace Recyclarr.TestLibrary.Tests;
[TestFixture]

@ -1,6 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
namespace Recyclarr.TestLibrary.Tests;
[TestFixture]

@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Autofac;
using AutoFixture;
using AutoFixture.NUnit3;
namespace Recyclarr.TestLibrary.AutoFixture;

@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using AutoFixture.NUnit3;
namespace Recyclarr.TestLibrary.AutoFixture;

@ -1,6 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture;
namespace Recyclarr.TestLibrary.AutoFixture;

@ -1,5 +1,4 @@
using Autofac;
using NSubstitute;
namespace Recyclarr.TestLibrary;

@ -1,4 +1,3 @@
using System.IO.Abstractions.TestingHelpers;
using System.Text.RegularExpressions;
namespace Recyclarr.TestLibrary;

@ -6,7 +6,9 @@ namespace Recyclarr.TestLibrary.FluentAssertions;
public class JsonEquivalencyStep : IEquivalencyStep
{
public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context,
public EquivalencyResult Handle(
Comparands comparands,
IEquivalencyValidationContext context,
IEquivalencyValidator nestedValidator)
{
var canHandle = comparands.Subject?.GetType().IsAssignableTo(typeof(JToken)) ?? false;

@ -1,4 +1,3 @@
using System.IO.Abstractions.TestingHelpers;
using Newtonsoft.Json;
namespace Recyclarr.TestLibrary;

@ -1,5 +1,4 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
namespace Recyclarr.TestLibrary;

@ -1,16 +0,0 @@
using Recyclarr.TrashLib.Services.CustomFormat.Models;
namespace Recyclarr.TrashLib.TestLibrary;
public static class CfTestUtils
{
public static QualityProfileCustomFormatScoreMapping NewMapping(params FormatMappingEntry[] entries)
{
return new QualityProfileCustomFormatScoreMapping(false) {Mapping = entries.ToList()};
}
public static QualityProfileCustomFormatScoreMapping NewMappingWithReset(params FormatMappingEntry[] entries)
{
return new QualityProfileCustomFormatScoreMapping(true) {Mapping = entries.ToList()};
}
}

@ -0,0 +1,30 @@
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Processors;
namespace Recyclarr.TrashLib.TestLibrary;
public static class MockSyncSettings
{
private static ISyncSettings MakeSyncSettings(SupportedServices? service, params string[] instances)
{
var settings = Substitute.For<ISyncSettings>();
settings.Service.Returns(service);
settings.Instances.Returns(instances);
return settings;
}
public static ISyncSettings Radarr(params string[] instances)
{
return MakeSyncSettings(SupportedServices.Radarr, instances);
}
public static ISyncSettings Sonarr(params string[] instances)
{
return MakeSyncSettings(SupportedServices.Sonarr, instances);
}
public static ISyncSettings AnyService(params string[] instances)
{
return MakeSyncSettings(null, instances);
}
}

@ -1,51 +1,27 @@
using Newtonsoft.Json.Linq;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.TestLibrary;
public static class NewCf
{
public static CustomFormatData Data(string name, string trashId, int? score = null)
public static CustomFormatData DataWithScore(string name, string trashId, int score, int id = 0)
{
return Data(name, trashId, score, JObject.Parse($"{{'name':'{name}'}}"));
}
public static CustomFormatData Data(string name, string trashId, int? score, JObject json)
{
return new CustomFormatData("", name, trashId, score, json);
}
public static ProcessedCustomFormatData ProcessedWithScore(string name, string trashId, int score, JObject json)
{
return new ProcessedCustomFormatData(Data(name, trashId, score, json));
}
public static ProcessedCustomFormatData ProcessedWithScore(string name, string trashId, int score, int formatId = 0)
{
return new ProcessedCustomFormatData(Data(name, trashId, score))
{
FormatId = formatId
};
}
public static ProcessedCustomFormatData Processed(string name, string trashId, JObject json)
{
return Processed(name, trashId, 0, json);
}
public static ProcessedCustomFormatData Processed(string name, string trashId, int formatId = 0)
{
return new ProcessedCustomFormatData(Data(name, trashId))
return new CustomFormatData
{
FormatId = formatId
Id = id,
Name = name,
TrashId = trashId,
TrashScore = score
};
}
public static ProcessedCustomFormatData Processed(string name, string trashId, int formatId, JObject json)
public static CustomFormatData Data(string name, string trashId, int id = 0)
{
return new ProcessedCustomFormatData(Data(name, trashId, null, json))
return new CustomFormatData
{
FormatId = formatId
Id = id,
Name = name,
TrashId = trashId
};
}
}

@ -0,0 +1,25 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Pipelines.QualityProfile.PipelinePhases;
namespace Recyclarr.TrashLib.TestLibrary;
public static class NewQp
{
public static ProcessedQualityProfileData Processed(
string profileName,
params (int FormatId, int Score)[] scores)
{
return Processed(profileName, false, scores);
}
public static ProcessedQualityProfileData Processed(
string profileName,
bool resetUnmatchedScores,
params (int FormatId, int Score)[] scores)
{
return new ProcessedQualityProfileData(new QualityProfileConfig(profileName, resetUnmatchedScores))
{
CfScores = scores.ToDictionary(x => x.FormatId, x => x.Score)
};
}
}

@ -1,13 +1,7 @@
using System.Collections.ObjectModel;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.Tests.Cache;
@ -208,7 +202,7 @@ public class ServiceCacheTest
var result = sut.Load<CustomFormatCache>(config);
result.Should().BeEquivalentTo(new CustomFormatCache
result.Should().BeEquivalentTo(new
{
TrashIdMappings = new Collection<TrashIdMapping>
{

@ -1,5 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TrashLib.Config.EnvironmentVariables;
namespace Recyclarr.TrashLib.Tests.Config.EnvironmentVariables;

@ -1,10 +1,10 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.Radarr.Config;
using Recyclarr.TrashLib.Services.Sonarr.Config;
using Recyclarr.TrashLib.Config.Services.Radarr;
using Recyclarr.TrashLib.Config.Services.Sonarr;
using Recyclarr.TrashLib.Processors;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
@ -28,7 +28,7 @@ public class ConfigRegistryTest
sut.Add(c);
}
var result = sut.GetConfigsOfType(SupportedServices.Sonarr);
var result = sut.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
result.Should().Equal(configs.Take(2));
}
@ -49,7 +49,7 @@ public class ConfigRegistryTest
sut.Add(c);
}
var result = sut.GetConfigsOfType(null);
var result = sut.GetConfigsBasedOnSettings(MockSyncSettings.AnyService());
result.Should().Equal(configs);
}
@ -60,8 +60,51 @@ public class ConfigRegistryTest
var sut = new ConfigRegistry();
sut.Add(new SonarrConfiguration());
var result = sut.GetConfigsOfType(SupportedServices.Radarr);
var settings = Substitute.For<ISyncSettings>();
settings.Service.Returns(SupportedServices.Radarr);
var result = sut.GetConfigsBasedOnSettings(settings);
result.Should().BeEmpty();
}
[Test]
public void Get_configs_by_type_and_instance_name()
{
var configs = new IServiceConfiguration[]
{
new SonarrConfiguration {InstanceName = "one"},
new SonarrConfiguration {InstanceName = "two"},
new RadarrConfiguration {InstanceName = "three"}
};
var sut = new ConfigRegistry();
foreach (var c in configs)
{
sut.Add(c);
}
var result = sut.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr("one"));
result.Should().Equal(configs.Take(1));
}
[Test]
public void Instance_matching_should_be_case_insensitive()
{
var configs = new IServiceConfiguration[]
{
new SonarrConfiguration {InstanceName = "one"}
};
var sut = new ConfigRegistry();
foreach (var c in configs)
{
sut.Add(c);
}
var result = sut.GetConfigsBasedOnSettings(MockSyncSettings.AnyService("ONE"));
result.Should().Equal(configs);
}
}

@ -1,8 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Config.Services;

@ -1,11 +1,6 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Startup;

@ -1,11 +1,8 @@
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.EnvironmentVariables;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.TestLibrary;
using YamlDotNet.Core;
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
@ -32,7 +29,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -57,7 +54,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -84,7 +81,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -111,7 +108,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -136,7 +133,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -160,7 +157,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new

@ -1,11 +1,8 @@
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Config.Secrets;
using Recyclarr.TrashLib.Services.Sonarr.Config;
using Recyclarr.TrashLib.Config.Services.Sonarr;
using Recyclarr.TrashLib.TestLibrary;
using Serilog.Sinks.TestCorrelator;
using YamlDotNet.Core;
@ -55,7 +52,7 @@ secret_rp: 1234567
};
var parsedSecret = configLoader.LoadFromStream(new StringReader(testYml), "sonarr");
parsedSecret.GetConfigsOfType(SupportedServices.Sonarr)
parsedSecret.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr())
.Should().BeEquivalentTo(expected, o => o.Excluding(x => x.LineNumber));
}

@ -1,20 +1,15 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using System.Text;
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common;
using Recyclarr.Common.Extensions;
using Recyclarr.TestLibrary;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Config.Services.Sonarr;
using Recyclarr.TrashLib.Config.Yaml;
using Recyclarr.TrashLib.Services.Sonarr.Config;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
@ -80,10 +75,10 @@ public class ConfigurationLoaderTest : IntegrationFixture
var loader = Resolve<IConfigurationLoader>();
var actual = loader.LoadMany(fileData.Select(x => x.Item1));
actual.GetConfigsOfType(SupportedServices.Sonarr)
actual.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr())
.Should().BeEquivalentTo(expectedSonarr);
actual.GetConfigsOfType(SupportedServices.Radarr)
actual.GetConfigsBasedOnSettings(MockSyncSettings.Radarr())
.Should().BeEquivalentTo(expectedRadarr);
}
@ -93,7 +88,7 @@ public class ConfigurationLoaderTest : IntegrationFixture
var configLoader = Resolve<ConfigurationLoader>();
var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr");
configs.GetConfigsOfType(SupportedServices.Sonarr)
configs.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr())
.Should().BeEquivalentTo(new List<SonarrConfiguration>
{
new()
@ -145,7 +140,7 @@ public class ConfigurationLoaderTest : IntegrationFixture
}
[Test, AutoMockData]
public void Do_not_throw_when_file_not_empty_but_has_no_desired_sections(ConfigurationLoader sut)
public void Throw_when_file_not_empty_but_has_no_desired_sections(ConfigurationLoader sut)
{
const string testYml = @"
not_wanted:
@ -156,6 +151,6 @@ not_wanted:
var act = () => sut.LoadFromStream(new StringReader(testYml), "fubar");
act.Should().NotThrow();
act.Should().Throw<EmptyYamlException>();
}
}

@ -1,5 +1,3 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TrashLib.Config.Secrets;
namespace Recyclarr.TrashLib.Tests.Config.Secrets;

@ -1,5 +1,4 @@
using FluentValidation.TestHelper;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.TestLibrary;

@ -1,8 +1,3 @@
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config.Settings;
using Recyclarr.TrashLib.Config.Yaml;
using Recyclarr.TrashLib.Startup;

@ -1,14 +1,9 @@
using System.Collections.ObjectModel;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Cache;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
using Recyclarr.TrashLib.TestLibrary;
using Serilog;
namespace Recyclarr.TrashLib.Tests.CustomFormat;
@ -31,7 +26,7 @@ public class CachePersisterTest
[TestCase(CustomFormatCache.LatestVersion - 1)]
[TestCase(CustomFormatCache.LatestVersion + 1)]
public void Set_loaded_cache_to_null_if_versions_mismatch(int versionToTest)
public void Throw_when_versions_mismatch(int versionToTest)
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
@ -41,9 +36,12 @@ public class CachePersisterTest
Version = versionToTest,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().BeNull();
var act = () => ctx.Persister.Load(config);
act.Should().Throw<CacheException>();
}
[Test]
@ -58,30 +56,20 @@ public class CachePersisterTest
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().NotBeNull();
var result = ctx.Persister.Load(config);
result.Should().NotBeNull();
}
[Test]
public void Cf_cache_is_valid_after_successful_load()
public void Cache_is_valid_after_successful_load()
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
var config = Substitute.For<IServiceConfiguration>();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().BeSameAs(testCfObj);
}
[Test]
public void Cf_cache_returns_null_if_not_loaded()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Load(config);
ctx.Persister.CfCache.Should().BeNull();
var result = ctx.Persister.Load(config);
result.Should().BeSameAs(testCfObj);
}
[Test]
@ -93,22 +81,12 @@ public class CachePersisterTest
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.Persister.Load(config);
ctx.Persister.Save(config);
var result = ctx.Persister.Load(config);
ctx.Persister.Save(config, result);
ctx.ServiceCache.Received().Save(testCfObj, config);
}
[Test]
public void Saving_without_loading_does_nothing()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Save(config);
ctx.ServiceCache.DidNotReceiveWithAnyArgs().Save(Arg.Any<object>(), default!);
}
[Test]
public void Updating_overwrites_previous_cf_cache_and_updates_cf_data()
{
@ -121,51 +99,40 @@ public class CachePersisterTest
TrashIdMappings = new Collection<TrashIdMapping> {new("trashid", "", 1)}
});
ctx.Persister.Load(config);
var result = ctx.Persister.Load(config);
// Update with new cached items
var customFormatData = new List<ProcessedCustomFormatData>
var customFormatData = new List<CustomFormatData>
{
NewCf.Processed("trashid", "name", 5)
NewCf.Data("trashid", "name", 5)
};
ctx.Persister.Update(customFormatData);
ctx.Persister.CfCache.Should().BeEquivalentTo(new CustomFormatCache
result = result.Update(customFormatData);
result.Should().BeEquivalentTo(new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping>
{
new(customFormatData[0].TrashId, customFormatData[0].Name, customFormatData[0].FormatId)
new(customFormatData[0].TrashId, customFormatData[0].Name, customFormatData[0].Id)
}
});
}
[Test]
public void Saving_skips_custom_formats_with_zero_id()
public void Cache_update_skips_custom_formats_with_zero_id()
{
var ctx = new Context();
// Update with new cached items
var customFormatData = new List<ProcessedCustomFormatData>
var customFormatData = new List<CustomFormatData>
{
NewCf.Processed("trashid1", "name", 5),
NewCf.Processed("trashid2", "invalid")
NewCf.Data("trashid1", "name", 5),
NewCf.Data("trashid2", "invalid")
};
ctx.Persister.Update(customFormatData);
ctx.Persister.CfCache.Should().BeEquivalentTo(new CustomFormatCache
var cache = new CustomFormatCache().Update(customFormatData);
cache.TrashIdMappings.Should().BeEquivalentTo(new Collection<TrashIdMapping>
{
TrashIdMappings = new Collection<TrashIdMapping>
{
new(customFormatData[0].TrashId, customFormatData[0].Name, customFormatData[0].FormatId)
}
new(customFormatData[0].TrashId, customFormatData[0].Name, customFormatData[0].Id)
});
}
[Test]
public void Updating_sets_cf_cache_without_loading()
{
var ctx = new Context();
ctx.Persister.Update(new List<ProcessedCustomFormatData>());
ctx.Persister.CfCache.Should().NotBeNull();
}
}

@ -1,10 +1,5 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Startup;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Guide;

@ -1,12 +1,8 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary.FluentAssertions;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Guide;
@ -30,32 +26,6 @@ public class CustomFormatLoaderTest : IntegrationFixture
{
NewCf.Data("first", "1") with {FileName = "first.json"},
NewCf.Data("second", "2") with {FileName = "second.json"}
});
}
[Test]
public void Trash_properties_are_removed()
{
Fs.AddFile("collection_of_cfs.md", new MockFileData(""));
Fs.AddFile("first.json", new MockFileData(@"
{
'name':'first',
'trash_id':'1',
'trash_foo': 'foo',
'trash_bar': 'bar',
'extra': 'e1'
}"));
var sut = Resolve<ICustomFormatLoader>();
var dir = Fs.CurrentDirectory();
var results = sut.LoadAllCustomFormatsAtPaths(
new[] {dir}, dir.File("collection_of_cfs.md"));
const string expectedExtraJson = @"{'name':'first','extra': 'e1'}";
results.Should()
.ContainSingle().Which.Json.Should()
.BeEquivalentTo(JObject.Parse(expectedExtraJson), op => op.Using(new JsonEquivalencyStep()));
}, o => o.Excluding(x => x.Type == typeof(JObject)));
}
}

@ -1,9 +1,4 @@
using AutoFixture.NUnit3;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.TestLibrary;
using Spectre.Console.Testing;

@ -1,15 +0,0 @@
{
"trash_id": "43bb5f09c79641e7a22e48d440bd8868",
"trash_score": 500,
"name": "Surround Sound",
"includeCustomFormatWhenRenaming": false,
"specifications": [{
"name": "dts\\-?(hd|x)|truehd|atmos|dd(\\+|p)(5|7)",
"implementation": "ReleaseTitleSpecification",
"negate": false,
"required": false,
"fields": {
"value": "dts\\-?(hd|x)|truehd|atmos|dd(\\+|p)(5|7)"
}
}]
}

@ -1,13 +0,0 @@
{
"name": "Surround Sound",
"includeCustomFormatWhenRenaming": false,
"specifications": [{
"name": "dts\\-?(hd|x)|truehd|atmos|dd(\\+|p)(5|7)",
"implementation": "ReleaseTitleSpecification",
"negate": false,
"required": false,
"fields": {
"value": "dts\\-?(hd|x)|truehd|atmos|dd(\\+|p)(5|7)"
}
}]
}

@ -1,15 +0,0 @@
{
"trash_id": "4eb3c272d48db8ab43c2c85283b69744",
"trash_score": 480,
"name": "DTS-HD/DTS:X",
"includeCustomFormatWhenRenaming": false,
"specifications": [{
"name": "dts.?(hd|es|x(?!\\d))",
"implementation": "ReleaseTitleSpecification",
"negate": false,
"required": false,
"fields": {
"value": "dts.?(hd|es|x(?!\\d))"
}
}]
}

@ -1,13 +0,0 @@
{
"name": "DTS-HD/DTS:X",
"includeCustomFormatWhenRenaming": false,
"specifications": [{
"name": "dts.?(hd|es|x(?!\\d))",
"implementation": "ReleaseTitleSpecification",
"negate": false,
"required": false,
"fields": {
"value": "dts.?(hd|es|x(?!\\d))"
}
}]
}

@ -1,4 +0,0 @@
{
"trash_id": "abc",
"name": "No Score"
}

@ -1,5 +0,0 @@
{
"trash_id": "xyz",
"trash_score": -100,
"name": "One that won't be in config"
}

@ -1,167 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Common;
using Recyclarr.TestLibrary.FluentAssertions;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Guide;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Processors;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.GuideSteps;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class GuideProcessorTest
{
private sealed class TestGuideProcessorSteps : IGuideProcessorSteps
{
public ICustomFormatStep CustomFormat { get; } = new CustomFormatStep();
public IConfigStep Config { get; } = new ConfigStep();
public IQualityProfileStep QualityProfile { get; } = new QualityProfileStep();
}
private sealed class Context
{
public Context()
{
Data = new ResourceDataReader(typeof(GuideProcessorTest), "Data");
}
public ResourceDataReader Data { get; }
public CustomFormatData ReadCustomFormat(string textFile)
{
var parser = new CustomFormatParser();
return parser.ParseCustomFormatData(ReadText(textFile), "");
}
public string ReadText(string textFile)
{
return Data.ReadData(textFile);
}
public JObject ReadJson(string jsonFile)
{
return JObject.Parse(ReadText(jsonFile));
}
}
[Test]
[SuppressMessage("Maintainability", "CA1506", Justification = "Designed to be a high-level integration test")]
public async Task Guide_processor_behaves_as_expected_with_normal_guide_data()
{
var ctx = new Context();
var guideService = Substitute.For<ICustomFormatGuideService>();
var guideProcessor = new GuideProcessor(new TestGuideProcessorSteps(), guideService);
// simulate guide data
guideService.GetCustomFormatData(default!).ReturnsForAnyArgs(new[]
{
ctx.ReadCustomFormat("ImportableCustomFormat1.json"),
ctx.ReadCustomFormat("ImportableCustomFormat2.json"),
ctx.ReadCustomFormat("NoScore.json"),
ctx.ReadCustomFormat("WontBeInConfig.json")
});
// Simulate user config in YAML
var config = new List<CustomFormatConfig>
{
new()
{
TrashIds = new List<string>
{
"43bb5f09c79641e7a22e48d440bd8868", // Surround SOUND
"4eb3c272d48db8ab43c2c85283b69744", // DTS-HD/DTS:X
"abc", // no score
"not in guide 1"
},
QualityProfiles = new List<QualityProfileScoreConfig>
{
new() {Name = "profile1"},
new() {Name = "profile2", Score = -1234}
}
},
new()
{
TrashIds = new List<string>
{
"abc", // no score
"not in guide 2"
},
QualityProfiles = new List<QualityProfileScoreConfig>
{
new() {Name = "profile3"},
new() {Name = "profile4", Score = 5678}
}
}
};
await guideProcessor.BuildGuideDataAsync(config, null, default!);
var expectedProcessedCustomFormatData = new List<ProcessedCustomFormatData>
{
NewCf.ProcessedWithScore("Surround Sound", "43bb5f09c79641e7a22e48d440bd8868", 500,
ctx.ReadJson("ImportableCustomFormat1_Processed.json")),
NewCf.ProcessedWithScore("DTS-HD/DTS:X", "4eb3c272d48db8ab43c2c85283b69744", 480,
ctx.ReadJson("ImportableCustomFormat2_Processed.json")),
NewCf.Processed("No Score", "abc")
};
guideProcessor.ProcessedCustomFormats.Should()
.BeEquivalentTo(expectedProcessedCustomFormatData, op => op.Using(new JsonEquivalencyStep()));
guideProcessor.ConfigData.Should()
.BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = expectedProcessedCustomFormatData,
QualityProfiles = config[0].QualityProfiles
},
new()
{
CustomFormats = expectedProcessedCustomFormatData.GetRange(2, 1),
QualityProfiles = config[1].QualityProfiles
}
}, op => op.Using(new JsonEquivalencyStep()));
guideProcessor.CustomFormatsWithoutScore.Should()
.Equal(new List<(string name, string trashId, string profileName)>
{
("No Score", "abc", "profile1"),
("No Score", "abc", "profile3")
});
guideProcessor.CustomFormatsNotInGuide.Should().Equal(new List<string>
{
"not in guide 1", "not in guide 2"
});
guideProcessor.ProfileScores.Should()
.BeEquivalentTo(new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{
"profile1", CfTestUtils.NewMapping(
new FormatMappingEntry(expectedProcessedCustomFormatData[0], 500),
new FormatMappingEntry(expectedProcessedCustomFormatData[1], 480))
},
{
"profile2", CfTestUtils.NewMapping(
new FormatMappingEntry(expectedProcessedCustomFormatData[0], -1234),
new FormatMappingEntry(expectedProcessedCustomFormatData[1], -1234),
new FormatMappingEntry(expectedProcessedCustomFormatData[2], -1234))
},
{
"profile4", CfTestUtils.NewMapping(
new FormatMappingEntry(expectedProcessedCustomFormatData[2], 5678))
}
}, op => op
.Using(new JsonEquivalencyStep())
.ComparingByMembers<FormatMappingEntry>());
}
}

@ -1,110 +0,0 @@
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.GuideSteps;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ConfigStepTest
{
[Test, AutoMockData]
public void Custom_formats_missing_from_config_are_skipped(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "id2")
};
var testConfig = new CustomFormatConfig[]
{
new()
{
TrashIds = new List<string> {"id1"}
}
};
processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEmpty();
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
}
}
}, op => op
.Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation))
.WhenTypeIs<JToken>());
}
[Test, AutoMockData]
public void Custom_formats_missing_from_guide_are_added_to_not_in_guide_list(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "id2")
};
var testConfig = new CustomFormatConfig[]
{
new()
{
TrashIds = new List<string> {"id1", "id3"}
}
};
processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEquivalentTo(new List<string> {"id3"}, op => op
.Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation))
.WhenTypeIs<JToken>());
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
}
}
}, op => op
.Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation))
.WhenTypeIs<JToken>());
}
[Test, AutoMockData]
public void Duplicate_config_trash_ids_are_ignored(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
};
var testConfig = new CustomFormatConfig[]
{
new() {TrashIds = new List<string> {"id1", "id1"}}
};
processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEmpty();
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData> {testProcessedCfs[0]}
}
});
}
}

@ -1,155 +0,0 @@
using System.Collections.ObjectModel;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TestLibrary.FluentAssertions;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.GuideSteps;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatStepTest
{
private sealed class Context
{
public List<CustomFormatData> TestGuideData { get; } = new()
{
NewCf.Data("name1", "id1"),
NewCf.Data("name2", "id2"),
NewCf.Data("name3", "id3")
};
}
[Test, AutoMockData]
public void Cfs_not_in_config_are_skipped(CustomFormatStep processor)
{
var ctx = new Context();
var testConfig = new List<CustomFormatConfig>
{
new() {TrashIds = new List<string> {"id1", "id3"}}
};
processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache());
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1"),
NewCf.Processed("name3", "id3")
});
}
[Test, AutoMockData]
public void Config_cfs_in_different_sections_are_processed(CustomFormatStep processor)
{
var ctx = new Context();
var testConfig = new List<CustomFormatConfig>
{
new() {TrashIds = new List<string> {"id1", "id3"}},
new() {TrashIds = new List<string> {"id2"}}
};
processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache());
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "id2"),
NewCf.Processed("name3", "id3")
},
op => op.Using(new JsonEquivalencyStep()));
}
[Test, AutoMockData]
public void Custom_format_is_deleted_if_in_config_and_cache_but_not_in_guide(CustomFormatStep processor)
{
var guideData = new List<CustomFormatData>
{
NewCf.Data("name1", "id1")
};
var testConfig = new List<CustomFormatConfig>
{
new() {TrashIds = new List<string> {"id1"}}
};
var testCache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("id1000", "", 1)}
};
processor.Process(guideData, testConfig, testCache);
processor.DeletedCustomFormatsInCache.Should()
.BeEquivalentTo(new[] {new TrashIdMapping("id1000", "", 1)});
processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
});
}
[Test, AutoMockData]
public void Custom_format_is_deleted_if_not_in_config_but_in_cache_and_in_guide(CustomFormatStep processor)
{
var cache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("id1", "", 9)}
};
var guideCfs = new List<CustomFormatData>
{
NewCf.Data("3D", "id1")
};
processor.Process(guideCfs, Array.Empty<CustomFormatConfig>(), cache);
processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(new[] {cache.TrashIdMappings[0]});
processor.ProcessedCustomFormats.Should().BeEmpty();
}
[Test, AutoMockData]
public void Match_custom_format_using_trash_id(CustomFormatStep processor)
{
var guideData = new List<CustomFormatData>
{
NewCf.Data("name1", "id1"),
NewCf.Data("name2", "id2")
};
var testConfig = new List<CustomFormatConfig>
{
new() {TrashIds = new List<string> {"id2"}}
};
processor.Process(guideData, testConfig, null);
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData>
{
NewCf.Processed("name2", "id2")
});
}
[Test, AutoMockData]
public void Non_existent_cfs_in_config_are_skipped(CustomFormatStep processor)
{
var ctx = new Context();
var testConfig = new List<CustomFormatConfig>
{
new() {TrashIds = new List<string> {"doesnt_exist"}}
};
processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache());
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should().BeEmpty();
}
}

@ -1,130 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.GuideSteps;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class QualityProfileStepTest
{
[Test]
public void No_score_used_if_no_score_in_config_or_guide()
{
var testConfigData = new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
},
QualityProfiles = new List<QualityProfileScoreConfig>
{
new() {Name = "profile1"}
}
}
};
var processor = new QualityProfileStep();
processor.Process(testConfigData);
processor.ProfileScores.Should().BeEmpty();
processor.CustomFormatsWithoutScore.Should().Equal(("name1", "id1", "profile1"));
}
[Test]
public void Overwrite_score_from_guide_if_config_defines_score()
{
var testConfigData = new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.Processed("", "id1", 100)
},
QualityProfiles = new List<QualityProfileScoreConfig>
{
new() {Name = "profile1", Score = 50}
}
}
};
var processor = new QualityProfileStep();
processor.Process(testConfigData);
processor.ProfileScores.Should()
.ContainKey("profile1").WhoseValue.Should()
.BeEquivalentTo(
CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 50)));
processor.CustomFormatsWithoutScore.Should().BeEmpty();
}
[Test]
public void Use_guide_score_if_no_score_in_config()
{
var testConfigData = new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.ProcessedWithScore("", "id1", 100)
},
QualityProfiles = new List<QualityProfileScoreConfig>
{
new() {Name = "profile1"},
new() {Name = "profile2", Score = null}
}
}
};
var processor = new QualityProfileStep();
processor.Process(testConfigData);
var expectedScoreEntries =
CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 100));
processor.ProfileScores.Should().BeEquivalentTo(
new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{"profile1", expectedScoreEntries},
{"profile2", expectedScoreEntries}
});
processor.CustomFormatsWithoutScore.Should().BeEmpty();
}
[Test]
public void Zero_score_is_not_ignored()
{
var testConfigData = new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.ProcessedWithScore("name1", "id1", 0)
},
QualityProfiles = new List<QualityProfileScoreConfig>
{
new() {Name = "profile1"}
}
}
};
var processor = new QualityProfileStep();
processor.Process(testConfigData);
processor.ProfileScores.Should()
.ContainKey("profile1").WhoseValue.Should()
.BeEquivalentTo(CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 0)));
processor.CustomFormatsWithoutScore.Should().BeEmpty();
}
}

@ -1,53 +0,0 @@
using System.Collections.ObjectModel;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Services.CustomFormat.Processors;
using Recyclarr.TrashLib.Services.Radarr.Config;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class PersistenceProcessorTest
{
[Test]
public async Task Custom_formats_are_deleted_if_deletion_option_is_enabled_in_config()
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var config = new RadarrConfiguration {DeleteOldCustomFormats = true};
var guideCfs = Array.Empty<ProcessedCustomFormatData>();
var deletedCfsInCache = new Collection<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, steps);
await processor.PersistCustomFormats(config, guideCfs, deletedCfsInCache, profileScores);
steps.JsonTransactionStep.Received().RecordDeletions(Arg.Is(deletedCfsInCache), Arg.Any<List<JObject>>());
}
[Test]
public async Task Custom_formats_are_not_deleted_if_deletion_option_is_disabled_in_config()
{
var steps = Substitute.For<IPersistenceProcessorSteps>();
var cfApi = Substitute.For<ICustomFormatService>();
var config = new RadarrConfiguration {DeleteOldCustomFormats = false};
var guideCfs = Array.Empty<ProcessedCustomFormatData>();
var deletedCfsInCache = Array.Empty<TrashIdMapping>();
var profileScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>();
var processor = new PersistenceProcessor(cfApi, steps);
await processor.PersistCustomFormats(config, guideCfs, deletedCfsInCache, profileScores);
steps.JsonTransactionStep.DidNotReceive()
.RecordDeletions(Arg.Any<IEnumerable<TrashIdMapping>>(), Arg.Any<List<JObject>>());
}
}

@ -1,43 +0,0 @@
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.PersistenceSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatApiPersistenceStepTest
{
private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId)
{
return NewCf.Processed(cfName, trashId, cfId);
}
[Test]
public async Task All_api_operations_behave_normally()
{
var transactions = new CustomFormatTransactionData();
transactions.NewCustomFormats.Add(QuickMakeCf("cfname1", "trashid1", 1));
transactions.UpdatedCustomFormats.Add(QuickMakeCf("cfname2", "trashid2", 2));
transactions.UnchangedCustomFormats.Add(QuickMakeCf("cfname3", "trashid3", 3));
transactions.DeletedCustomFormatIds.Add(new TrashIdMapping("trashid4", "cfname4", 4));
var api = Substitute.For<ICustomFormatService>();
var processor = new CustomFormatApiPersistenceStep(api);
var config = Substitute.For<IServiceConfiguration>();
await processor.Process(config, transactions);
Received.InOrder(() =>
{
api.CreateCustomFormat(config, transactions.NewCustomFormats.First());
api.UpdateCustomFormat(config, transactions.UpdatedCustomFormats.First());
api.DeleteCustomFormat(config, 4);
});
}
}

@ -1,359 +0,0 @@
using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TestLibrary.FluentAssertions;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using Recyclarr.TrashLib.TestLibrary;
/* Sample Custom Format response from Radarr API
{
"id": 1,
"name": "test",
"includeCustomFormatWhenRenaming": false,
"specifications": [
{
"name": "asdf",
"implementation": "ReleaseTitleSpecification",
"implementationName": "Release Title",
"infoLink": "https://wiki.servarr.com/Radarr_Settings#Custom_Formats_2",
"negate": false,
"required": false,
"fields": [
{
"order": 0,
"name": "value",
"label": "Regular Expression",
"value": "asdf",
"type": "textbox",
"advanced": false
}
]
}
]
}
*/
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.PersistenceSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class JsonTransactionStepTest
{
[Test, AutoMockData]
public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging(
JsonTransactionStep processor)
{
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(@"
[{
'id': 1,
'name': 'user_defined',
'specifications': [{
'name': 'spec1',
'negate': false,
'fields': [{
'name': 'value',
'value': 'value1'
}]
}]
}, {
'id': 2,
'name': 'updated',
'specifications': [{
'name': 'spec2',
'negate': false,
'fields': [{
'name': 'value',
'untouchable': 'field',
'value': 'value1'
}]
}]
}, {
'id': 3,
'name': 'no_change',
'specifications': [{
'name': 'spec4',
'negate': false,
'fields': [{
'name': 'value',
'value': 'value1'
}]
}]
}]")!;
var guideCfData = JsonConvert.DeserializeObject<List<JObject>>(@"
[{
'name': 'created',
'specifications': [{
'name': 'spec5',
'fields': {
'value': 'value2'
}
}]
}, {
'name': 'updated_different_name',
'specifications': [{
'name': 'spec2',
'negate': true,
'new_spec_field': 'new_spec_value',
'fields': {
'value': 'value2',
'new_field': 'new_value'
}
}, {
'name': 'new_spec',
'fields': {
'value': 'value3'
}
}]
}, {
'name': 'no_change',
'specifications': [{
'name': 'spec4',
'negate': false,
'fields': {
'value': 'value1'
}
}]
}]")!;
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("created", "id1", guideCfData[0]),
NewCf.Processed("updated_different_name", "id2", 2, guideCfData[1]),
NewCf.Processed("no_change", "id3", 3, guideCfData[2])
};
processor.Process(guideCfs, radarrCfs);
var expectedJson = new[]
{
@"{
'name': 'created',
'specifications': [{
'name': 'spec5',
'fields': [{
'name': 'value',
'value': 'value2'
}]
}]
}",
@"{
'id': 2,
'name': 'updated_different_name',
'specifications': [{
'name': 'spec2',
'negate': true,
'new_spec_field': 'new_spec_value',
'fields': [{
'name': 'value',
'untouchable': 'field',
'value': 'value2',
'new_field': 'new_value'
}]
}, {
'name': 'new_spec',
'fields': [{
'name': 'value',
'value': 'value3'
}]
}]
}",
@"{
'id': 3,
'name': 'no_change',
'specifications': [{
'name': 'spec4',
'negate': false,
'fields': [{
'name': 'value',
'value': 'value1'
}]
}]
}"
};
var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.NewCustomFormats.Add(guideCfs[0]);
expectedTransactions.UpdatedCustomFormats.Add(guideCfs[1]);
expectedTransactions.UnchangedCustomFormats.Add(guideCfs[2]);
processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
processor.Transactions.NewCustomFormats.First().Json.Should()
.BeEquivalentTo(JObject.Parse(expectedJson[0]), op => op.Using(new JsonEquivalencyStep()));
processor.Transactions.UpdatedCustomFormats.First().Json.Should()
.BeEquivalentTo(JObject.Parse(expectedJson[1]), op => op.Using(new JsonEquivalencyStep()));
processor.Transactions.UnchangedCustomFormats.First().Json.Should()
.BeEquivalentTo(JObject.Parse(expectedJson[2]), op => op.Using(new JsonEquivalencyStep()));
processor.Transactions.ConflictingCustomFormats.Should().BeEmpty();
}
[Test, AutoMockData]
public void Deletes_happen_before_updates(
JsonTransactionStep processor)
{
const string radarrCfData = @"[{
'id': 1,
'name': 'updated',
'specifications': [{
'name': 'spec1',
'fields': [{
'name': 'value',
'value': 'value1'
}]
}]
}, {
'id': 2,
'name': 'deleted',
'specifications': [{
'name': 'spec2',
'negate': false,
'fields': [{
'name': 'value',
'untouchable': 'field',
'value': 'value1'
}]
}]
}]";
var guideCfData = JObject.Parse(@"{
'name': 'updated',
'specifications': [{
'name': 'spec2',
'fields': {
'value': 'value2'
}
}]
}");
var deletedCfsInCache = new List<TrashIdMapping>
{
new("", "", 1) {CustomFormatId = 2}
};
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("updated", "", 1, guideCfData)
};
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
processor.Process(guideCfs, radarrCfs!);
processor.RecordDeletions(deletedCfsInCache, radarrCfs!);
const string expectedJson = @"{
'id': 1,
'name': 'updated',
'specifications': [{
'name': 'spec2',
'fields': [{
'name': 'value',
'value': 'value2'
}]
}]
}";
var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", "", 2));
expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]);
processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
processor.Transactions.UpdatedCustomFormats.First().Json.Should()
.BeEquivalentTo(JObject.Parse(expectedJson), op => op.Using(new JsonEquivalencyStep()));
}
[Test, AutoMockData]
public void Only_delete_correct_cfs(
JsonTransactionStep processor)
{
const string radarrCfData = @"[{
'id': 1,
'name': 'not_deleted',
'specifications': [{
'name': 'spec1',
'negate': false,
'fields': [{
'name': 'value',
'value': 'value1'
}]
}]
}, {
'id': 2,
'name': 'deleted',
'specifications': [{
'name': 'spec2',
'negate': false,
'fields': [{
'name': 'value',
'untouchable': 'field',
'value': 'value1'
}]
}]
}]";
var deletedCfsInCache = new List<TrashIdMapping>
{
new("testtrashid", "", 2),
new("", "", 3)
};
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
processor.RecordDeletions(deletedCfsInCache, radarrCfs!);
var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "", 2));
processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
}
[Test, AutoMockData]
public void Conflicting_ids_detected(
JsonTransactionStep processor)
{
const string serviceCfData = @"
[{
'id': 1,
'name': 'first'
}, {
'id': 2,
'name': 'second'
}]";
var serviceCfs = JsonConvert.DeserializeObject<List<JObject>>(serviceCfData)!;
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("first", "", 2)
};
processor.Process(guideCfs, serviceCfs);
var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.ConflictingCustomFormats.Add(new ConflictingCustomFormat(guideCfs[0], 1));
processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
}
[Test, AutoMockData]
public void Service_cf_id_set_when_no_cache_entry(JsonTransactionStep processor)
{
const string serviceCfData = @"
[{
'id': 1,
'name': 'first'
}]";
var serviceCfs = JsonConvert.DeserializeObject<List<JObject>>(serviceCfData)!;
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("first", "")
};
processor.Process(guideCfs, serviceCfs);
processor.Transactions.UpdatedCustomFormats.Should().BeEquivalentTo(
new[] {NewCf.Processed("first", "", 1)},
o => o.Including(x => x.FormatId));
}
}

@ -1,257 +0,0 @@
using FluentAssertions;
using FluentAssertions.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TestLibrary.NSubstitute;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Services.CustomFormat.Api;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.CustomFormat.Processors.PersistenceSteps;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class QualityProfileApiPersistenceStepTest
{
[Test]
public async Task Do_not_invoke_api_if_no_scores_to_update()
{
const string radarrQualityProfileData = @"[{
'name': 'profile1',
'formatItems': [{
'format': 1,
'name': 'cf1',
'score': 1
},
{
'format': 2,
'name': 'cf2',
'score': 0
},
{
'format': 3,
'name': 'cf3',
'score': 3
}
],
'id': 1
}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{
"profile1", CfTestUtils.NewMapping(new FormatMappingEntry(NewCf.Processed("", "", 4), 100))
}
};
var processor = new QualityProfileApiPersistenceStep(api);
await processor.Process(Substitute.For<IServiceConfiguration>(), cfScores);
await api.DidNotReceiveWithAnyArgs().UpdateQualityProfile(default!, default!, default!);
}
[Test]
public async Task Invalid_quality_profile_names_are_reported()
{
const string radarrQualityProfileData = @"[{'name': 'profile1'}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{"wrong_profile_name", CfTestUtils.NewMapping()}
};
var processor = new QualityProfileApiPersistenceStep(api);
await processor.Process(Substitute.For<IServiceConfiguration>(), cfScores);
processor.InvalidProfileNames.Should().Equal("wrong_profile_name");
processor.UpdatedScores.Should().BeEmpty();
}
[Test]
public async Task Reset_scores_for_unmatched_cfs_if_enabled()
{
const string radarrQualityProfileData = @"[{
'name': 'profile1',
'formatItems': [{
'format': 1,
'name': 'cf1',
'score': 1
},
{
'format': 2,
'name': 'cf2',
'score': 50
},
{
'format': 3,
'name': 'cf3',
'score': 3
}
],
'id': 1
}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{
"profile1", CfTestUtils.NewMappingWithReset(
new FormatMappingEntry(NewCf.Processed("", "", 2), 100))
}
};
var processor = new QualityProfileApiPersistenceStep(api);
await processor.Process(Substitute.For<IServiceConfiguration>(), cfScores);
processor.InvalidProfileNames.Should().BeEmpty();
processor.UpdatedScores.Should()
.ContainKey("profile1").WhoseValue.Should()
.BeEquivalentTo(new List<UpdatedFormatScore>
{
new("cf1", 0, FormatScoreUpdateReason.Reset),
new("cf2", 100, FormatScoreUpdateReason.Updated),
new("cf3", 0, FormatScoreUpdateReason.Reset)
});
await api.Received().UpdateQualityProfile(
Arg.Any<IServiceConfiguration>(),
Verify.That<JObject>(j => j["formatItems"]!.Children().Should().HaveCount(3)),
Arg.Any<int>());
}
[Test]
public async Task Scores_are_set_in_quality_profile()
{
const string radarrQualityProfileData = @"[{
'name': 'profile1',
'upgradeAllowed': false,
'cutoff': 20,
'items': [{
'quality': {
'id': 10,
'name': 'Raw-HD',
'source': 'tv',
'resolution': 1080,
'modifier': 'rawhd'
},
'items': [],
'allowed': false
}
],
'minFormatScore': 0,
'cutoffFormatScore': 0,
'formatItems': [{
'format': 4,
'name': '3D',
'score': 0
},
{
'format': 3,
'name': 'BR-DISK',
'score': 0
},
{
'format': 1,
'name': 'asdf2',
'score': 0
}
],
'language': {
'id': 1,
'name': 'English'
},
'id': 1
}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles(default!)!.ReturnsForAnyArgs(
JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{
"profile1", CfTestUtils.NewMapping(
// First match by ID
new FormatMappingEntry(NewCf.Processed("", "", 4), 100),
// Should NOT match because we do not use names to assign scores
new FormatMappingEntry(NewCf.Processed("", ""), 101),
// Second match by ID
new FormatMappingEntry(NewCf.Processed("", "", 1), 102))
}
};
var processor = new QualityProfileApiPersistenceStep(api);
var config = Substitute.For<IServiceConfiguration>();
await processor.Process(config, cfScores);
var expectedProfileJson = JObject.Parse(@"{
'name': 'profile1',
'upgradeAllowed': false,
'cutoff': 20,
'items': [{
'quality': {
'id': 10,
'name': 'Raw-HD',
'source': 'tv',
'resolution': 1080,
'modifier': 'rawhd'
},
'items': [],
'allowed': false
}
],
'minFormatScore': 0,
'cutoffFormatScore': 0,
'formatItems': [{
'format': 4,
'name': '3D',
'score': 100
},
{
'format': 3,
'name': 'BR-DISK',
'score': 0
},
{
'format': 1,
'name': 'asdf2',
'score': 102
}
],
'language': {
'id': 1,
'name': 'English'
},
'id': 1
}");
await api.Received()
.UpdateQualityProfile(
config,
Verify.That<JObject>(a => a.Should().BeEquivalentTo(expectedProfileJson)),
1);
processor.InvalidProfileNames.Should().BeEmpty();
processor.UpdatedScores.Should()
.ContainKey("profile1").WhoseValue.Should()
.BeEquivalentTo(new List<UpdatedFormatScore>
{
new("3D", 100, FormatScoreUpdateReason.Updated),
new("asdf2", 102, FormatScoreUpdateReason.Updated)
});
}
}

@ -1,6 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using NUnit.Framework;
namespace Recyclarr.TrashLib.Tests;

@ -0,0 +1,3 @@
// Global using directives
global using Recyclarr.TestLibrary.AutoFixture;

@ -0,0 +1,116 @@
using Newtonsoft.Json.Linq;
using Recyclarr.Common.Extensions;
using Recyclarr.TrashLib.Json;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.Tests.Pipelines.CustomFormat.Guide;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatParserTest
{
[Test, AutoMockData]
public void Deserialize_works(CustomFormatParser sut)
{
var jsonData = @"
{
'trash_id': '90cedc1fea7ea5d11298bebd3d1d3223',
'trash_score': '-10000',
'name': 'EVO (no WEBDL)',
'includeCustomFormatWhenRenaming': false,
'specifications': [
{
'name': 'EVO',
'implementation': 'ReleaseTitleSpecification',
'negate': false,
'required': true,
'fields': [{
'value': '\\bEVO(TGX)?\\b'
}]
},
{
'name': 'WEBDL',
'implementation': 'SourceSpecification',
'negate': true,
'required': true,
'fields': {
'value': 7
}
},
{
'name': 'WEBRIP',
'implementation': 'SourceSpecification',
'negate': true,
'required': true,
'fields': {
'value': 8
}
}
]
}";
var result = sut.ParseCustomFormatData(jsonData, "file.json");
result.Should().BeEquivalentTo(new CustomFormatData
{
FileName = "file.json",
TrashId = "90cedc1fea7ea5d11298bebd3d1d3223",
TrashScore = -10000,
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 7
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
});
}
[Test, AutoMockData]
public void Serialize_skips_trash_properties(CustomFormatParser sut)
{
var cf = new CustomFormatData
{
FileName = "file.json",
TrashId = "90cedc1fea7ea5d11298bebd3d1d3223",
TrashScore = -10000,
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false
};
var json = JObject.FromObject(cf, ServiceJsonSerializerFactory.Create());
json.Children<JProperty>().Should().NotContain(x => x.Name.ContainsIgnoreCase("trash"));
}
}

@ -0,0 +1,371 @@
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.Tests.Pipelines.CustomFormat.Models;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatDataComparerTest
{
[Test]
public void Custom_formats_equal()
{
var a = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 7
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
};
var b = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 7
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
};
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeTrue();
}
[Test]
public void Custom_formats_not_equal_when_field_value_different()
{
var a = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 7
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
};
var b = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 10 // this is different
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
};
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeFalse();
}
[Test]
public void Equal_when_ignored_fields_are_different()
{
var a = new CustomFormatData
{
FileName = "file1.json",
TrashId = "a",
TrashScore = 1,
Category = "one"
};
var b = new CustomFormatData
{
FileName = "file2.json",
TrashId = "b",
TrashScore = 2,
Category = "two"
};
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeTrue();
}
[Test]
public void Not_equal_when_right_is_null()
{
var a = new CustomFormatData();
var b = (CustomFormatData?) null;
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeFalse();
}
[Test]
public void Not_equal_when_left_is_null()
{
var a = (CustomFormatData?) null;
var b = new CustomFormatData();
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeFalse();
}
[Test]
public void Equal_for_same_reference()
{
var a = new CustomFormatData();
var result = CustomFormatData.Comparer.Equals(a, a);
result.Should().BeTrue();
}
[Test]
public void Not_equal_when_different_spec_count()
{
var a = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData(),
new CustomFormatSpecificationData()
}
};
var b = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData(),
new CustomFormatSpecificationData(),
new CustomFormatSpecificationData()
}
};
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeFalse();
}
[Test]
public void Not_equal_when_non_matching_spec_names()
{
var a = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 7
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
};
var b = new CustomFormatData
{
Name = "EVO (no WEBDL)",
IncludeCustomFormatWhenRenaming = false,
Specifications = new[]
{
new CustomFormatSpecificationData
{
Name = "EVO",
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
},
new CustomFormatSpecificationData
{
Name = "WEBDL",
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 7
}
},
new CustomFormatSpecificationData
{
Name = "WEBRIP2", // This name is different
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
{
Value = 8
}
}
}
};
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeFalse();
}
}

@ -0,0 +1,77 @@
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.Pipelines.CustomFormat.PipelinePhases;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.Pipelines.CustomFormat.PipelinePhases;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatConfigPhaseTest
{
[Test, AutoMockData]
public void Return_configs_that_exist_in_guide(
[Frozen] ICustomFormatGuideService guide,
CustomFormatConfigPhase sut)
{
guide.GetCustomFormatData(default!).ReturnsForAnyArgs(new[]
{
NewCf.Data("one", "cf1"),
NewCf.Data("two", "cf2")
});
var config = new TestConfig
{
CustomFormats = new List<CustomFormatConfig>
{
new()
{
TrashIds = new List<string>
{
"cf1",
"cf2"
}
}
}
};
var result = sut.Execute(config);
result.Should().BeEquivalentTo(new[]
{
NewCf.Data("one", "cf1"),
NewCf.Data("two", "cf2")
});
}
[Test, AutoMockData]
public void Skip_configs_that_do_not_exist_in_guide(
[Frozen] ICustomFormatGuideService guide,
CustomFormatConfigPhase sut)
{
guide.GetCustomFormatData(default!).ReturnsForAnyArgs(new[]
{
NewCf.Data("", "cf4")
});
var config = new TestConfig
{
CustomFormats = new List<CustomFormatConfig>
{
new()
{
TrashIds = new List<string>
{
"cf1",
"cf2",
"cf3"
}
}
}
};
var result = sut.Execute(config);
result.Should().BeEmpty();
}
}

@ -0,0 +1,413 @@
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TrashLib.Pipelines.CustomFormat;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
using Recyclarr.TrashLib.Pipelines.CustomFormat.PipelinePhases;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.Pipelines.CustomFormat.PipelinePhases;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatTransactionPhaseTest : IntegrationFixture
{
[Test]
public void Add_new_cf()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
NewCf.Data("one", "cf1")
};
var serviceData = Array.Empty<CustomFormatData>();
var cache = new CustomFormatCache();
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
NewCustomFormats =
{
NewCf.Data("one", "cf1")
}
});
}
[Test]
public void Update_cf_by_matching_name()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
new CustomFormatData
{
Name = "one",
TrashId = "cf1",
// Only set the below value to make it different from the service CF
IncludeCustomFormatWhenRenaming = true
}
};
var serviceData = new[]
{
new CustomFormatData {Name = "one"}
};
var cache = new CustomFormatCache();
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
UpdatedCustomFormats =
{
new CustomFormatData
{
Name = "one",
IncludeCustomFormatWhenRenaming = true
}
}
});
}
[Test]
public void Update_cf_by_matching_id_different_names()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
new CustomFormatData
{
Name = "different1",
TrashId = "cf1",
// Only set the below value to make it different from the service CF
IncludeCustomFormatWhenRenaming = true
}
};
var serviceData = new[]
{
new CustomFormatData {Name = "different2", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
new TrashIdMapping("cf1", "", 2)
}
};
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
UpdatedCustomFormats =
{
new CustomFormatData
{
Name = "different1",
Id = 2,
IncludeCustomFormatWhenRenaming = true
}
}
});
}
[Test]
public void Update_cf_by_matching_id_same_names()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
new CustomFormatData
{
Name = "different1",
TrashId = "cf1",
// Only set the below value to make it different from the service CF
IncludeCustomFormatWhenRenaming = true
}
};
var serviceData = new[]
{
new CustomFormatData {Name = "different1", Id = 2}
};
var cache = new CustomFormatCache();
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
UpdatedCustomFormats =
{
new CustomFormatData
{
Name = "different1",
Id = 2,
IncludeCustomFormatWhenRenaming = true
}
}
});
}
[Test]
public void Conflicting_cf_when_new_cf_has_name_of_existing()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
NewCf.Data("one", "cf1")
};
var serviceData = new[]
{
new CustomFormatData {Name = "one", Id = 2},
new CustomFormatData {Name = "two", Id = 1}
};
var cache = new CustomFormatCache();
var config = new TestConfig
{
ReplaceExistingCustomFormats = false
};
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
ConflictingCustomFormats =
{
new ConflictingCustomFormat(guideCfs[0], 2)
}
});
}
[Test]
public void Conflicting_cf_when_cached_cf_has_name_of_existing()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
NewCf.Data("one", "cf1")
};
var serviceData = new[]
{
new CustomFormatData {Name = "one", Id = 2},
new CustomFormatData {Name = "two", Id = 1}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
new TrashIdMapping("cf1", "one", 1)
}
};
var config = new TestConfig
{
ReplaceExistingCustomFormats = false
};
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
ConflictingCustomFormats =
{
new ConflictingCustomFormat(guideCfs[0], 2)
}
});
}
[Test]
public void Updated_cf_with_matching_name_and_id()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
new CustomFormatData
{
Name = "one",
TrashId = "cf1",
// Only set the below value to make it different from the service CF
IncludeCustomFormatWhenRenaming = true
}
};
var serviceData = new[]
{
new CustomFormatData {Name = "two", Id = 2},
new CustomFormatData {Name = "one", Id = 1}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
new TrashIdMapping("cf1", "one", 1)
}
};
var config = new TestConfig
{
ReplaceExistingCustomFormats = false
};
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
UpdatedCustomFormats =
{
NewCf.Data("one", "cf1", 1) with {IncludeCustomFormatWhenRenaming = true}
}
});
}
[Test]
public void Unchanged_cfs_with_replace()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
NewCf.Data("one", "cf1")
};
var serviceData = new[]
{
new CustomFormatData {Name = "one", Id = 1}
};
var cache = new CustomFormatCache();
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
UnchangedCustomFormats = {guideCfs[0]}
});
}
[Test]
public void Unchanged_cfs_without_replace()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
NewCf.Data("one", "cf1")
};
var serviceData = new[]
{
new CustomFormatData {Name = "one", Id = 1}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
new TrashIdMapping("cf1", "one", 1)
}
};
var config = new TestConfig
{
ReplaceExistingCustomFormats = false
};
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
UnchangedCustomFormats = {guideCfs[0]}
});
}
[Test]
public void Deleted_cfs()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = Array.Empty<CustomFormatData>();
var serviceData = new[]
{
new CustomFormatData {Name = "two", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
new TrashIdMapping("cf2", "two", 2)
}
};
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.Should().BeEquivalentTo(new CustomFormatTransactionData
{
DeletedCustomFormats =
{
new TrashIdMapping("cf2", "two", 2)
}
});
}
[Test]
public void Do_not_delete_cfs_in_config()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var guideCfs = new[]
{
NewCf.Data("two", "cf2", 2)
};
var serviceData = new[]
{
new CustomFormatData {Name = "two", Id = 2}
};
var cache = new CustomFormatCache
{
TrashIdMappings = new[]
{
new TrashIdMapping("cf2", "two", 2)
}
};
var config = new TestConfig();
var result = sut.Execute(config, guideCfs, serviceData, cache);
result.DeletedCustomFormats.Should().BeEmpty();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save