fix: Add validation for duplicate instances

Two separate duplicate checks have been introduced:

1. Within the same YAML file, YamlDotNet has been instructed to error on
   duplicate keys.
2. Between different YAML files, custom logic enforces that there should
   be no duplicate instance names.
json-serializing-nullable-fields-issue
Robert Dailey 9 months ago
parent 6706a87972
commit 3a50b9fa61

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Service failures (e.g. HTTP 500) no longer cause exceptions (#206).
- Error out when duplicate instance names are used.
## [5.3.1] - 2023-08-21

@ -39,6 +39,11 @@ public class ConsoleExceptionHandler
_log.Error("The following instances do not exist: {Names}", e.InstanceNames);
break;
case DuplicateInstancesException e:
_log.Error("The following instance names are duplicated: {Names}", e.InstanceNames);
_log.Error("Instance names are unique and may not be reused");
break;
case SplitInstancesException e:
_log.Error("The following configs share the same `base_url`, which isn't allowed: {Instances}",
e.InstanceNames);

@ -52,4 +52,13 @@ public static class ConfigExtensions
return criteria.Instances
.Where(x => !configInstances.Contains(x, StringComparer.InvariantCultureIgnoreCase));
}
public static IEnumerable<string> GetDuplicateInstanceNames(this IEnumerable<IServiceConfiguration> configs)
{
return configs
.GroupBy(x => x.InstanceName, StringComparer.InvariantCultureIgnoreCase)
.Where(x => x.Count() > 1)
.Select(x => x.First().InstanceName)
.ToList();
}
}

@ -51,6 +51,12 @@ public class ConfigurationRegistry : IConfigurationRegistry
{
var loadedConfigs = configs.SelectMany(x => _loader.Load(x)).ToList();
var dupeInstances = loadedConfigs.GetDuplicateInstanceNames().ToList();
if (dupeInstances.Any())
{
throw new DuplicateInstancesException(dupeInstances);
}
var invalidInstances = loadedConfigs.GetInvalidInstanceNames(filterCriteria).ToList();
if (invalidInstances.Any())
{

@ -30,7 +30,8 @@ public class YamlSerializerFactory : IYamlSerializerFactory
builder
.WithNodeDeserializer(new ForceEmptySequences(_objectFactory))
.WithNodeTypeResolver(new ReadOnlyCollectionNodeTypeResolver())
.WithObjectFactory(_objectFactory);
.WithObjectFactory(_objectFactory)
.WithDuplicateKeyChecking();
foreach (var behavior in _behaviors)
{

@ -0,0 +1,11 @@
namespace Recyclarr.TrashLib.ExceptionTypes;
public class DuplicateInstancesException : Exception
{
public IReadOnlyCollection<string> InstanceNames { get; }
public DuplicateInstancesException(IReadOnlyCollection<string> instanceNames)
{
InstanceNames = instanceNames;
}
}

@ -105,4 +105,22 @@ public class ConfigExtensionsTest
result.Should().BeEquivalentTo("radarr1", "radarr2", "sonarr2", "sonarr3");
}
[Test]
public void Get_duplicate_instance_names()
{
var configs = new IServiceConfiguration[]
{
new RadarrConfiguration {InstanceName = "radarr1"},
new RadarrConfiguration {InstanceName = "radarr2"},
new RadarrConfiguration {InstanceName = "radarr2"},
new RadarrConfiguration {InstanceName = "radarr3"},
new SonarrConfiguration {InstanceName = "sonarr1"},
new SonarrConfiguration {InstanceName = "sonarr1"}
};
var result = configs.GetDuplicateInstanceNames();
result.Should().BeEquivalentTo("radarr2", "sonarr1");
}
}

@ -99,4 +99,40 @@ public class ConfigurationRegistryTest : TrashLibIntegrationFixture
act.Should().ThrowExactly<SplitInstancesException>()
.Which.InstanceNames.Should().BeEquivalentTo("instance1", "instance2");
}
[Test]
public void Duplicate_instance_names_are_prohibited()
{
var sut = Resolve<ConfigurationRegistry>();
Fs.AddFile("config1.yml", new MockFileData(
"""
radarr:
unique_name1:
base_url: http://localhost:7879
api_key: fdsa
same_instance_name:
base_url: http://localhost:7878
api_key: asdf
"""));
Fs.AddFile("config2.yml", new MockFileData(
"""
radarr:
same_instance_name:
base_url: http://localhost:7879
api_key: fdsa
unique_name2:
base_url: http://localhost:7879
api_key: fdsa
"""));
var act = () => sut.FindAndLoadConfigs(new ConfigFilterCriteria
{
ManualConfigFiles = new[] {"config1.yml", "config2.yml"}
});
act.Should().ThrowExactly<DuplicateInstancesException>()
.Which.InstanceNames.Should().BeEquivalentTo("same_instance_name");
}
}

Loading…
Cancel
Save