Fix: Validate release/push download client configuration

This commit is contained in:
soup 2025-10-11 20:06:46 +02:00
parent a4f210855e
commit b197a2d9cb
No known key found for this signature in database
2 changed files with 244 additions and 7 deletions

View file

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using Moq;
using NLog;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Download;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Test.Common;
using Sonarr.Api.V3.Indexers;
namespace NzbDrone.Api.Test.v3.Indexers;
[TestFixture]
public class ReleasePushControllerFixture : TestBase<ReleasePushController>
{
[SetUp]
public void SetupController()
{
var qualityProfile = new QualityProfile
{
Items = new List<QualityProfileQualityItem>
{
new()
{
Allowed = true,
Quality = Quality.Bluray720p
}
}
};
Mocker.SetConstant(LogManager.GetLogger(nameof(ReleasePushControllerFixture)));
Mocker.GetMock<IQualityProfileService>()
.Setup(x => x.GetDefaultProfile(It.IsAny<string>()))
.Returns(qualityProfile);
}
private ReleaseResource BuildRelease(Action<ReleaseResource> configure = null)
{
var resource = new ReleaseResource
{
Title = "Test Release",
DownloadUrl = "https://example.com/release.torrent",
PublishDate = DateTime.UtcNow,
Protocol = DownloadProtocol.Torrent
};
configure?.Invoke(resource);
return resource;
}
[Test]
public void should_fail_when_download_client_name_unknown()
{
Mocker.GetMock<IDownloadClientFactory>()
.Setup(x => x.All())
.Returns(new List<DownloadClientDefinition>());
var release = BuildRelease(r => r.DownloadClient = "missing-client");
var exception = Assert.Throws<ValidationException>(() => Subject.Create(release));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Errors.Select(e => e.PropertyName), Does.Contain("DownloadClient"));
}
[Test]
public void should_fail_when_download_client_name_disabled()
{
var disabledClient = new DownloadClientDefinition
{
Id = 5,
Name = "Disabled Client",
Enable = false
};
Mocker.GetMock<IDownloadClientFactory>()
.Setup(x => x.All())
.Returns(new List<DownloadClientDefinition> { disabledClient });
var release = BuildRelease(r => r.DownloadClient = "Disabled Client");
var exception = Assert.Throws<ValidationException>(() => Subject.Create(release));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Errors.Select(e => e.PropertyName), Does.Contain("DownloadClient"));
}
[Test]
public void should_fail_when_download_client_id_unknown()
{
const int requestedId = 42;
Mocker.GetMock<IDownloadClientFactory>()
.Setup(x => x.Get(requestedId))
.Throws(new ModelNotFoundException(typeof(DownloadClientDefinition), requestedId));
var release = BuildRelease(r => r.DownloadClientId = requestedId);
var exception = Assert.Throws<ValidationException>(() => Subject.Create(release));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Errors.Select(e => e.PropertyName), Does.Contain("DownloadClientId"));
}
[Test]
public void should_fail_when_download_client_id_disabled()
{
const int requestedId = 11;
var disabledClient = new DownloadClientDefinition
{
Id = requestedId,
Name = "Disabled Client",
Enable = false
};
Mocker.GetMock<IDownloadClientFactory>()
.Setup(x => x.Get(requestedId))
.Returns(disabledClient);
var release = BuildRelease(r => r.DownloadClientId = requestedId);
var exception = Assert.Throws<ValidationException>(() => Subject.Create(release));
Assert.That(exception, Is.Not.Null);
Assert.That(exception!.Errors.Select(e => e.PropertyName), Does.Contain("DownloadClientId"));
}
[Test]
public void should_fail_when_download_client_name_and_id_mismatch()
{
const int requestedId = 7;
var definitionByName = new DownloadClientDefinition
{
Id = 21,
Name = "Known Client",
Enable = true
};
var definitionById = new DownloadClientDefinition
{
Id = requestedId,
Name = "Different Client",
Enable = true
};
Mocker.GetMock<IDownloadClientFactory>()
.Setup(x => x.All())
.Returns(new List<DownloadClientDefinition> { definitionByName });
Mocker.GetMock<IDownloadClientFactory>()
.Setup(x => x.Get(requestedId))
.Returns(definitionById);
var release = BuildRelease(r =>
{
r.DownloadClient = "Known Client";
r.DownloadClientId = requestedId;
});
var exception = Assert.Throws<ValidationException>(() => Subject.Create(release));
Assert.That(exception, Is.Not.Null);
var properties = exception!.Errors.Select(e => e.PropertyName).ToList();
Assert.That(properties, Does.Contain("DownloadClient"));
Assert.That(properties, Does.Contain("DownloadClientId"));
}
}

View file

@ -120,20 +120,84 @@ private void ResolveIndexer(ReleaseInfo release)
private int? ResolveDownloadClientId(ReleaseResource release)
{
var downloadClientId = release.DownloadClientId.GetValueOrDefault();
var requestedId = release.DownloadClientId.GetValueOrDefault();
var requestedName = release.DownloadClient?.Trim();
var resolvedClientByName = default(DownloadClientDefinition);
var resolvedClient = default(DownloadClientDefinition);
if (downloadClientId == 0 && release.DownloadClient.IsNotNullOrWhiteSpace())
if (requestedName.IsNotNullOrWhiteSpace())
{
var downloadClient = _downloadClientFactory.All().FirstOrDefault(v => v.Name.EqualsIgnoreCase(release.DownloadClient));
resolvedClientByName = _downloadClientFactory.All()
.FirstOrDefault(v => v.Name.EqualsIgnoreCase(requestedName));
if (downloadClient != null)
if (resolvedClientByName != null)
{
_logger.Debug("Push Release {0} associated with download client {1} - {2}.", release.Title, downloadClientId, release.DownloadClient);
if (!resolvedClientByName.Enable)
{
throw new ValidationException(new List<ValidationFailure>
{
new("DownloadClient", "Download client is disabled.", requestedName)
});
}
return downloadClient.Id;
release.DownloadClient = resolvedClientByName.Name;
resolvedClient = resolvedClientByName;
}
else if (requestedId == 0)
{
throw new ValidationException(new List<ValidationFailure>
{
new("DownloadClient", "Download client does not exist.", requestedName)
});
}
}
if (requestedId != 0)
{
DownloadClientDefinition clientById;
try
{
clientById = _downloadClientFactory.Get(requestedId);
}
catch (ModelNotFoundException)
{
throw new ValidationException(new List<ValidationFailure>
{
new("DownloadClientId", "Download client does not exist.", requestedId.ToString())
});
}
_logger.Debug("Push Release {0} not associated with known download client {1}.", release.Title, release.DownloadClient);
if (resolvedClientByName != null && clientById.Id != resolvedClientByName.Id)
{
throw new ValidationException(new List<ValidationFailure>
{
new("DownloadClientId", "Download client id does not match the provided name.", requestedId.ToString()),
new("DownloadClient", "Download client name does not match the provided id.", requestedName)
});
}
if (!clientById.Enable)
{
throw new ValidationException(new List<ValidationFailure>
{
new("DownloadClientId", "Download client is disabled.", requestedId.ToString())
});
}
if (resolvedClientByName == null && requestedName.IsNotNullOrWhiteSpace())
{
release.DownloadClient = clientById.Name;
}
resolvedClient = clientById;
}
if (resolvedClient != null)
{
_logger.Debug("Push Release {0} associated with download client {1} - {2}.", release.Title, resolvedClient.Id, resolvedClient.Name);
return resolvedClient.Id;
}
return release.DownloadClientId;