refactor: Utilize lifetime scopes for operating on instances

To simplify logic in the system, a child lifetime scope is created for
each distinct configuration instance that is processed by Recyclarr. The
main driver for this is to avoid objects being reused between instances
and thus needing setup & teardown logic to deal with state.
Robert Dailey 4 months ago
parent 1bc91b63a3
commit 0467bada74

@ -7,17 +7,17 @@ using Recyclarr.Platform;
namespace Recyclarr.Cli.Cache;
public class CacheStoragePath(IAppPaths paths) : ICacheStoragePath
public class CacheStoragePath(IAppPaths paths, IServiceConfiguration config) : ICacheStoragePath
{
private readonly IFNV1a _hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(64));
private string BuildUniqueServiceDir(IServiceConfiguration config)
private string BuildUniqueServiceDir()
{
var url = config.BaseUrl.OriginalString;
return _hash.ComputeHash(Encoding.ASCII.GetBytes(url)).AsHexString();
}
private IFileInfo CalculatePathInternal(IServiceConfiguration config, string cacheObjectName, string serviceDir)
private IFileInfo CalculatePathInternal(string cacheObjectName, string serviceDir)
{
return paths.CacheDirectory
.SubDirectory(config.ServiceType.ToString().ToLower(CultureInfo.CurrentCulture))
@ -25,8 +25,8 @@ public class CacheStoragePath(IAppPaths paths) : ICacheStoragePath
.File(cacheObjectName + ".json");
}
public IFileInfo CalculatePath(IServiceConfiguration config, string cacheObjectName)
public IFileInfo CalculatePath(string cacheObjectName)
{
return CalculatePathInternal(config, cacheObjectName, BuildUniqueServiceDir(config));
return CalculatePathInternal(cacheObjectName, BuildUniqueServiceDir());
}
}

@ -1,9 +1,8 @@
using System.IO.Abstractions;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Cache;
public interface ICacheStoragePath
{
IFileInfo CalculatePath(IServiceConfiguration config, string cacheObjectName);
IFileInfo CalculatePath(string cacheObjectName);
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Cache;
public interface IServiceCache
{
T? Load<T>(IServiceConfiguration config) where T : class;
void Save<T>(T obj, IServiceConfiguration config) where T : class;
T? Load<T>() where T : class;
void Save<T>(T obj) where T : class;
}

@ -3,7 +3,6 @@ using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
using Recyclarr.Common.Extensions;
using Recyclarr.Config.Models;
using Recyclarr.Json;
namespace Recyclarr.Cli.Cache;
@ -12,9 +11,9 @@ public partial class ServiceCache(ICacheStoragePath storagePath, ILogger log) :
{
private readonly JsonSerializerOptions _jsonSettings = GlobalJsonSerializerSettings.Recyclarr;
public T? Load<T>(IServiceConfiguration config) where T : class
public T? Load<T>() where T : class
{
var path = PathFromAttribute<T>(config);
var path = PathFromAttribute<T>();
log.Debug("Loading cache from path: {Path}", path.FullName);
if (!path.Exists)
{
@ -35,9 +34,9 @@ public partial class ServiceCache(ICacheStoragePath storagePath, ILogger log) :
return null;
}
public void Save<T>(T obj, IServiceConfiguration config) where T : class
public void Save<T>(T obj) where T : class
{
var path = PathFromAttribute<T>(config);
var path = PathFromAttribute<T>();
log.Debug("Saving cache to path: {Path}", path.FullName);
path.CreateParentDirectory();
@ -56,7 +55,7 @@ public partial class ServiceCache(ICacheStoragePath storagePath, ILogger log) :
return attribute.Name;
}
private IFileInfo PathFromAttribute<T>(IServiceConfiguration config)
private IFileInfo PathFromAttribute<T>()
{
var objectName = GetCacheObjectNameAttribute<T>();
if (!AllowedObjectNameCharactersRegex().IsMatch(objectName))
@ -64,7 +63,7 @@ public partial class ServiceCache(ICacheStoragePath storagePath, ILogger log) :
throw new ArgumentException($"Object name '{objectName}' has unacceptable characters");
}
return storagePath.CalculatePath(config, objectName);
return storagePath.CalculatePath(objectName);
}
[GeneratedRegex(@"^[\w-]+$", RegexOptions.None, 1000)]

@ -4,13 +4,16 @@ using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
public class CustomFormatCachePersister(ILogger log, IServiceCache serviceCache) : ICustomFormatCachePersister
public class CustomFormatCachePersister(
ILogger log,
IServiceCache serviceCache,
IServiceConfiguration config) : ICustomFormatCachePersister
{
public const int LatestVersion = 1;
public CustomFormatCache Load(IServiceConfiguration config)
public CustomFormatCache Load()
{
var cacheData = serviceCache.Load<CustomFormatCacheData>(config);
var cacheData = serviceCache.Load<CustomFormatCacheData>();
if (cacheData == null)
{
log.Debug("Custom format cache does not exist; proceeding without it");
@ -29,10 +32,10 @@ public class CustomFormatCachePersister(ILogger log, IServiceCache serviceCache)
return new CustomFormatCache(cacheData.TrashIdMappings);
}
public void Save(IServiceConfiguration config, CustomFormatCache cache)
public void Save(CustomFormatCache cache)
{
var data = new CustomFormatCacheData(LatestVersion, config.InstanceName, cache.Mappings);
log.Debug("Saving Custom Format Cache with {Mappings}", JsonSerializer.Serialize(data.TrashIdMappings));
serviceCache.Save(data, config);
serviceCache.Save(data);
}
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.CustomFormat.Cache;
public interface ICustomFormatCachePersister
{
CustomFormatCache Load(IServiceConfiguration config);
void Save(IServiceConfiguration config, CustomFormatCache cache);
CustomFormatCache Load();
void Save(CustomFormatCache cache);
}

@ -1,6 +1,5 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Common.Extensions;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.CustomFormat;
namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
@ -8,9 +7,9 @@ namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
public class CustomFormatApiFetchPhase(ICustomFormatApiService api)
: IApiFetchPipelinePhase<CustomFormatPipelineContext>
{
public async Task Execute(CustomFormatPipelineContext context, IServiceConfiguration config)
public async Task Execute(CustomFormatPipelineContext context)
{
var result = await api.GetCustomFormats(config);
var result = await api.GetCustomFormats();
context.ApiFetchOutput.AddRange(result);
context.Cache.RemoveStale(result);
}

@ -1,6 +1,5 @@
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.CustomFormat;
namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
@ -8,13 +7,13 @@ namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
public class CustomFormatApiPersistencePhase(ICustomFormatApiService api, ICustomFormatCachePersister cachePersister)
: IApiPersistencePipelinePhase<CustomFormatPipelineContext>
{
public async Task Execute(CustomFormatPipelineContext context, IServiceConfiguration config)
public async Task Execute(CustomFormatPipelineContext context)
{
var transactions = context.TransactionOutput;
foreach (var cf in transactions.NewCustomFormats)
{
var response = await api.CreateCustomFormat(config, cf);
var response = await api.CreateCustomFormat(cf);
if (response is not null)
{
cf.Id = response.Id;
@ -23,15 +22,15 @@ public class CustomFormatApiPersistencePhase(ICustomFormatApiService api, ICusto
foreach (var dto in transactions.UpdatedCustomFormats)
{
await api.UpdateCustomFormat(config, dto);
await api.UpdateCustomFormat(dto);
}
foreach (var map in transactions.DeletedCustomFormats)
{
await api.DeleteCustomFormat(config, map.CustomFormatId);
await api.DeleteCustomFormat(map.CustomFormatId);
}
context.Cache.Update(transactions);
cachePersister.Save(config, context.Cache);
cachePersister.Save(context.Cache);
}
}

@ -10,10 +10,11 @@ namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
public class CustomFormatConfigPhase(
ICustomFormatGuideService guide,
ProcessedCustomFormatCache cache,
ICustomFormatCachePersister cachePersister)
ICustomFormatCachePersister cachePersister,
IServiceConfiguration config)
: IConfigPipelinePhase<CustomFormatPipelineContext>
{
public Task Execute(CustomFormatPipelineContext context, IServiceConfiguration config)
public Task Execute(CustomFormatPipelineContext context)
{
// Match custom formats in the YAML config to those in the guide, by Trash ID
//
@ -33,7 +34,7 @@ public class CustomFormatConfigPhase(
context.InvalidFormats = processedCfs[false].Select(x => x.Id).ToList();
context.ConfigOutput.AddRange(processedCfs[true].SelectMany(x => x.CustomFormats));
context.Cache = cachePersister.Load(config);
context.Cache = cachePersister.Load();
cache.AddCustomFormats(context.ConfigOutput);
return Task.CompletedTask;

@ -6,9 +6,10 @@ using Recyclarr.TrashGuide.CustomFormat;
namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
public class CustomFormatTransactionPhase(ILogger log) : ITransactionPipelinePhase<CustomFormatPipelineContext>
public class CustomFormatTransactionPhase(ILogger log, IServiceConfiguration config)
: ITransactionPipelinePhase<CustomFormatPipelineContext>
{
public void Execute(CustomFormatPipelineContext context, IServiceConfiguration config)
public void Execute(CustomFormatPipelineContext context)
{
var transactions = new CustomFormatTransactionData();
@ -21,7 +22,7 @@ public class CustomFormatTransactionPhase(ILogger log) : ITransactionPipelinePha
var serviceCf = FindServiceCfByName(context.ApiFetchOutput, guideCf.Name);
if (serviceCf is not null)
{
ProcessExistingCf(config, guideCf, serviceCf, transactions);
ProcessExistingCf(guideCf, serviceCf, transactions);
continue;
}
@ -52,7 +53,6 @@ public class CustomFormatTransactionPhase(ILogger log) : ITransactionPipelinePha
}
private void ProcessExistingCf(
IServiceConfiguration config,
CustomFormatData guideCf,
CustomFormatData serviceCf,
CustomFormatTransactionData transactions)

@ -3,10 +3,14 @@ using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.Generic;
public class GenericSyncPipeline<TContext>(ILogger log, GenericPipelinePhases<TContext> phases) : ISyncPipeline
public class GenericSyncPipeline<TContext>(
ILogger log,
GenericPipelinePhases<TContext> phases,
IServiceConfiguration config
) : ISyncPipeline
where TContext : IPipelineContext, new()
{
public async Task Execute(ISyncSettings settings, IServiceConfiguration config)
public async Task Execute(ISyncSettings settings)
{
var context = new TContext();
if (!context.SupportedServiceTypes.Contains(config.ServiceType))
@ -16,14 +20,14 @@ public class GenericSyncPipeline<TContext>(ILogger log, GenericPipelinePhases<TC
return;
}
await phases.ConfigPhase.Execute(context, config);
await phases.ConfigPhase.Execute(context);
if (phases.LogPhase.LogConfigPhaseAndExitIfNeeded(context))
{
return;
}
await phases.ApiFetchPhase.Execute(context, config);
phases.TransactionPhase.Execute(context, config);
await phases.ApiFetchPhase.Execute(context);
phases.TransactionPhase.Execute(context);
phases.LogPhase.LogTransactionNotices(context);
@ -33,7 +37,7 @@ public class GenericSyncPipeline<TContext>(ILogger log, GenericPipelinePhases<TC
return;
}
await phases.ApiPersistencePhase.Execute(context, config);
await phases.ApiPersistencePhase.Execute(context);
phases.LogPhase.LogPersistenceResults(context);
}
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.Generic;
public interface IApiFetchPipelinePhase<in TContext>
where TContext : IPipelineContext
{
Task Execute(TContext context, IServiceConfiguration config);
Task Execute(TContext context);
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.Generic;
public interface IApiPersistencePipelinePhase<in TContext>
where TContext : IPipelineContext
{
Task Execute(TContext context, IServiceConfiguration config);
Task Execute(TContext context);
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.Generic;
public interface IConfigPipelinePhase<in TContext>
where TContext : IPipelineContext
{
Task Execute(TContext context, IServiceConfiguration config);
Task Execute(TContext context);
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines.Generic;
public interface ITransactionPipelinePhase<in TContext>
where TContext : IPipelineContext
{
void Execute(TContext context, IServiceConfiguration config);
void Execute(TContext context);
}

@ -1,9 +1,8 @@
using Recyclarr.Cli.Console.Settings;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Pipelines;
public interface ISyncPipeline
{
public Task Execute(ISyncSettings settings, IServiceConfiguration config);
public Task Execute(ISyncSettings settings);
}

@ -1,4 +1,3 @@
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
using Recyclarr.TrashGuide.MediaNaming;
@ -6,8 +5,5 @@ namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
public interface IServiceBasedMediaNamingConfigPhase
{
Task<MediaNamingDto> ProcessNaming(
IServiceConfiguration config,
IMediaNamingGuideService guide,
NamingFormatLookup lookup);
Task<MediaNamingDto> ProcessNaming(IMediaNamingGuideService guide, NamingFormatLookup lookup);
}

@ -4,12 +4,9 @@ using Recyclarr.TrashGuide.MediaNaming;
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
public class RadarrMediaNamingConfigPhase : ServiceBasedMediaNamingConfigPhase<RadarrConfiguration>
public class RadarrMediaNamingConfigPhase(RadarrConfiguration config) : IServiceBasedMediaNamingConfigPhase
{
protected override Task<MediaNamingDto> ProcessNaming(
RadarrConfiguration config,
IMediaNamingGuideService guide,
NamingFormatLookup lookup)
public Task<MediaNamingDto> ProcessNaming(IMediaNamingGuideService guide, NamingFormatLookup lookup)
{
var guideData = guide.GetRadarrNamingData();
var configData = config.MediaNaming;

@ -1,22 +0,0 @@
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
using Recyclarr.TrashGuide.MediaNaming;
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
public abstract class ServiceBasedMediaNamingConfigPhase<TConfig> : IServiceBasedMediaNamingConfigPhase
where TConfig : IServiceConfiguration
{
public Task<MediaNamingDto> ProcessNaming(
IServiceConfiguration config,
IMediaNamingGuideService guide,
NamingFormatLookup lookup)
{
return ProcessNaming((TConfig) config, guide, lookup);
}
protected abstract Task<MediaNamingDto> ProcessNaming(
TConfig config,
IMediaNamingGuideService guide,
NamingFormatLookup lookup);
}

@ -4,16 +4,13 @@ using Recyclarr.TrashGuide.MediaNaming;
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
public class SonarrMediaNamingConfigPhase : ServiceBasedMediaNamingConfigPhase<SonarrConfiguration>
public class SonarrMediaNamingConfigPhase(SonarrConfiguration config) : IServiceBasedMediaNamingConfigPhase
{
protected override Task<MediaNamingDto> ProcessNaming(
SonarrConfiguration config,
IMediaNamingGuideService guide,
NamingFormatLookup lookup)
public Task<MediaNamingDto> ProcessNaming(IMediaNamingGuideService guide, NamingFormatLookup lookup)
{
var guideData = guide.GetSonarrNamingData();
var configData = config.MediaNaming;
var keySuffix = ":4";
const string keySuffix = ":4";
return Task.FromResult<MediaNamingDto>(new SonarrMediaNamingDto
{

@ -1,13 +1,12 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
public class MediaNamingApiFetchPhase(IMediaNamingApiService api) : IApiFetchPipelinePhase<MediaNamingPipelineContext>
{
public async Task Execute(MediaNamingPipelineContext context, IServiceConfiguration config)
public async Task Execute(MediaNamingPipelineContext context)
{
context.ApiFetchOutput = await api.GetNaming(config);
context.ApiFetchOutput = await api.GetNaming();
}
}

@ -1,5 +1,4 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
@ -7,8 +6,8 @@ namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
public class MediaNamingApiPersistencePhase(IMediaNamingApiService api)
: IApiPersistencePipelinePhase<MediaNamingPipelineContext>
{
public async Task Execute(MediaNamingPipelineContext context, IServiceConfiguration config)
public async Task Execute(MediaNamingPipelineContext context)
{
await api.UpdateNaming(config, context.TransactionOutput);
await api.UpdateNaming(context.TransactionOutput);
}
}

@ -18,14 +18,15 @@ public record ProcessedNamingConfig
public class MediaNamingConfigPhase(
IMediaNamingGuideService guide,
IIndex<SupportedServices, IServiceBasedMediaNamingConfigPhase> configPhaseStrategyFactory)
IIndex<SupportedServices, IServiceBasedMediaNamingConfigPhase> configPhaseStrategyFactory,
IServiceConfiguration config)
: IConfigPipelinePhase<MediaNamingPipelineContext>
{
public async Task Execute(MediaNamingPipelineContext context, IServiceConfiguration config)
public async Task Execute(MediaNamingPipelineContext context)
{
var lookup = new NamingFormatLookup();
var strategy = configPhaseStrategyFactory[config.ServiceType];
var dto = await strategy.ProcessNaming(config, guide, lookup);
var dto = await strategy.ProcessNaming(guide, lookup);
context.ConfigOutput = new ProcessedNamingConfig {Dto = dto, InvalidNaming = lookup.Errors};
}

@ -1,12 +1,11 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
public class MediaNamingTransactionPhase : ITransactionPipelinePhase<MediaNamingPipelineContext>
{
public void Execute(MediaNamingPipelineContext context, IServiceConfiguration config)
public void Execute(MediaNamingPipelineContext context)
{
context.TransactionOutput = context.ApiFetchOutput switch
{

@ -1,5 +1,4 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityProfile;
namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
@ -9,10 +8,10 @@ public record QualityProfileServiceData(IReadOnlyList<QualityProfileDto> Profile
public class QualityProfileApiFetchPhase(IQualityProfileApiService api)
: IApiFetchPipelinePhase<QualityProfilePipelineContext>
{
public async Task Execute(QualityProfilePipelineContext context, IServiceConfiguration config)
public async Task Execute(QualityProfilePipelineContext context)
{
var profiles = await api.GetQualityProfiles(config);
var schema = await api.GetSchema(config);
var profiles = await api.GetQualityProfiles();
var schema = await api.GetSchema();
context.ApiFetchOutput = new QualityProfileServiceData(profiles.AsReadOnly(), schema);
}
}

@ -1,6 +1,5 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Cli.Pipelines.QualityProfile.Models;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityProfile;
namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
@ -8,7 +7,7 @@ namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
public class QualityProfileApiPersistencePhase(IQualityProfileApiService api)
: IApiPersistencePipelinePhase<QualityProfilePipelineContext>
{
public async Task Execute(QualityProfilePipelineContext context, IServiceConfiguration config)
public async Task Execute(QualityProfilePipelineContext context)
{
var changedProfiles = context.TransactionOutput.ChangedProfiles;
foreach (var profile in changedProfiles.Select(x => x.Profile))
@ -18,11 +17,11 @@ public class QualityProfileApiPersistencePhase(IQualityProfileApiService api)
switch (profile.UpdateReason)
{
case QualityProfileUpdateReason.New:
await api.CreateQualityProfile(config, dto);
await api.CreateQualityProfile(dto);
break;
case QualityProfileUpdateReason.Changed:
await api.UpdateQualityProfile(config, dto);
await api.UpdateQualityProfile(dto);
break;
default:

@ -7,10 +7,10 @@ using Recyclarr.TrashGuide.CustomFormat;
namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
public class QualityProfileConfigPhase(ILogger log, ProcessedCustomFormatCache cache)
public class QualityProfileConfigPhase(ILogger log, ProcessedCustomFormatCache cache, IServiceConfiguration config)
: IConfigPipelinePhase<QualityProfilePipelineContext>
{
public Task Execute(QualityProfilePipelineContext context, IServiceConfiguration config)
public Task Execute(QualityProfilePipelineContext context)
{
// 1. For each group of CFs that has a quality profile specified
// 2. For each quality profile score config in that CF group

@ -10,7 +10,7 @@ namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
public class QualityProfileTransactionPhase(QualityProfileStatCalculator statCalculator)
: ITransactionPipelinePhase<QualityProfilePipelineContext>
{
public void Execute(QualityProfilePipelineContext context, IServiceConfiguration config)
public void Execute(QualityProfilePipelineContext context)
{
var transactions = new QualityProfileTransactionData();

@ -1,5 +1,4 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityDefinition;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
@ -7,8 +6,8 @@ namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeApiFetchPhase(IQualityDefinitionApiService api)
: IApiFetchPipelinePhase<QualitySizePipelineContext>
{
public async Task Execute(QualitySizePipelineContext context, IServiceConfiguration config)
public async Task Execute(QualitySizePipelineContext context)
{
context.ApiFetchOutput = await api.GetQualityDefinition(config);
context.ApiFetchOutput = await api.GetQualityDefinition();
}
}

@ -1,5 +1,4 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityDefinition;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
@ -7,8 +6,8 @@ namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeApiPersistencePhase(IQualityDefinitionApiService api)
: IApiPersistencePipelinePhase<QualitySizePipelineContext>
{
public async Task Execute(QualitySizePipelineContext context, IServiceConfiguration config)
public async Task Execute(QualitySizePipelineContext context)
{
await api.UpdateQualityDefinition(config, context.TransactionOutput);
await api.UpdateQualityDefinition(context.TransactionOutput);
}
}

@ -5,10 +5,10 @@ using Recyclarr.TrashGuide.QualitySize;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeConfigPhase(ILogger log, IQualitySizeGuideService guide)
public class QualitySizeConfigPhase(ILogger log, IQualitySizeGuideService guide, IServiceConfiguration config)
: IConfigPipelinePhase<QualitySizePipelineContext>
{
public Task Execute(QualitySizePipelineContext context, IServiceConfiguration config)
public Task Execute(QualitySizePipelineContext context)
{
var qualityDef = config.QualityDefinition;
if (qualityDef is null)
@ -32,9 +32,9 @@ public class QualitySizeConfigPhase(ILogger log, IQualitySizeGuideService guide)
return Task.CompletedTask;
}
private void AdjustPreferredRatio(QualityDefinitionConfig config, QualitySizeData selectedQuality)
private void AdjustPreferredRatio(QualityDefinitionConfig qualityDefConfig, QualitySizeData selectedQuality)
{
if (config.PreferredRatio is null)
if (qualityDefConfig.PreferredRatio is null)
{
return;
}
@ -42,20 +42,20 @@ public class QualitySizeConfigPhase(ILogger log, IQualitySizeGuideService guide)
log.Information("Using an explicit preferred ratio which will override values from the guide");
// Fix an out of range ratio and warn the user
if (config.PreferredRatio is < 0 or > 1)
if (qualityDefConfig.PreferredRatio is < 0 or > 1)
{
var clampedRatio = Math.Clamp(config.PreferredRatio.Value, 0, 1);
var clampedRatio = Math.Clamp(qualityDefConfig.PreferredRatio.Value, 0, 1);
log.Warning("Your `preferred_ratio` of {CurrentRatio} is out of range. " +
"It must be a decimal between 0.0 and 1.0. It has been clamped to {ClampedRatio}",
config.PreferredRatio, clampedRatio);
qualityDefConfig.PreferredRatio, clampedRatio);
config.PreferredRatio = clampedRatio;
qualityDefConfig.PreferredRatio = clampedRatio;
}
// Apply a calculated preferred size
foreach (var quality in selectedQuality.Qualities)
{
quality.Preferred = quality.InterpolatedPreferred(config.PreferredRatio.Value);
quality.Preferred = quality.InterpolatedPreferred(qualityDefConfig.PreferredRatio.Value);
}
}
}

@ -1,6 +1,5 @@
using System.Collections.ObjectModel;
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityDefinition;
using Recyclarr.TrashGuide.QualitySize;
@ -8,7 +7,7 @@ namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeTransactionPhase(ILogger log) : ITransactionPipelinePhase<QualitySizePipelineContext>
{
public void Execute(QualitySizePipelineContext context, IServiceConfiguration config)
public void Execute(QualitySizePipelineContext context)
{
// Do not check ConfigOutput for null since the LogPhase does it for us
var guideQuality = context.ConfigOutput!.Qualities;

@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Autofac;
using JetBrains.Annotations;
using Recyclarr.Cli.Console;
using Recyclarr.Cli.Console.Settings;
using Recyclarr.Config;
@ -9,18 +11,24 @@ using Spectre.Console;
namespace Recyclarr.Cli.Processors.Delete;
[UsedImplicitly]
internal class CustomFormatConfigurationScope(ILifetimeScope scope) : ConfigurationScope(scope)
{
public ICustomFormatApiService CustomFormatApi { get; } = scope.Resolve<ICustomFormatApiService>();
}
public class DeleteCustomFormatsProcessor(
ILogger log,
IAnsiConsole console,
ICustomFormatApiService api,
IConfigurationRegistry configRegistry)
IConfigurationRegistry configRegistry,
ConfigurationScopeFactory scopeFactory)
: IDeleteCustomFormatsProcessor
{
public async Task Process(IDeleteCustomFormatSettings settings)
{
var config = GetTargetConfig(settings);
using var scope = scopeFactory.Start<CustomFormatConfigurationScope>(GetTargetConfig(settings));
var cfs = await ObtainCustomFormats(config);
var cfs = await ObtainCustomFormats(scope.CustomFormatApi);
if (!settings.All)
{
@ -53,11 +61,11 @@ public class DeleteCustomFormatsProcessor(
return;
}
await DeleteCustomFormats(cfs, config);
await DeleteCustomFormats(scope.CustomFormatApi, cfs);
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
private async Task DeleteCustomFormats(ICollection<CustomFormatData> cfs, IServiceConfiguration config)
private async Task DeleteCustomFormats(ICustomFormatApiService api, ICollection<CustomFormatData> cfs)
{
await console.Progress().StartAsync(async ctx =>
{
@ -68,7 +76,7 @@ public class DeleteCustomFormatsProcessor(
{
try
{
await api.DeleteCustomFormat(config, cf.Id, token);
await api.DeleteCustomFormat(cf.Id, token);
log.Debug("Deleted {Name}", cf.Name);
}
catch (Exception e)
@ -82,13 +90,13 @@ public class DeleteCustomFormatsProcessor(
});
}
private async Task<IList<CustomFormatData>> ObtainCustomFormats(IServiceConfiguration config)
private async Task<IList<CustomFormatData>> ObtainCustomFormats(ICustomFormatApiService api)
{
IList<CustomFormatData> cfs = new List<CustomFormatData>();
await console.Status().StartAsync("Obtaining custom formats...", async _ =>
{
cfs = await api.GetCustomFormats(config);
cfs = await api.GetCustomFormats();
});
return cfs;

@ -1,16 +1,25 @@
using Recyclarr.Cli.Console.Settings;
using Recyclarr.Cli.Pipelines;
using Recyclarr.Compatibility;
using Recyclarr.Config.Models;
using Spectre.Console;
namespace Recyclarr.Cli.Processors.Sync;
public class SyncPipelineExecutor(
ILogger log,
IAnsiConsole console,
IOrderedEnumerable<ISyncPipeline> pipelines,
IEnumerable<IPipelineCache> caches)
IEnumerable<IPipelineCache> caches,
ServiceAgnosticCapabilityEnforcer enforcer,
IServiceConfiguration config)
{
public async Task Process(ISyncSettings settings, IServiceConfiguration config)
public async Task Process(ISyncSettings settings)
{
PrintProcessingHeader();
await enforcer.Check(config);
foreach (var cache in caches)
{
cache.Clear();
@ -19,7 +28,25 @@ public class SyncPipelineExecutor(
foreach (var pipeline in pipelines)
{
log.Debug("Executing Pipeline: {Pipeline}", pipeline.GetType().Name);
await pipeline.Execute(settings, config);
await pipeline.Execute(settings);
}
log.Information("Completed at {Date}", DateTime.Now);
}
private void PrintProcessingHeader()
{
var instanceName = config.InstanceName;
console.WriteLine(
$"""
===========================================
Processing {config.ServiceType} Server: [{instanceName}]
===========================================
""");
log.Debug("Processing {Server} server {Name}", config.ServiceType, instanceName);
}
}

@ -1,21 +1,21 @@
using System.Diagnostics.CodeAnalysis;
using Autofac;
using Recyclarr.Cli.Console.Settings;
using Recyclarr.Cli.Processors.ErrorHandling;
using Recyclarr.Compatibility;
using Recyclarr.Config;
using Recyclarr.Config.Models;
using Recyclarr.TrashGuide;
using Spectre.Console;
namespace Recyclarr.Cli.Processors.Sync;
public class SyncBasedConfigurationScope(ILifetimeScope scope) : ConfigurationScope(scope)
{
public SyncPipelineExecutor Pipelines { get; } = scope.Resolve<SyncPipelineExecutor>();
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public class SyncProcessor(
IAnsiConsole console,
ILogger log,
IConfigurationRegistry configRegistry,
SyncPipelineExecutor pipelines,
ServiceAgnosticCapabilityEnforcer capabilityEnforcer,
ConfigurationScopeFactory configScopeFactory,
ConsoleExceptionHandler exceptionHandler)
: ISyncProcessor
{
@ -55,10 +55,8 @@ public class SyncProcessor(
{
try
{
PrintProcessingHeader(config.ServiceType, config);
await capabilityEnforcer.Check(config);
await pipelines.Process(settings, config);
log.Information("Completed at {Date}", DateTime.Now);
using var scope = configScopeFactory.Start<SyncBasedConfigurationScope>(config);
await scope.Pipelines.Process(settings);
}
catch (Exception e)
{
@ -74,20 +72,4 @@ public class SyncProcessor(
return failureDetected;
}
private void PrintProcessingHeader(SupportedServices serviceType, IServiceConfiguration config)
{
var instanceName = config.InstanceName;
console.WriteLine(
$"""
===========================================
Processing {serviceType} Server: [{instanceName}]
===========================================
""");
log.Debug("Processing {Server} server {Name}", serviceType, instanceName);
}
}

@ -1,8 +1,6 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Compatibility;
public interface IServiceInformation
{
public Task<Version> GetVersion(IServiceConfiguration config);
public Task<Version> GetVersion();
}

@ -1,8 +1,6 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Compatibility.Radarr;
public interface IRadarrCapabilityFetcher
{
Task<RadarrCapabilities> GetCapabilities(IServiceConfiguration config);
Task<RadarrCapabilities> GetCapabilities();
}

@ -1,12 +1,10 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Compatibility.Radarr;
public class RadarrCapabilityEnforcer(IRadarrCapabilityFetcher capabilityFetcher)
{
public async Task Check(RadarrConfiguration config)
public async Task Check()
{
_ = await capabilityFetcher.GetCapabilities(config);
_ = await capabilityFetcher.GetCapabilities();
// For the future: Add more capability checks here as needed
}

@ -12,12 +12,12 @@ public class ServiceAgnosticCapabilityEnforcer(
{
switch (config)
{
case SonarrConfiguration c:
await sonarrEnforcer.Check(c);
case SonarrConfiguration:
await sonarrEnforcer.Check();
break;
case RadarrConfiguration c:
await radarrEnforcer.Check(c);
case RadarrConfiguration:
await radarrEnforcer.Check();
break;
}
}

@ -1,13 +1,11 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Compatibility;
public abstract class ServiceCapabilityFetcher<T>(IServiceInformation info)
where T : class
{
public async Task<T> GetCapabilities(IServiceConfiguration config)
public async Task<T> GetCapabilities()
{
var version = await info.GetVersion(config);
var version = await info.GetVersion();
return BuildCapabilitiesObject(version);
}

@ -1,5 +1,4 @@
using Flurl.Http;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.System;
using Serilog;
@ -7,11 +6,11 @@ namespace Recyclarr.Compatibility;
public class ServiceInformation(ISystemApiService api, ILogger log) : IServiceInformation
{
public async Task<Version> GetVersion(IServiceConfiguration config)
public async Task<Version> GetVersion()
{
try
{
var status = await api.GetStatus(config);
var status = await api.GetStatus();
log.Debug("{Service} Version: {Version}", status.AppName, status.Version);
return new Version(status.Version);
}

@ -1,8 +1,6 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Compatibility.Sonarr;
public interface ISonarrCapabilityFetcher
{
Task<SonarrCapabilities> GetCapabilities(IServiceConfiguration config);
Task<SonarrCapabilities> GetCapabilities();
}

@ -1,12 +1,10 @@
using Recyclarr.Config.Models;
namespace Recyclarr.Compatibility.Sonarr;
public class SonarrCapabilityEnforcer(ISonarrCapabilityFetcher capabilityFetcher)
{
public async Task Check(SonarrConfiguration config)
public async Task Check()
{
var capabilities = await capabilityFetcher.GetCapabilities(config);
var capabilities = await capabilityFetcher.GetCapabilities();
if (capabilities.Version < SonarrCapabilities.MinimumVersion)
{

@ -29,6 +29,7 @@ public class ConfigAutofacModule : Module
builder.RegisterType<ConfigValidationExecutor>();
builder.RegisterType<ConfigParser>();
builder.RegisterType<ConfigSaver>();
builder.RegisterType<ConfigurationScopeFactory>();
// Config Post Processors
builder.RegisterType<ImplicitUrlAndKeyPostProcessor>().As<IConfigPostProcessor>();

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using Autofac;
namespace Recyclarr.Config;
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor", Justification =
"Base types are required to instruct Autofac which types we want to resolve")]
public abstract class ConfigurationScope(ILifetimeScope scope) : IDisposable
{
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
scope.Dispose();
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}

@ -0,0 +1,20 @@
using Autofac;
using JetBrains.Annotations;
using Recyclarr.Config.Models;
namespace Recyclarr.Config;
[UsedImplicitly]
public class ConfigurationScopeFactory(ILifetimeScope scope)
{
public T Start<T>(IServiceConfiguration config) where T : ConfigurationScope
{
var childScope = scope.BeginLifetimeScope(c =>
{
c.RegisterInstance(config).As(config.GetType()).As<IServiceConfiguration>();
c.RegisterType<T>();
});
return childScope.Resolve<T>();
}
}

@ -1,36 +1,32 @@
using Flurl.Http;
using Recyclarr.Config.Models;
using Recyclarr.TrashGuide.CustomFormat;
namespace Recyclarr.ServarrApi.CustomFormat;
public class CustomFormatApiService(IServarrRequestBuilder service) : ICustomFormatApiService
{
public async Task<IList<CustomFormatData>> GetCustomFormats(IServiceConfiguration config)
public async Task<IList<CustomFormatData>> GetCustomFormats()
{
return await service.Request(config, "customformat")
return await service.Request("customformat")
.GetJsonAsync<IList<CustomFormatData>>();
}
public async Task<CustomFormatData?> CreateCustomFormat(IServiceConfiguration config, CustomFormatData cf)
public async Task<CustomFormatData?> CreateCustomFormat(CustomFormatData cf)
{
return await service.Request(config, "customformat")
return await service.Request("customformat")
.PostJsonAsync(cf)
.ReceiveJson<CustomFormatData>();
}
public async Task UpdateCustomFormat(IServiceConfiguration config, CustomFormatData cf)
public async Task UpdateCustomFormat(CustomFormatData cf)
{
await service.Request(config, "customformat", cf.Id)
await service.Request("customformat", cf.Id)
.PutJsonAsync(cf);
}
public async Task DeleteCustomFormat(
IServiceConfiguration config,
int customFormatId,
CancellationToken cancellationToken = default)
public async Task DeleteCustomFormat(int customFormatId, CancellationToken cancellationToken = default)
{
await service.Request(config, "customformat", customFormatId)
await service.Request("customformat", customFormatId)
.DeleteAsync(cancellationToken: cancellationToken);
}
}

@ -1,16 +1,11 @@
using Recyclarr.Config.Models;
using Recyclarr.TrashGuide.CustomFormat;
namespace Recyclarr.ServarrApi.CustomFormat;
public interface ICustomFormatApiService
{
Task<IList<CustomFormatData>> GetCustomFormats(IServiceConfiguration config);
Task<CustomFormatData?> CreateCustomFormat(IServiceConfiguration config, CustomFormatData cf);
Task UpdateCustomFormat(IServiceConfiguration config, CustomFormatData cf);
Task DeleteCustomFormat(
IServiceConfiguration config,
int customFormatId,
CancellationToken cancellationToken = default);
Task<IList<CustomFormatData>> GetCustomFormats();
Task<CustomFormatData?> CreateCustomFormat(CustomFormatData cf);
Task UpdateCustomFormat(CustomFormatData cf);
Task DeleteCustomFormat(int customFormatId, CancellationToken cancellationToken = default);
}

@ -1,9 +1,8 @@
using Flurl.Http;
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi;
public interface IServarrRequestBuilder
{
IFlurlRequest Request(IServiceConfiguration config, params object[] path);
IFlurlRequest Request(params object[] path);
}

@ -1,9 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.MediaNaming;
public interface IMediaNamingApiService
{
Task<MediaNamingDto> GetNaming(IServiceConfiguration config);
Task UpdateNaming(IServiceConfiguration config, MediaNamingDto dto);
Task<MediaNamingDto> GetNaming();
Task UpdateNaming(MediaNamingDto dto);
}

@ -4,13 +4,12 @@ using Recyclarr.TrashGuide;
namespace Recyclarr.ServarrApi.MediaNaming;
public class MediaNamingApiService(IServarrRequestBuilder service) : IMediaNamingApiService
public class MediaNamingApiService(IServarrRequestBuilder service, IServiceConfiguration config)
: IMediaNamingApiService
{
public async Task<MediaNamingDto> GetNaming(IServiceConfiguration config)
public async Task<MediaNamingDto> GetNaming()
{
var response = await service.Request(config, "config", "naming")
.GetAsync();
var response = await service.Request("config", "naming").GetAsync();
return config.ServiceType switch
{
SupportedServices.Radarr => await response.GetJsonAsync<RadarrMediaNamingDto>(),
@ -19,9 +18,9 @@ public class MediaNamingApiService(IServarrRequestBuilder service) : IMediaNamin
};
}
public async Task UpdateNaming(IServiceConfiguration config, MediaNamingDto dto)
public async Task UpdateNaming(MediaNamingDto dto)
{
await service.Request(config, "config", "naming")
await service.Request("config", "naming")
.PutJsonAsync(dto);
}
}

@ -1,12 +1,7 @@
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.QualityDefinition;
public interface IQualityDefinitionApiService
{
Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition(IServiceConfiguration config);
Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(
IServiceConfiguration config,
IList<ServiceQualityDefinitionItem> newQuality);
Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition();
Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(IList<ServiceQualityDefinitionItem> newQuality);
}

@ -1,21 +1,19 @@
using Flurl.Http;
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.QualityDefinition;
internal class QualityDefinitionApiService(IServarrRequestBuilder service) : IQualityDefinitionApiService
{
public async Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition(IServiceConfiguration config)
public async Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition()
{
return await service.Request(config, "qualitydefinition")
return await service.Request("qualitydefinition")
.GetJsonAsync<List<ServiceQualityDefinitionItem>>();
}
public async Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(
IServiceConfiguration config,
IList<ServiceQualityDefinitionItem> newQuality)
{
return await service.Request(config, "qualityDefinition", "update")
return await service.Request("qualityDefinition", "update")
.PutJsonAsync(newQuality)
.ReceiveJson<List<ServiceQualityDefinitionItem>>();
}

@ -1,11 +1,9 @@
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.QualityProfile;
public interface IQualityProfileApiService
{
Task<IList<QualityProfileDto>> GetQualityProfiles(IServiceConfiguration config);
Task UpdateQualityProfile(IServiceConfiguration config, QualityProfileDto profile);
Task<QualityProfileDto> GetSchema(IServiceConfiguration config);
Task CreateQualityProfile(IServiceConfiguration config, QualityProfileDto profile);
Task<IList<QualityProfileDto>> GetQualityProfiles();
Task UpdateQualityProfile(QualityProfileDto profile);
Task<QualityProfileDto> GetSchema();
Task CreateQualityProfile(QualityProfileDto profile);
}

@ -1,40 +1,39 @@
using Flurl.Http;
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.QualityProfile;
internal class QualityProfileApiService(IServarrRequestBuilder service) : IQualityProfileApiService
{
public async Task<IList<QualityProfileDto>> GetQualityProfiles(IServiceConfiguration config)
public async Task<IList<QualityProfileDto>> GetQualityProfiles()
{
var response = await service.Request(config, "qualityprofile")
var response = await service.Request("qualityprofile")
.GetJsonAsync<IList<QualityProfileDto>>();
return response.Select(x => x.ReverseItems()).ToList();
}
public async Task<QualityProfileDto> GetSchema(IServiceConfiguration config)
public async Task<QualityProfileDto> GetSchema()
{
var response = await service.Request(config, "qualityprofile", "schema")
var response = await service.Request("qualityprofile", "schema")
.GetJsonAsync<QualityProfileDto>();
return response.ReverseItems();
}
public async Task UpdateQualityProfile(IServiceConfiguration config, QualityProfileDto profile)
public async Task UpdateQualityProfile(QualityProfileDto profile)
{
if (profile.Id is null)
{
throw new ArgumentException($"Profile's ID property must not be null: {profile.Name}");
}
await service.Request(config, "qualityprofile", profile.Id)
await service.Request("qualityprofile", profile.Id)
.PutJsonAsync(profile.ReverseItems());
}
public async Task CreateQualityProfile(IServiceConfiguration config, QualityProfileDto profile)
public async Task CreateQualityProfile(QualityProfileDto profile)
{
var response = await service.Request(config, "qualityprofile")
var response = await service.Request("qualityprofile")
.PostJsonAsync(profile.ReverseItems())
.ReceiveJson<QualityProfileDto>();

@ -12,11 +12,20 @@ public class ServarrApiAutofacModule : Module
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<SystemApiService>().As<ISystemApiService>();
// This is used by all specific API service classes registered below.
builder.RegisterType<ServarrRequestBuilder>().As<IServarrRequestBuilder>();
builder.RegisterType<QualityProfileApiService>().As<IQualityProfileApiService>();
builder.RegisterType<CustomFormatApiService>().As<ICustomFormatApiService>();
builder.RegisterType<QualityDefinitionApiService>().As<IQualityDefinitionApiService>();
builder.RegisterType<MediaNamingApiService>().As<IMediaNamingApiService>();
builder.RegisterType<SystemApiService>().As<ISystemApiService>()
.InstancePerLifetimeScope();
builder.RegisterType<QualityProfileApiService>().As<IQualityProfileApiService>()
.InstancePerLifetimeScope();
builder.RegisterType<CustomFormatApiService>().As<ICustomFormatApiService>()
.InstancePerLifetimeScope();
builder.RegisterType<QualityDefinitionApiService>().As<IQualityDefinitionApiService>()
.InstancePerLifetimeScope();
builder.RegisterType<MediaNamingApiService>().As<IMediaNamingApiService>()
.InstancePerLifetimeScope();
}
}

@ -14,10 +14,11 @@ public class ServarrRequestBuilder(
ILogger log,
IFlurlClientCache clientCache,
ISettingsProvider settingsProvider,
IEnumerable<FlurlSpecificEventHandler> eventHandlers)
IEnumerable<FlurlSpecificEventHandler> eventHandlers,
IServiceConfiguration config)
: IServarrRequestBuilder
{
public IFlurlRequest Request(IServiceConfiguration config, params object[] path)
public IFlurlRequest Request(params object[] path)
{
var client = clientCache.GetOrAdd(
config.InstanceName,
@ -42,16 +43,16 @@ public class ServarrRequestBuilder(
settings.JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services);
});
builder.ConfigureInnerHandler(handler =>
if (!settingsProvider.Settings.EnableSslCertificateValidation)
{
if (!settingsProvider.Settings.EnableSslCertificateValidation)
builder.ConfigureInnerHandler(handler =>
{
log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting " +
"`enable_ssl_certificate_validation` is set to `false`");
handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
}
});
});
}
}
}

@ -1,8 +1,6 @@
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.System;
public interface ISystemApiService
{
Task<SystemStatus> GetStatus(IServiceConfiguration config);
Task<SystemStatus> GetStatus();
}

@ -1,13 +1,12 @@
using Flurl.Http;
using Recyclarr.Config.Models;
namespace Recyclarr.ServarrApi.System;
public class SystemApiService(IServarrRequestBuilder service) : ISystemApiService
{
public async Task<SystemStatus> GetStatus(IServiceConfiguration config)
public async Task<SystemStatus> GetStatus()
{
return await service.Request(config, "system", "status")
return await service.Request("system", "status")
.GetJsonAsync<SystemStatus>();
}
}

@ -1,8 +1,8 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using Autofac;
using Autofac.Core;
using NUnit.Framework.Internal;
using Recyclarr.Config.Models;
using Recyclarr.Platform;
using Recyclarr.TestLibrary.Autofac;
using Serilog.Core;
@ -13,9 +13,6 @@ namespace Recyclarr.Cli.IntegrationTests;
[TestFixture]
public class CompositionRootTest
{
// Warning CA1812 : CompositionRootTest.ConcreteTypeEnumerator is an internal class that is apparently never
// instantiated.
[SuppressMessage("Performance", "CA1812", Justification = "Created via reflection by TestCaseSource attribute")]
private sealed class ConcreteTypeEnumerator : IEnumerable
{
public IEnumerator GetEnumerator()
@ -28,6 +25,11 @@ public class CompositionRootTest
// in the CompositionRoot. Register mocks/stubs here.
builder.RegisterMockFor<IAnsiConsole>();
// Normally in per-instance syncing, a child lifetime scope is created to register IServiceConfiguration.
// However, in the test for checking whether all necessary dependencies are registered, we provide a mock
// registration here for the purposes of getting the test to pass.
builder.RegisterMockFor<IServiceConfiguration>();
var container = builder.Build();
return container.ComponentRegistry.Registrations
.SelectMany(x => x.Services)

@ -2,6 +2,7 @@ using Recyclarr.Cli.Pipelines.CustomFormat;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Models;
using Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
using Recyclarr.Config;
using Recyclarr.Tests.TestLibrary;
using Recyclarr.TrashGuide.CustomFormat;
@ -13,9 +14,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Add_new_cf()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var config = NewConfig.Radarr();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr());
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -24,7 +25,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("one", "cf1")]
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -38,7 +39,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Update_cf_by_matching_name()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr());
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -56,9 +59,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
]
};
var config = NewConfig.Radarr();
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -75,7 +76,9 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Update_cf_by_matching_id_different_names()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr());
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -93,9 +96,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
]
};
var config = NewConfig.Radarr();
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -112,7 +113,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Update_cf_by_matching_id_same_names()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = true
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -130,12 +137,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
]
};
var config = NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = true
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -152,7 +154,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Conflicting_cf_when_new_cf_has_name_of_existing()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -165,12 +173,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("one", "cf1")]
};
var config = NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -184,7 +187,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Conflicting_cf_when_cached_cf_has_name_of_existing()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -197,12 +206,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("one", "cf1")]
};
var config = NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -216,7 +220,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Updated_cf_with_matching_name_and_id()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -238,12 +248,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
]
};
var config = NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -257,7 +262,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Unchanged_cfs_with_replace_enabled()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = true
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -266,12 +277,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("one", "cf1")]
};
var config = NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = true
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -282,7 +288,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Unchanged_cfs_without_replace()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -291,12 +303,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("one", "cf1")]
};
var config = NewConfig.Radarr() with
{
ReplaceExistingCustomFormats = false
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -307,7 +314,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Deleted_cfs_when_enabled()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
DeleteOldCustomFormats = true
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -316,12 +329,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = []
};
var config = NewConfig.Radarr() with
{
DeleteOldCustomFormats = true
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{
@ -335,7 +343,13 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void No_deleted_cfs_when_disabled()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr() with
{
DeleteOldCustomFormats = false
});
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -344,12 +358,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = []
};
var config = NewConfig.Radarr() with
{
DeleteOldCustomFormats = false
};
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData());
}
@ -357,7 +366,10 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Do_not_delete_cfs_in_config()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr());
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -366,9 +378,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("two", "cf2", 2)]
};
var config = NewConfig.Radarr();
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.DeletedCustomFormats.Should().BeEmpty();
}
@ -376,7 +386,10 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
[Test]
public void Add_new_cf_when_in_cache_but_not_in_service()
{
var sut = Resolve<CustomFormatTransactionPhase>();
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(NewConfig.Radarr());
var sut = scope.Resolve<CustomFormatTransactionPhase>();
var context = new CustomFormatPipelineContext
{
@ -385,9 +398,7 @@ internal class CustomFormatTransactionPhaseTest : CliIntegrationFixture
ConfigOutput = [NewCf.Data("two", "cf2", 2)]
};
var config = NewConfig.Radarr();
sut.Execute(context, config);
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new CustomFormatTransactionData
{

@ -1,25 +0,0 @@
using Autofac.Core.Registration;
using Recyclarr.Cli.Pipelines.MediaNaming;
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
using Recyclarr.Config.Models;
using Recyclarr.TrashGuide;
namespace Recyclarr.Cli.IntegrationTests;
[TestFixture]
[Parallelizable(ParallelScope.All)]
internal class MediaNamingConfigPhaseIntegrationTest : CliIntegrationFixture
{
private sealed record UnsupportedConfigType : ServiceConfiguration
{
public override SupportedServices ServiceType => (SupportedServices) 999;
}
[Test]
public async Task Throw_on_unknown_config_type()
{
var sut = Resolve<MediaNamingConfigPhase>();
var act = () => sut.Execute(new MediaNamingPipelineContext(), new UnsupportedConfigType {InstanceName = ""});
await act.Should().ThrowAsync<ComponentNotRegisteredException>();
}
}

@ -1,3 +1,4 @@
using AutoFixture;
using Recyclarr.Cli.Cache;
using Recyclarr.Config.Models;
@ -6,16 +7,19 @@ namespace Recyclarr.Cli.Tests.Cache;
[TestFixture]
public class CacheStoragePathTest
{
[Test, AutoMockData]
public void Use_correct_name_in_path(CacheStoragePath sut)
[Test]
public void Use_correct_name_in_path()
{
var config = new SonarrConfiguration
var fixture = NSubstituteFixture.Create();
fixture.Inject<IServiceConfiguration>(new SonarrConfiguration
{
BaseUrl = new Uri("http://something/foo/bar"),
InstanceName = "thename"
};
});
var result = sut.CalculatePath(config, "obj");
var sut = fixture.Create<CacheStoragePath>();
var result = sut.CalculatePath("obj");
result.FullName.Should().MatchRegex(@".*[/\\][a-f0-9]+[/\\]obj\.json$");
}

@ -2,7 +2,6 @@ using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Tests.Cache;
@ -27,10 +26,9 @@ 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>(config);
var result = sut.Load<ObjectWithAttribute>();
result.Should().BeNull();
}
@ -38,7 +36,6 @@ 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 =
@ -49,9 +46,9 @@ public class ServiceCacheTest
const string testJsonPath = "cacheFile.json";
fs.AddFile(testJsonPath, new MockFileData(testJson));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
var obj = sut.Load<ObjectWithAttribute>(config);
var obj = sut.Load<ObjectWithAttribute>();
obj.Should().NotBeNull();
obj!.TestValue.Should().Be("Foo");
@ -59,10 +56,9 @@ public class ServiceCacheTest
[Test, AutoMockData]
public void Loading_with_invalid_object_name_throws(
IServiceConfiguration config,
ServiceCache sut)
{
Action act = () => sut.Load<ObjectWithAttributeInvalidChars>(config);
Action act = () => sut.Load<ObjectWithAttributeInvalidChars>();
act.Should()
.Throw<ArgumentException>()
@ -71,10 +67,9 @@ public class ServiceCacheTest
[Test, AutoMockData]
public void Loading_without_attribute_throws(
IServiceConfiguration config,
ServiceCache sut)
{
Action act = () => sut.Load<ObjectWithoutAttribute>(config);
Action act = () => sut.Load<ObjectWithoutAttribute>();
act.Should()
.Throw<ArgumentException>()
@ -85,17 +80,16 @@ 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!, default!)
storage.CalculatePath(default!)
.ReturnsForAnyArgs(_ => fs.FileInfo.New($"{ValidObjectName}.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
fs.AllFiles.Should().ContainMatch($"*{ValidObjectName}.json");
var file = fs.GetFile(storage.CalculatePath(config, "").FullName);
var file = fs.GetFile(storage.CalculatePath("").FullName);
file.Should().NotBeNull();
file.TextContents.Should().Contain("\"test_value\"");
}
@ -104,13 +98,12 @@ 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!, default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New(testJsonPath));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
var expectedFile = fs.GetFile(testJsonPath);
expectedFile.Should().NotBeNull();
@ -124,10 +117,9 @@ public class ServiceCacheTest
[Test, AutoMockData]
public void Saving_with_invalid_object_name_throws(
IServiceConfiguration config,
ServiceCache sut)
{
var act = () => sut.Save(new ObjectWithAttributeInvalidChars(), config);
var act = () => sut.Save(new ObjectWithAttributeInvalidChars());
act.Should()
.Throw<ArgumentException>()
@ -136,10 +128,9 @@ public class ServiceCacheTest
[Test, AutoMockData]
public void Saving_without_attribute_throws(
IServiceConfiguration config,
ServiceCache sut)
{
var act = () => sut.Save(new ObjectWithoutAttribute(), config);
var act = () => sut.Save(new ObjectWithoutAttribute());
act.Should()
.Throw<ArgumentException>()
@ -150,14 +141,13 @@ 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!, default!).ReturnsForAnyArgs(fs.FileInfo.New("Foo.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"}, config);
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("Foo.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Foo"});
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("Bar.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Bar"}, config);
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("Bar.json"));
sut.Save(new ObjectWithAttribute {TestValue = "Bar"});
var expectedFiles = new[] {"*Foo.json", "*Bar.json"};
foreach (var expectedFile in expectedFiles)
@ -170,13 +160,12 @@ 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!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
fs.AddFile("cacheFile.json", new MockFileData(""));
Action act = () => sut.Load<ObjectWithAttribute>(config);
Action act = () => sut.Load<ObjectWithAttribute>();
act.Should().NotThrow();
}
@ -185,7 +174,6 @@ 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 =
@ -203,9 +191,9 @@ public class ServiceCacheTest
""";
fs.AddFile("cacheFile.json", new MockFileData(cacheJson));
storage.CalculatePath(default!, default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
storage.CalculatePath(default!).ReturnsForAnyArgs(fs.FileInfo.New("cacheFile.json"));
var result = sut.Load<CustomFormatCacheData>(config);
var result = sut.Load<CustomFormatCacheData>();
result.Should().BeEquivalentTo(new
{

@ -1,7 +1,6 @@
using AutoFixture;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Pipelines.CustomFormat.Cache;
using Recyclarr.Config.Models;
namespace Recyclarr.Cli.Tests.Pipelines.CustomFormat.Cache;
@ -16,16 +15,14 @@ public class CustomFormatCachePersisterTest
var serviceCache = fixture.Freeze<IServiceCache>();
var sut = fixture.Create<CustomFormatCachePersister>();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCacheData(versionToTest, "",
[
new TrashIdMapping("", "", 5)
]);
serviceCache.Load<CustomFormatCacheData>(config).Returns(testCfObj);
serviceCache.Load<CustomFormatCacheData>().Returns(testCfObj);
var act = () => sut.Load(config);
var act = () => sut.Load();
act.Should().Throw<CacheException>();
}
@ -37,16 +34,14 @@ public class CustomFormatCachePersisterTest
var serviceCache = fixture.Freeze<IServiceCache>();
var sut = fixture.Create<CustomFormatCachePersister>();
var config = Substitute.For<IServiceConfiguration>();
var testCfObj = new CustomFormatCacheData(CustomFormatCachePersister.LatestVersion, "",
[
new TrashIdMapping("", "", 5)
]);
serviceCache.Load<CustomFormatCacheData>(config).Returns(testCfObj);
serviceCache.Load<CustomFormatCacheData>().Returns(testCfObj);
var result = sut.Load(config);
var result = sut.Load();
result.Should().NotBeNull();
}
@ -59,11 +54,10 @@ public class CustomFormatCachePersisterTest
var sut = fixture.Create<CustomFormatCachePersister>();
TrashIdMapping[] mappings = [new TrashIdMapping("abc", "name", 123)];
var config = Substitute.For<IServiceConfiguration>();
serviceCache.Load<CustomFormatCacheData>(config).Returns(new CustomFormatCacheData(1, "", mappings));
serviceCache.Load<CustomFormatCacheData>().Returns(new CustomFormatCacheData(1, "", mappings));
var result = sut.Load(config);
var result = sut.Load();
result.Mappings.Should().BeEquivalentTo(mappings);
}

@ -1,3 +1,4 @@
using AutoFixture;
using Recyclarr.Cli.Pipelines.CustomFormat;
using Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
using Recyclarr.Config.Models;
@ -9,18 +10,19 @@ namespace Recyclarr.Cli.Tests.Pipelines.CustomFormat.PipelinePhases;
[TestFixture]
public class CustomFormatConfigPhaseTest
{
[Test, AutoMockData]
public void Return_configs_that_exist_in_guide(
[Frozen] ICustomFormatGuideService guide,
CustomFormatConfigPhase sut)
[Test]
public void Return_configs_that_exist_in_guide()
{
var fixture = NSubstituteFixture.Create();
var guide = fixture.Freeze<ICustomFormatGuideService>();
guide.GetCustomFormatData(default!).ReturnsForAnyArgs(new[]
{
NewCf.Data("one", "cf1"),
NewCf.Data("two", "cf2")
});
var config = NewConfig.Radarr() with
fixture.Inject<IServiceConfiguration>(NewConfig.Radarr() with
{
CustomFormats = new List<CustomFormatConfig>
{
@ -33,11 +35,11 @@ public class CustomFormatConfigPhaseTest
}
}
}
};
});
var context = new CustomFormatPipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<CustomFormatConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEquivalentTo(new[]
{
@ -46,17 +48,18 @@ public class CustomFormatConfigPhaseTest
});
}
[Test, AutoMockData]
public void Skip_configs_that_do_not_exist_in_guide(
[Frozen] ICustomFormatGuideService guide,
CustomFormatConfigPhase sut)
[Test]
public void Skip_configs_that_do_not_exist_in_guide()
{
var fixture = NSubstituteFixture.Create();
var guide = fixture.Freeze<ICustomFormatGuideService>();
guide.GetCustomFormatData(default!).ReturnsForAnyArgs(new[]
{
NewCf.Data("", "cf4")
});
var config = NewConfig.Radarr() with
fixture.Inject<IServiceConfiguration>(NewConfig.Radarr() with
{
CustomFormats = new List<CustomFormatConfig>
{
@ -70,11 +73,11 @@ public class CustomFormatConfigPhaseTest
}
}
}
};
});
var context = new CustomFormatPipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<CustomFormatConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEmpty();
}

@ -1,3 +1,4 @@
using AutoFixture;
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
@ -25,14 +26,15 @@ public class RadarrMediaNamingConfigPhaseTest
}
};
[Test, AutoMockData]
public async Task Radarr_naming(
[Frozen] IMediaNamingGuideService guide,
RadarrMediaNamingConfigPhase sut)
[Test]
public async Task Radarr_naming()
{
var fixture = NSubstituteFixture.Create();
var guide = fixture.Freeze<IMediaNamingGuideService>();
guide.GetRadarrNamingData().Returns(RadarrNamingData);
var config = new RadarrConfiguration
fixture.Inject(new RadarrConfiguration
{
InstanceName = "radarr",
MediaNaming = new RadarrMediaNamingConfig
@ -44,9 +46,10 @@ public class RadarrMediaNamingConfigPhaseTest
Standard = "emby"
}
}
};
});
var result = await sut.ProcessNaming(config, guide, new NamingFormatLookup());
var sut = fixture.Create<RadarrMediaNamingConfigPhase>();
var result = await sut.ProcessNaming(guide, new NamingFormatLookup());
result.Should().NotBeNull();
result.Should().BeEquivalentTo(new RadarrMediaNamingDto

@ -1,3 +1,4 @@
using AutoFixture;
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases.Config;
using Recyclarr.Compatibility.Sonarr;
@ -45,15 +46,16 @@ public class SonarrMediaNamingConfigPhaseTest
}
};
[Test, AutoMockData]
public async Task Sonarr_v4_naming(
[Frozen] ISonarrCapabilityFetcher capabilities,
[Frozen] IMediaNamingGuideService guide,
SonarrMediaNamingConfigPhase sut)
[Test]
public async Task Sonarr_v4_naming()
{
var fixture = NSubstituteFixture.Create();
fixture.Freeze<ISonarrCapabilityFetcher>(); // Frozen for instance sharing
var guide = fixture.Freeze<IMediaNamingGuideService>();
guide.GetSonarrNamingData().Returns(SonarrNamingData);
var config = new SonarrConfiguration
fixture.Inject(new SonarrConfiguration
{
InstanceName = "sonarr",
MediaNaming = new SonarrMediaNamingConfig
@ -68,9 +70,10 @@ public class SonarrMediaNamingConfigPhaseTest
Anime = "default"
}
}
};
});
var result = await sut.ProcessNaming(config, guide, new NamingFormatLookup());
var sut = fixture.Create<SonarrMediaNamingConfigPhase>();
var result = await sut.ProcessNaming(guide, new NamingFormatLookup());
result.Should().NotBeNull();
result.Should().BeEquivalentTo(new SonarrMediaNamingDto
@ -84,15 +87,16 @@ public class SonarrMediaNamingConfigPhaseTest
});
}
[Test, AutoMockData]
public async Task Sonarr_invalid_names(
[Frozen] ISonarrCapabilityFetcher capabilities,
[Frozen] IMediaNamingGuideService guide,
SonarrMediaNamingConfigPhase sut)
[Test]
public async Task Sonarr_invalid_names()
{
var fixture = NSubstituteFixture.Create();
fixture.Freeze<ISonarrCapabilityFetcher>(); // Frozen for instance sharing
var guide = fixture.Freeze<IMediaNamingGuideService>();
guide.GetSonarrNamingData().Returns(SonarrNamingData);
var config = new SonarrConfiguration
fixture.Inject(new SonarrConfiguration
{
InstanceName = "sonarr",
MediaNaming = new SonarrMediaNamingConfig
@ -107,10 +111,11 @@ public class SonarrMediaNamingConfigPhaseTest
Anime = "bad5"
}
}
};
});
var sut = fixture.Create<SonarrMediaNamingConfigPhase>();
var lookup = new NamingFormatLookup();
var result = await sut.ProcessNaming(config, guide, lookup);
var result = await sut.ProcessNaming(guide, lookup);
result.Should().NotBeNull();
result.Should().BeEquivalentTo(new SonarrMediaNamingDto

@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Recyclarr.Cli.Pipelines.MediaNaming;
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming;
@ -29,7 +28,7 @@ public class MediaNamingTransactionPhaseRadarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(context.ConfigOutput.Dto, o => o.RespectingRuntimeTypes());
}
@ -52,7 +51,7 @@ public class MediaNamingTransactionPhaseRadarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(context.ApiFetchOutput, o => o.RespectingRuntimeTypes());
}
@ -80,7 +79,7 @@ public class MediaNamingTransactionPhaseRadarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(context.ConfigOutput.Dto, o => o.RespectingRuntimeTypes());
}
@ -108,7 +107,7 @@ public class MediaNamingTransactionPhaseRadarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new RadarrMediaNamingDto
{

@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Recyclarr.Cli.Pipelines.MediaNaming;
using Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.MediaNaming;
namespace Recyclarr.Cli.Tests.Pipelines.MediaNaming;
@ -31,7 +30,7 @@ public class MediaNamingTransactionPhaseSonarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(context.ConfigOutput.Dto, o => o.RespectingRuntimeTypes());
}
@ -57,7 +56,7 @@ public class MediaNamingTransactionPhaseSonarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(context.ApiFetchOutput, o => o.RespectingRuntimeTypes());
}
@ -91,7 +90,7 @@ public class MediaNamingTransactionPhaseSonarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(context.ConfigOutput.Dto, o => o.RespectingRuntimeTypes());
}
@ -125,7 +124,7 @@ public class MediaNamingTransactionPhaseSonarrTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new SonarrMediaNamingDto
{

@ -1,3 +1,4 @@
using AutoFixture;
using Recyclarr.Cli.Pipelines.CustomFormat.Models;
using Recyclarr.Cli.Pipelines.QualityProfile;
using Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
@ -17,18 +18,19 @@ public class QualityProfileConfigPhaseTest
};
}
[Test, AutoMockData]
public void All_cfs_use_score_override(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void All_cfs_use_score_override()
{
var fixture = NSubstituteFixture.Create();
var cache = fixture.Freeze<ProcessedCustomFormatCache>();
cache.AddCustomFormats(new[]
{
NewCf.DataWithScore("", "id1", 101, 1),
NewCf.DataWithScore("", "id2", 201, 2)
});
var config = SetupCfs(new CustomFormatConfig
fixture.Inject<IServiceConfiguration>(SetupCfs(new CustomFormatConfig
{
TrashIds = new[] {"id1", "id2"},
QualityProfiles = new List<QualityProfileScoreConfig>
@ -39,10 +41,12 @@ public class QualityProfileConfigPhaseTest
Score = 100
}
}
});
}));
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEquivalentTo(new[]
{
@ -51,18 +55,19 @@ public class QualityProfileConfigPhaseTest
o => o.Excluding(x => x.ShouldCreate));
}
[Test, AutoMockData]
public void All_cfs_use_guide_scores_with_no_override(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void All_cfs_use_guide_scores_with_no_override()
{
var fixture = NSubstituteFixture.Create();
var cache = fixture.Freeze<ProcessedCustomFormatCache>();
cache.AddCustomFormats(new[]
{
NewCf.DataWithScore("", "id1", 100, 1),
NewCf.DataWithScore("", "id2", 200, 2)
});
var config = SetupCfs(new CustomFormatConfig
fixture.Inject<IServiceConfiguration>(SetupCfs(new CustomFormatConfig
{
TrashIds = new[] {"id1", "id2"},
QualityProfiles = new List<QualityProfileScoreConfig>
@ -72,10 +77,11 @@ public class QualityProfileConfigPhaseTest
Name = "test_profile"
}
}
});
}));
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEquivalentTo(new[]
{
@ -84,18 +90,19 @@ public class QualityProfileConfigPhaseTest
o => o.Excluding(x => x.ShouldCreate));
}
[Test, AutoMockData]
public void No_cfs_returned_when_no_score_in_guide_or_config(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void No_cfs_returned_when_no_score_in_guide_or_config()
{
var fixture = NSubstituteFixture.Create();
var cache = fixture.Freeze<ProcessedCustomFormatCache>();
cache.AddCustomFormats(new[]
{
NewCf.Data("", "id1", 1),
NewCf.Data("", "id2", 2)
});
var config = SetupCfs(new CustomFormatConfig
fixture.Inject<IServiceConfiguration>(SetupCfs(new CustomFormatConfig
{
TrashIds = new[] {"id1", "id2"},
QualityProfiles = new List<QualityProfileScoreConfig>
@ -105,10 +112,11 @@ public class QualityProfileConfigPhaseTest
Name = "test_profile"
}
}
});
}));
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEquivalentTo(new[]
{
@ -117,17 +125,18 @@ public class QualityProfileConfigPhaseTest
o => o.Excluding(x => x.ShouldCreate).Excluding(x => x.ScorelessCfs));
}
[Test, AutoMockData]
public void Skip_duplicate_cfs_with_same_and_different_scores(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void Skip_duplicate_cfs_with_same_and_different_scores()
{
var fixture = NSubstituteFixture.Create();
var cache = fixture.Freeze<ProcessedCustomFormatCache>();
cache.AddCustomFormats(new[]
{
NewCf.DataWithScore("", "id1", 100, 1)
});
var config = SetupCfs(
fixture.Inject<IServiceConfiguration>(SetupCfs(
new CustomFormatConfig
{
TrashIds = new[] {"id1"}
@ -164,10 +173,11 @@ public class QualityProfileConfigPhaseTest
new() {Name = "test_profile2", Score = 100}
}
}
);
));
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEquivalentTo(new[]
{
@ -177,11 +187,12 @@ public class QualityProfileConfigPhaseTest
o => o.Excluding(x => x.ShouldCreate));
}
[Test, AutoMockData]
public void All_cfs_use_score_set(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void All_cfs_use_score_set()
{
var fixture = NSubstituteFixture.Create();
var cache = fixture.Freeze<ProcessedCustomFormatCache>();
cache.AddCustomFormats(new[]
{
NewCf.DataWithScores("", "id1", 1, ("default", 101), ("set1", 102)),
@ -210,9 +221,11 @@ public class QualityProfileConfigPhaseTest
}
}
};
fixture.Inject<IServiceConfiguration>(config);
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEquivalentTo(new[]
{
@ -224,12 +237,12 @@ public class QualityProfileConfigPhaseTest
o => o.Excluding(x => x.ShouldCreate));
}
[Test, AutoMockData]
public void Empty_trash_ids_list_is_ignored(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void Empty_trash_ids_list_is_ignored()
{
var config = SetupCfs(new CustomFormatConfig
var fixture = NSubstituteFixture.Create();
fixture.Inject<IServiceConfiguration>(SetupCfs(new CustomFormatConfig
{
TrashIds = Array.Empty<string>(),
QualityProfiles = new List<QualityProfileScoreConfig>
@ -240,33 +253,36 @@ public class QualityProfileConfigPhaseTest
Score = 100
}
}
});
}));
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEmpty();
}
[Test, AutoMockData]
public void Empty_quality_profiles_is_ignored(
[Frozen] ProcessedCustomFormatCache cache,
QualityProfileConfigPhase sut)
[Test]
public void Empty_quality_profiles_is_ignored()
{
var fixture = NSubstituteFixture.Create();
var cache = fixture.Freeze<ProcessedCustomFormatCache>();
cache.AddCustomFormats(new[]
{
NewCf.DataWithScore("", "id1", 101, 1),
NewCf.DataWithScore("", "id2", 201, 2)
});
var config = SetupCfs(new CustomFormatConfig
fixture.Inject<IServiceConfiguration>(SetupCfs(new CustomFormatConfig
{
TrashIds = new[] {"id1", "id2"},
QualityProfiles = Array.Empty<QualityProfileScoreConfig>()
});
}));
var context = new QualityProfilePipelineContext();
sut.Execute(context, config);
var sut = fixture.Create<QualityProfileConfigPhase>();
sut.Execute(context);
context.ConfigOutput.Should().BeEmpty();
}

@ -31,7 +31,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new QualityProfileTransactionData
{
@ -88,7 +88,7 @@ public class QualityProfileTransactionPhaseTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new QualityProfileTransactionData
{
@ -159,7 +159,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.ChangedProfiles.Should()
.ContainSingle().Which.Profile.UpdatedScores.Should()
@ -203,7 +203,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new QualityProfileTransactionData());
}
@ -246,7 +246,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.UnchangedProfiles.Should()
.ContainSingle().Which.Profile.UpdatedScores.Should()
@ -293,7 +293,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.ChangedProfiles.Should()
.ContainSingle().Which.Profile.UpdatedScores.Should()
@ -352,7 +352,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.ChangedProfiles.Should()
.ContainSingle().Which.Profile.UpdatedScores.Should()
@ -409,7 +409,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.ChangedProfiles.Should()
.ContainSingle().Which.Profile.UpdatedScores.Should()
@ -465,7 +465,7 @@ public class QualityProfileTransactionPhaseTest
ApiFetchOutput = new QualityProfileServiceData(dtos, new QualityProfileDto())
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.ChangedProfiles.Should()
.ContainSingle().Which.Profile.InvalidExceptCfNames.Should()

@ -10,13 +10,14 @@ namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeConfigPhaseTest
{
[Test, AutoMockData]
public void Do_nothing_if_no_quality_definition(QualitySizeConfigPhase sut)
public void Do_nothing_if_no_quality_definition(
[Frozen] IServiceConfiguration config,
QualitySizeConfigPhase sut)
{
var context = new QualitySizePipelineContext();
var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.ReturnsNull();
sut.Execute(context, config);
sut.Execute(context);
context.ConfigOutput.Should().BeNull();
}
@ -24,9 +25,9 @@ public class QualitySizeConfigPhaseTest
[Test, AutoMockData]
public void Do_nothing_if_no_matching_quality_definition(
[Frozen] IQualitySizeGuideService guide,
[Frozen] IServiceConfiguration config,
QualitySizeConfigPhase sut)
{
var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig {Type = "not_real"});
guide.GetQualitySizeData(default!).ReturnsForAnyArgs(new[]
@ -36,7 +37,7 @@ public class QualitySizeConfigPhaseTest
var context = new QualitySizePipelineContext();
sut.Execute(context, config);
sut.Execute(context);
context.ConfigOutput.Should().BeNull();
}
@ -48,9 +49,9 @@ public class QualitySizeConfigPhaseTest
string testPreferred,
string expectedPreferred,
[Frozen] IQualitySizeGuideService guide,
[Frozen] IServiceConfiguration config,
QualitySizeConfigPhase sut)
{
var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig
{
Type = "real",
@ -64,7 +65,7 @@ public class QualitySizeConfigPhaseTest
var context = new QualitySizePipelineContext();
sut.Execute(context, config);
sut.Execute(context);
config.QualityDefinition.Should().NotBeNull();
config.QualityDefinition!.PreferredRatio.Should().Be(decimal.Parse(expectedPreferred));
@ -73,9 +74,9 @@ public class QualitySizeConfigPhaseTest
[Test, AutoMockData]
public void Preferred_is_set_via_ratio(
[Frozen] IQualitySizeGuideService guide,
[Frozen] IServiceConfiguration config,
QualitySizeConfigPhase sut)
{
var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig
{
Type = "real",
@ -96,7 +97,7 @@ public class QualitySizeConfigPhaseTest
var context = new QualitySizePipelineContext();
sut.Execute(context, config);
sut.Execute(context);
context.ConfigOutput.Should().NotBeNull();
context.ConfigOutput!.Qualities.Should().BeEquivalentTo(new[]
@ -113,9 +114,9 @@ public class QualitySizeConfigPhaseTest
[Test, AutoMockData]
public void Preferred_is_set_via_guide(
[Frozen] IQualitySizeGuideService guide,
[Frozen] IServiceConfiguration config,
QualitySizeConfigPhase sut)
{
var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig
{
Type = "real"
@ -135,7 +136,7 @@ public class QualitySizeConfigPhaseTest
var context = new QualitySizePipelineContext();
sut.Execute(context, config);
sut.Execute(context);
context.ConfigOutput.Should().NotBeNull();
context.ConfigOutput!.Qualities.Should().BeEquivalentTo(new[]

@ -1,6 +1,5 @@
using Recyclarr.Cli.Pipelines.QualitySize;
using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityDefinition;
using Recyclarr.TrashGuide.QualitySize;
@ -32,7 +31,7 @@ public class QualitySizeTransactionPhaseTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEmpty();
}
@ -70,7 +69,7 @@ public class QualitySizeTransactionPhaseTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEmpty();
}
@ -108,7 +107,7 @@ public class QualitySizeTransactionPhaseTest
}
};
sut.Execute(context, Substitute.For<IServiceConfiguration>());
sut.Execute(context);
context.TransactionOutput.Should().BeEquivalentTo(new List<ServiceQualityDefinitionItem>
{

@ -1,7 +1,9 @@
using Flurl.Http.Testing;
using Recyclarr.Common;
using Recyclarr.Config;
using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.CustomFormat;
using Recyclarr.Tests.TestLibrary;
namespace Recyclarr.IntegrationTests;
@ -13,13 +15,18 @@ public class CustomFormatServiceTest : IntegrationTestFixture
{
var resourceData = new ResourceDataReader(typeof(CustomFormatServiceTest), "Data");
var jsonBody = resourceData.ReadData("issue_178.json");
var config = new RadarrConfiguration {InstanceName = "instance"};
using var http = new HttpTest();
http.RespondWith(jsonBody);
var sut = Resolve<CustomFormatApiService>();
var result = await sut.GetCustomFormats(config);
var scopeFactory = Resolve<ConfigurationScopeFactory>();
using var scope = scopeFactory.Start<TestConfigurationScope>(new RadarrConfiguration
{
InstanceName = "instance"
});
var sut = scope.Resolve<CustomFormatApiService>();
var result = await sut.GetCustomFormats();
result.Should().HaveCountGreaterThan(5);
}

@ -91,7 +91,7 @@ public abstract class IntegrationTestFixture : IDisposable
builder.RegisterMockFor<IServiceInformation>(m =>
{
// By default, choose some extremely high number so that all the newest features are enabled.
m.GetVersion(default!).ReturnsForAnyArgs(_ => new Version("99.0.0.0"));
m.GetVersion().ReturnsForAnyArgs(_ => new Version("99.0.0.0"));
});
}

@ -0,0 +1,12 @@
using Autofac;
using Recyclarr.Config;
namespace Recyclarr.Tests.TestLibrary;
public class TestConfigurationScope(ILifetimeScope scope) : ConfigurationScope(scope)
{
public T Resolve<T>() where T : notnull
{
return scope.Resolve<T>();
}
}

@ -1,6 +1,5 @@
using Recyclarr.Compatibility;
using Recyclarr.Compatibility.Sonarr;
using Recyclarr.Tests.TestLibrary;
namespace Recyclarr.Tests.Compatibility.Sonarr;
@ -12,13 +11,12 @@ public class SonarrCapabilityEnforcerTest
[Frozen] ISonarrCapabilityFetcher fetcher,
SonarrCapabilityEnforcer sut)
{
var config = NewConfig.Sonarr();
var min = SonarrCapabilities.MinimumVersion;
fetcher.GetCapabilities(default!).ReturnsForAnyArgs(
fetcher.GetCapabilities().ReturnsForAnyArgs(
new SonarrCapabilities(new Version(min.Major - 1, min.Minor, min.Build, min.Revision)));
var act = () => sut.Check(config);
var act = sut.Check;
act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*minimum*");
}

Loading…
Cancel
Save