Rename validation exception and fix its references (#842)

This commit is contained in:
Flaminel authored and GitHub committed 2026-10-15 10:50:12 +03:00
1 parent 40291e974f
commit f1b4bb674c
80 files changed
+409 -413

No files matched your search

@@ -132,7 +132,7 @@ public class ArrConfigControllerTests : IDisposable
var request = new UpdateArrConfigRequest { FailedImportMaxStrikes = strikes };
// Act & Assert
await Should.ThrowAsync<ValidationException>(() => _controller.UpdateSonarrConfig(request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.UpdateSonarrConfig(request));
}
#endregion
@@ -177,7 +177,7 @@ public class ArrConfigControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.CreateSonarrInstance(request));
}
@@ -4,6 +4,7 @@ using Cleanuparr.Api.Features.DownloadCleaner.Controllers;
using Cleanuparr.Api.Tests.Features.DownloadCleaner.TestHelpers;
using Cleanuparr.Api.Tests.TestHelpers;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Microsoft.AspNetCore.Http;
@@ -12,7 +13,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Shouldly;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Api.Tests.Features.DownloadCleaner;
@@ -83,7 +83,7 @@ public class DeadTorrentConfigControllerTests : IDisposable
{
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
await Should.ThrowAsync<ValidationException>(() => _controller.UpdateDeadTorrentConfig(client.Id, ValidRequest(maxStrikes: 2)));
await Should.ThrowAsync<CustomValidationException>(() => _controller.UpdateDeadTorrentConfig(client.Id, ValidRequest(maxStrikes: 2)));
}
[Fact]
@@ -134,7 +134,7 @@ public class DownloadCleanerConfigControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateDownloadCleanerConfig(request));
}
@@ -269,7 +269,7 @@ public class SeedingRulesControllerTests : IDisposable
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
var request = CreateValidRequest(categories: []);
await Should.ThrowAsync<ValidationException>(() => _controller.CreateSeedingRule(client.Id, request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.CreateSeedingRule(client.Id, request));
}
[Fact]
@@ -338,7 +338,7 @@ public class SeedingRulesControllerTests : IDisposable
DownloadClientConfig client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
SeedingRuleRequest request = CreateValidRequest(action: SeedingRuleAction.Unknown);
await Should.ThrowAsync<ValidationException>(() => _controller.CreateSeedingRule(client.Id, request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.CreateSeedingRule(client.Id, request));
}
[Fact]
@@ -347,7 +347,7 @@ public class SeedingRulesControllerTests : IDisposable
DownloadClientConfig client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
SeedingRuleRequest request = CreateValidRequest(action: (SeedingRuleAction)5);
await Should.ThrowAsync<ValidationException>(() => _controller.CreateSeedingRule(client.Id, request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.CreateSeedingRule(client.Id, request));
}
// ──────────────────────────────────────────────────────────────────────
@@ -444,7 +444,7 @@ public class SeedingRulesControllerTests : IDisposable
// Both maxRatio and maxSeedTime negative → validation failure
var request = CreateValidRequest(maxRatio: -1, maxSeedTime: -1);
await Should.ThrowAsync<ValidationException>(() => _controller.UpdateSeedingRule(rule.Id, request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.UpdateSeedingRule(rule.Id, request));
}
[Theory]
@@ -474,7 +474,7 @@ public class SeedingRulesControllerTests : IDisposable
SeedingRuleRequest request = CreateValidRequest(action: SeedingRuleAction.Unknown);
await Should.ThrowAsync<ValidationException>(() => _controller.UpdateSeedingRule(rule.Id, request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.UpdateSeedingRule(rule.Id, request));
}
[Fact]
@@ -485,7 +485,7 @@ public class SeedingRulesControllerTests : IDisposable
SeedingRuleRequest request = CreateValidRequest(action: (SeedingRuleAction)5);
await Should.ThrowAsync<ValidationException>(() => _controller.UpdateSeedingRule(rule.Id, request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.UpdateSeedingRule(rule.Id, request));
}
// ──────────────────────────────────────────────────────────────────────
@@ -164,7 +164,7 @@ public class UnlinkedConfigControllerTests : IDisposable
};
// Act + Assert
await Should.ThrowAsync<ValidationException>(
await Should.ThrowAsync<CustomValidationException>(
() => _controller.UpdateUnlinkedConfig(client.Id, dto));
}
@@ -181,7 +181,7 @@ public class UnlinkedConfigControllerTests : IDisposable
};
// Act + Assert
await Should.ThrowAsync<ValidationException>(
await Should.ThrowAsync<CustomValidationException>(
() => _controller.UpdateUnlinkedConfig(client.Id, dto));
}
@@ -139,7 +139,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
Cleanuparr.Domain.Exceptions.ValidationException exception = await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
Cleanuparr.Domain.Exceptions.CustomValidationException exception = await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.CreateDownloadClientConfig(request));
exception.Message.ShouldBe("Use either an API key or a username and password, not both");
}
@@ -204,7 +204,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert — Validate throws, controller's generic catch logs and re-throws
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.CreateDownloadClientConfig(request));
}
@@ -220,7 +220,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.CreateDownloadClientConfig(request));
}
@@ -236,7 +236,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.CreateDownloadClientConfig(request));
}
@@ -339,7 +339,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateDownloadClientConfig(client.Id, request));
}
@@ -359,7 +359,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateDownloadClientConfig(client.Id, request));
}
@@ -379,7 +379,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateDownloadClientConfig(client.Id, request));
}
@@ -399,7 +399,7 @@ public class DownloadClientControllerTests : IDisposable
};
// Act / Assert
Cleanuparr.Domain.Exceptions.ValidationException exception = await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
Cleanuparr.Domain.Exceptions.CustomValidationException exception = await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateDownloadClientConfig(client.Id, request));
exception.Message.ShouldContain("cannot be changed");
@@ -118,7 +118,7 @@ public class MalwareBlockerConfigControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateMalwareBlockerConfig(request));
await _jobManagementService.DidNotReceive().StartJob(Arg.Any<JobType>(), Arg.Any<Cleanuparr.Infrastructure.Models.JobSchedule?>(), Arg.Any<string?>());
}
@@ -106,7 +106,7 @@ public class QueueCleanerConfigControllerTests : IDisposable
};
// Act / Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateQueueCleanerConfig(request));
await _jobManagementService.DidNotReceive().StartJob(Arg.Any<JobType>(), Arg.Any<Cleanuparr.Infrastructure.Models.JobSchedule?>(), Arg.Any<string?>());
}
@@ -125,7 +125,7 @@ public class QueueCleanerConfigControllerTests : IDisposable
};
// Act + Assert
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.ValidationException>(
await Should.ThrowAsync<Cleanuparr.Domain.Exceptions.CustomValidationException>(
() => _controller.UpdateQueueCleanerConfig(request));
}
@@ -3,6 +3,7 @@ using Cleanuparr.Api.Features.Seeker.Contracts.Responses;
using Cleanuparr.Api.Features.Seeker.Controllers;
using Cleanuparr.Api.Tests.Features.Seeker.TestHelpers;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.Seeker;
@@ -11,7 +12,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Shouldly;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Api.Tests.Features.Seeker;
@@ -115,7 +115,7 @@ public class SeekerConfigControllerTests : IDisposable
]
};
await Should.ThrowAsync<ValidationException>(() => _controller.UpdateSeekerConfig(request));
await Should.ThrowAsync<CustomValidationException>(() => _controller.UpdateSeekerConfig(request));
}
[Fact]
@@ -3,12 +3,12 @@ using Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
using Cleanuparr.Api.Features.Auth.Contracts.Requests;
using Cleanuparr.Api.Features.General.Contracts.Requests;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Auth;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Shared.Helpers;
using Shouldly;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Api.Tests.Features.SensitiveData;
@@ -90,7 +90,7 @@ public class SensitiveDataInputTests
Version = 4,
};
Should.Throw<ValidationException>(() => request.ToEntity(Guid.NewGuid()));
Should.Throw<CustomValidationException>(() => request.ToEntity(Guid.NewGuid()));
}
[Fact]
@@ -122,7 +122,7 @@ public class SensitiveDataInputTests
Version = 4,
};
Should.Throw<ValidationException>(() => request.ToTestInstance());
Should.Throw<CustomValidationException>(() => request.ToTestInstance());
}
[Fact]
@@ -228,7 +228,7 @@ public class SensitiveDataInputTests
Password = Placeholder,
};
Should.Throw<ValidationException>(() => request.Validate());
Should.Throw<CustomValidationException>(() => request.Validate());
}
[Fact]
@@ -274,7 +274,7 @@ public class SensitiveDataInputTests
};
request.Validate();
Should.Throw<ValidationException>(() => request.ToTestConfig());
Should.Throw<CustomValidationException>(() => request.ToTestConfig());
}
[Fact]
@@ -47,7 +47,7 @@ public class GlobalExceptionHandlerTests
[Fact]
public async Task ValidationException_MapsTo400_WithMessageAsDetail()
{
(bool handled, HttpContext context, ProblemDetails problemDetails) = await Handle(new ValidationException("Name is required"));
(bool handled, HttpContext context, ProblemDetails problemDetails) = await Handle(new CustomValidationException("Name is required"));
handled.ShouldBeTrue();
context.Response.StatusCode.ShouldBe(StatusCodes.Status400BadRequest);
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Shared.Helpers;
@@ -29,7 +28,7 @@ public sealed record ArrInstanceRequest
{
if (ApiKey.IsPlaceholder())
{
throw new ValidationException("API key is required when creating a new instance");
throw new CustomValidationException("API key is required when creating a new instance");
}
return new()
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Shared.Helpers;
@@ -26,7 +25,7 @@ public sealed record TestArrInstanceRequest
if (apiKey.IsPlaceholder())
{
throw new ValidationException("API key cannot be a placeholder value");
throw new CustomValidationException("API key cannot be a placeholder value");
}
return new()
@@ -1,8 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Responses;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Infrastructure.Utilities;
using Cleanuparr.Persistence;
@@ -11,7 +10,6 @@ using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Api.Features.DownloadCleaner.Controllers;
@@ -113,7 +111,7 @@ public sealed class DownloadCleanerConfigController : ControllerBase
{
if (newConfigDto is null)
{
throw new ValidationException("Request body cannot be null");
throw new CustomValidationException("Request body cannot be null");
}
// Validate cron expression format
@@ -35,42 +35,42 @@ public sealed record CreateDownloadClientRequest
{
if (TypeName is DownloadClientTypeName.Unknown || !Enum.IsDefined(TypeName))
{
throw new ValidationException("Unsupported download client type");
throw new CustomValidationException("Unsupported download client type");
}
if (string.IsNullOrWhiteSpace(Name))
{
throw new ValidationException("Client name cannot be empty");
throw new CustomValidationException("Client name cannot be empty");
}
if (string.IsNullOrWhiteSpace(Host))
{
throw new ValidationException("Host cannot be empty");
throw new CustomValidationException("Host cannot be empty");
}
if (!Uri.TryCreate(Host, UriKind.RelativeOrAbsolute, out _))
{
throw new ValidationException("Host is not a valid URL");
throw new CustomValidationException("Host is not a valid URL");
}
if (!string.IsNullOrWhiteSpace(ExternalUrl) && !Uri.TryCreate(ExternalUrl, UriKind.RelativeOrAbsolute, out _))
{
throw new ValidationException("External URL is not a valid URL");
throw new CustomValidationException("External URL is not a valid URL");
}
if (Password.IsPlaceholder())
{
throw new ValidationException("Password cannot be a placeholder value");
throw new CustomValidationException("Password cannot be a placeholder value");
}
if (ApiKey.IsPlaceholder())
{
throw new ValidationException("API key cannot be a placeholder value");
throw new CustomValidationException("API key cannot be a placeholder value");
}
if (TypeName.RequiresAuthField(DownloadClientAuthField.ApiKey) && string.IsNullOrWhiteSpace(ApiKey))
{
throw new ValidationException("API key cannot be empty");
throw new CustomValidationException("API key cannot be empty");
}
}
@@ -27,28 +27,28 @@ public sealed record TestDownloadClientRequest
{
if (TypeName is DownloadClientTypeName.Unknown || !Enum.IsDefined(TypeName))
{
throw new ValidationException("Unsupported download client type");
throw new CustomValidationException("Unsupported download client type");
}
if (string.IsNullOrWhiteSpace(Host))
{
throw new ValidationException("Host cannot be empty");
throw new CustomValidationException("Host cannot be empty");
}
if (!Uri.TryCreate(Host, UriKind.RelativeOrAbsolute, out _))
{
throw new ValidationException("Host is not a valid URL");
throw new CustomValidationException("Host is not a valid URL");
}
if (TypeName.RequiresAuthField(DownloadClientAuthField.ApiKey) && string.IsNullOrWhiteSpace(ApiKey) && !ApiKey.IsPlaceholder())
{
throw new ValidationException("API key cannot be empty");
throw new CustomValidationException("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");
throw new CustomValidationException("Use either an API key or a username and password, not both");
}
}
@@ -59,12 +59,12 @@ public sealed record TestDownloadClientRequest
if (password.IsPlaceholder())
{
throw new ValidationException("Password cannot be a placeholder value");
throw new CustomValidationException("Password cannot be a placeholder value");
}
if (apiKey.IsPlaceholder())
{
throw new ValidationException("API key cannot be a placeholder value");
throw new CustomValidationException("API key cannot be a placeholder value");
}
return new()
@@ -35,32 +35,32 @@ public sealed record UpdateDownloadClientRequest
{
if (TypeName is DownloadClientTypeName.Unknown || !Enum.IsDefined(TypeName))
{
throw new ValidationException("Unsupported download client type");
throw new CustomValidationException("Unsupported download client type");
}
if (string.IsNullOrWhiteSpace(Name))
{
throw new ValidationException("Client name cannot be empty");
throw new CustomValidationException("Client name cannot be empty");
}
if (string.IsNullOrWhiteSpace(Host))
{
throw new ValidationException("Host cannot be empty");
throw new CustomValidationException("Host cannot be empty");
}
if (!Uri.TryCreate(Host, UriKind.RelativeOrAbsolute, out _))
{
throw new ValidationException("Host is not a valid URL");
throw new CustomValidationException("Host is not a valid URL");
}
if (!string.IsNullOrWhiteSpace(ExternalUrl) && !Uri.TryCreate(ExternalUrl, UriKind.RelativeOrAbsolute, out _))
{
throw new ValidationException("External URL is not a valid URL");
throw new CustomValidationException("External URL is not a valid URL");
}
if (TypeName.RequiresAuthField(DownloadClientAuthField.ApiKey) && string.IsNullOrWhiteSpace(ApiKey) && !ApiKey.IsPlaceholder())
{
throw new ValidationException("API key cannot be empty");
throw new CustomValidationException("API key cannot be empty");
}
}
@@ -104,7 +104,7 @@ public sealed class DownloadClientController : ControllerBase
if (updatedClient.TypeName != existingClient.TypeName)
{
throw new ValidationException("Download client type cannot be changed");
throw new CustomValidationException("Download client type cannot be changed");
}
var clientToPersist = updatedClient.ApplyTo(existingClient);
@@ -107,7 +107,7 @@ public sealed class SeekerConfigController : ControllerBase
if (request.ProactiveSearchEnabled && !request.Instances.Any(i => i.Enabled))
{
throw new Domain.Exceptions.ValidationException(
throw new Domain.Exceptions.CustomValidationException(
"At least one instance must be enabled when proactive search is enabled");
}
@@ -238,16 +238,16 @@ public class BackgroundJobManager : IHostedService
if (triggerValue > Constants.TriggerMaxLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
throw new CustomValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
}
if (typeof(T) == typeof(SeekerJob) && triggerValue < Constants.SeekerMinLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.SeekerMinLimit.TotalMinutes} minutes");
throw new CustomValidationException($"{cronExpression} should have a fire time of minimum {Constants.SeekerMinLimit.TotalMinutes} minutes");
}
else if (typeof(T) != typeof(MalwareBlocker) && triggerValue < Constants.TriggerMinLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
throw new CustomValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
}
if (triggerValue > StaticConfiguration.TriggerValue)
@@ -30,7 +30,7 @@ public sealed class GlobalExceptionHandler : IExceptionHandler
{
(int status, string title, string detail) = exception switch
{
ValidationException => (StatusCodes.Status400BadRequest, "Validation failed", exception.Message),
CustomValidationException => (StatusCodes.Status400BadRequest, "Validation failed", exception.Message),
NotificationTestException => (StatusCodes.Status400BadRequest, "Notification test failed", exception.Message),
RateLimitException => (StatusCodes.Status429TooManyRequests, "Too many requests", exception.Message),
_ => (StatusCodes.Status500InternalServerError, "An error occurred", "An unexpected error occurred"),
@@ -0,0 +1,16 @@
namespace Cleanuparr.Domain.Exceptions;
public sealed class CustomValidationException : Exception
{
public CustomValidationException()
{
}
public CustomValidationException(string message) : base(message)
{
}
public CustomValidationException(string message, Exception inner) : base(message, inner)
{
}
}
@@ -1,16 +0,0 @@
namespace Cleanuparr.Domain.Exceptions;
public sealed class ValidationException : Exception
{
public ValidationException()
{
}
public ValidationException(string message) : base(message)
{
}
public ValidationException(string message, Exception inner) : base(message, inner)
{
}
}
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Models;
using Cleanuparr.Infrastructure.Utilities;
using Shouldly;
@@ -134,7 +134,7 @@ public class CronExpressionConverterTests
var schedule = new JobSchedule { Every = value, Type = unit };
// Act & Assert
Should.Throw<ValidationException>(() => CronExpressionConverter.ConvertToCronExpression(schedule));
Should.Throw<CustomValidationException>(() => CronExpressionConverter.ConvertToCronExpression(schedule));
}
[Theory]
@@ -27,7 +27,7 @@ public class CronValidationHelperTests
public void ValidateCronExpression_InvalidSyntax_ThrowsValidationException(string cronExpression)
{
// Act & Assert
var exception = Should.Throw<ValidationException>(
var exception = Should.Throw<CustomValidationException>(
() => CronValidationHelper.ValidateCronExpression(cronExpression));
exception.Message.ShouldContain("Invalid cron expression");
}
@@ -39,7 +39,7 @@ public class CronValidationHelperTests
public void ValidateCronExpression_NullOrEmpty_ThrowsValidationException(string? cronExpression)
{
// Act & Assert
var exception = Should.Throw<ValidationException>(
var exception = Should.Throw<CustomValidationException>(
() => CronValidationHelper.ValidateCronExpression(cronExpression!));
exception.Message.ShouldContain("cannot be null or empty");
}
@@ -52,7 +52,7 @@ public class CronValidationHelperTests
public void ValidateCronExpression_TriggersTooFast_ThrowsValidationException(string cronExpression)
{
// Act & Assert - minimum is 30 seconds
var exception = Should.Throw<ValidationException>(
var exception = Should.Throw<CustomValidationException>(
() => CronValidationHelper.ValidateCronExpression(cronExpression));
exception.Message.ShouldContain("minimum");
}
@@ -63,7 +63,7 @@ public class CronValidationHelperTests
public void ValidateCronExpression_TriggersTooSlow_ThrowsValidationException(string cronExpression)
{
// Act & Assert - maximum is 6 hours
var exception = Should.Throw<ValidationException>(
var exception = Should.Throw<CustomValidationException>(
() => CronValidationHelper.ValidateCronExpression(cronExpression));
exception.Message.ShouldContain("maximum");
}
@@ -104,7 +104,7 @@ public class CronValidationHelperTests
const string cronExpression = "*/5 * * * * ?";
// Act & Assert - should fail because null uses default limits (30 second minimum)
var exception = Should.Throw<ValidationException>(
var exception = Should.Throw<CustomValidationException>(
() => CronValidationHelper.ValidateCronExpression(cronExpression, null));
exception.Message.ShouldContain("minimum");
}
@@ -119,7 +119,7 @@ public class CronValidationHelperTests
const string cronExpression = "*/5 * * * * ?";
// Act & Assert
var exception = Should.Throw<ValidationException>(
var exception = Should.Throw<CustomValidationException>(
() => CronValidationHelper.ValidateCronExpression(cronExpression, jobType));
exception.Message.ShouldContain("minimum");
}
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Utilities;
namespace Cleanuparr.Infrastructure.Models;
@@ -30,13 +30,13 @@ public class JobSchedule
/// <summary>
/// Validates the job schedule against the predefined valid values
/// </summary>
/// <exception cref="ValidationException">Thrown when the value is not valid for the selected unit</exception>
/// <exception cref="CustomValidationException">Thrown when the value is not valid for the selected unit</exception>
public void Validate()
{
if (!ScheduleOptions.IsValidValue(Type, Every))
{
var validValues = string.Join(", ", ScheduleOptions.GetValidValues(Type));
throw new ValidationException($"Invalid value for {Type}: {Every}. Valid values are: {validValues}");
throw new CustomValidationException($"Invalid value for {Type}: {Every}. Valid values are: {validValues}");
}
}
}
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Models;
using Quartz;
@@ -24,7 +24,7 @@ public static class CronExpressionConverter
if (!ScheduleOptions.IsValidValue(schedule.Type, schedule.Every))
{
var validValues = string.Join(", ", ScheduleOptions.GetValidValues(schedule.Type));
throw new ValidationException($"Invalid value for {schedule.Type}: {schedule.Every}. Valid values are: {validValues}");
throw new CustomValidationException($"Invalid value for {schedule.Type}: {schedule.Every}. Valid values are: {validValues}");
}
// Cron format: Seconds Minutes Hours Day-of-month Month Day-of-week Year
@@ -16,18 +16,18 @@ public static class CronValidationHelper
/// Validates a cron expression against the application's trigger limits
/// </summary>
/// <param name="cronExpression">The cron expression to validate</param>
/// <exception cref="ValidationException">Thrown when the cron expression is invalid or violates trigger limits</exception>
/// <exception cref="CustomValidationException">Thrown when the cron expression is invalid or violates trigger limits</exception>
public static void ValidateCronExpression(string cronExpression, JobType? jobType = null)
{
if (string.IsNullOrWhiteSpace(cronExpression))
{
throw new ValidationException("Cron expression cannot be null or empty");
throw new CustomValidationException("Cron expression cannot be null or empty");
}
// First check if it's a valid cron expression
if (!CronExpression.IsValidExpression(cronExpression))
{
throw new ValidationException($"Invalid cron expression: {cronExpression}");
throw new CustomValidationException($"Invalid cron expression: {cronExpression}");
}
// Validate the trigger timing limits
@@ -43,34 +43,34 @@ public static class CronValidationHelper
if (nextFireTimes.Count < 2)
{
throw new ValidationException($"Could not compute fire times for cron expression: {cronExpression}");
throw new CustomValidationException($"Could not compute fire times for cron expression: {cronExpression}");
}
TimeSpan triggerValue = nextFireTimes[1] - nextFireTimes[0];
if (triggerValue > Constants.TriggerMaxLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
throw new CustomValidationException($"{cronExpression} should have a fire time of maximum {Constants.TriggerMaxLimit.TotalHours} hours");
}
if (jobType is JobType.Seeker && triggerValue < Constants.SeekerMinLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.SeekerMinLimit.TotalMinutes} minutes");
throw new CustomValidationException($"{cronExpression} should have a fire time of minimum {Constants.SeekerMinLimit.TotalMinutes} minutes");
}
if (jobType is not JobType.MalwareBlocker && triggerValue < Constants.TriggerMinLimit)
{
throw new ValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
throw new CustomValidationException($"{cronExpression} should have a fire time of minimum {Constants.TriggerMinLimit.TotalSeconds} seconds");
}
}
catch (ValidationException)
catch (CustomValidationException)
{
// Re-throw validation exceptions as-is
throw;
}
catch (Exception ex)
{
throw new ValidationException($"Error validating cron expression '{cronExpression}'", ex);
throw new CustomValidationException($"Error validating cron expression '{cronExpression}'", ex);
}
}
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Arr;
@@ -41,7 +41,7 @@ public sealed class ArrConfigTests
FailedImportMaxStrikes = strikes,
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Failed import max strikes must be -1 or greater");
}
@@ -60,7 +60,7 @@ public sealed class ArrConfigTests
FailedImportMaxStrikes = strikes,
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("The minimum value for failed import max strikes must be 3");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.BlacklistSync;
@@ -46,7 +46,7 @@ public sealed class BlacklistSyncConfigTests
BlacklistPath = null
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Blacklist sync is enabled but the path is not configured");
}
@@ -59,7 +59,7 @@ public sealed class BlacklistSyncConfigTests
BlacklistPath = ""
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Blacklist sync is enabled but the path is not configured");
}
@@ -72,7 +72,7 @@ public sealed class BlacklistSyncConfigTests
BlacklistPath = " "
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Blacklist sync is enabled but the path is not configured");
}
@@ -109,7 +109,7 @@ public sealed class BlacklistSyncConfigTests
BlacklistPath = "/non/existent/path/blacklist.txt"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Blacklist path must be a valid URL or an existing local file path");
}
@@ -122,7 +122,7 @@ public sealed class BlacklistSyncConfigTests
BlacklistPath = "not-a-valid-url-or-path"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Blacklist path must be a valid URL or an existing local file path");
}
@@ -137,7 +137,7 @@ public sealed class BlacklistSyncConfigTests
BlacklistPath = url
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Blacklist path must be a valid URL or an existing local file path");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.DownloadCleaner;
@@ -60,7 +60,7 @@ public sealed class DeadTorrentConfigTests
MaxStrikes = 3
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Dead torrent target category is required");
}
@@ -75,7 +75,7 @@ public sealed class DeadTorrentConfigTests
MaxStrikes = 3
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("No dead torrent categories configured");
}
@@ -90,7 +90,7 @@ public sealed class DeadTorrentConfigTests
MaxStrikes = 3
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("The dead torrent target category should not be present in dead torrent categories");
}
@@ -105,7 +105,7 @@ public sealed class DeadTorrentConfigTests
MaxStrikes = 3
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Empty dead torrent category filter found");
}
@@ -122,7 +122,7 @@ public sealed class DeadTorrentConfigTests
MaxStrikes = strikes
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Dead torrent max strikes must be at least 3");
}
}
@@ -1,9 +1,9 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.DownloadCleaner;
@@ -64,7 +64,7 @@ public sealed class OrphanedFilesConfigTests
{
var config = new OrphanedFilesConfig { Enabled = true, ScanDirectories = [], OrphanedDirectory = "/downloads/orphaned" };
Should.Throw<ValidationException>(() => config.Validate())
Should.Throw<CustomValidationException>(() => config.Validate())
.Message.ShouldContain("scan directory");
}
@@ -78,7 +78,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = string.Empty,
};
Should.Throw<ValidationException>(() => config.Validate())
Should.Throw<CustomValidationException>(() => config.Validate())
.Message.ShouldContain("Orphaned directory");
}
@@ -116,7 +116,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = "/downloads/orphaned-b",
};
Should.Throw<ValidationException>(() => config.Validate([sibling]))
Should.Throw<CustomValidationException>(() => config.Validate([sibling]))
.Message.ShouldContain("overlap");
}
@@ -137,7 +137,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = "/downloads/orphaned-b",
};
Should.Throw<ValidationException>(() => config.Validate([sibling]))
Should.Throw<CustomValidationException>(() => config.Validate([sibling]))
.Message.ShouldContain("overlap");
}
@@ -158,7 +158,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = "/orphaned-b",
};
Should.Throw<ValidationException>(() => config.Validate([sibling]))
Should.Throw<CustomValidationException>(() => config.Validate([sibling]))
.Message.ShouldContain("overlap");
}
@@ -179,7 +179,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = "/downloads/orphaned",
};
Should.Throw<ValidationException>(() => config.Validate([sibling]))
Should.Throw<CustomValidationException>(() => config.Validate([sibling]))
.Message.ShouldContain("overlap");
}
@@ -200,7 +200,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = "/orphaned-b",
};
Should.Throw<ValidationException>(() => config.Validate([sibling]))
Should.Throw<CustomValidationException>(() => config.Validate([sibling]))
.Message.ShouldContain("overlap");
}
@@ -241,7 +241,7 @@ public sealed class OrphanedFilesConfigTests
OrphanedDirectory = "/orphaned-b",
};
Should.Throw<ValidationException>(() => config.Validate([sibling]));
Should.Throw<CustomValidationException>(() => config.Validate([sibling]));
}
[Fact]
@@ -273,7 +273,7 @@ public sealed class OrphanedFilesConfigTests
var otherClient = MakeDownloadClient("Other", "/data/downloads/movies");
Should.Throw<ValidationException>(() => config.Validate([], [otherClient]))
Should.Throw<CustomValidationException>(() => config.Validate([], [otherClient]))
.Message.ShouldContain("overlap");
}
@@ -289,7 +289,7 @@ public sealed class OrphanedFilesConfigTests
var otherClient = MakeDownloadClient("Other", "/data/other-downloads/movies");
Should.Throw<ValidationException>(() => config.Validate([], [otherClient]))
Should.Throw<CustomValidationException>(() => config.Validate([], [otherClient]))
.Message.ShouldContain("overlap");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.DownloadCleaner;
@@ -141,7 +141,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("At least one category must be specified");
}
@@ -161,7 +161,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Rule name can not be empty");
}
@@ -177,7 +177,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Rule name can not be empty");
}
@@ -193,7 +193,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Rule name can not be empty");
}
@@ -214,7 +214,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Either max ratio or max seed time must be set to a non-negative value");
}
@@ -234,7 +234,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Either max ratio or max seed time must be set to a non-negative value");
}
@@ -253,7 +253,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Max ratio must be -1 (disabled) or a non-negative number");
}
@@ -272,7 +272,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Max seed time must be -1 (disabled) or a non-negative number");
}
@@ -293,7 +293,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Min seed time can not be negative");
}
@@ -313,7 +313,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Min seed time can not be negative");
}
@@ -386,7 +386,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Min seeders can not be less than 0");
}
@@ -445,7 +445,7 @@ public sealed class QBitSeedingRuleTests
DeleteSourceFiles = true
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Max inactive days must be -1 (disabled) or a non-negative number");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.DownloadCleaner;
@@ -64,7 +64,7 @@ public sealed class UnlinkedConfigTests
Categories = ["movies"]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Unlinked target category is required");
}
@@ -78,7 +78,7 @@ public sealed class UnlinkedConfigTests
Categories = []
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("No unlinked categories configured");
}
@@ -92,7 +92,7 @@ public sealed class UnlinkedConfigTests
Categories = ["movies", "cleanuparr-unlinked"]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("The unlinked target category should not be present in unlinked categories");
}
@@ -106,7 +106,7 @@ public sealed class UnlinkedConfigTests
Categories = ["movies", ""]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Empty unlinked category filter found");
}
@@ -121,7 +121,7 @@ public sealed class UnlinkedConfigTests
IgnoredRootDirs = ["/non/existent/path/that/should/not/exist"]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("root directory does not exist");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration;
@@ -53,7 +53,7 @@ public sealed class DownloadClientConfigTests
Host = new Uri("http://localhost:8080")
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Client name cannot be empty");
}
@@ -68,7 +68,7 @@ public sealed class DownloadClientConfigTests
Host = new Uri("http://localhost:8080")
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Client name cannot be empty");
}
@@ -83,7 +83,7 @@ public sealed class DownloadClientConfigTests
Host = new Uri("http://localhost:8080")
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Client name cannot be empty");
}
@@ -102,7 +102,7 @@ public sealed class DownloadClientConfigTests
Host = null
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Host cannot be empty");
}
@@ -123,7 +123,7 @@ public sealed class DownloadClientConfigTests
DownloadDirectoryTarget = null
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Both download directory source and target must be set, or both must be empty");
}
@@ -140,7 +140,7 @@ public sealed class DownloadClientConfigTests
DownloadDirectoryTarget = "/data/downloads"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Both download directory source and target must be set, or both must be empty");
}
@@ -192,7 +192,7 @@ public sealed class DownloadClientConfigTests
ApiKey = null
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("API key cannot be empty");
}
@@ -254,7 +254,7 @@ public sealed class DownloadClientConfigTests
Password = "pass"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Use either an API key or a username and password, not both");
}
@@ -271,7 +271,7 @@ public sealed class DownloadClientConfigTests
Username = "admin"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Use either an API key or a username and password, not both");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.General;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.General;
@@ -29,7 +29,7 @@ public sealed class GeneralConfigTests
HttpTimeout = 0,
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("HTTP_TIMEOUT must be greater than 0");
}
@@ -64,7 +64,7 @@ public sealed class GeneralConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Log rolling size cannot exceed 100 MB");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.General;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.General;
@@ -58,7 +58,7 @@ public sealed class LoggingConfigTests
RollingSizeMB = 101
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Log rolling size cannot exceed 100 MB");
}
@@ -90,7 +90,7 @@ public sealed class LoggingConfigTests
RetainedFileCount = 51
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Log retained file count cannot exceed 50");
}
@@ -122,7 +122,7 @@ public sealed class LoggingConfigTests
TimeLimitHours = 1441
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Log time limit cannot exceed 60 days");
}
@@ -154,7 +154,7 @@ public sealed class LoggingConfigTests
ArchiveRetainedCount = 101
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Log archive retained count cannot exceed 100");
}
@@ -186,7 +186,7 @@ public sealed class LoggingConfigTests
ArchiveTimeLimitHours = 1441
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Log archive time limit cannot exceed 60 days");
}
@@ -220,7 +220,7 @@ public sealed class LoggingConfigTests
ArchiveTimeLimitHours = 0
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Archiving is enabled, but no retention policy is set. Please set either a retained file count or time limit");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Auth;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Auth;
@@ -65,7 +65,7 @@ public sealed class OidcConfigTests
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL is required when OIDC is enabled");
}
@@ -79,7 +79,7 @@ public sealed class OidcConfigTests
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL must be a valid absolute URL");
}
@@ -93,7 +93,7 @@ public sealed class OidcConfigTests
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL must use HTTPS");
}
@@ -144,7 +144,7 @@ public sealed class OidcConfigTests
ClientId = clientId
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Client ID is required when OIDC is enabled");
}
@@ -165,7 +165,7 @@ public sealed class OidcConfigTests
ProviderName = providerName
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Provider Name is required when OIDC is enabled");
}
@@ -238,7 +238,7 @@ public sealed class OidcConfigTests
AuthorizedSubject = "some-subject"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC must be enabled to use exclusive mode");
}
@@ -349,7 +349,7 @@ public sealed class OidcConfigTests
ClientId = "my-client"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("OIDC Issuer URL must use HTTPS");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.MalwareBlocker;
@@ -54,7 +54,7 @@ public sealed class ContentBlockerConfigTests
Whisparr = new BlocklistSettings { Enabled = false }
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("At least one blocklist must be configured when Malware Blocker is enabled");
}
@@ -107,7 +107,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Sonarr blocklist is enabled but path is not specified");
}
@@ -124,7 +124,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Radarr blocklist is enabled but path is not specified");
}
@@ -141,7 +141,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Lidarr blocklist is enabled but path is not specified");
}
@@ -158,7 +158,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("Readarr blocklist does not exist");
}
@@ -175,7 +175,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("Whisparr blocklist does not exist");
}
@@ -218,7 +218,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("Sonarr blocklist does not exist");
}
@@ -265,7 +265,7 @@ public sealed class ContentBlockerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Radarr blocklist is enabled but path is not specified");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -142,7 +142,7 @@ public sealed class AppriseConfigTests
Key = "my-config-key"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Apprise server URL is required for API mode");
}
@@ -155,7 +155,7 @@ public sealed class AppriseConfigTests
Key = "my-config-key"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Apprise server URL must be a valid HTTP or HTTPS URL");
}
@@ -171,7 +171,7 @@ public sealed class AppriseConfigTests
Key = key ?? string.Empty
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Apprise configuration key is required for API mode");
}
@@ -184,7 +184,7 @@ public sealed class AppriseConfigTests
Key = "a"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Apprise configuration key must be at least 2 characters long");
}
@@ -255,7 +255,7 @@ public sealed class AppriseConfigTests
ServiceUrls = serviceUrls
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("At least one service URL is required for CLI mode");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -75,7 +75,7 @@ public sealed class DiscordConfigTests
WebhookUrl = webhookUrl ?? string.Empty
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("required");
}
@@ -91,7 +91,7 @@ public sealed class DiscordConfigTests
WebhookUrl = webhookUrl
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("valid Discord webhook URL");
}
@@ -117,7 +117,7 @@ public sealed class DiscordConfigTests
AvatarUrl = "not-a-valid-url"
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("valid URL");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -80,7 +80,7 @@ public sealed class GotifyConfigTests
ApplicationToken = "test-token"
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("required");
}
@@ -96,7 +96,7 @@ public sealed class GotifyConfigTests
ApplicationToken = "test-token"
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("valid HTTP or HTTPS URL");
}
@@ -127,7 +127,7 @@ public sealed class GotifyConfigTests
ApplicationToken = token ?? string.Empty
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("required");
}
@@ -144,7 +144,7 @@ public sealed class GotifyConfigTests
Priority = priority
};
var ex = Should.Throw<ValidationException>(() => config.Validate());
var ex = Should.Throw<CustomValidationException>(() => config.Validate());
ex.Message.ShouldContain("Priority");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -91,7 +91,7 @@ public sealed class NotifiarrConfigTests
ChannelId = "123456789012345678"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Notifiarr API key is required");
}
@@ -104,7 +104,7 @@ public sealed class NotifiarrConfigTests
ChannelId = "123456789012345678"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Notifiarr API key must be at least 10 characters long");
}
@@ -132,7 +132,7 @@ public sealed class NotifiarrConfigTests
ChannelId = channelId ?? string.Empty
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Discord channel ID is required");
}
@@ -149,7 +149,7 @@ public sealed class NotifiarrConfigTests
ChannelId = channelId
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Discord channel ID must be a valid numeric ID");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -248,7 +248,7 @@ public sealed class NtfyConfigTests
AuthenticationType = NtfyAuthenticationType.None
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("ntfy server URL is required");
}
@@ -262,7 +262,7 @@ public sealed class NtfyConfigTests
AuthenticationType = NtfyAuthenticationType.None
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("ntfy server URL must be a valid HTTP or HTTPS URL");
}
@@ -276,7 +276,7 @@ public sealed class NtfyConfigTests
AuthenticationType = NtfyAuthenticationType.None
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("At least one ntfy topic is required");
}
@@ -290,7 +290,7 @@ public sealed class NtfyConfigTests
AuthenticationType = NtfyAuthenticationType.None
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("At least one ntfy topic is required");
}
@@ -328,7 +328,7 @@ public sealed class NtfyConfigTests
Password = "pass"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Username is required for Basic Auth");
}
@@ -347,7 +347,7 @@ public sealed class NtfyConfigTests
Password = password
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Password is required for Basic Auth");
}
@@ -379,7 +379,7 @@ public sealed class NtfyConfigTests
AccessToken = accessToken
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Access token is required for Token authentication");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -215,7 +215,7 @@ public sealed class PushoverConfigTests
Priority = PushoverPriority.Normal
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Pushover API token is required");
}
@@ -232,7 +232,7 @@ public sealed class PushoverConfigTests
Priority = PushoverPriority.Normal
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Pushover user key is required");
}
@@ -300,7 +300,7 @@ public sealed class PushoverConfigTests
Expire = 3600
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Retry interval must be at least 30 seconds for emergency priority");
}
@@ -318,7 +318,7 @@ public sealed class PushoverConfigTests
Expire = expire
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Expire time is required for emergency priority");
}
@@ -334,7 +334,7 @@ public sealed class PushoverConfigTests
Expire = 10801
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Expire time cannot exceed 10800 seconds (3 hours)");
}
@@ -382,7 +382,7 @@ public sealed class PushoverConfigTests
Devices = [longDeviceName]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe($"Device name '{longDeviceName}' exceeds 25 character limit");
}
@@ -416,7 +416,7 @@ public sealed class PushoverConfigTests
Devices = [deviceName]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe($"Device name '{deviceName}' contains invalid characters. Only letters, numbers, underscores, and hyphens are allowed.");
}
@@ -489,7 +489,7 @@ public sealed class PushoverConfigTests
Sound = sound
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Sound name cannot be empty or whitespace when specified");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Notification;
@@ -151,7 +151,7 @@ public sealed class TelegramConfigTests
ChatId = "123456789"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Telegram bot token is required");
}
@@ -167,7 +167,7 @@ public sealed class TelegramConfigTests
ChatId = "123456789"
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Telegram bot token must be at least 10 characters long");
}
@@ -195,7 +195,7 @@ public sealed class TelegramConfigTests
ChatId = chatId ?? string.Empty
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Telegram chat ID is required");
}
@@ -211,7 +211,7 @@ public sealed class TelegramConfigTests
ChatId = chatId
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Telegram chat ID must be a valid integer (negative IDs allowed for groups)");
}
@@ -240,7 +240,7 @@ public sealed class TelegramConfigTests
TopicId = topicId
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Telegram topic ID must be a valid integer when specified");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.QueueCleaner;
@@ -76,7 +76,7 @@ public sealed class FailedImportConfigTests
Patterns = ["pattern"]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("The minimum value for failed imports max strikes must be 3");
}
@@ -107,7 +107,7 @@ public sealed class FailedImportConfigTests
Patterns = []
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("At least one pattern must be specified when using the Include pattern mode");
}
@@ -162,7 +162,7 @@ public sealed class FailedImportConfigTests
Patterns = []
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("The minimum value for failed imports max strikes must be 3");
}
@@ -180,7 +180,7 @@ public sealed class FailedImportConfigTests
ForceImportMaxTries = 0,
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Force import max tries must be at least 1");
}
@@ -194,7 +194,7 @@ public sealed class FailedImportConfigTests
ForceImportMaxTries = 5001,
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Force import max tries must be at most 5000");
}
@@ -305,7 +305,7 @@ public sealed class FailedImportConfigTests
DeletePrivate = true,
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Cannot enable both deletion and category changing");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.QueueCleaner;
@@ -84,7 +84,7 @@ public sealed class QueueCleanerConfigTests
DownloadingMetadataMaxStrikes = maxStrikes
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("the minimum value for downloading metadata max strikes must be 3");
}
@@ -127,7 +127,7 @@ public sealed class QueueCleanerConfigTests
Errored = new ErroredConfig { MaxStrikes = maxStrikes }
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("the minimum value for errored max strikes must be 3");
}
@@ -153,7 +153,7 @@ public sealed class QueueCleanerConfigTests
Errored = new ErroredConfig { MaxStrikes = 3, DeletePrivate = true, ChangeCategory = true }
};
ValidationException exception = Should.Throw<ValidationException>(() => config.Validate());
CustomValidationException exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Cannot enable both deletion and category changing");
}
@@ -172,7 +172,7 @@ public sealed class QueueCleanerConfigTests
}
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("The minimum value for failed imports max strikes must be 3");
}
@@ -191,7 +191,7 @@ public sealed class QueueCleanerConfigTests
]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Rule name cannot be empty");
}
@@ -207,7 +207,7 @@ public sealed class QueueCleanerConfigTests
]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Duplicate stall rule names found");
}
@@ -256,7 +256,7 @@ public sealed class QueueCleanerConfigTests
]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Rule name cannot be empty");
}
@@ -272,7 +272,7 @@ public sealed class QueueCleanerConfigTests
]
};
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldBe("Duplicate slow rule names found");
}
@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.QueueCleaner;
@@ -57,7 +57,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 100
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Rule name cannot be empty");
}
@@ -72,7 +72,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 100
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Rule name cannot be empty");
}
@@ -87,7 +87,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 100
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Rule name cannot be empty");
}
@@ -109,7 +109,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 100
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Max strikes must be at least 3");
}
@@ -148,7 +148,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 100
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Minimum completion percentage must be between 0 and 100");
}
@@ -187,7 +187,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = maxCompletionPercentage
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Maximum completion percentage must be between 1 and 100");
}
@@ -202,7 +202,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 0
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Maximum completion percentage must be greater than 0");
}
@@ -238,7 +238,7 @@ public sealed class QueueRuleTests
MaxCompletionPercentage = 25
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Maximum completion percentage must be greater than or equal to the minimum completion percentage");
}
@@ -345,7 +345,7 @@ public sealed class QueueRuleTests
DeletePrivateTorrentsFromClient = true,
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Cannot enable both deletion and category changing");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.QueueCleaner;
@@ -89,7 +89,7 @@ public sealed class SlowRuleTests
};
// Base class validation runs first
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Max strikes must be at least 3");
}
@@ -108,7 +108,7 @@ public sealed class SlowRuleTests
MaxTimeHours = -1
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Maximum time cannot be negative");
}
@@ -141,7 +141,7 @@ public sealed class SlowRuleTests
MaxTimeHours = 0
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Either minimum speed or maximum time must be specified");
}
@@ -156,7 +156,7 @@ public sealed class SlowRuleTests
MaxTimeHours = 0
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Either minimum speed or maximum time must be specified");
}
@@ -179,7 +179,7 @@ public sealed class SlowRuleTests
MaxTimeHours = 0
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Invalid minimum speed format");
}
@@ -250,7 +250,7 @@ public sealed class SlowRuleTests
IgnoreAboveSize = ignoreAboveSize
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldContain("invalid value for slow ignore above size");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.QueueCleaner;
@@ -108,7 +108,7 @@ public sealed class StallRuleTests
};
// Base class validation runs first
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldBe("Max strikes must be at least 3");
}
@@ -130,7 +130,7 @@ public sealed class StallRuleTests
MinimumProgress = minimumProgress
};
var exception = Should.Throw<ValidationException>(() => rule.Validate());
var exception = Should.Throw<CustomValidationException>(() => rule.Validate());
exception.Message.ShouldContain("Invalid minimum progress value");
}
@@ -1,7 +1,7 @@
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration.Seeker;
using Shouldly;
using Xunit;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.Seeker;
@@ -49,7 +49,7 @@ public sealed class SeekerConfigTests
{
var config = new SeekerConfig { SearchInterval = 1 };
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("at least");
}
@@ -58,7 +58,7 @@ public sealed class SeekerConfigTests
{
var config = new SeekerConfig { SearchInterval = 361 };
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("at most");
}
@@ -73,7 +73,7 @@ public sealed class SeekerConfigTests
{
var config = new SeekerConfig { SearchInterval = interval };
var exception = Should.Throw<ValidationException>(() => config.Validate());
var exception = Should.Throw<CustomValidationException>(() => config.Validate());
exception.Message.ShouldContain("Invalid search interval");
}
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Auth;
@@ -66,7 +66,7 @@ public sealed record OidcConfig
{
if (ExclusiveMode && !Enabled)
{
throw new ValidationException("OIDC must be enabled to use exclusive mode");
throw new CustomValidationException("OIDC must be enabled to use exclusive mode");
}
if (!Enabled)
@@ -76,40 +76,40 @@ public sealed record OidcConfig
if (string.IsNullOrWhiteSpace(IssuerUrl))
{
throw new ValidationException("OIDC Issuer URL is required when OIDC is enabled");
throw new CustomValidationException("OIDC Issuer URL is required when OIDC is enabled");
}
if (!Uri.TryCreate(IssuerUrl, UriKind.Absolute, out var issuerUri))
{
throw new ValidationException("OIDC Issuer URL must be a valid absolute URL");
throw new CustomValidationException("OIDC Issuer URL must be a valid absolute URL");
}
// Enforce HTTPS except for localhost (development)
if (issuerUri.Scheme != "https" && !IsLocalhost(issuerUri))
{
throw new ValidationException("OIDC Issuer URL must use HTTPS");
throw new CustomValidationException("OIDC Issuer URL must use HTTPS");
}
if (string.IsNullOrWhiteSpace(ClientId))
{
throw new ValidationException("OIDC Client ID is required when OIDC is enabled");
throw new CustomValidationException("OIDC Client ID is required when OIDC is enabled");
}
if (string.IsNullOrWhiteSpace(ProviderName))
{
throw new ValidationException("OIDC Provider Name is required when OIDC is enabled");
throw new CustomValidationException("OIDC Provider Name is required when OIDC is enabled");
}
if (!string.IsNullOrWhiteSpace(RedirectUrl))
{
if (!Uri.TryCreate(RedirectUrl, UriKind.Absolute, out var redirectUri))
{
throw new ValidationException("OIDC Redirect URL must be a valid absolute URL");
throw new CustomValidationException("OIDC Redirect URL must be a valid absolute URL");
}
if (redirectUri.Scheme is not ("http" or "https"))
{
throw new ValidationException("OIDC Redirect URL must use HTTP or HTTPS");
throw new CustomValidationException("OIDC Redirect URL must use HTTP or HTTPS");
}
}
}
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Enums;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.Arr;
@@ -21,12 +21,12 @@ public class ArrConfig : IConfig
{
if (FailedImportMaxStrikes < -1)
{
throw new ValidationException("Failed import max strikes must be -1 or greater");
throw new CustomValidationException("Failed import max strikes must be -1 or greater");
}
if (FailedImportMaxStrikes is > 0 and < 3)
{
throw new ValidationException("The minimum value for failed import max strikes must be 3");
throw new CustomValidationException("The minimum value for failed import max strikes must be 3");
}
}
}
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.BlacklistSync;
@@ -28,7 +28,7 @@ public sealed record BlacklistSyncConfig : IJobConfig
if (string.IsNullOrWhiteSpace(BlacklistPath))
{
throw new ValidationException("Blacklist sync is enabled but the path is not configured");
throw new CustomValidationException("Blacklist sync is enabled but the path is not configured");
}
bool isValidPath = Uri.TryCreate(BlacklistPath, UriKind.Absolute, out var uri) &&
@@ -37,7 +37,7 @@ public sealed record BlacklistSyncConfig : IJobConfig
if (!isValidPath)
{
throw new ValidationException("Blacklist path must be a valid URL or an existing local file path");
throw new CustomValidationException("Blacklist path must be a valid URL or an existing local file path");
}
}
}
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
@@ -45,27 +45,27 @@ public sealed record DeadTorrentConfig : IConfig
if (string.IsNullOrWhiteSpace(TargetCategory))
{
throw new ValidationException("Dead torrent target category is required");
throw new CustomValidationException("Dead torrent target category is required");
}
if (Categories.Count is 0)
{
throw new ValidationException("No dead torrent categories configured");
throw new CustomValidationException("No dead torrent categories configured");
}
if (Categories.Contains(TargetCategory, StringComparer.OrdinalIgnoreCase))
{
throw new ValidationException("The dead torrent target category should not be present in dead torrent categories");
throw new CustomValidationException("The dead torrent target category should not be present in dead torrent categories");
}
if (Categories.Any(string.IsNullOrWhiteSpace))
{
throw new ValidationException("Empty dead torrent category filter found");
throw new CustomValidationException("Empty dead torrent category filter found");
}
if (MaxStrikes < 3)
{
throw new ValidationException("Dead torrent max strikes must be at least 3");
throw new CustomValidationException("Dead torrent max strikes must be at least 3");
}
}
}
@@ -1,4 +1,4 @@
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
@@ -13,7 +13,7 @@ public interface IInactivityFilterable
{
if (MaxInactiveDays is < 0 and not -1)
{
throw new ValidationException("Max inactive days must be -1 (disabled) or a non-negative number");
throw new CustomValidationException("Max inactive days must be -1 (disabled) or a non-negative number");
}
}
}
@@ -1,4 +1,4 @@
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
@@ -13,7 +13,7 @@ public interface ISeedersFilterable
{
if (MinSeeders < 0)
{
throw new ValidationException("Min seeders can not be less than 0");
throw new CustomValidationException("Min seeders can not be less than 0");
}
}
}
@@ -1,5 +1,5 @@
using Cleanuparr.Domain.Enums;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
@@ -51,37 +51,37 @@ public interface ISeedingRule : IConfig
{
if (string.IsNullOrEmpty(Name.Trim()))
{
throw new ValidationException("Rule name can not be empty");
throw new CustomValidationException("Rule name can not be empty");
}
if (Categories.Count == 0)
{
throw new ValidationException("At least one category must be specified");
throw new CustomValidationException("At least one category must be specified");
}
if (MaxRatio < 0 && MaxSeedTime < 0)
{
throw new ValidationException("Either max ratio or max seed time must be set to a non-negative value");
throw new CustomValidationException("Either max ratio or max seed time must be set to a non-negative value");
}
if (MaxRatio is < 0 and not -1)
{
throw new ValidationException("Max ratio must be -1 (disabled) or a non-negative number");
throw new CustomValidationException("Max ratio must be -1 (disabled) or a non-negative number");
}
if (MaxSeedTime is < 0 and not -1)
{
throw new ValidationException("Max seed time must be -1 (disabled) or a non-negative number");
throw new CustomValidationException("Max seed time must be -1 (disabled) or a non-negative number");
}
if (MinSeedTime < 0)
{
throw new ValidationException("Min seed time can not be negative");
throw new CustomValidationException("Min seed time can not be negative");
}
if (Action is not SeedingRuleAction.Delete and not SeedingRuleAction.Stop)
{
throw new ValidationException("Unknown seeding rule action");
throw new CustomValidationException("Unknown seeding rule action");
}
}
}
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
@@ -87,12 +87,12 @@ public sealed record OrphanedFilesConfig : IConfig
if (ScanDirectories.Count == 0)
{
throw new ValidationException("At least one scan directory is required when orphaned files cleanup is enabled for this client");
throw new CustomValidationException("At least one scan directory is required when orphaned files cleanup is enabled for this client");
}
if (string.IsNullOrWhiteSpace(OrphanedDirectory))
{
throw new ValidationException("Orphaned directory is required when orphaned files cleanup is enabled for this client");
throw new CustomValidationException("Orphaned directory is required when orphaned files cleanup is enabled for this client");
}
foreach (var scanDir in ScanDirectories)
@@ -130,13 +130,13 @@ public sealed record OrphanedFilesConfig : IConfig
if (string.Equals(normalizedScan, normalizedOrphaned, StringComparison.OrdinalIgnoreCase))
{
throw new ValidationException(
throw new CustomValidationException(
$"Orphaned directory '{normalizedOrphaned}' cannot equal scan directory '{normalizedScan}'.");
}
if (normalizedScan.StartsWith(normalizedOrphaned + sep, StringComparison.OrdinalIgnoreCase))
{
throw new ValidationException(
throw new CustomValidationException(
$"Orphaned directory '{normalizedOrphaned}' cannot be an ancestor of scan directory '{normalizedScan}'.");
}
}
@@ -171,7 +171,7 @@ public sealed record OrphanedFilesConfig : IConfig
|| a.StartsWith(b + sep, StringComparison.OrdinalIgnoreCase)
|| b.StartsWith(a + sep, StringComparison.OrdinalIgnoreCase))
{
throw new ValidationException(
throw new CustomValidationException(
$"Path overlap detected: {aLabel} '{a}' overlaps with {bLabel} '{b}'. Scan directories and orphaned directories must not overlap across clients.");
}
}
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
@@ -33,29 +33,29 @@ public sealed record UnlinkedConfig : IConfig
if (string.IsNullOrWhiteSpace(TargetCategory))
{
throw new ValidationException("Unlinked target category is required");
throw new CustomValidationException("Unlinked target category is required");
}
if (Categories.Count is 0)
{
throw new ValidationException("No unlinked categories configured");
throw new CustomValidationException("No unlinked categories configured");
}
if (Categories.Contains(TargetCategory, StringComparer.OrdinalIgnoreCase))
{
throw new ValidationException("The unlinked target category should not be present in unlinked categories");
throw new CustomValidationException("The unlinked target category should not be present in unlinked categories");
}
if (Categories.Any(string.IsNullOrWhiteSpace))
{
throw new ValidationException("Empty unlinked category filter found");
throw new CustomValidationException("Empty unlinked category filter found");
}
foreach (var dir in IgnoredRootDirs.Where(d => !string.IsNullOrEmpty(d)))
{
if (!Directory.Exists(dir))
{
throw new ValidationException($"{dir} root directory does not exist or is not accessible (check permissions)");
throw new CustomValidationException($"{dir} root directory does not exist or is not accessible (check permissions)");
}
}
}
@@ -103,28 +103,28 @@ public sealed record DownloadClientConfig
{
if (string.IsNullOrWhiteSpace(Name))
{
throw new ValidationException($"Client name cannot be empty");
throw new CustomValidationException($"Client name cannot be empty");
}
if (Host is null)
{
throw new ValidationException($"Host cannot be empty");
throw new CustomValidationException($"Host cannot be empty");
}
if (!string.IsNullOrWhiteSpace(DownloadDirectorySource) != !string.IsNullOrWhiteSpace(DownloadDirectoryTarget))
{
throw new ValidationException("Both download directory source and target must be set, or both must be empty");
throw new CustomValidationException("Both download directory source and target must be set, or both must be empty");
}
if (TypeName.RequiresAuthField(DownloadClientAuthField.ApiKey) && string.IsNullOrWhiteSpace(ApiKey))
{
throw new ValidationException("API key cannot be empty");
throw new CustomValidationException("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");
throw new CustomValidationException("Use either an API key or a username and password, not both");
}
}
}
@@ -19,7 +19,7 @@ public sealed record AuthConfig : IConfig
{
if (!IsValidIpOrCidr(entry))
{
throw new ValidationException($"Invalid IP address or CIDR range: {entry}");
throw new CustomValidationException($"Invalid IP address or CIDR range: {entry}");
}
}
}
@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Shared.Helpers;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.General;
@@ -51,32 +51,32 @@ public sealed record GeneralConfig : IConfig
{
if (HttpTimeout is 0)
{
throw new ValidationException("HTTP_TIMEOUT must be greater than 0");
throw new CustomValidationException("HTTP_TIMEOUT must be greater than 0");
}
if (StrikeInactivityWindowHours is 0)
{
throw new ValidationException("STRIKE_INACTIVITY_WINDOW_HOURS must be greater than 0");
throw new CustomValidationException("STRIKE_INACTIVITY_WINDOW_HOURS must be greater than 0");
}
if (StrikeInactivityWindowHours > 168)
{
throw new ValidationException("STRIKE_INACTIVITY_WINDOW_HOURS must be less than or equal to 168");
throw new CustomValidationException("STRIKE_INACTIVITY_WINDOW_HOURS must be less than or equal to 168");
}
if (HistoryRetentionDays is 0)
{
throw new ValidationException("HISTORY_RETENTION_DAYS must be greater than 0");
throw new CustomValidationException("HISTORY_RETENTION_DAYS must be greater than 0");
}
if (HistoryRetentionDays > 3650)
{
throw new ValidationException("HISTORY_RETENTION_DAYS must be less than or equal to 3650");
throw new CustomValidationException("HISTORY_RETENTION_DAYS must be less than or equal to 3650");
}
if (ConnectivityCheckEnabled && ConnectivityCheckUrls.Count is 0)
{
throw new ValidationException("At least one connectivity check URL is required when the internet connectivity check is enabled");
throw new CustomValidationException("At least one connectivity check URL is required when the internet connectivity check is enabled");
}
Log.Validate();
@@ -27,32 +27,32 @@ public sealed record LoggingConfig : IConfig
{
if (RollingSizeMB > 100)
{
throw new ValidationException("Log rolling size cannot exceed 100 MB");
throw new CustomValidationException("Log rolling size cannot exceed 100 MB");
}
if (RetainedFileCount > 50)
{
throw new ValidationException("Log retained file count cannot exceed 50");
throw new CustomValidationException("Log retained file count cannot exceed 50");
}
if (TimeLimitHours > 1440) // 24 * 60
{
throw new ValidationException("Log time limit cannot exceed 60 days");
throw new CustomValidationException("Log time limit cannot exceed 60 days");
}
if (ArchiveRetainedCount > 100)
{
throw new ValidationException("Log archive retained count cannot exceed 100");
throw new CustomValidationException("Log archive retained count cannot exceed 100");
}
if (ArchiveTimeLimitHours > 1440) // 24 * 60
{
throw new ValidationException("Log archive time limit cannot exceed 60 days");
throw new CustomValidationException("Log archive time limit cannot exceed 60 days");
}
if (ArchiveRetainedCount is 0 && ArchiveTimeLimitHours is 0 && ArchiveEnabled)
{
throw new ValidationException("Archiving is enabled, but no retention policy is set. Please set either a retained file count or time limit");
throw new CustomValidationException("Archiving is enabled, but no retention policy is set. Please set either a retained file count or time limit");
}
}
}
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
using Cleanuparr.Domain.Exceptions;
namespace Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
@@ -49,7 +49,7 @@ public sealed record ContentBlockerConfig : IJobConfig
if (!Sonarr.Enabled && !Radarr.Enabled && !Lidarr.Enabled && !Readarr.Enabled && !Whisparr.Enabled && !Sportarr.Enabled && !LazyLibrarian.Enabled)
{
throw new ValidationException("At least one blocklist must be configured when Malware Blocker is enabled");
throw new CustomValidationException("At least one blocklist must be configured when Malware Blocker is enabled");
}
ValidateBlocklistSettings(Sonarr, "Sonarr");
@@ -70,7 +70,7 @@ public sealed record ContentBlockerConfig : IJobConfig
if (string.IsNullOrWhiteSpace(settings.BlocklistPath))
{
throw new ValidationException($"{context} blocklist is enabled but path is not specified");
throw new CustomValidationException($"{context} blocklist is enabled but path is not specified");
}
if (Uri.TryCreate(settings.BlocklistPath, UriKind.Absolute, out var uri) &&
@@ -81,7 +81,7 @@ public sealed record ContentBlockerConfig : IJobConfig
if (!File.Exists(settings.BlocklistPath))
{
throw new ValidationException($"{context} blocklist does not exist: {settings.BlocklistPath}");
throw new CustomValidationException($"{context} blocklist does not exist: {settings.BlocklistPath}");
}
}
}
@@ -2,9 +2,9 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -86,22 +86,22 @@ public sealed record AppriseConfig : IConfig
{
if (string.IsNullOrWhiteSpace(Url))
{
throw new ValidationException("Apprise server URL is required for API mode");
throw new CustomValidationException("Apprise server URL is required for API mode");
}
if (Uri is null)
{
throw new ValidationException("Apprise server URL must be a valid HTTP or HTTPS URL");
throw new CustomValidationException("Apprise server URL must be a valid HTTP or HTTPS URL");
}
if (string.IsNullOrWhiteSpace(Key))
{
throw new ValidationException("Apprise configuration key is required for API mode");
throw new CustomValidationException("Apprise configuration key is required for API mode");
}
if (Key.Length < 2)
{
throw new ValidationException("Apprise configuration key must be at least 2 characters long");
throw new CustomValidationException("Apprise configuration key must be at least 2 characters long");
}
}
@@ -109,7 +109,7 @@ public sealed record AppriseConfig : IConfig
{
if (string.IsNullOrWhiteSpace(ServiceUrls))
{
throw new ValidationException("At least one service URL is required for CLI mode");
throw new CustomValidationException("At least one service URL is required for CLI mode");
}
}
}
@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -38,19 +38,19 @@ public sealed record DiscordConfig : IConfig
{
if (string.IsNullOrWhiteSpace(WebhookUrl))
{
throw new ValidationException("Discord webhook URL is required");
throw new CustomValidationException("Discord webhook URL is required");
}
if (!WebhookUrl.StartsWith("https://discord.com/api/webhooks/", StringComparison.OrdinalIgnoreCase) &&
!WebhookUrl.StartsWith("https://discordapp.com/api/webhooks/", StringComparison.OrdinalIgnoreCase))
{
throw new ValidationException("Discord webhook URL must be a valid Discord webhook URL");
throw new CustomValidationException("Discord webhook URL must be a valid Discord webhook URL");
}
if (!string.IsNullOrWhiteSpace(AvatarUrl) &&
!Uri.TryCreate(AvatarUrl, UriKind.Absolute, out var uri))
{
throw new ValidationException("Avatar URL must be a valid URL");
throw new CustomValidationException("Avatar URL must be a valid URL");
}
}
}
@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -37,23 +37,23 @@ public sealed record GotifyConfig : IConfig
{
if (string.IsNullOrWhiteSpace(ServerUrl))
{
throw new ValidationException("Gotify server URL is required");
throw new CustomValidationException("Gotify server URL is required");
}
if (!Uri.TryCreate(ServerUrl, UriKind.Absolute, out var uri) ||
(uri.Scheme != "http" && uri.Scheme != "https"))
{
throw new ValidationException("Gotify server URL must be a valid HTTP or HTTPS URL");
throw new CustomValidationException("Gotify server URL must be a valid HTTP or HTTPS URL");
}
if (string.IsNullOrWhiteSpace(ApplicationToken))
{
throw new ValidationException("Gotify application token is required");
throw new CustomValidationException("Gotify application token is required");
}
if (Priority < 0 || Priority > 10)
{
throw new ValidationException("Priority must be between 0 and 10");
throw new CustomValidationException("Priority must be between 0 and 10");
}
}
}
@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -37,22 +37,22 @@ public sealed record NotifiarrConfig : IConfig
{
if (string.IsNullOrWhiteSpace(ApiKey))
{
throw new ValidationException("Notifiarr API key is required");
throw new CustomValidationException("Notifiarr API key is required");
}
if (ApiKey.Length < 10)
{
throw new ValidationException("Notifiarr API key must be at least 10 characters long");
throw new CustomValidationException("Notifiarr API key must be at least 10 characters long");
}
if (string.IsNullOrWhiteSpace(ChannelId))
{
throw new ValidationException("Discord channel ID is required");
throw new CustomValidationException("Discord channel ID is required");
}
if (!ulong.TryParse(ChannelId, out _))
{
throw new ValidationException("Discord channel ID must be a valid numeric ID");
throw new CustomValidationException("Discord channel ID must be a valid numeric ID");
}
}
}
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -71,17 +71,17 @@ public sealed record NtfyConfig : IConfig
{
if (string.IsNullOrWhiteSpace(ServerUrl))
{
throw new ValidationException("ntfy server URL is required");
throw new CustomValidationException("ntfy server URL is required");
}
if (Uri == null)
{
throw new ValidationException("ntfy server URL must be a valid HTTP or HTTPS URL");
throw new CustomValidationException("ntfy server URL must be a valid HTTP or HTTPS URL");
}
if (!Topics.Any(t => !string.IsNullOrWhiteSpace(t)))
{
throw new ValidationException("At least one ntfy topic is required");
throw new CustomValidationException("At least one ntfy topic is required");
}
ValidateAuthentication();
@@ -105,18 +105,18 @@ public sealed record NtfyConfig : IConfig
case NtfyAuthenticationType.BasicAuth:
if (string.IsNullOrWhiteSpace(Username))
{
throw new ValidationException("Username is required for Basic Auth");
throw new CustomValidationException("Username is required for Basic Auth");
}
if (string.IsNullOrWhiteSpace(Password))
{
throw new ValidationException("Password is required for Basic Auth");
throw new CustomValidationException("Password is required for Basic Auth");
}
break;
case NtfyAuthenticationType.AccessToken:
if (string.IsNullOrWhiteSpace(AccessToken))
{
throw new ValidationException("Access token is required for Token authentication");
throw new CustomValidationException("Access token is required for Token authentication");
}
break;
}
@@ -2,8 +2,8 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -102,29 +102,29 @@ public sealed partial record PushoverConfig : IConfig
{
if (string.IsNullOrWhiteSpace(ApiToken))
{
throw new ValidationException("Pushover API token is required");
throw new CustomValidationException("Pushover API token is required");
}
if (string.IsNullOrWhiteSpace(UserKey))
{
throw new ValidationException("Pushover user key is required");
throw new CustomValidationException("Pushover user key is required");
}
if (Priority == PushoverPriority.Emergency)
{
if (!Retry.HasValue || Retry.Value < 30)
{
throw new ValidationException("Retry interval must be at least 30 seconds for emergency priority");
throw new CustomValidationException("Retry interval must be at least 30 seconds for emergency priority");
}
if (!Expire.HasValue || Expire.Value < 1)
{
throw new ValidationException("Expire time is required for emergency priority");
throw new CustomValidationException("Expire time is required for emergency priority");
}
if (Expire.Value > 10800)
{
throw new ValidationException("Expire time cannot exceed 10800 seconds (3 hours)");
throw new CustomValidationException("Expire time cannot exceed 10800 seconds (3 hours)");
}
}
@@ -133,19 +133,19 @@ public sealed partial record PushoverConfig : IConfig
{
if (device.Length > 25)
{
throw new ValidationException($"Device name '{device}' exceeds 25 character limit");
throw new CustomValidationException($"Device name '{device}' exceeds 25 character limit");
}
if (!DeviceNameRegex().IsMatch(device))
{
throw new ValidationException($"Device name '{device}' contains invalid characters. Only letters, numbers, underscores, and hyphens are allowed.");
throw new CustomValidationException($"Device name '{device}' contains invalid characters. Only letters, numbers, underscores, and hyphens are allowed.");
}
}
// Validate sound - if provided, must not be whitespace-only
if (Sound is not null && Sound.Length > 0 && string.IsNullOrWhiteSpace(Sound))
{
throw new ValidationException("Sound name cannot be empty or whitespace when specified");
throw new CustomValidationException("Sound name cannot be empty or whitespace when specified");
}
}
}
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Shared.Attributes;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
@@ -45,27 +45,27 @@ public sealed record TelegramConfig : IConfig
{
if (string.IsNullOrWhiteSpace(BotToken))
{
throw new ValidationException("Telegram bot token is required");
throw new CustomValidationException("Telegram bot token is required");
}
if (BotToken.Length < 10)
{
throw new ValidationException("Telegram bot token must be at least 10 characters long");
throw new CustomValidationException("Telegram bot token must be at least 10 characters long");
}
if (string.IsNullOrWhiteSpace(ChatId))
{
throw new ValidationException("Telegram chat ID is required");
throw new CustomValidationException("Telegram chat ID is required");
}
if (!IsChatIdValid(ChatId))
{
throw new ValidationException("Telegram chat ID must be a valid integer (negative IDs allowed for groups)");
throw new CustomValidationException("Telegram chat ID must be a valid integer (negative IDs allowed for groups)");
}
if (!IsTopicValid(TopicId))
{
throw new ValidationException("Telegram topic ID must be a valid integer when specified");
throw new CustomValidationException("Telegram topic ID must be a valid integer when specified");
}
}
@@ -21,12 +21,12 @@ public sealed record ErroredConfig
{
if (MaxStrikes is > 0 and < 3)
{
throw new ValidationException("the minimum value for errored max strikes must be 3");
throw new CustomValidationException("the minimum value for errored max strikes must be 3");
}
if (ChangeCategory && DeletePrivate)
{
throw new ValidationException("Cannot enable both deletion and category changing");
throw new CustomValidationException("Cannot enable both deletion and category changing");
}
}
}
@@ -36,27 +36,27 @@ public sealed record FailedImportConfig
{
if (MaxStrikes is > 0 and < 3)
{
throw new ValidationException("The minimum value for failed imports max strikes must be 3");
throw new CustomValidationException("The minimum value for failed imports max strikes must be 3");
}
if (MaxStrikes >= 3 && PatternMode is PatternMode.Include && Patterns.Count is 0)
{
throw new ValidationException("At least one pattern must be specified when using the Include pattern mode");
throw new CustomValidationException("At least one pattern must be specified when using the Include pattern mode");
}
if (ForceImport && ForceImportMaxTries is 0)
{
throw new ValidationException("Force import max tries must be at least 1");
throw new CustomValidationException("Force import max tries must be at least 1");
}
if (ForceImport && ForceImportMaxTries > 5000)
{
throw new ValidationException("Force import max tries must be at most 5000");
throw new CustomValidationException("Force import max tries must be at most 5000");
}
if (ChangeCategory && DeletePrivate)
{
throw new ValidationException("Cannot enable both deletion and category changing");
throw new CustomValidationException("Cannot enable both deletion and category changing");
}
}
}
@@ -40,7 +40,7 @@ public sealed record QueueCleanerConfig : IJobConfig
if (DownloadingMetadataMaxStrikes is > 0 and < 3)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("the minimum value for downloading metadata max strikes must be 3");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("the minimum value for downloading metadata max strikes must be 3");
}
foreach (var rule in StallRules)
@@ -57,13 +57,13 @@ public sealed record QueueCleanerConfig : IJobConfig
var stallNames = StallRules.Where(r => r.Enabled).Select(r => r.Name).ToList();
if (stallNames.Count != stallNames.Distinct().Count())
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Duplicate stall rule names found");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Duplicate stall rule names found");
}
var slowNames = SlowRules.Where(r => r.Enabled).Select(r => r.Name).ToList();
if (slowNames.Count != slowNames.Distinct().Count())
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Duplicate slow rule names found");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Duplicate slow rule names found");
}
}
}
@@ -37,37 +37,37 @@ public abstract record QueueRule : IConfig
{
if (string.IsNullOrWhiteSpace(Name))
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Rule name cannot be empty");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Rule name cannot be empty");
}
if (MaxStrikes < 3)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Max strikes must be at least 3");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Max strikes must be at least 3");
}
if (MinCompletionPercentage > 100)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Minimum completion percentage must be between 0 and 100");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Minimum completion percentage must be between 0 and 100");
}
if (MaxCompletionPercentage == 0)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Maximum completion percentage must be greater than 0");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Maximum completion percentage must be greater than 0");
}
if (MaxCompletionPercentage > 100)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Maximum completion percentage must be between 1 and 100");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Maximum completion percentage must be between 1 and 100");
}
if (MaxCompletionPercentage < MinCompletionPercentage)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Maximum completion percentage must be greater than or equal to the minimum completion percentage");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Maximum completion percentage must be greater than or equal to the minimum completion percentage");
}
if (ChangeCategory && DeletePrivateTorrentsFromClient)
{
throw new Cleanuparr.Domain.Exceptions.ValidationException("Cannot enable both deletion and category changing");
throw new Cleanuparr.Domain.Exceptions.CustomValidationException("Cannot enable both deletion and category changing");
}
}
@@ -50,12 +50,12 @@ public sealed record SlowRule : QueueRule
if (MaxStrikes < 3)
{
throw new ValidationException("Slow rule max strikes must be at least 3");
throw new CustomValidationException("Slow rule max strikes must be at least 3");
}
if (MaxTimeHours < 0)
{
throw new ValidationException("Maximum time cannot be negative");
throw new CustomValidationException("Maximum time cannot be negative");
}
bool hasMinSpeed = !string.IsNullOrEmpty(MinSpeed);
@@ -63,19 +63,19 @@ public sealed record SlowRule : QueueRule
if (!hasMinSpeed && !hasMaxTime)
{
throw new ValidationException("Either minimum speed or maximum time must be specified");
throw new CustomValidationException("Either minimum speed or maximum time must be specified");
}
if (hasMinSpeed && !ByteSize.TryParse(MinSpeed, out _))
{
throw new ValidationException("Invalid minimum speed format");
throw new CustomValidationException("Invalid minimum speed format");
}
bool isIgnoreAboveSizeSet = !string.IsNullOrEmpty(IgnoreAboveSize);
if (isIgnoreAboveSizeSet && ByteSize.TryParse(IgnoreAboveSize, out _) is false)
{
throw new ValidationException($"invalid value for slow ignore above size: {IgnoreAboveSize}");
throw new CustomValidationException($"invalid value for slow ignore above size: {IgnoreAboveSize}");
}
}
@@ -37,19 +37,19 @@ public sealed record StallRule : QueueRule
if (MaxStrikes < 3)
{
throw new ValidationException("Stall rule max strikes must be at least 3");
throw new CustomValidationException("Stall rule max strikes must be at least 3");
}
if (!string.IsNullOrWhiteSpace(MinimumProgress))
{
if (!ByteSize.TryParse(MinimumProgress, out var parsed))
{
throw new ValidationException($"Invalid minimum progress value: {MinimumProgress}");
throw new CustomValidationException($"Invalid minimum progress value: {MinimumProgress}");
}
if (parsed.HasValue && parsed.Value.Bytes < 0)
{
throw new ValidationException("Minimum progress must be zero or greater");
throw new CustomValidationException("Minimum progress must be zero or greater");
}
}
}
@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Shared.Helpers;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Seeker;
@@ -53,24 +53,24 @@ public sealed record SeekerConfig : IConfig
{
if (SearchInterval < Constants.MinSearchIntervalMinutes)
{
throw new ValidationException(
throw new CustomValidationException(
$"{nameof(SearchInterval)} must be at least {Constants.MinSearchIntervalMinutes} minute(s)");
}
if (SearchInterval > Constants.MaxSearchIntervalMinutes)
{
throw new ValidationException(
throw new CustomValidationException(
$"{nameof(SearchInterval)} must be at most {Constants.MaxSearchIntervalMinutes} minutes");
}
if (!new List<int> { 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60, 120, 180, 240, 360 }.Contains(SearchInterval))
{
throw new ValidationException($"Invalid search interval {SearchInterval}");
throw new CustomValidationException($"Invalid search interval {SearchInterval}");
}
if (PostReleaseGraceHours is < 0 or > 72)
{
throw new ValidationException($"{nameof(PostReleaseGraceHours)} must be between 0 and 72");
throw new CustomValidationException($"{nameof(PostReleaseGraceHours)} must be between 0 and 72");
}
}