mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
Add support for qBittorrent API key (#832)
This commit is contained in:
1 parent
85e956dd92
commit
705957759e
19 files changed
+770
-9
No files matched your search
+101
@@ -101,6 +101,49 @@ public class DownloadClientControllerTests : IDisposable
|
||||
(await _dataContext.DownloadClients.CountAsync()).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateDownloadClientConfig_QBittorrentWithApiKey_PersistsApiKey()
|
||||
{
|
||||
// Arrange
|
||||
var request = new CreateDownloadClientRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "qbit-key-client",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Host = "http://localhost:8080",
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
};
|
||||
|
||||
// Act
|
||||
await _controller.CreateDownloadClientConfig(request);
|
||||
|
||||
// Assert
|
||||
var saved = await _dataContext.DownloadClients.AsNoTracking().FirstAsync(c => c.Name == "qbit-key-client");
|
||||
saved.ApiKey.ShouldBe("qbt_abcdefghijklmnopqrstuvwxyz12");
|
||||
saved.Username.ShouldBeNull();
|
||||
saved.Password.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateDownloadClientConfig_QBittorrentWithApiKeyAndPassword_PropagatesValidationException()
|
||||
{
|
||||
// Arrange
|
||||
var request = new CreateDownloadClientRequest
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "qbit-conflict-client",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Host = "http://localhost:8080",
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
Password = "pass",
|
||||
};
|
||||
|
||||
// Act / Assert
|
||||
Cleanuparr.Domain.Exceptions.ValidationException exception = await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
|
||||
() => _controller.CreateDownloadClientConfig(request));
|
||||
exception.Message.ShouldBe("Use either an API key or a username and password, not both");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateDownloadClientConfig_SabnzbdWithNoType_StoresUsenet()
|
||||
{
|
||||
@@ -241,6 +284,44 @@ public class DownloadClientControllerTests : IDisposable
|
||||
saved.Host!.ToString().ShouldContain("newhost");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDownloadClientConfig_SwitchFromCredentialsToApiKey_ClearingUsernameAndPassword_Succeeds()
|
||||
{
|
||||
// Arrange
|
||||
DownloadClientConfig client = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "qbit-client",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Type = DownloadClientType.Torrent,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
Username = "admin",
|
||||
Password = "admin",
|
||||
};
|
||||
_dataContext.DownloadClients.Add(client);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
UpdateDownloadClientRequest request = new()
|
||||
{
|
||||
Name = "qbit-client",
|
||||
Host = "http://localhost:8080",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Username = null,
|
||||
Password = null,
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _controller.UpdateDownloadClientConfig(client.Id, request);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<OkObjectResult>();
|
||||
DownloadClientConfig saved = await _dataContext.DownloadClients.AsNoTracking().FirstAsync(c => c.Id == client.Id);
|
||||
saved.ApiKey.ShouldBe("qbt_abcdefghijklmnopqrstuvwxyz12");
|
||||
saved.Username.ShouldBeNull();
|
||||
saved.Password.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDownloadClientConfig_PlaceholderApiKeyOnClientWithNoStoredKey_PropagatesValidationException()
|
||||
{
|
||||
@@ -406,6 +487,26 @@ public class DownloadClientControllerTests : IDisposable
|
||||
result.ShouldBeOfType<ObjectResult>().StatusCode.ShouldBe(400);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestDownloadClient_QBittorrentWithApiKeyAndPassword_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var request = new TestDownloadClientRequest
|
||||
{
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Host = "http://localhost:8080",
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
Password = "pass",
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _controller.TestDownloadClient(request);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<ObjectResult>().StatusCode.ShouldBe(400);
|
||||
_downloadServiceFactory.DidNotReceive().GetDownloadService(Arg.Any<DownloadClientConfig>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestDownloadClient_NewApiKeyWithPlaceholderPassword_UsesNewApiKeyAndStoredPassword()
|
||||
{
|
||||
|
||||
+6
@@ -44,6 +44,12 @@ public sealed record TestDownloadClientRequest
|
||||
{
|
||||
throw new ValidationException("API key cannot be empty");
|
||||
}
|
||||
|
||||
if (TypeName.SupportsApiKeyAndCredentials() && !string.IsNullOrWhiteSpace(ApiKey) &&
|
||||
(!string.IsNullOrWhiteSpace(Username) || !string.IsNullOrWhiteSpace(Password)))
|
||||
{
|
||||
throw new ValidationException("Use either an API key or a username and password, not both");
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadClientConfig ToTestConfig(string? resolvedPassword = null, string? resolvedApiKey = null)
|
||||
|
||||
@@ -4,7 +4,7 @@ public static class DownloadClientTypeNameExtensions
|
||||
{
|
||||
private static readonly Dictionary<DownloadClientTypeName, DownloadClientAuthField[]> AuthFields = new()
|
||||
{
|
||||
[DownloadClientTypeName.qBittorrent] = [DownloadClientAuthField.Username, DownloadClientAuthField.Password],
|
||||
[DownloadClientTypeName.qBittorrent] = [DownloadClientAuthField.Username, DownloadClientAuthField.Password, DownloadClientAuthField.ApiKey],
|
||||
[DownloadClientTypeName.Deluge] = [DownloadClientAuthField.Password],
|
||||
[DownloadClientTypeName.Transmission] = [DownloadClientAuthField.Username, DownloadClientAuthField.Password],
|
||||
[DownloadClientTypeName.uTorrent] = [DownloadClientAuthField.Username, DownloadClientAuthField.Password],
|
||||
@@ -38,6 +38,13 @@ public static class DownloadClientTypeNameExtensions
|
||||
public static bool RequiresAuthField(this DownloadClientTypeName typeName, DownloadClientAuthField field) =>
|
||||
AuthFields.TryGetValue(typeName, out var fields) && fields is [var only] && only == field;
|
||||
|
||||
/// <summary>
|
||||
/// True when the client type accepts an API key alongside a username or password, so the two must not be mixed.
|
||||
/// </summary>
|
||||
public static bool SupportsApiKeyAndCredentials(this DownloadClientTypeName typeName) =>
|
||||
typeName.SupportsAuthField(DownloadClientAuthField.ApiKey) &&
|
||||
(typeName.SupportsAuthField(DownloadClientAuthField.Username) || typeName.SupportsAuthField(DownloadClientAuthField.Password));
|
||||
|
||||
private static readonly DownloadClientCapability[] BaseTorrentCapabilities =
|
||||
[
|
||||
DownloadClientCapability.QueueCheck,
|
||||
|
||||
+196
@@ -1,10 +1,12 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using NSubstitute;
|
||||
@@ -1699,4 +1701,198 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<TorrentContentPriority>());
|
||||
}
|
||||
}
|
||||
|
||||
public class ApiKeyAuthScenarios : QBitServiceTests
|
||||
{
|
||||
public ApiKeyAuthScenarios(QBitServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
private static DownloadClientConfig CreateApiKeyConfig() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Client",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Type = DownloadClientType.Torrent,
|
||||
Enabled = true,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
UrlBase = ""
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ApiKeyConfigured_SetsBearerAuthorizationHeader()
|
||||
{
|
||||
// Arrange
|
||||
HttpClient httpClient = new();
|
||||
_fixture.HttpClientProvider.CreateClient(Arg.Any<DownloadClientConfig>()).Returns(httpClient);
|
||||
DownloadClientConfig config = CreateApiKeyConfig();
|
||||
|
||||
// Act
|
||||
using QBitService sut = new(
|
||||
_fixture.Logger,
|
||||
_fixture.FilenameEvaluator,
|
||||
_fixture.Striker,
|
||||
_fixture.DryRunInterceptor,
|
||||
_fixture.HardLinkFileService,
|
||||
_fixture.HttpClientProvider,
|
||||
_fixture.EventPublisher,
|
||||
_fixture.BlocklistProvider,
|
||||
config,
|
||||
_fixture.RuleEvaluator,
|
||||
_fixture.SeedingRuleEvaluator,
|
||||
TimeProvider.System
|
||||
);
|
||||
|
||||
// Assert
|
||||
httpClient.DefaultRequestHeaders.Authorization?.Scheme.ShouldBe("Bearer");
|
||||
httpClient.DefaultRequestHeaders.Authorization?.Parameter.ShouldBe(config.ApiKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WhitespaceApiKey_DoesNotSetAuthorizationHeader()
|
||||
{
|
||||
// Arrange
|
||||
HttpClient httpClient = new();
|
||||
_fixture.HttpClientProvider.CreateClient(Arg.Any<DownloadClientConfig>()).Returns(httpClient);
|
||||
DownloadClientConfig config = CreateApiKeyConfig();
|
||||
config.ApiKey = " ";
|
||||
|
||||
// Act
|
||||
using QBitService sut = new(
|
||||
_fixture.Logger,
|
||||
_fixture.FilenameEvaluator,
|
||||
_fixture.Striker,
|
||||
_fixture.DryRunInterceptor,
|
||||
_fixture.HardLinkFileService,
|
||||
_fixture.HttpClientProvider,
|
||||
_fixture.EventPublisher,
|
||||
_fixture.BlocklistProvider,
|
||||
config,
|
||||
_fixture.RuleEvaluator,
|
||||
_fixture.SeedingRuleEvaluator,
|
||||
TimeProvider.System
|
||||
);
|
||||
|
||||
// Assert
|
||||
httpClient.DefaultRequestHeaders.Authorization.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_CredentialsConfigured_LogsIn()
|
||||
{
|
||||
// Arrange
|
||||
DownloadClientConfig config = CreateApiKeyConfig();
|
||||
config.ApiKey = null;
|
||||
config.Username = "admin";
|
||||
config.Password = "admin";
|
||||
QBitService sut = _fixture.CreateSut(config);
|
||||
|
||||
// Act
|
||||
await sut.LoginAsync();
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.LoginAsync(config.Username, config.Password);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HealthCheckAsync_CredentialsConfigured_LogsInAndReportsHealthy()
|
||||
{
|
||||
// Arrange
|
||||
DownloadClientConfig config = CreateApiKeyConfig();
|
||||
config.ApiKey = null;
|
||||
config.Username = "admin";
|
||||
config.Password = "admin";
|
||||
QBitService sut = _fixture.CreateSut(config);
|
||||
|
||||
// Act
|
||||
HealthCheckResult result = await sut.HealthCheckAsync();
|
||||
|
||||
// Assert
|
||||
result.IsHealthy.ShouldBeTrue();
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.LoginAsync(config.Username, config.Password);
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.GetAlternativeSpeedLimitsEnabledAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HealthCheckAsync_WhitespaceApiKey_UsesCredentials()
|
||||
{
|
||||
// Arrange
|
||||
DownloadClientConfig config = CreateApiKeyConfig();
|
||||
config.ApiKey = " ";
|
||||
config.Username = "admin";
|
||||
config.Password = "admin";
|
||||
QBitService sut = _fixture.CreateSut(config);
|
||||
|
||||
// Act
|
||||
HealthCheckResult result = await sut.HealthCheckAsync();
|
||||
|
||||
// Assert
|
||||
result.IsHealthy.ShouldBeTrue();
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.LoginAsync(config.Username, config.Password);
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.GetAlternativeSpeedLimitsEnabledAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoginAsync_ApiKeyConfigured_SkipsLogin()
|
||||
{
|
||||
// Arrange
|
||||
QBitService sut = _fixture.CreateSut(CreateApiKeyConfig());
|
||||
|
||||
// Act
|
||||
await sut.LoginAsync();
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.LoginAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HealthCheckAsync_ApiKeyConfigured_ChecksAlternativeSpeedLimitsEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
QBitService sut = _fixture.CreateSut(CreateApiKeyConfig());
|
||||
_fixture.ClientWrapper.GetAlternativeSpeedLimitsEnabledAsync().Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sut.HealthCheckAsync();
|
||||
|
||||
// Assert
|
||||
result.IsHealthy.ShouldBeTrue();
|
||||
await _fixture.ClientWrapper.Received(1).GetAlternativeSpeedLimitsEnabledAsync();
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.LoginAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HealthCheckAsync_ApiKeyConfigured_EndpointThrows_ReportsUnhealthy()
|
||||
{
|
||||
// Arrange
|
||||
QBitService sut = _fixture.CreateSut(CreateApiKeyConfig());
|
||||
_fixture.ClientWrapper
|
||||
.GetAlternativeSpeedLimitsEnabledAsync()
|
||||
.ThrowsAsync(new HttpRequestException("403 Forbidden"));
|
||||
|
||||
// Act
|
||||
var result = await sut.HealthCheckAsync();
|
||||
|
||||
// Assert
|
||||
result.IsHealthy.ShouldBeFalse();
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.LoginAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -1,3 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
@@ -40,6 +41,11 @@ public partial class QBitService : TorrentDownloadService, IDeadTorrentCapable
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator, timeProvider
|
||||
)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(downloadClientConfig.ApiKey))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", downloadClientConfig.ApiKey);
|
||||
}
|
||||
|
||||
var qBittorrentClient = new QBittorrentClient(_httpClient, downloadClientConfig.Url);
|
||||
_client = new QBittorrentClientWrapper(qBittorrentClient);
|
||||
}
|
||||
@@ -69,6 +75,12 @@ public partial class QBitService : TorrentDownloadService, IDeadTorrentCapable
|
||||
|
||||
public override async Task LoginAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_downloadClientConfig.ApiKey))
|
||||
{
|
||||
_logger.LogDebug("API key configured for client {ClientId}, skipping login", _downloadClientConfig.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_downloadClientConfig.Username) && string.IsNullOrEmpty(_downloadClientConfig.Password))
|
||||
{
|
||||
_logger.LogDebug("No credentials configured for client {ClientId}, skipping login", _downloadClientConfig.Id);
|
||||
@@ -93,10 +105,16 @@ public partial class QBitService : TorrentDownloadService, IDeadTorrentCapable
|
||||
|
||||
try
|
||||
{
|
||||
bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) ||
|
||||
bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) ||
|
||||
!string.IsNullOrEmpty(_downloadClientConfig.Password);
|
||||
|
||||
if (hasCredentials)
|
||||
if (!string.IsNullOrWhiteSpace(_downloadClientConfig.ApiKey))
|
||||
{
|
||||
// login with an API key returns 403, so probe an auth-protected endpoint
|
||||
await _client.GetAlternativeSpeedLimitsEnabledAsync();
|
||||
_logger.LogDebug("Health check: Successfully authenticated to qBittorrent client {ClientId}", _downloadClientConfig.Id);
|
||||
}
|
||||
else if (hasCredentials)
|
||||
{
|
||||
// If credentials are provided, we must be able to log in for the service to be healthy
|
||||
await _client.LoginAsync(_downloadClientConfig.Username, _downloadClientConfig.Password);
|
||||
|
||||
+65
@@ -226,6 +226,71 @@ public sealed class DownloadClientConfigTests
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_QBittorrentWithApiKeyOnly_DoesNotThrow()
|
||||
{
|
||||
var config = new DownloadClientConfig
|
||||
{
|
||||
Name = "My qBittorrent",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Type = DownloadClientType.Torrent,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12"
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_QBittorrentWithApiKeyAndPassword_ThrowsValidationException()
|
||||
{
|
||||
var config = new DownloadClientConfig
|
||||
{
|
||||
Name = "My qBittorrent",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Type = DownloadClientType.Torrent,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
Password = "pass"
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("Use either an API key or a username and password, not both");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_QBittorrentWithApiKeyAndUsername_ThrowsValidationException()
|
||||
{
|
||||
var config = new DownloadClientConfig
|
||||
{
|
||||
Name = "My qBittorrent",
|
||||
TypeName = DownloadClientTypeName.qBittorrent,
|
||||
Type = DownloadClientType.Torrent,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
ApiKey = "qbt_abcdefghijklmnopqrstuvwxyz12",
|
||||
Username = "admin"
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("Use either an API key or a username and password, not both");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SabnzbdWithApiKeyAndPassword_DoesNotThrow()
|
||||
{
|
||||
var config = new DownloadClientConfig
|
||||
{
|
||||
Name = "My Sabnzbd",
|
||||
TypeName = DownloadClientTypeName.Sabnzbd,
|
||||
Type = DownloadClientType.Usenet,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
ApiKey = "a-real-key",
|
||||
Password = "unused"
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Url Property Tests
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Persistence.Tests.Models.Configuration;
|
||||
|
||||
public sealed class DownloadClientTypeNameExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SupportsAuthField_QBittorrent_SupportsApiKey()
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent.SupportsAuthField(DownloadClientAuthField.ApiKey).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAuthField_QBittorrent_StillSupportsUsernameAndPassword()
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent.SupportsAuthField(DownloadClientAuthField.Username).ShouldBeTrue();
|
||||
DownloadClientTypeName.qBittorrent.SupportsAuthField(DownloadClientAuthField.Password).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresAuthField_QBittorrentApiKey_IsFalse()
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent.RequiresAuthField(DownloadClientAuthField.ApiKey).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsApiKeyAndCredentials_QBittorrent_IsTrue()
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent.SupportsApiKeyAndCredentials().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsApiKeyAndCredentials_Sabnzbd_IsFalse()
|
||||
{
|
||||
DownloadClientTypeName.Sabnzbd.SupportsApiKeyAndCredentials().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsApiKeyAndCredentials_Transmission_IsFalse()
|
||||
{
|
||||
DownloadClientTypeName.Transmission.SupportsApiKeyAndCredentials().ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
@@ -120,5 +120,11 @@ public sealed record DownloadClientConfig
|
||||
{
|
||||
throw new ValidationException("API key cannot be empty");
|
||||
}
|
||||
|
||||
if (TypeName.SupportsApiKeyAndCredentials() && !string.IsNullOrWhiteSpace(ApiKey) &&
|
||||
(!string.IsNullOrWhiteSpace(Username) || !string.IsNullOrWhiteSpace(Password)))
|
||||
{
|
||||
throw new ValidationException("Use either an API key or a username and password, not both");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user