feat: Merge branch 'pipeline-design' into master

pull/201/head
Robert Dailey 1 year ago
commit 7c6afd4f11

@ -17,6 +17,7 @@ csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = false
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
csharp_prefer_braces = true:none
csharp_preserve_single_line_blocks = true
csharp_space_after_cast = true
@ -325,13 +326,21 @@ resharper_csharp_align_multiple_declaration = false
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 120
resharper_csharp_naming_rule.constants = AaBb
resharper_csharp_naming_rule.enum_member = AaBb
resharper_csharp_naming_rule.local_constants = aaBb
resharper_csharp_naming_rule.method_property_event = AaBb
resharper_csharp_naming_rule.other = AaBb
resharper_csharp_naming_rule.private_constants = AaBb
resharper_csharp_naming_rule.private_static_fields = _ + aaBb
resharper_csharp_naming_rule.private_static_readonly = AaBb
resharper_csharp_naming_rule.static_readonly = AaBb
resharper_csharp_prefer_qualified_reference = false
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
@ -434,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
@ -502,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
@ -733,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

1
.gitattributes vendored

@ -6,3 +6,4 @@ Dockerfile eol=lf
# Ignore whitespace in these files
*.sln -whitespace
*.DotSettings -whitespace

@ -8,11 +8,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- New `replace_existing_custom_formats` property that can be set to `false` to disallow updates to
existing CFs that Recyclarr never created in the first place. The default is `true`.
- New `quality_profiles` section supported for specifying information about quality profiles. For
now, this section doesn't do much, but paves the way for quality profile syncing.
### Changed
- Log files are restructured. They are now under `logs/cli`.
- Log files are split. There is now a `verbose.log` and `debug.log` for every run. The time stamps
between the two will be identical.
(in the file name) between the two will be identical.
### Deprecated
- `replace_existing_custom_formats` must be explicitly specified, otherwise you will get a
deprecation warning. In a future release, the default will change from `true` to `false`. To
prepare for that, users must explicitly state what behavior they want to avoid unwanted behavior
in the future.
- `reset_unmatched_scores` is being moved to the `quality_profiles` section; a deprecation message
will be logged until it is moved.
### Fixed
- Deleted custom formats are now included in the log message showing the count of CFs synced.
## [4.3.0] - 2023-01-22

@ -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,
@ -92,11 +97,6 @@
"score": {
"type": "integer",
"description": "A positive or negative number representing the score to apply to *all* custom formats listed in the trash_ids list."
},
"reset_unmatched_scores": {
"type": "boolean",
"description": "If set to true, enables setting scores to 0 in quality profiles where either a CF was not mentioned in the trash_ids array or it was in that list but did not get a score (e.g. no score in guide).",
"default": false
}
}
}
@ -120,6 +120,25 @@
}
}
},
"quality_profiles": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"reset_unmatched_scores": {
"type": "boolean",
"description": "If set to true, enables setting scores to 0 in quality profiles where either a CF was not mentioned in the trash_ids array or it was in that list but did not get a score (e.g. no score in guide).",
"default": false
}
}
}
},
"radarr_instance": {
"type": "object",
"additionalProperties": false,
@ -136,14 +155,20 @@
"api_key": {
"type": "string",
"minLength": 1,
"description": "The API key from Radarr."
"description": "The API key from Radarr"
},
"quality_definition": {
"$ref": "#/$defs/quality_definition"
},
"quality_profiles": {
"$ref": "#/$defs/quality_profiles"
},
"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"
}
@ -170,9 +195,15 @@
"quality_definition": {
"$ref": "#/$defs/quality_definition"
},
"quality_profiles": {
"$ref": "#/$defs/quality_profiles"
},
"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,20 +1,14 @@
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
using System.Reactive.Linq;
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.Config.Services;
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;
@ -43,12 +37,11 @@ public abstract class IntegrationFixture : IDisposable
builder.RegisterMockFor<IGitRepository>();
builder.RegisterMockFor<IGitRepositoryFactory>();
builder.RegisterMockFor<IServiceConfiguration>();
builder.RegisterMockFor<IEnvironment>();
builder.RegisterMockFor<IServiceInformation>(m =>
{
// By default, choose some extremely high number so that all the newest features are enabled.
m.Version.Returns(_ => Observable.Return(new Version("99.0.0.0")));
m.GetVersion(default!).ReturnsForAnyArgs(_ => new Version("99.0.0.0"));
});
RegisterExtraTypes(builder);
@ -77,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,49 +1,33 @@
using Autofac;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Cli.Console.Helpers;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.Cli.Tests.Console.Helpers;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CacheStoragePathTest : IntegrationFixture
public class CacheStoragePathTest
{
[Test]
public void Use_guid_when_no_name()
[Test, AutoMockData]
public void Use_guid_when_no_name(CacheStoragePath sut)
{
var config = Substitute.ForPartsOf<ServiceConfiguration>();
config.BaseUrl = new Uri("http://something");
config.InstanceName = null;
using var scope = Container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).AsImplementedInterfaces();
});
var sut = scope.Resolve<CacheStoragePath>();
var result = sut.CalculatePath("obj");
var result = sut.CalculatePath(config, "obj");
result.FullName.Should().MatchRegex(@".*[/\\][a-f0-9]+[/\\]obj\.json$");
}
[Test]
public void Use_name_when_not_null()
[Test, AutoMockData]
public void Use_name_when_not_null(CacheStoragePath sut)
{
var config = Substitute.ForPartsOf<ServiceConfiguration>();
config.BaseUrl = new Uri("http://something");
config.InstanceName = "thename";
using var scope = Container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).AsImplementedInterfaces();
});
var sut = scope.Resolve<CacheStoragePath>();
var result = sut.CalculatePath("obj");
var result = sut.CalculatePath(config, "obj");
result.FullName.Should().MatchRegex(@".*[/\\]thename_[a-f0-9]+[/\\]obj\.json$");
}

@ -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,23 +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.Common;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.Processors;
using Recyclarr.TrashLib.Services.Radarr;
using Recyclarr.TrashLib.Services.Sonarr;
using Recyclarr.TrashLib.Services.System;
using Recyclarr.TrashLib.Startup;
using Spectre.Console.Cli;
@ -36,14 +40,11 @@ public static class CompositionRoot
RegisterAppPaths(builder);
RegisterLogger(builder);
builder.RegisterModule<SonarrAutofacModule>();
builder.RegisterModule<RadarrAutofacModule>();
builder.RegisterModule<VersionControlAutofacModule>();
builder.RegisterModule<MigrationAutofacModule>();
builder.RegisterModule<RepoAutofacModule>();
builder.RegisterModule<CustomFormatAutofacModule>();
builder.RegisterModule<GuideServicesAutofacModule>();
builder.RegisterModule<SystemServiceAutofacModule>();
builder.RegisterModule<CompatibilityAutofacModule>();
builder.RegisterModule<ApiServicesAutofacModule>();
builder.RegisterModule(new ConfigAutofacModule(assemblies));
builder.RegisterModule<ServiceProcessorsAutofacModule>();
builder.RegisterModule(new CommonAutofacModule(Assembly.GetExecutingAssembly()));
@ -56,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;

@ -1,11 +1,10 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Autofac.Features.Indexed;
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.Common;
using Spectre.Console.Cli;
#pragma warning disable CS8765
@ -16,8 +15,7 @@ namespace Recyclarr.Cli.Console.Commands;
[Description("List custom formats in the guide for a particular service.")]
internal class ListCustomFormatsCommand : AsyncCommand<ListCustomFormatsCommand.CliSettings>
{
private readonly IGuideDataLister _lister;
private readonly IIndex<SupportedServices, IGuideService> _guideService;
private readonly CustomFormatDataLister _lister;
private readonly IRepoUpdater _repoUpdater;
[UsedImplicitly]
@ -31,20 +29,17 @@ internal class ListCustomFormatsCommand : AsyncCommand<ListCustomFormatsCommand.
}
public ListCustomFormatsCommand(
IGuideDataLister lister,
IIndex<SupportedServices, IGuideService> guideService,
CustomFormatDataLister lister,
IRepoUpdater repoUpdater)
{
_lister = lister;
_guideService = guideService;
_repoUpdater = repoUpdater;
}
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
{
await _repoUpdater.UpdateRepo();
var guideService = _guideService[settings.Service];
_lister.ListCustomFormats(guideService.GetCustomFormatData());
_lister.ListCustomFormats(settings.Service);
return 0;
}
}

@ -1,11 +1,10 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Autofac.Features.Indexed;
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.Common;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;
@ -15,8 +14,7 @@ namespace Recyclarr.Cli.Console.Commands;
[Description("List quality definitions in the guide for a particular service.")]
internal class ListQualitiesCommand : AsyncCommand<ListQualitiesCommand.CliSettings>
{
private readonly IGuideDataLister _lister;
private readonly IIndex<SupportedServices, IGuideService> _guideService;
private readonly QualitySizeDataLister _lister;
private readonly IRepoUpdater _repoUpdater;
[UsedImplicitly]
@ -29,21 +27,16 @@ internal class ListQualitiesCommand : AsyncCommand<ListQualitiesCommand.CliSetti
public required SupportedServices Service { get; init; }
}
public ListQualitiesCommand(
IGuideDataLister lister,
IIndex<SupportedServices, IGuideService> guideService,
IRepoUpdater repoUpdater)
public ListQualitiesCommand(QualitySizeDataLister lister, IRepoUpdater repoUpdater)
{
_lister = lister;
_guideService = guideService;
_repoUpdater = repoUpdater;
}
public override async Task<int> ExecuteAsync(CommandContext context, CliSettings settings)
{
await _repoUpdater.UpdateRepo();
var guideService = _guideService[settings.Service];
_lister.ListQualities(guideService.GetQualities());
_lister.ListQualities(settings.Service);
return 0;
}
}

@ -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.Sonarr;
using Spectre.Console.Cli;
#pragma warning disable CS8765
@ -14,7 +14,7 @@ namespace Recyclarr.Cli.Console.Commands;
internal class ListReleaseProfilesCommand : AsyncCommand<ListReleaseProfilesCommand.CliSettings>
{
private readonly ILogger _log;
private readonly ISonarrGuideDataLister _lister;
private readonly ReleaseProfileDataLister _lister;
private readonly IRepoUpdater _repoUpdater;
[UsedImplicitly]
@ -31,7 +31,7 @@ internal class ListReleaseProfilesCommand : AsyncCommand<ListReleaseProfilesComm
public ListReleaseProfilesCommand(
ILogger log,
ISonarrGuideDataLister lister,
ReleaseProfileDataLister lister,
IRepoUpdater repoUpdater)
{
_log = log;

@ -5,9 +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.Processors;
using Recyclarr.TrashLib.Services.Radarr;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;
@ -17,7 +18,8 @@ namespace Recyclarr.Cli.Console.Commands;
internal class RadarrCommand : AsyncCommand<RadarrCommand.CliSettings>
{
private readonly ILogger _log;
private readonly IRadarrGuideDataLister _lister;
private readonly CustomFormatDataLister _cfLister;
private readonly QualitySizeDataLister _qualityLister;
private readonly IMigrationExecutor _migration;
private readonly IRepoUpdater _repoUpdater;
private readonly ISyncProcessor _syncProcessor;
@ -56,13 +58,15 @@ internal class RadarrCommand : AsyncCommand<RadarrCommand.CliSettings>
public RadarrCommand(
ILogger log,
IRadarrGuideDataLister lister,
CustomFormatDataLister cfLister,
QualitySizeDataLister qualityLister,
IMigrationExecutor migration,
IRepoUpdater repoUpdater,
ISyncProcessor syncProcessor)
{
_log = log;
_lister = lister;
_cfLister = cfLister;
_qualityLister = qualityLister;
_migration = migration;
_repoUpdater = repoUpdater;
_syncProcessor = syncProcessor;
@ -77,14 +81,14 @@ internal class RadarrCommand : AsyncCommand<RadarrCommand.CliSettings>
if (settings.ListCustomFormats)
{
_log.Warning("The `radarr` subcommand is DEPRECATED -- Use `list custom-formats radarr` instead!");
_lister.ListCustomFormats();
_cfLister.ListCustomFormats(SupportedServices.Radarr);
return 0;
}
if (settings.ListQualities)
{
_log.Warning("The `radarr` subcommand is DEPRECATED -- Use `list qualities radarr` instead!");
_lister.ListQualities();
_qualityLister.ListQualities(SupportedServices.Radarr);
return 0;
}

@ -5,9 +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.Processors;
using Recyclarr.TrashLib.Services.Sonarr;
using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Commands;
@ -17,7 +19,9 @@ namespace Recyclarr.Cli.Console.Commands;
internal class SonarrCommand : AsyncCommand<SonarrCommand.CliSettings>
{
private readonly ILogger _log;
private readonly ISonarrGuideDataLister _lister;
private readonly CustomFormatDataLister _cfLister;
private readonly QualitySizeDataLister _qualityLister;
private readonly ReleaseProfileDataLister _rpLister;
private readonly IMigrationExecutor _migration;
private readonly IRepoUpdater _repoUpdater;
private readonly ISyncProcessor _syncProcessor;
@ -70,13 +74,17 @@ internal class SonarrCommand : AsyncCommand<SonarrCommand.CliSettings>
public SonarrCommand(
ILogger log,
ISonarrGuideDataLister lister,
CustomFormatDataLister cfLister,
QualitySizeDataLister qualityLister,
ReleaseProfileDataLister rpLister,
IMigrationExecutor migration,
IRepoUpdater repoUpdater,
ISyncProcessor syncProcessor)
{
_log = log;
_lister = lister;
_cfLister = cfLister;
_qualityLister = qualityLister;
_rpLister = rpLister;
_migration = migration;
_repoUpdater = repoUpdater;
_syncProcessor = syncProcessor;
@ -91,27 +99,27 @@ internal class SonarrCommand : AsyncCommand<SonarrCommand.CliSettings>
if (settings.ListCustomFormats)
{
_log.Warning("The `sonarr` subcommand is DEPRECATED -- Use `list custom-formats sonarr` instead!");
_lister.ListCustomFormats();
_cfLister.ListCustomFormats(SupportedServices.Sonarr);
return 0;
}
if (settings.ListQualities)
{
_log.Warning("The `sonarr` subcommand is DEPRECATED -- Use `list qualities sonarr` instead!");
_lister.ListQualities();
_qualityLister.ListQualities(SupportedServices.Sonarr);
return 0;
}
if (settings.ListReleaseProfiles)
{
_log.Warning("The `sonarr` subcommand is DEPRECATED -- Use `list release-profiles` instead!");
_lister.ListReleaseProfiles();
_rpLister.ListReleaseProfiles();
return 0;
}
if (settings.ListTerms is not null)
{
_lister.ListTerms(settings.ListTerms);
_rpLister.ListTerms(settings.ListTerms);
return 0;
}

@ -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;

@ -11,39 +11,36 @@ namespace Recyclarr.Cli.Console.Helpers;
public class CacheStoragePath : ICacheStoragePath
{
private readonly IAppPaths _paths;
private readonly IServiceConfiguration _config;
private readonly IFNV1a _hash;
public CacheStoragePath(
IAppPaths paths,
IServiceConfiguration config)
IAppPaths paths)
{
_paths = paths;
_config = config;
_hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32));
}
private string BuildUniqueServiceDir()
private string BuildUniqueServiceDir(IServiceConfiguration config)
{
// In the future, once array-style configurations are removed, the service name will no longer be optional
// and the below condition can be removed and the logic simplified.
var dirName = new StringBuilder();
if (_config.InstanceName is not null)
if (config.InstanceName is not null)
{
dirName.Append($"{_config.InstanceName}_");
dirName.Append($"{config.InstanceName}_");
}
var url = _config.BaseUrl.OriginalString;
var url = config.BaseUrl.OriginalString;
var guid = _hash.ComputeHash(Encoding.ASCII.GetBytes(url)).AsHexString();
dirName.Append(guid);
return dirName.ToString();
}
public IFileInfo CalculatePath(string cacheObjectName)
public IFileInfo CalculatePath(IServiceConfiguration config, string cacheObjectName)
{
return _paths.CacheDirectory
.SubDirectory(_config.ServiceName.ToLower(CultureInfo.CurrentCulture))
.SubDirectory(BuildUniqueServiceDir())
.SubDirectory(config.ServiceType.ToString().ToLower(CultureInfo.CurrentCulture))
.SubDirectory(BuildUniqueServiceDir(config))
.File(cacheObjectName + ".json");
}
}

@ -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>

@ -3,6 +3,9 @@
@namespace Recyclarr.Gui.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@* ReSharper disable Html.PathError *@
<!--suppress ALL -->
<!DOCTYPE html>
<html lang="en">
<head>

@ -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,4 +1,5 @@
using JetBrains.Annotations;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Services;
namespace Recyclarr.TrashLib.TestLibrary;
@ -6,5 +7,5 @@ namespace Recyclarr.TrashLib.TestLibrary;
[UsedImplicitly]
public class TestConfig : ServiceConfiguration
{
public override string ServiceName => "Test";
public override SupportedServices ServiceType => SupportedServices.Sonarr;
}

@ -1,12 +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.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.Tests.Cache;
@ -34,9 +29,10 @@ public class ServiceCacheTest
[Test, AutoMockData]
public void Load_returns_null_when_file_does_not_exist(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
IServiceConfiguration config,
ServiceCache sut)
{
var result = sut.Load<ObjectWithAttribute>();
var result = sut.Load<ObjectWithAttribute>(config);
result.Should().BeNull();
}
@ -44,6 +40,7 @@ public class ServiceCacheTest
public void Loading_with_attribute_parses_correctly(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
const string testJson = @"{'test_value': 'Foo'}";
@ -51,18 +48,20 @@ public class ServiceCacheTest
const string testJsonPath = "cacheFile.json";
fs.AddFile(testJsonPath, new MockFileData(testJson));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
var obj = sut.Load<ObjectWithAttribute>();
var obj = sut.Load<ObjectWithAttribute>(config);
obj.Should().NotBeNull();
obj!.TestValue.Should().Be("Foo");
}
[Test, AutoMockData]
public void Loading_with_invalid_object_name_throws(ServiceCache sut)
public void Loading_with_invalid_object_name_throws(
IServiceConfiguration config,
ServiceCache sut)
{
Action act = () => sut.Load<ObjectWithAttributeInvalidChars>();
Action act = () => sut.Load<ObjectWithAttributeInvalidChars>(config);
act.Should()
.Throw<ArgumentException>()
@ -70,9 +69,11 @@ public class ServiceCacheTest
}
[Test, AutoMockData]
public void Loading_without_attribute_throws(ServiceCache sut)
public void Loading_without_attribute_throws(
IServiceConfiguration config,
ServiceCache sut)
{
Action act = () => sut.Load<ObjectWithoutAttribute>();
Action act = () => sut.Load<ObjectWithoutAttribute>(config);
act.Should()
.Throw<ArgumentException>()
@ -83,15 +84,17 @@ public class ServiceCacheTest
public void Properties_are_saved_using_snake_case(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
storage.CalculatePath(default!).ReturnsForAnyArgs(_ => fs.FileInfo.New($"{ValidObjectName}.json"));
storage.CalculatePath(default!, default!)
.ReturnsForAnyArgs(_ => fs.FileInfo.New($"{ValidObjectName}.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
fs.AllFiles.Should().ContainMatch($"*{ValidObjectName}.json");
var file = fs.GetFile(storage.CalculatePath("").FullName);
var file = fs.GetFile(storage.CalculatePath(config, "").FullName);
file.Should().NotBeNull();
file.TextContents.Should().Contain("\"test_value\"");
}
@ -100,12 +103,13 @@ public class ServiceCacheTest
public void Saving_with_attribute_parses_correctly(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
const string testJsonPath = "cacheFile.json";
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
var expectedFile = fs.GetFile(testJsonPath);
expectedFile.Should().NotBeNull();
@ -115,9 +119,11 @@ public class ServiceCacheTest
}
[Test, AutoMockData]
public void Saving_with_invalid_object_name_throws(ServiceCache sut)
public void Saving_with_invalid_object_name_throws(
IServiceConfiguration config,
ServiceCache sut)
{
var act = () => sut.Save(new ObjectWithAttributeInvalidChars());
var act = () => sut.Save(new ObjectWithAttributeInvalidChars(), config);
act.Should()
.Throw<ArgumentException>()
@ -125,9 +131,11 @@ public class ServiceCacheTest
}
[Test, AutoMockData]
public void Saving_without_attribute_throws(ServiceCache sut)
public void Saving_without_attribute_throws(
IServiceConfiguration config,
ServiceCache sut)
{
var act = () => sut.Save(new ObjectWithoutAttribute());
var act = () => sut.Save(new ObjectWithoutAttribute(), config);
act.Should()
.Throw<ArgumentException>()
@ -138,13 +146,14 @@ public class ServiceCacheTest
public void Switching_config_and_base_url_should_yield_different_cache_paths(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("Foo.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("Foo.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("Bar.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Bar"});
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("Bar.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Bar"}, config);
var expectedFiles = new[] {"*Foo.json", "*Bar.json"};
foreach (var expectedFile in expectedFiles)
@ -157,12 +166,13 @@ public class ServiceCacheTest
public void When_cache_file_is_empty_do_not_throw(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
fs.AddFile("cacheFile.json", new MockFileData(""));
Action act = () => sut.Load<ObjectWithAttribute>();
Action act = () => sut.Load<ObjectWithAttribute>(config);
act.Should().NotThrow();
}
@ -171,6 +181,7 @@ public class ServiceCacheTest
public void Name_properties_are_set_on_load(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
[Frozen] ICacheStoragePath storage,
IServiceConfiguration config,
ServiceCache sut)
{
const string cacheJson = @"
@ -187,11 +198,11 @@ public class ServiceCacheTest
";
fs.AddFile("cacheFile.json", new MockFileData(cacheJson));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
var result = sut.Load<CustomFormatCache>();
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,9 +1,10 @@
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TrashLib.Config;
using Recyclarr.TrashLib.Config.Parsing;
using Recyclarr.TrashLib.Services.Radarr.Config;
using Recyclarr.TrashLib.Services.Sonarr.Config;
using Recyclarr.TrashLib.Config.Services;
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;
@ -14,17 +15,41 @@ public class ConfigRegistryTest
[Test]
public void Get_configs_by_type()
{
var configs = new[]
var configs = new IServiceConfiguration[]
{
new SonarrConfiguration {InstanceName = "one"},
new SonarrConfiguration {InstanceName = "two"}
new SonarrConfiguration {InstanceName = "two"},
new RadarrConfiguration {InstanceName = "three"}
};
var sut = new ConfigRegistry();
sut.Add(SupportedServices.Sonarr, configs[0]);
sut.Add(SupportedServices.Sonarr, configs[1]);
foreach (var c in configs)
{
sut.Add(c);
}
var result = sut.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
var result = sut.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr);
result.Should().Equal(configs.Take(2));
}
[Test]
public void Null_service_type_returns_configs_of_all_types()
{
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.AnyService());
result.Should().Equal(configs);
}
@ -33,10 +58,53 @@ public class ConfigRegistryTest
public void Get_empty_collection_when_no_configs_of_type()
{
var sut = new ConfigRegistry();
sut.Add(SupportedServices.Sonarr, new SonarrConfiguration());
sut.Add(new SonarrConfiguration());
var result = sut.GetConfigsOfType<RadarrConfiguration>(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,12 +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.Services.Sonarr.Config;
using Recyclarr.TrashLib.TestLibrary;
using YamlDotNet.Core;
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
@ -33,7 +29,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -58,7 +54,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -85,7 +81,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -112,7 +108,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -137,7 +133,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr);
var config = configCollection.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr());
config.Should().BeEquivalentTo(new[]
{
new
@ -161,7 +157,7 @@ sonarr:
var configCollection = sut.LoadFromStream(new StringReader(testYml));
var config = configCollection.GetConfigsOfType<SonarrConfiguration>(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<SonarrConfiguration>(SupportedServices.Sonarr)
parsedSecret.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr())
.Should().BeEquivalentTo(expected, o => o.Excluding(x => x.LineNumber));
}

@ -1,21 +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.Radarr.Config;
using Recyclarr.TrashLib.Services.Sonarr.Config;
using Recyclarr.TrashLib.TestLibrary;
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
@ -81,10 +75,10 @@ public class ConfigurationLoaderTest : IntegrationFixture
var loader = Resolve<IConfigurationLoader>();
var actual = loader.LoadMany(fileData.Select(x => x.Item1));
actual.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr)
actual.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr())
.Should().BeEquivalentTo(expectedSonarr);
actual.GetConfigsOfType<RadarrConfiguration>(SupportedServices.Radarr)
actual.GetConfigsBasedOnSettings(MockSyncSettings.Radarr())
.Should().BeEquivalentTo(expectedRadarr);
}
@ -94,7 +88,7 @@ public class ConfigurationLoaderTest : IntegrationFixture
var configLoader = Resolve<ConfigurationLoader>();
var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr");
configs.GetConfigsOfType<SonarrConfiguration>(SupportedServices.Sonarr)
configs.GetConfigsBasedOnSettings(MockSyncSettings.Sonarr())
.Should().BeEquivalentTo(new List<SonarrConfiguration>
{
new()
@ -146,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:
@ -157,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,13 +1,9 @@
using System.Collections.ObjectModel;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.TrashLib.Cache;
using Recyclarr.TrashLib.Services.CustomFormat;
using Recyclarr.TrashLib.Services.CustomFormat.Models;
using Recyclarr.TrashLib.Services.CustomFormat.Models.Cache;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Cache;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
using Recyclarr.TrashLib.TestLibrary;
using Serilog;
namespace Recyclarr.TrashLib.Tests.CustomFormat;
@ -30,52 +26,50 @@ 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>();
var testCfObj = new CustomFormatCache
{
Version = versionToTest,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.CfCache.Should().BeNull();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
var act = () => ctx.Persister.Load(config);
act.Should().Throw<CacheException>();
}
[Test]
public void Accept_loaded_cache_when_versions_match()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCache
{
Version = CustomFormatCache.LatestVersion,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)}
};
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load();
ctx.Persister.CfCache.Should().NotBeNull();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
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();
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Load();
ctx.Persister.CfCache.Should().BeSameAs(testCfObj);
}
[Test]
public void Cf_cache_returns_null_if_not_loaded()
{
var ctx = new Context();
ctx.Persister.Load();
ctx.Persister.CfCache.Should().BeNull();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
var result = ctx.Persister.Load(config);
result.Should().BeSameAs(testCfObj);
}
[Test]
@ -83,77 +77,62 @@ public class CachePersisterTest
{
var ctx = new Context();
var testCfObj = new CustomFormatCache();
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
var config = Substitute.For<IServiceConfiguration>();
ctx.Persister.Load();
ctx.Persister.Save();
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(testCfObj);
ctx.ServiceCache.Received().Save(Arg.Is(testCfObj));
}
var result = ctx.Persister.Load(config);
ctx.Persister.Save(config, result);
[Test]
public void Saving_without_loading_does_nothing()
{
var ctx = new Context();
ctx.Persister.Save();
ctx.ServiceCache.DidNotReceive().Save(Arg.Any<object>());
ctx.ServiceCache.Received().Save(testCfObj, config);
}
[Test]
public void Updating_overwrites_previous_cf_cache_and_updates_cf_data()
{
var ctx = new Context();
var config = Substitute.For<IServiceConfiguration>();
// Load initial CfCache just to test that it gets replaced
ctx.ServiceCache.Load<CustomFormatCache>().Returns(new CustomFormatCache
ctx.ServiceCache.Load<CustomFormatCache>(config).Returns(new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("trashid", "", 1)}
});
ctx.Persister.Load();
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,8 +1,4 @@
using AutoFixture.NUnit3;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary.AutoFixture;
using Recyclarr.TrashLib.Services.Common;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Guide;
using Recyclarr.TrashLib.TestLibrary;
using Spectre.Console.Testing;
@ -15,7 +11,8 @@ public class GuideDataListerTest
[Test, AutoMockData]
public void Custom_formats_appear_in_console_output(
[Frozen(Matching.ImplementedInterfaces)] TestConsole console,
GuideDataLister sut)
[Frozen] ICustomFormatGuideService guide,
CustomFormatDataLister sut)
{
var testData = new[]
{
@ -23,7 +20,9 @@ public class GuideDataListerTest
NewCf.Data("Second", "456")
};
sut.ListCustomFormats(testData);
guide.GetCustomFormatData(default!).ReturnsForAnyArgs(testData);
sut.ListCustomFormats(default!);
console.Output.Should().ContainAll(
testData.SelectMany(x => new[] {x.Name, x.TrashId}));

@ -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,168 +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.Services.Radarr;
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<RadarrGuideService>();
var guideProcessor = new GuideProcessor(new TestGuideProcessorSteps());
// simulate guide data
guideService.GetCustomFormatData().Returns(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, guideService);
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,55 +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 qpApi = Substitute.For<IQualityProfileService>();
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, qpApi, config, steps);
await processor.PersistCustomFormats(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 qpApi = Substitute.For<IQualityProfileService>();
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, qpApi, config, steps);
await processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
steps.JsonTransactionStep.DidNotReceive()
.RecordDeletions(Arg.Any<IEnumerable<TrashIdMapping>>(), Arg.Any<List<JObject>>());
}
}

@ -1,41 +0,0 @@
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.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();
await processor.Process(api, transactions);
Received.InOrder(() =>
{
api.CreateCustomFormat(transactions.NewCustomFormats.First());
api.UpdateCustomFormat(transactions.UpdatedCustomFormats.First());
api.DeleteCustomFormat(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,247 +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.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()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{
"profile1", CfTestUtils.NewMapping(new FormatMappingEntry(NewCf.Processed("", "", 4), 100))
}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, cfScores);
await api.DidNotReceive().UpdateQualityProfile(Arg.Any<JObject>(), Arg.Any<int>());
}
[Test]
public async Task Invalid_quality_profile_names_are_reported()
{
const string radarrQualityProfileData = @"[{'name': 'profile1'}]";
var api = Substitute.For<IQualityProfileService>();
api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{"wrong_profile_name", CfTestUtils.NewMapping()}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, 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()!.Returns(JsonConvert.DeserializeObject<List<JObject>>(radarrQualityProfileData));
var cfScores = new Dictionary<string, QualityProfileCustomFormatScoreMapping>
{
{
"profile1", CfTestUtils.NewMappingWithReset(
new FormatMappingEntry(NewCf.Processed("", "", 2), 100))
}
};
var processor = new QualityProfileApiPersistenceStep();
await processor.Process(api, 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(
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()!.Returns(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();
await processor.Process(api, 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(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;

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

Loading…
Cancel
Save