mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-08 23:03:13 -04:00
Improve seeding rule customization (#553)
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
using System.Text.Json;
|
||||
using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
|
||||
using Cleanuparr.Api.Features.DownloadCleaner.Controllers;
|
||||
using Cleanuparr.Api.Tests.Features.DownloadCleaner.TestHelpers;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
|
||||
namespace Cleanuparr.Api.Tests.Features.DownloadCleaner;
|
||||
|
||||
public class SeedingRulesControllerTests : IDisposable
|
||||
{
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly SeedingRulesController _controller;
|
||||
|
||||
public SeedingRulesControllerTests()
|
||||
{
|
||||
_dataContext = SeedingRulesTestDataFactory.CreateDataContext();
|
||||
var logger = Substitute.For<ILogger<SeedingRulesController>>();
|
||||
_controller = new SeedingRulesController(logger, _dataContext);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dataContext.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static SeedingRuleRequest CreateValidRequest(
|
||||
string name = "Test Rule",
|
||||
List<string>? categories = null,
|
||||
List<string>? trackerPatterns = null,
|
||||
List<string>? tagsAny = null,
|
||||
List<string>? tagsAll = null,
|
||||
int? priority = null,
|
||||
double maxRatio = 2.0,
|
||||
double minSeedTime = 0,
|
||||
double maxSeedTime = -1,
|
||||
bool deleteSourceFiles = true)
|
||||
{
|
||||
return new SeedingRuleRequest
|
||||
{
|
||||
Name = name,
|
||||
Categories = categories ?? ["movies"],
|
||||
TrackerPatterns = trackerPatterns ?? [],
|
||||
TagsAny = tagsAny ?? [],
|
||||
TagsAll = tagsAll ?? [],
|
||||
Priority = priority,
|
||||
PrivacyType = TorrentPrivacyType.Both,
|
||||
MaxRatio = maxRatio,
|
||||
MinSeedTime = minSeedTime,
|
||||
MaxSeedTime = maxSeedTime,
|
||||
DeleteSourceFiles = deleteSourceFiles,
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonElement GetJsonBody(IActionResult result)
|
||||
{
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
return JsonDocument.Parse(json).RootElement;
|
||||
}
|
||||
|
||||
private static JsonElement GetCreatedJsonBody(IActionResult result)
|
||||
{
|
||||
var createdResult = result.ShouldBeOfType<CreatedAtActionResult>();
|
||||
var json = JsonSerializer.Serialize(createdResult.Value);
|
||||
return JsonDocument.Parse(json).RootElement;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// GetSeedingRules
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeedingRules_EmptyRules_ReturnsEmptyList()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
|
||||
var result = await _controller.GetSeedingRules(client.Id);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
var array = JsonDocument.Parse(json).RootElement;
|
||||
array.GetArrayLength().ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeedingRules_ReturnsRulesOrderedByPriority()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "Rule C", priority: 3);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "Rule A", priority: 1);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "Rule B", priority: 2);
|
||||
|
||||
var result = await _controller.GetSeedingRules(client.Id);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
var array = JsonDocument.Parse(json).RootElement;
|
||||
array.GetArrayLength().ShouldBe(3);
|
||||
array[0].GetProperty("name").GetString().ShouldBe("Rule A");
|
||||
array[1].GetProperty("name").GetString().ShouldBe("Rule B");
|
||||
array[2].GetProperty("name").GetString().ShouldBe("Rule C");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeedingRules_NonExistentClient_ReturnsNotFound()
|
||||
{
|
||||
var result = await _controller.GetSeedingRules(Guid.NewGuid());
|
||||
result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeedingRules_QBitClient_ReturnsTagFields()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id,
|
||||
tagsAny: ["hd", "private"], tagsAll: ["required"]);
|
||||
|
||||
var result = await _controller.GetSeedingRules(client.Id);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
var rule = JsonDocument.Parse(json).RootElement[0];
|
||||
rule.GetProperty("tagsAny").GetArrayLength().ShouldBe(2);
|
||||
rule.GetProperty("tagsAll").GetArrayLength().ShouldBe(1);
|
||||
rule.GetProperty("tagsAll")[0].GetString().ShouldBe("required");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeedingRules_DelugeClient_ReturnsEmptyTagFields()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext, DownloadClientTypeName.Deluge, "Test Deluge");
|
||||
SeedingRulesTestDataFactory.AddDelugeSeedingRule(_dataContext, client.Id);
|
||||
|
||||
var result = await _controller.GetSeedingRules(client.Id);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
var rule = JsonDocument.Parse(json).RootElement[0];
|
||||
rule.GetProperty("tagsAny").GetArrayLength().ShouldBe(0);
|
||||
rule.GetProperty("tagsAll").GetArrayLength().ShouldBe(0);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// CreateSeedingRule
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_ValidRequest_ReturnsCreated()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var request = CreateValidRequest(name: "Movies Rule", categories: ["movies", "films"]);
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
|
||||
var createdResult = result.ShouldBeOfType<CreatedAtActionResult>();
|
||||
createdResult.StatusCode.ShouldBe(201);
|
||||
|
||||
var body = GetCreatedJsonBody(result);
|
||||
body.GetProperty("Name").GetString().ShouldBe("Movies Rule");
|
||||
body.GetProperty("Categories").GetArrayLength().ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_AutoAssignsPriority_WhenNotProvided()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var request = CreateValidRequest();
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
|
||||
var body = GetCreatedJsonBody(result);
|
||||
body.GetProperty("Priority").GetInt32().ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_AutoAssignsSequentialPriority()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, priority: 1);
|
||||
|
||||
var request = CreateValidRequest(name: "Second Rule", categories: ["tv"]);
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
|
||||
var body = GetCreatedJsonBody(result);
|
||||
body.GetProperty("Priority").GetInt32().ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_DuplicatePriority_ReturnsBadRequest()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, priority: 1);
|
||||
|
||||
var request = CreateValidRequest(priority: 1);
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
result.ShouldBeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_NonExistentClient_ReturnsNotFound()
|
||||
{
|
||||
var request = CreateValidRequest();
|
||||
|
||||
var result = await _controller.CreateSeedingRule(Guid.NewGuid(), request);
|
||||
result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_EmptyCategories_ReturnsBadRequest()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var request = CreateValidRequest(categories: []);
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
|
||||
// Validate() throws ValidationException → caught → BadRequest
|
||||
result.ShouldBeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_SanitizesWhitespaceInLists()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var request = CreateValidRequest(
|
||||
trackerPatterns: ["", " ", "valid.com", " trimmed.com "]);
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
|
||||
var body = GetCreatedJsonBody(result);
|
||||
var patterns = body.GetProperty("TrackerPatterns");
|
||||
patterns.GetArrayLength().ShouldBe(2);
|
||||
patterns[0].GetString().ShouldBe("valid.com");
|
||||
patterns[1].GetString().ShouldBe("trimmed.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateSeedingRule_ForTransmission_CreatesTransmissionRule()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext,
|
||||
DownloadClientTypeName.Transmission, "Test Transmission");
|
||||
var request = CreateValidRequest(tagsAny: ["tag1"]);
|
||||
|
||||
var result = await _controller.CreateSeedingRule(client.Id, request);
|
||||
|
||||
var createdResult = result.ShouldBeOfType<CreatedAtActionResult>();
|
||||
createdResult.Value.ShouldBeOfType<TransmissionSeedingRule>();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// UpdateSeedingRule
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeedingRule_ValidRequest_ReturnsOk()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id);
|
||||
|
||||
var request = CreateValidRequest(name: "Updated Name", categories: ["tv", "anime"]);
|
||||
|
||||
var result = await _controller.UpdateSeedingRule(rule.Id, request);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var updated = okResult.Value.ShouldBeOfType<QBitSeedingRule>();
|
||||
updated.Name.ShouldBe("Updated Name");
|
||||
updated.Categories.ShouldBe(new List<string> { "tv", "anime" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeedingRule_DoesNotChangePriority()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, priority: 5);
|
||||
|
||||
var request = CreateValidRequest(priority: 1);
|
||||
|
||||
var result = await _controller.UpdateSeedingRule(rule.Id, request);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var updated = okResult.Value.ShouldBeOfType<QBitSeedingRule>();
|
||||
updated.Priority.ShouldBe(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeedingRule_UpdatesTagsForTagFilterableClient()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id);
|
||||
|
||||
var request = CreateValidRequest(tagsAny: ["new-tag"], tagsAll: ["must-have"]);
|
||||
|
||||
var result = await _controller.UpdateSeedingRule(rule.Id, request);
|
||||
|
||||
var okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
var updated = okResult.Value.ShouldBeOfType<QBitSeedingRule>();
|
||||
updated.TagsAny.ShouldBe(new List<string> { "new-tag" });
|
||||
updated.TagsAll.ShouldBe(new List<string> { "must-have" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeedingRule_NonExistentRule_ReturnsNotFound()
|
||||
{
|
||||
var request = CreateValidRequest();
|
||||
|
||||
var result = await _controller.UpdateSeedingRule(Guid.NewGuid(), request);
|
||||
result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeedingRule_ValidationFailure_ReturnsBadRequest()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id);
|
||||
|
||||
// Both maxRatio and maxSeedTime negative → validation failure
|
||||
var request = CreateValidRequest(maxRatio: -1, maxSeedTime: -1);
|
||||
|
||||
var result = await _controller.UpdateSeedingRule(rule.Id, request);
|
||||
result.ShouldBeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// ReorderSeedingRules
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ReorderSeedingRules_ValidRequest_ReturnsNoContent()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule1 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "A", priority: 1);
|
||||
var rule2 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "B", priority: 2);
|
||||
|
||||
var request = new ReorderSeedingRulesRequest { OrderedIds = [rule2.Id, rule1.Id] };
|
||||
|
||||
var result = await _controller.ReorderSeedingRules(client.Id, request);
|
||||
result.ShouldBeOfType<NoContentResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReorderSeedingRules_AssignsSequentialPriorities()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule1 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "A", priority: 1);
|
||||
var rule2 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "B", priority: 2);
|
||||
var rule3 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "C", priority: 3);
|
||||
|
||||
// Reverse order
|
||||
var request = new ReorderSeedingRulesRequest { OrderedIds = [rule3.Id, rule2.Id, rule1.Id] };
|
||||
await _controller.ReorderSeedingRules(client.Id, request);
|
||||
|
||||
// Verify via GET
|
||||
var getResult = await _controller.GetSeedingRules(client.Id);
|
||||
var okResult = getResult.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
var array = JsonDocument.Parse(json).RootElement;
|
||||
|
||||
array[0].GetProperty("name").GetString().ShouldBe("C");
|
||||
array[0].GetProperty("priority").GetInt32().ShouldBe(1);
|
||||
array[1].GetProperty("name").GetString().ShouldBe("B");
|
||||
array[1].GetProperty("priority").GetInt32().ShouldBe(2);
|
||||
array[2].GetProperty("name").GetString().ShouldBe("A");
|
||||
array[2].GetProperty("priority").GetInt32().ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReorderSeedingRules_NonExistentClient_ReturnsNotFound()
|
||||
{
|
||||
var request = new ReorderSeedingRulesRequest { OrderedIds = [Guid.NewGuid()] };
|
||||
|
||||
var result = await _controller.ReorderSeedingRules(Guid.NewGuid(), request);
|
||||
result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReorderSeedingRules_DuplicateIds_ReturnsBadRequest()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule1 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "A", priority: 1);
|
||||
var rule2 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "B", priority: 2);
|
||||
|
||||
var request = new ReorderSeedingRulesRequest { OrderedIds = [rule1.Id, rule1.Id] };
|
||||
|
||||
var result = await _controller.ReorderSeedingRules(client.Id, request);
|
||||
result.ShouldBeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReorderSeedingRules_WrongCount_ReturnsBadRequest()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule1 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "A", priority: 1);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "B", priority: 2);
|
||||
|
||||
// Only send 1 of 2 IDs
|
||||
var request = new ReorderSeedingRulesRequest { OrderedIds = [rule1.Id] };
|
||||
|
||||
var result = await _controller.ReorderSeedingRules(client.Id, request);
|
||||
result.ShouldBeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReorderSeedingRules_UnknownRuleId_ReturnsBadRequest()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule1 = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "A", priority: 1);
|
||||
SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id, name: "B", priority: 2);
|
||||
|
||||
var request = new ReorderSeedingRulesRequest { OrderedIds = [rule1.Id, Guid.NewGuid()] };
|
||||
|
||||
var result = await _controller.ReorderSeedingRules(client.Id, request);
|
||||
result.ShouldBeOfType<BadRequestObjectResult>();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// DeleteSeedingRule
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSeedingRule_ExistingRule_ReturnsNoContent()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id);
|
||||
|
||||
var result = await _controller.DeleteSeedingRule(rule.Id);
|
||||
result.ShouldBeOfType<NoContentResult>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSeedingRule_VerifiesRuleRemoved()
|
||||
{
|
||||
var client = SeedingRulesTestDataFactory.AddDownloadClient(_dataContext);
|
||||
var rule = SeedingRulesTestDataFactory.AddQBitSeedingRule(_dataContext, client.Id);
|
||||
|
||||
await _controller.DeleteSeedingRule(rule.Id);
|
||||
|
||||
// Verify rule no longer exists
|
||||
var getResult = await _controller.GetSeedingRules(client.Id);
|
||||
var okResult = getResult.ShouldBeOfType<OkObjectResult>();
|
||||
var json = JsonSerializer.Serialize(okResult.Value);
|
||||
var array = JsonDocument.Parse(json).RootElement;
|
||||
array.GetArrayLength().ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSeedingRule_NonExistentRule_ReturnsNotFound()
|
||||
{
|
||||
var result = await _controller.DeleteSeedingRule(Guid.NewGuid());
|
||||
result.ShouldBeOfType<NotFoundObjectResult>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Seeker;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Cleanuparr.Api.Tests.Features.DownloadCleaner.TestHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating SQLite in-memory contexts for SeedingRulesController tests
|
||||
/// </summary>
|
||||
public static class SeedingRulesTestDataFactory
|
||||
{
|
||||
public static DataContext CreateDataContext()
|
||||
{
|
||||
var connection = new SqliteConnection("DataSource=:memory:");
|
||||
connection.Open();
|
||||
|
||||
var options = new DbContextOptionsBuilder<DataContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
var context = new DataContext(options);
|
||||
context.Database.EnsureCreated();
|
||||
|
||||
SeedDefaultData(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
private static void SeedDefaultData(DataContext context)
|
||||
{
|
||||
context.GeneralConfigs.Add(new GeneralConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DryRun = false,
|
||||
IgnoredDownloads = [],
|
||||
Log = new LoggingConfig()
|
||||
});
|
||||
|
||||
context.ArrConfigs.AddRange(
|
||||
new ArrConfig { Id = Guid.NewGuid(), Type = InstanceType.Sonarr, Instances = [], FailedImportMaxStrikes = 3 },
|
||||
new ArrConfig { Id = Guid.NewGuid(), Type = InstanceType.Radarr, Instances = [], FailedImportMaxStrikes = 3 },
|
||||
new ArrConfig { Id = Guid.NewGuid(), Type = InstanceType.Lidarr, Instances = [], FailedImportMaxStrikes = 3 },
|
||||
new ArrConfig { Id = Guid.NewGuid(), Type = InstanceType.Readarr, Instances = [], FailedImportMaxStrikes = 3 },
|
||||
new ArrConfig { Id = Guid.NewGuid(), Type = InstanceType.Whisparr, Instances = [], FailedImportMaxStrikes = 3 }
|
||||
);
|
||||
|
||||
context.QueueCleanerConfigs.Add(new QueueCleanerConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
IgnoredDownloads = [],
|
||||
FailedImport = new FailedImportConfig()
|
||||
});
|
||||
|
||||
context.ContentBlockerConfigs.Add(new ContentBlockerConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
IgnoredDownloads = [],
|
||||
DeletePrivate = false,
|
||||
Sonarr = new BlocklistSettings { Enabled = false },
|
||||
Radarr = new BlocklistSettings { Enabled = false },
|
||||
Lidarr = new BlocklistSettings { Enabled = false },
|
||||
Readarr = new BlocklistSettings { Enabled = false },
|
||||
Whisparr = new BlocklistSettings { Enabled = false }
|
||||
});
|
||||
|
||||
context.DownloadCleanerConfigs.Add(new DownloadCleanerConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
IgnoredDownloads = []
|
||||
});
|
||||
|
||||
context.SeekerConfigs.Add(new SeekerConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SearchEnabled = true,
|
||||
ProactiveSearchEnabled = false
|
||||
});
|
||||
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public static DownloadClientConfig AddDownloadClient(
|
||||
DataContext context,
|
||||
DownloadClientTypeName typeName = DownloadClientTypeName.qBittorrent,
|
||||
string name = "Test qBittorrent")
|
||||
{
|
||||
var config = new DownloadClientConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
TypeName = typeName,
|
||||
Type = DownloadClientType.Torrent,
|
||||
Enabled = true,
|
||||
Host = new Uri("http://localhost:8080"),
|
||||
Username = "admin",
|
||||
Password = "admin"
|
||||
};
|
||||
|
||||
context.DownloadClients.Add(config);
|
||||
context.SaveChanges();
|
||||
return config;
|
||||
}
|
||||
|
||||
public static QBitSeedingRule AddQBitSeedingRule(
|
||||
DataContext context,
|
||||
Guid downloadClientId,
|
||||
string name = "Test Rule",
|
||||
int priority = 1,
|
||||
List<string>? categories = null,
|
||||
List<string>? trackerPatterns = null,
|
||||
List<string>? tagsAny = null,
|
||||
List<string>? tagsAll = null,
|
||||
double maxRatio = 2.0,
|
||||
double minSeedTime = 0,
|
||||
double maxSeedTime = -1)
|
||||
{
|
||||
var rule = new QBitSeedingRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DownloadClientConfigId = downloadClientId,
|
||||
Name = name,
|
||||
Priority = priority,
|
||||
Categories = categories ?? ["movies"],
|
||||
TrackerPatterns = trackerPatterns ?? [],
|
||||
TagsAny = tagsAny ?? [],
|
||||
TagsAll = tagsAll ?? [],
|
||||
PrivacyType = TorrentPrivacyType.Both,
|
||||
MaxRatio = maxRatio,
|
||||
MinSeedTime = minSeedTime,
|
||||
MaxSeedTime = maxSeedTime,
|
||||
DeleteSourceFiles = true,
|
||||
};
|
||||
|
||||
context.QBitSeedingRules.Add(rule);
|
||||
context.SaveChanges();
|
||||
return rule;
|
||||
}
|
||||
|
||||
public static DelugeSeedingRule AddDelugeSeedingRule(
|
||||
DataContext context,
|
||||
Guid downloadClientId,
|
||||
string name = "Test Rule",
|
||||
int priority = 1,
|
||||
List<string>? categories = null,
|
||||
double maxRatio = 2.0,
|
||||
double maxSeedTime = -1)
|
||||
{
|
||||
var rule = new DelugeSeedingRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DownloadClientConfigId = downloadClientId,
|
||||
Name = name,
|
||||
Priority = priority,
|
||||
Categories = categories ?? ["movies"],
|
||||
TrackerPatterns = [],
|
||||
PrivacyType = TorrentPrivacyType.Both,
|
||||
MaxRatio = maxRatio,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = maxSeedTime,
|
||||
DeleteSourceFiles = true,
|
||||
};
|
||||
|
||||
context.DelugeSeedingRules.Add(rule);
|
||||
context.SaveChanges();
|
||||
return rule;
|
||||
}
|
||||
|
||||
public static TransmissionSeedingRule AddTransmissionSeedingRule(
|
||||
DataContext context,
|
||||
Guid downloadClientId,
|
||||
string name = "Test Rule",
|
||||
int priority = 1,
|
||||
List<string>? categories = null,
|
||||
double maxRatio = 2.0,
|
||||
double maxSeedTime = -1)
|
||||
{
|
||||
var rule = new TransmissionSeedingRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DownloadClientConfigId = downloadClientId,
|
||||
Name = name,
|
||||
Priority = priority,
|
||||
Categories = categories ?? ["movies"],
|
||||
TrackerPatterns = [],
|
||||
TagsAny = [],
|
||||
TagsAll = [],
|
||||
PrivacyType = TorrentPrivacyType.Both,
|
||||
MaxRatio = maxRatio,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = maxSeedTime,
|
||||
DeleteSourceFiles = true,
|
||||
};
|
||||
|
||||
context.TransmissionSeedingRules.Add(rule);
|
||||
context.SaveChanges();
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,9 @@ public static class ServicesDI
|
||||
.AddScoped<IDownloadServiceFactory, DownloadServiceFactory>()
|
||||
.AddScoped<IStriker, Striker>()
|
||||
.AddScoped<FileReader>()
|
||||
.AddScoped<IRuleManager, RuleManager>()
|
||||
.AddScoped<IRuleEvaluator, RuleEvaluator>()
|
||||
.AddScoped<IQueueRuleManager, QueueRuleManager>()
|
||||
.AddScoped<IQueueRuleEvaluator, QueueRuleEvaluator>()
|
||||
.AddScoped<ISeedingRuleEvaluator, SeedingRuleEvaluator>()
|
||||
.AddScoped<IRuleIntervalValidator, RuleIntervalValidator>()
|
||||
.AddScoped<IStatsService, StatsService>()
|
||||
.AddSingleton<IJobManagementService, JobManagementService>()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
|
||||
|
||||
public record ReorderSeedingRulesRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// IDs of seeding rules in the desired priority order (first = highest priority).
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<Guid> OrderedIds { get; init; } = [];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
|
||||
@@ -8,6 +8,36 @@ public record SeedingRuleRequest
|
||||
[Required]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Categories this rule applies to. At least one must be specified.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(1, ErrorMessage = "At least one category must be specified.")]
|
||||
public List<string> Categories { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Tracker domain suffixes to match (e.g. "tracker.example.com"). Empty = any tracker.
|
||||
/// </summary>
|
||||
public List<string> TrackerPatterns { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Torrent must have at least one of these tags/labels. Accepted for all clients;
|
||||
/// silently ignored for Deluge, rTorrent, and µTorrent.
|
||||
/// </summary>
|
||||
public List<string> TagsAny { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Torrent must have ALL of these tags/labels. Accepted for all clients;
|
||||
/// silently ignored for Deluge, rTorrent, and µTorrent.
|
||||
/// </summary>
|
||||
public List<string> TagsAll { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation priority (lower = evaluated first). Auto-assigned if not provided.
|
||||
/// </summary>
|
||||
[Range(1, int.MaxValue, ErrorMessage = "Priority must be a positive integer.")]
|
||||
public int? Priority { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which torrent privacy types this rule applies to.
|
||||
/// </summary>
|
||||
@@ -32,4 +62,4 @@ public record SeedingRuleRequest
|
||||
/// Whether to delete the source files when cleaning the download.
|
||||
/// </summary>
|
||||
public bool DeleteSourceFiles { get; init; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,11 @@ public sealed class DownloadCleanerConfigController : ControllerBase
|
||||
{
|
||||
id = r.Id,
|
||||
name = r.Name,
|
||||
categories = r.Categories,
|
||||
trackerPatterns = r.TrackerPatterns,
|
||||
tagsAny = (r as ITagFilterable)?.TagsAny ?? new List<string>(),
|
||||
tagsAll = (r as ITagFilterable)?.TagsAll ?? new List<string>(),
|
||||
priority = r.Priority,
|
||||
privacyType = r.PrivacyType,
|
||||
maxRatio = r.MaxRatio,
|
||||
minSeedTime = r.MinSeedTime,
|
||||
|
||||
@@ -44,7 +44,21 @@ public class SeedingRulesController : ControllerBase
|
||||
|
||||
var rules = await SeedingRuleHelper.GetForClientAsync(_dataContext, client);
|
||||
|
||||
return Ok(rules);
|
||||
return Ok(rules.Select(r => new
|
||||
{
|
||||
id = r.Id,
|
||||
name = r.Name,
|
||||
categories = r.Categories,
|
||||
trackerPatterns = r.TrackerPatterns,
|
||||
tagsAny = (r as ITagFilterable)?.TagsAny ?? new List<string>(),
|
||||
tagsAll = (r as ITagFilterable)?.TagsAll ?? new List<string>(),
|
||||
priority = r.Priority,
|
||||
privacyType = r.PrivacyType,
|
||||
maxRatio = r.MaxRatio,
|
||||
minSeedTime = r.MinSeedTime,
|
||||
maxSeedTime = r.MaxSeedTime,
|
||||
deleteSourceFiles = r.DeleteSourceFiles,
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -78,22 +92,15 @@ public class SeedingRulesController : ControllerBase
|
||||
}
|
||||
|
||||
var existingRules = await SeedingRuleHelper.GetForClientAsync(_dataContext, client);
|
||||
var duplicate = existingRules.FirstOrDefault(r =>
|
||||
r.Name.Equals(ruleDto.Name.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
r.PrivacyType == ruleDto.PrivacyType);
|
||||
|
||||
if (duplicate is not null)
|
||||
if (ruleDto.Priority.HasValue && existingRules.Any(r => r.Priority == ruleDto.Priority.Value))
|
||||
{
|
||||
return BadRequest(new { Message = "A seeding rule with this name and privacy type already exists for this client" });
|
||||
return BadRequest(new { Message = $"A seeding rule with priority {ruleDto.Priority.Value} already exists for this client" });
|
||||
}
|
||||
|
||||
var overlapError = GetPrivacyTypeOverlapError(ruleDto.Name.Trim(), ruleDto.PrivacyType, existingRules, excludeId: null);
|
||||
if (overlapError is not null)
|
||||
{
|
||||
return BadRequest(new { Message = overlapError });
|
||||
}
|
||||
int priority = ruleDto.Priority ?? (existingRules.Count == 0 ? 1 : existingRules.Max(r => r.Priority) + 1);
|
||||
|
||||
var rule = CreateRule(client.TypeName, client.Id, ruleDto);
|
||||
var rule = CreateRule(client.TypeName, client.Id, ruleDto, priority);
|
||||
rule.Validate();
|
||||
|
||||
AddRuleToDbSet(rule);
|
||||
@@ -139,30 +146,21 @@ public class SeedingRulesController : ControllerBase
|
||||
return NotFound(new { Message = $"Seeding rule with ID {id} not found" });
|
||||
}
|
||||
|
||||
// Check for duplicate name+privacyType on the same client, excluding this rule
|
||||
var clientRules = await SeedingRuleHelper.GetForClientIdAsync(_dataContext, existingRule.DownloadClientConfigId);
|
||||
var duplicate = clientRules.FirstOrDefault(r =>
|
||||
r.Id != id &&
|
||||
r.Name.Equals(ruleDto.Name.Trim(), StringComparison.OrdinalIgnoreCase) &&
|
||||
r.PrivacyType == ruleDto.PrivacyType);
|
||||
|
||||
if (duplicate is not null)
|
||||
{
|
||||
return BadRequest(new { Message = "A seeding rule with this name and privacy type already exists for this client" });
|
||||
}
|
||||
|
||||
var overlapError = GetPrivacyTypeOverlapError(ruleDto.Name.Trim(), ruleDto.PrivacyType, clientRules, excludeId: id);
|
||||
if (overlapError is not null)
|
||||
{
|
||||
return BadRequest(new { Message = overlapError });
|
||||
}
|
||||
|
||||
existingRule.Name = ruleDto.Name.Trim();
|
||||
existingRule.Categories = SanitizeStringList(ruleDto.Categories);
|
||||
existingRule.TrackerPatterns = SanitizeStringList(ruleDto.TrackerPatterns);
|
||||
existingRule.PrivacyType = ruleDto.PrivacyType;
|
||||
existingRule.MaxRatio = ruleDto.MaxRatio;
|
||||
existingRule.MinSeedTime = ruleDto.MinSeedTime;
|
||||
existingRule.MaxSeedTime = ruleDto.MaxSeedTime;
|
||||
existingRule.DeleteSourceFiles = ruleDto.DeleteSourceFiles;
|
||||
// Priority is intentionally NOT updated here — use the reorder endpoint
|
||||
|
||||
if (existingRule is ITagFilterable tagFilterable)
|
||||
{
|
||||
tagFilterable.TagsAny = SanitizeStringList(ruleDto.TagsAny);
|
||||
tagFilterable.TagsAll = SanitizeStringList(ruleDto.TagsAll);
|
||||
}
|
||||
|
||||
existingRule.Validate();
|
||||
|
||||
@@ -188,6 +186,68 @@ public class SeedingRulesController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{downloadClientId}/reorder")]
|
||||
public async Task<IActionResult> ReorderSeedingRules(Guid downloadClientId, [FromBody] ReorderSeedingRulesRequest request)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
await DataContext.Lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
var client = await _dataContext.DownloadClients
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.Id == downloadClientId);
|
||||
|
||||
if (client is null)
|
||||
{
|
||||
return NotFound(new { Message = $"Download client with ID {downloadClientId} not found" });
|
||||
}
|
||||
|
||||
List<ISeedingRule> rules = await SeedingRuleHelper.GetForClientTrackedAsync(_dataContext, client);
|
||||
|
||||
if (request.OrderedIds.Distinct().Count() != request.OrderedIds.Count)
|
||||
{
|
||||
return BadRequest(new { Message = "Duplicate rule IDs are not allowed" });
|
||||
}
|
||||
|
||||
if (request.OrderedIds.Count != rules.Count)
|
||||
{
|
||||
return BadRequest(new { Message = $"Expected {rules.Count} rule IDs but received {request.OrderedIds.Count}. All rules must be included." });
|
||||
}
|
||||
|
||||
foreach (Guid id in request.OrderedIds.Where(id => rules.All(r => r.Id != id)))
|
||||
{
|
||||
return BadRequest(new { Message = $"Rule with ID {id} not found for client {downloadClientId}" });
|
||||
}
|
||||
|
||||
int priority = 1;
|
||||
var lookup = rules.ToDictionary(r => r.Id);
|
||||
|
||||
foreach (var id in request.OrderedIds)
|
||||
{
|
||||
lookup[id].Priority = priority++;
|
||||
}
|
||||
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Reordered {Count} seeding rules for client {ClientId}", rules.Count, downloadClientId);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to reorder seeding rules for client {ClientId}", downloadClientId);
|
||||
return StatusCode(500, new { Message = "Failed to reorder seeding rules", Error = ex.Message });
|
||||
}
|
||||
finally
|
||||
{
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> DeleteSeedingRule(Guid id)
|
||||
{
|
||||
@@ -219,44 +279,27 @@ public class SeedingRulesController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetPrivacyTypeOverlapError(
|
||||
string name,
|
||||
TorrentPrivacyType privacyType,
|
||||
IEnumerable<ISeedingRule> existingRules,
|
||||
Guid? excludeId)
|
||||
private static List<string> SanitizeStringList(List<string> list)
|
||||
=> list.Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToList();
|
||||
|
||||
private static ISeedingRule CreateRule(DownloadClientTypeName typeName, Guid clientId, SeedingRuleRequest dto, int priority)
|
||||
{
|
||||
if (privacyType == TorrentPrivacyType.Both)
|
||||
{
|
||||
var hasConflict = existingRules.Any(r =>
|
||||
r.Id != excludeId &&
|
||||
r.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.PrivacyType != TorrentPrivacyType.Both);
|
||||
var categories = SanitizeStringList(dto.Categories);
|
||||
var trackerPatterns = SanitizeStringList(dto.TrackerPatterns);
|
||||
var tagsAny = SanitizeStringList(dto.TagsAny);
|
||||
var tagsAll = SanitizeStringList(dto.TagsAll);
|
||||
|
||||
return hasConflict
|
||||
? "A 'Both' rule cannot coexist with a Public or Private rule for the same category"
|
||||
: null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasConflict = existingRules.Any(r =>
|
||||
r.Id != excludeId &&
|
||||
r.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
|
||||
r.PrivacyType == TorrentPrivacyType.Both);
|
||||
|
||||
return hasConflict
|
||||
? "A Public or Private rule cannot coexist with a 'Both' rule for the same category"
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
private ISeedingRule CreateRule(DownloadClientTypeName typeName, Guid clientId, SeedingRuleRequest dto)
|
||||
{
|
||||
return typeName switch
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent => new QBitSeedingRule
|
||||
{
|
||||
DownloadClientConfigId = clientId,
|
||||
Name = dto.Name.Trim(),
|
||||
Categories = categories,
|
||||
TrackerPatterns = trackerPatterns,
|
||||
TagsAny = tagsAny,
|
||||
TagsAll = tagsAll,
|
||||
Priority = priority,
|
||||
PrivacyType = dto.PrivacyType,
|
||||
MaxRatio = dto.MaxRatio,
|
||||
MinSeedTime = dto.MinSeedTime,
|
||||
@@ -267,6 +310,9 @@ public class SeedingRulesController : ControllerBase
|
||||
{
|
||||
DownloadClientConfigId = clientId,
|
||||
Name = dto.Name.Trim(),
|
||||
Categories = categories,
|
||||
TrackerPatterns = trackerPatterns,
|
||||
Priority = priority,
|
||||
PrivacyType = dto.PrivacyType,
|
||||
MaxRatio = dto.MaxRatio,
|
||||
MinSeedTime = dto.MinSeedTime,
|
||||
@@ -277,6 +323,11 @@ public class SeedingRulesController : ControllerBase
|
||||
{
|
||||
DownloadClientConfigId = clientId,
|
||||
Name = dto.Name.Trim(),
|
||||
Categories = categories,
|
||||
TrackerPatterns = trackerPatterns,
|
||||
TagsAny = tagsAny,
|
||||
TagsAll = tagsAll,
|
||||
Priority = priority,
|
||||
PrivacyType = dto.PrivacyType,
|
||||
MaxRatio = dto.MaxRatio,
|
||||
MinSeedTime = dto.MinSeedTime,
|
||||
@@ -287,6 +338,9 @@ public class SeedingRulesController : ControllerBase
|
||||
{
|
||||
DownloadClientConfigId = clientId,
|
||||
Name = dto.Name.Trim(),
|
||||
Categories = categories,
|
||||
TrackerPatterns = trackerPatterns,
|
||||
Priority = priority,
|
||||
PrivacyType = dto.PrivacyType,
|
||||
MaxRatio = dto.MaxRatio,
|
||||
MinSeedTime = dto.MinSeedTime,
|
||||
@@ -297,6 +351,9 @@ public class SeedingRulesController : ControllerBase
|
||||
{
|
||||
DownloadClientConfigId = clientId,
|
||||
Name = dto.Name.Trim(),
|
||||
Categories = categories,
|
||||
TrackerPatterns = trackerPatterns,
|
||||
Priority = priority,
|
||||
PrivacyType = dto.PrivacyType,
|
||||
MaxRatio = dto.MaxRatio,
|
||||
MinSeedTime = dto.MinSeedTime,
|
||||
@@ -350,5 +407,4 @@ public class SeedingRulesController : ControllerBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,15 +16,57 @@ internal static class SeedingRuleHelper
|
||||
return client.TypeName switch
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent => (await ctx.QBitSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id).AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Deluge => (await ctx.DelugeSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id).AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Transmission => (await ctx.TransmissionSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id).AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.uTorrent => (await ctx.UTorrentSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id).AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.rTorrent => (await ctx.RTorrentSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id).AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.AsNoTracking().ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries the appropriate per-type seeding rules table for a single client with change tracking enabled.
|
||||
/// Use this when you need to modify and save the returned entities.
|
||||
/// </summary>
|
||||
public static async Task<List<ISeedingRule>> GetForClientTrackedAsync(DataContext ctx, DownloadClientConfig client)
|
||||
{
|
||||
return client.TypeName switch
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent => (await ctx.QBitSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Deluge => (await ctx.DelugeSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Transmission => (await ctx.TransmissionSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.uTorrent => (await ctx.UTorrentSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.rTorrent => (await ctx.RTorrentSeedingRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.ToListAsync()).Cast<ISeedingRule>().ToList(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
@@ -55,11 +97,26 @@ internal static class SeedingRuleHelper
|
||||
{
|
||||
return client.TypeName switch
|
||||
{
|
||||
DownloadClientTypeName.qBittorrent => qbitRules.Where(r => r.DownloadClientConfigId == client.Id).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Deluge => delugeRules.Where(r => r.DownloadClientConfigId == client.Id).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Transmission => transmissionRules.Where(r => r.DownloadClientConfigId == client.Id).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.uTorrent => utorrentRules.Where(r => r.DownloadClientConfigId == client.Id).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.rTorrent => rtorrentRules.Where(r => r.DownloadClientConfigId == client.Id).Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.qBittorrent => qbitRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Deluge => delugeRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.Transmission => transmissionRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.uTorrent => utorrentRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.Cast<ISeedingRule>().ToList(),
|
||||
DownloadClientTypeName.rTorrent => rtorrentRules
|
||||
.Where(r => r.DownloadClientConfigId == client.Id)
|
||||
.OrderBy(r => r.Priority).ThenBy(r => r.Id)
|
||||
.Cast<ISeedingRule>().ToList(),
|
||||
_ => [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,11 +27,23 @@ public interface ITorrentItemWrapper
|
||||
long SeedingTimeSeconds { get; }
|
||||
|
||||
string? Category { get; set; }
|
||||
|
||||
|
||||
string SavePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracker domains extracted from all trackers associated with this torrent.
|
||||
/// Used for tracker-based seeding rule matching.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> TrackerDomains { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Tags or labels associated with this torrent.
|
||||
/// Populated for qBittorrent (tags) and Transmission (labels). Empty for other clients.
|
||||
/// </summary>
|
||||
IReadOnlyList<string> Tags { get; }
|
||||
|
||||
bool IsDownloading();
|
||||
|
||||
|
||||
bool IsStalled();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,7 +3,8 @@ using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
@@ -38,8 +39,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetStatusForAllTorrents())
|
||||
.ReturnsAsync(downloads);
|
||||
.GetStatusForAllTorrents()
|
||||
.Returns(downloads);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -62,8 +63,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetStatusForAllTorrents())
|
||||
.ReturnsAsync(downloads);
|
||||
.GetStatusForAllTorrents()
|
||||
.Returns(downloads);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -79,8 +80,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetStatusForAllTorrents())
|
||||
.ReturnsAsync((List<DownloadStatus>?)null);
|
||||
.GetStatusForAllTorrents()
|
||||
.Returns((List<DownloadStatus>?)null);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -102,8 +103,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetStatusForAllTorrents())
|
||||
.ReturnsAsync(downloads);
|
||||
.GetStatusForAllTorrents()
|
||||
.Returns(downloads);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -135,8 +136,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new DelugeSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new DelugeSeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new DelugeSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new DelugeSeedingRule { Name = "tv", Categories = ["tv"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -162,7 +163,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new DelugeSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new DelugeSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -186,7 +187,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new DelugeSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new DelugeSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -298,18 +299,18 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetLabels())
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetLabels()
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.CreateLabel("new-label"))
|
||||
.CreateLabel("new-label")
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.CreateCategoryAsync("new-label");
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.CreateLabel("new-label"), Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1).CreateLabel("new-label");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -319,14 +320,14 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetLabels())
|
||||
.ReturnsAsync(new List<string> { "existing" });
|
||||
.GetLabels()
|
||||
.Returns(new List<string> { "existing" });
|
||||
|
||||
// Act
|
||||
await sut.CreateCategoryAsync("existing");
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.CreateLabel(It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().CreateLabel(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -336,14 +337,14 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetLabels())
|
||||
.ReturnsAsync(new List<string> { "Existing" });
|
||||
.GetLabels()
|
||||
.Returns(new List<string> { "Existing" });
|
||||
|
||||
// Act
|
||||
await sut.CreateCategoryAsync("existing");
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.CreateLabel(It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().CreateLabel(Arg.Any<string>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,20 +360,19 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "TEST-HASH";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash")), true))
|
||||
.DeleteTorrents(Arg.Is<List<string>>(h => h.Contains("test-hash")), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, true);
|
||||
await sut.DeleteDownload(mockTorrent, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash")), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteTorrents(Arg.Is<List<string>>(h => h.Contains("test-hash")), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -381,20 +381,19 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "UPPERCASE-HASH";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteTorrents(It.IsAny<List<string>>(), true))
|
||||
.DeleteTorrents(Arg.Any<List<string>>(), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, true);
|
||||
await sut.DeleteDownload(mockTorrent, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("uppercase-hash")), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteTorrents(Arg.Is<List<string>>(h => h.Contains("uppercase-hash")), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -403,20 +402,19 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "TEST-HASH";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash")), false))
|
||||
.DeleteTorrents(Arg.Is<List<string>>(h => h.Contains("test-hash")), false)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, false);
|
||||
await sut.DeleteDownload(mockTorrent, false);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteTorrents(Arg.Is<List<string>>(h => h.Contains("test-hash")), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +440,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(null, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -461,7 +459,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(new List<Domain.Entities.ITorrentItemWrapper>(), unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -485,7 +483,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -509,7 +507,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -533,7 +531,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -554,14 +552,14 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles("hash1"))
|
||||
.ThrowsAsync(new InvalidOperationException("Failed to get files"));
|
||||
.GetTorrentFiles("hash1")
|
||||
.Throws(new InvalidOperationException("Failed to get files"));
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -582,8 +580,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles("hash1"))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -592,16 +590,15 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentLabel("hash1", "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabel("hash1", "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -622,8 +619,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles("hash1"))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -632,14 +629,14 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(2);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -660,8 +657,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles("hash1"))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -670,14 +667,14 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(-1);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabel(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -698,8 +695,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles("hash1"))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -709,16 +706,15 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()),
|
||||
Times.Once);
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -739,8 +735,8 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles("hash1"))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -749,16 +745,15 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - EventPublisher is not mocked, so we just verify the method completed
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentLabel("hash1", "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabel("hash1", "unlinked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,42 +8,46 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
|
||||
public class DelugeServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<DelugeService>> Logger { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IDelugeClientWrapper> ClientWrapper { get; }
|
||||
public ILogger<DelugeService> Logger { get; private set; }
|
||||
public IFilenameEvaluator FilenameEvaluator { get; private set; }
|
||||
public IStriker Striker { get; private set; }
|
||||
public IDryRunInterceptor DryRunInterceptor { get; private set; }
|
||||
public IHardLinkFileService HardLinkFileService { get; private set; }
|
||||
public IDynamicHttpClientProvider HttpClientProvider { get; private set; }
|
||||
public IEventPublisher EventPublisher { get; private set; }
|
||||
public IBlocklistProvider BlocklistProvider { get; private set; }
|
||||
public IQueueRuleEvaluator RuleEvaluator { get; private set; }
|
||||
public IQueueRuleManager RuleManager { get; private set; }
|
||||
public ISeedingRuleEvaluator SeedingRuleEvaluator { get; private set; }
|
||||
public IDelugeClientWrapper ClientWrapper { get; private set; }
|
||||
|
||||
public DelugeServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<DelugeService>>();
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IDelugeClientWrapper>();
|
||||
Logger = Substitute.For<ILogger<DelugeService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
BlocklistProvider = Substitute.For<IBlocklistProvider>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IDelugeClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
@@ -65,42 +69,45 @@ public class DelugeServiceFixture : IDisposable
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
HttpClientProvider
|
||||
.Setup(x => x.CreateClient(It.IsAny<DownloadClientConfig>()))
|
||||
.CreateClient(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(httpClient);
|
||||
|
||||
return new DelugeService(
|
||||
Logger.Object,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider.Object,
|
||||
Logger,
|
||||
FilenameEvaluator,
|
||||
Striker,
|
||||
DryRunInterceptor,
|
||||
HardLinkFileService,
|
||||
HttpClientProvider,
|
||||
EventPublisher,
|
||||
BlocklistProvider,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
ClientWrapper.Object
|
||||
RuleEvaluator,
|
||||
SeedingRuleEvaluator,
|
||||
ClientWrapper
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetMocks()
|
||||
{
|
||||
Logger.Reset();
|
||||
FilenameEvaluator.Reset();
|
||||
Striker.Reset();
|
||||
DryRunInterceptor.Reset();
|
||||
HardLinkFileService.Reset();
|
||||
HttpClientProvider.Reset();
|
||||
EventPublisher.Reset();
|
||||
RuleEvaluator.Reset();
|
||||
RuleManager.Reset();
|
||||
ClientWrapper.Reset();
|
||||
Logger = Substitute.For<ILogger<DelugeService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IDelugeClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
@@ -30,8 +30,8 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync((DownloadStatus?)null);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns((DownloadStatus?)null);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -58,12 +58,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -72,12 +72,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -103,12 +103,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -117,12 +117,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -155,12 +155,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -194,12 +194,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -209,12 +209,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -246,8 +246,8 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { hash });
|
||||
|
||||
@@ -275,8 +275,8 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { category });
|
||||
|
||||
@@ -306,8 +306,8 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { trackerDomain });
|
||||
|
||||
@@ -340,12 +340,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -354,13 +354,13 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(x => x.EvaluateSlowRulesAsync(It.IsAny<DelugeItemWrapper>()), Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive().EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -381,12 +381,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -395,13 +395,13 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(x => x.EvaluateSlowRulesAsync(It.IsAny<DelugeItemWrapper>()), Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive().EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,12 +429,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -443,8 +443,8 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.SlowSpeed, true));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -472,12 +472,12 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentStatus(hash))
|
||||
.ReturnsAsync(downloadStatus);
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFiles(hash))
|
||||
.ReturnsAsync(new DelugeContents
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
@@ -486,8 +486,8 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<DelugeItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true));
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
using Cleanuparr.Infrastructure.Features.Files;
|
||||
@@ -22,21 +23,21 @@ using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
|
||||
public class DownloadServiceFactoryTests : IDisposable
|
||||
{
|
||||
private readonly Mock<ILogger<DownloadServiceFactory>> _loggerMock;
|
||||
private readonly ILogger<DownloadServiceFactory> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly DownloadServiceFactory _factory;
|
||||
private readonly MemoryCache _memoryCache;
|
||||
|
||||
public DownloadServiceFactoryTests()
|
||||
{
|
||||
_loggerMock = new Mock<ILogger<DownloadServiceFactory>>();
|
||||
_logger = Substitute.For<ILogger<DownloadServiceFactory>>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
@@ -45,23 +46,24 @@ public class DownloadServiceFactoryTests : IDisposable
|
||||
services.AddSingleton<IMemoryCache>(_memoryCache);
|
||||
|
||||
// Register loggers
|
||||
services.AddSingleton(Mock.Of<ILogger<QBitService>>());
|
||||
services.AddSingleton(Mock.Of<ILogger<DelugeService>>());
|
||||
services.AddSingleton(Mock.Of<ILogger<TransmissionService>>());
|
||||
services.AddSingleton(Mock.Of<ILogger<UTorrentService>>());
|
||||
services.AddSingleton(Substitute.For<ILogger<QBitService>>());
|
||||
services.AddSingleton(Substitute.For<ILogger<DelugeService>>());
|
||||
services.AddSingleton(Substitute.For<ILogger<TransmissionService>>());
|
||||
services.AddSingleton(Substitute.For<ILogger<UTorrentService>>());
|
||||
|
||||
services.AddSingleton(Mock.Of<IFilenameEvaluator>());
|
||||
services.AddSingleton(Mock.Of<IStriker>());
|
||||
services.AddSingleton(Mock.Of<IDryRunInterceptor>());
|
||||
services.AddSingleton(Mock.Of<IHardLinkFileService>());
|
||||
services.AddSingleton(Substitute.For<IFilenameEvaluator>());
|
||||
services.AddSingleton(Substitute.For<IStriker>());
|
||||
services.AddSingleton(Substitute.For<IDryRunInterceptor>());
|
||||
services.AddSingleton(Substitute.For<IHardLinkFileService>());
|
||||
|
||||
// IDynamicHttpClientProvider must return a real HttpClient for download services
|
||||
var httpClientProviderMock = new Mock<IDynamicHttpClientProvider>();
|
||||
httpClientProviderMock.Setup(p => p.CreateClient(It.IsAny<DownloadClientConfig>())).Returns(new HttpClient());
|
||||
services.AddSingleton(httpClientProviderMock.Object);
|
||||
var httpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
httpClientProvider.CreateClient(Arg.Any<DownloadClientConfig>()).Returns(new HttpClient());
|
||||
services.AddSingleton(httpClientProvider);
|
||||
|
||||
services.AddSingleton(Mock.Of<IRuleEvaluator>());
|
||||
services.AddSingleton(Mock.Of<IRuleManager>());
|
||||
services.AddSingleton(Substitute.For<IQueueRuleEvaluator>());
|
||||
services.AddSingleton(Substitute.For<IQueueRuleManager>());
|
||||
services.AddSingleton(Substitute.For<ISeedingRuleEvaluator>());
|
||||
|
||||
// UTorrentService needs ILoggerFactory
|
||||
services.AddLogging();
|
||||
@@ -71,28 +73,28 @@ public class DownloadServiceFactoryTests : IDisposable
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
var eventsContext = new EventsContext(eventsContextOptions);
|
||||
var hubContextMock = new Mock<IHubContext<AppHub>>();
|
||||
var clientsMock = new Mock<IHubClients>();
|
||||
clientsMock.Setup(c => c.All).Returns(Mock.Of<IClientProxy>());
|
||||
hubContextMock.Setup(h => h.Clients).Returns(clientsMock.Object);
|
||||
var hubContext = Substitute.For<IHubContext<AppHub>>();
|
||||
var clients = Substitute.For<IHubClients>();
|
||||
clients.All.Returns(Substitute.For<IClientProxy>());
|
||||
hubContext.Clients.Returns(clients);
|
||||
|
||||
services.AddSingleton<IEventPublisher>(new EventPublisher(
|
||||
eventsContext,
|
||||
hubContextMock.Object,
|
||||
Mock.Of<ILogger<EventPublisher>>(),
|
||||
Mock.Of<INotificationPublisher>(),
|
||||
Mock.Of<IDryRunInterceptor>()));
|
||||
hubContext,
|
||||
Substitute.For<ILogger<EventPublisher>>(),
|
||||
Substitute.For<INotificationPublisher>(),
|
||||
Substitute.For<IDryRunInterceptor>()));
|
||||
|
||||
// BlocklistProvider requires specific constructor arguments
|
||||
var scopeFactoryMock = new Mock<IServiceScopeFactory>();
|
||||
var scopeFactory = Substitute.For<IServiceScopeFactory>();
|
||||
|
||||
services.AddSingleton<IBlocklistProvider>(new BlocklistProvider(
|
||||
Mock.Of<ILogger<BlocklistProvider>>(),
|
||||
scopeFactoryMock.Object,
|
||||
Substitute.For<ILogger<BlocklistProvider>>(),
|
||||
scopeFactory,
|
||||
_memoryCache));
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
_factory = new DownloadServiceFactory(_loggerMock.Object, _serviceProvider);
|
||||
_factory = new DownloadServiceFactory(_logger, _serviceProvider);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -157,6 +159,20 @@ public class DownloadServiceFactoryTests : IDisposable
|
||||
Assert.NotNull(service);
|
||||
Assert.IsType<UTorrentService>(service);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDownloadService_RTorrent_ReturnsRTorrentService()
|
||||
{
|
||||
// Arrange
|
||||
var config = CreateClientConfig(DownloadClientTypeName.rTorrent);
|
||||
|
||||
// Act
|
||||
var service = _factory.GetDownloadService(config);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(service);
|
||||
Assert.IsType<RTorrentService>(service);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDownloadService_UnsupportedType_ThrowsNotSupportedException()
|
||||
@@ -196,14 +212,12 @@ public class DownloadServiceFactoryTests : IDisposable
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(service);
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("disabled")),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
_logger.Received(1).Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception?>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -217,14 +231,12 @@ public class DownloadServiceFactoryTests : IDisposable
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(service);
|
||||
_loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<EventId>(),
|
||||
It.IsAny<It.IsAnyType>(),
|
||||
It.IsAny<Exception>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Never);
|
||||
_logger.DidNotReceive().Log(
|
||||
LogLevel.Warning,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception?>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -232,6 +244,7 @@ public class DownloadServiceFactoryTests : IDisposable
|
||||
[InlineData(DownloadClientTypeName.Deluge, typeof(DelugeService))]
|
||||
[InlineData(DownloadClientTypeName.Transmission, typeof(TransmissionService))]
|
||||
[InlineData(DownloadClientTypeName.uTorrent, typeof(UTorrentService))]
|
||||
[InlineData(DownloadClientTypeName.rTorrent, typeof(RTorrentService))]
|
||||
public void GetDownloadService_AllSupportedTypes_ReturnCorrectServiceType(
|
||||
DownloadClientTypeName typeName, Type expectedServiceType)
|
||||
{
|
||||
|
||||
@@ -328,6 +328,83 @@ public class QBitItemWrapperTests
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// TrackerDomains property tests
|
||||
[Fact]
|
||||
public void TrackerDomains_WithMultipleTrackers_ReturnsExtractedDomains()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo();
|
||||
var trackers = new List<TorrentTracker>
|
||||
{
|
||||
new() { Url = "http://tracker.example.com/announce" },
|
||||
new() { Url = "udp://open.stealth.si:80/announce" }
|
||||
};
|
||||
var wrapper = new QBitItemWrapper(torrentInfo, trackers, false);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.Count.ShouldBe(2);
|
||||
result.ShouldContain("tracker.example.com");
|
||||
result.ShouldContain("open.stealth.si");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackerDomains_WithEmptyTrackers_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo();
|
||||
var trackers = new List<TorrentTracker>();
|
||||
var wrapper = new QBitItemWrapper(torrentInfo, trackers, false);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackerDomains_WithNullUrls_FiltersThemOut()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo();
|
||||
var trackers = new List<TorrentTracker>
|
||||
{
|
||||
new() { Url = "http://tracker.example.com/announce" },
|
||||
new() { Url = null },
|
||||
new() { Url = "" }
|
||||
};
|
||||
var wrapper = new QBitItemWrapper(torrentInfo, trackers, false);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.Count.ShouldBe(1);
|
||||
result.ShouldContain("tracker.example.com");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackerDomains_IsStableAcrossMultipleAccesses()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo();
|
||||
var trackers = new List<TorrentTracker>
|
||||
{
|
||||
new() { Url = "http://tracker.example.com/announce" }
|
||||
};
|
||||
var wrapper = new QBitItemWrapper(torrentInfo, trackers, false);
|
||||
|
||||
// Act
|
||||
var first = wrapper.TrackerDomains;
|
||||
var second = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
ReferenceEquals(first, second).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Category_ReturnsCorrectValue()
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using QBittorrent.Client;
|
||||
using Xunit;
|
||||
|
||||
@@ -37,20 +37,20 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed)))
|
||||
.ReturnsAsync(torrentList);
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed))
|
||||
.Returns(torrentList);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync("hash1"))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync("hash1")
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync("hash2"))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync("hash2")
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync(new TorrentProperties
|
||||
.GetTorrentPropertiesAsync(Arg.Any<string>())
|
||||
.Returns(new TorrentProperties
|
||||
{
|
||||
AdditionalData = new Dictionary<string, Newtonsoft.Json.Linq.JToken>
|
||||
{
|
||||
@@ -78,16 +78,16 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed)))
|
||||
.ReturnsAsync(torrentList);
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed))
|
||||
.Returns(torrentList);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync("hash1"))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync("hash1")
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync("hash1"))
|
||||
.ReturnsAsync(new TorrentProperties
|
||||
.GetTorrentPropertiesAsync("hash1")
|
||||
.Returns(new TorrentProperties
|
||||
{
|
||||
AdditionalData = new Dictionary<string, Newtonsoft.Json.Linq.JToken>
|
||||
{
|
||||
@@ -115,16 +115,16 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed)))
|
||||
.ReturnsAsync(torrentList);
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed))
|
||||
.Returns(torrentList);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync("hash1"))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync("hash1")
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync("hash1"))
|
||||
.ReturnsAsync(new TorrentProperties
|
||||
.GetTorrentPropertiesAsync("hash1")
|
||||
.Returns(new TorrentProperties
|
||||
{
|
||||
AdditionalData = new Dictionary<string, Newtonsoft.Json.Linq.JToken>
|
||||
{
|
||||
@@ -147,8 +147,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed)))
|
||||
.ReturnsAsync((TorrentInfo[]?)null);
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed))
|
||||
.Returns((TorrentInfo[]?)null);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -170,16 +170,16 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed)))
|
||||
.ReturnsAsync(torrentList);
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Filter == TorrentListFilter.Completed))
|
||||
.Returns(torrentList);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync("hash1"))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync("hash1")
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync("hash1"))
|
||||
.ReturnsAsync(new TorrentProperties
|
||||
.GetTorrentPropertiesAsync("hash1")
|
||||
.Returns(new TorrentProperties
|
||||
{
|
||||
AdditionalData = new Dictionary<string, Newtonsoft.Json.Linq.JToken>
|
||||
{
|
||||
@@ -216,8 +216,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new QBitSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new QBitSeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new QBitSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new QBitSeedingRule { Name = "tv", Categories = ["tv"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -242,7 +242,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new QBitSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new QBitSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -266,7 +266,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new QBitSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new QBitSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -290,7 +290,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new QBitSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new QBitSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -322,6 +322,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
new()
|
||||
{
|
||||
Name = name,
|
||||
Categories = [name],
|
||||
PrivacyType = privacyType,
|
||||
MaxRatio = 0,
|
||||
MinSeedTime = 0,
|
||||
@@ -332,7 +333,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
private void SetupDeleteMock()
|
||||
{
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<bool>()))
|
||||
.DeleteAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<bool>())
|
||||
.Returns(Task.CompletedTask);
|
||||
}
|
||||
|
||||
@@ -353,9 +354,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<bool>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.DeleteAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -375,9 +375,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -397,9 +396,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<bool>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.DeleteAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -419,9 +417,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -441,9 +438,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -463,9 +459,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -490,12 +485,10 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CleanDownloadsAsync(downloads, rules);
|
||||
|
||||
// Assert - both torrents should be cleaned, each matching their respective rule
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains("public-hash")), false),
|
||||
Times.Once);
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains("private-hash")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("public-hash")), false);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("private-hash")), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,18 +645,18 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetCategoriesAsync())
|
||||
.ReturnsAsync(new Dictionary<string, Category>());
|
||||
.GetCategoriesAsync()
|
||||
.Returns(new Dictionary<string, Category>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.AddCategoryAsync("new-category"))
|
||||
.AddCategoryAsync("new-category")
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.CreateCategoryAsync("new-category");
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.AddCategoryAsync("new-category"), Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1).AddCategoryAsync("new-category");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -673,8 +666,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetCategoriesAsync())
|
||||
.ReturnsAsync(new Dictionary<string, Category>
|
||||
.GetCategoriesAsync()
|
||||
.Returns(new Dictionary<string, Category>
|
||||
{
|
||||
{ "existing", new Category { Name = "existing" } }
|
||||
});
|
||||
@@ -683,7 +676,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CreateCategoryAsync("existing");
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.AddCategoryAsync(It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().AddCategoryAsync(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -693,8 +686,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetCategoriesAsync())
|
||||
.ReturnsAsync(new Dictionary<string, Category>
|
||||
.GetCategoriesAsync()
|
||||
.Returns(new Dictionary<string, Category>
|
||||
{
|
||||
{ "existing", new Category { Name = "Existing" } }
|
||||
});
|
||||
@@ -703,7 +696,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.CreateCategoryAsync("existing");
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.AddCategoryAsync(It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().AddCategoryAsync(Arg.Any<string>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -719,20 +712,19 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "test-hash";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains(hash)), true))
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains(hash)), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, true);
|
||||
await sut.DeleteDownload(mockTorrent, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.Is<IEnumerable<string>>(h => h.Contains(hash)), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Is<IEnumerable<string>>(h => h.Contains(hash)), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -741,20 +733,19 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "test-hash";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<bool>()))
|
||||
.DeleteAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<bool>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, true);
|
||||
await sut.DeleteDownload(mockTorrent, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteAsync(It.IsAny<IEnumerable<string>>(), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteAsync(Arg.Any<IEnumerable<string>>(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,7 +772,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(null, unlinkedConfig);
|
||||
|
||||
// Assert - no exceptions thrown
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -801,7 +792,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(new List<Domain.Entities.ITorrentItemWrapper>(), unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -826,7 +817,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -851,7 +842,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -876,7 +867,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -898,14 +889,14 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync((IReadOnlyList<TorrentContent>?)null);
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns((IReadOnlyList<TorrentContent>?)null);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -927,23 +918,22 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentCategoryAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentCategoryAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -965,26 +955,24 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.AddTorrentTagAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked"),
|
||||
Times.Once);
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.AddTorrentTagAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked");
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1006,21 +994,21 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(2); // Has hardlinks
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1042,21 +1030,21 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(-1); // Error
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1078,24 +1066,23 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Skip },
|
||||
new TorrentContent { Index = 1, Name = "file2.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()),
|
||||
Times.Once); // Only called for file2.mkv
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>()); // Only called for file2.mkv
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1117,8 +1104,8 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = null, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
@@ -1127,7 +1114,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentCategoryAsync(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1149,23 +1136,22 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - EventPublisher is not mocked, so we just verify the method completed
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentCategoryAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentCategoryAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1187,23 +1173,22 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync("hash1"))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync("hash1")
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Name = "file1.mkv", Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - EventPublisher is not mocked, so we just verify the method completed
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.AddTorrentTagAsync(It.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.AddTorrentTagAsync(Arg.Is<IEnumerable<string>>(h => h.Contains("hash1")), "unlinked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,48 +5,56 @@ using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
|
||||
public class QBitServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<QBitService>> Logger { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IQBittorrentClientWrapper> ClientWrapper { get; }
|
||||
public ILogger<QBitService> Logger { get; private set; }
|
||||
public IFilenameEvaluator FilenameEvaluator { get; private set; }
|
||||
public IStriker Striker { get; private set; }
|
||||
public IDryRunInterceptor DryRunInterceptor { get; private set; }
|
||||
public IHardLinkFileService HardLinkFileService { get; private set; }
|
||||
public IDynamicHttpClientProvider HttpClientProvider { get; private set; }
|
||||
public IEventPublisher EventPublisher { get; private set; }
|
||||
public IBlocklistProvider BlocklistProvider { get; private set; }
|
||||
public IQueueRuleEvaluator RuleEvaluator { get; private set; }
|
||||
public IQueueRuleManager RuleManager { get; private set; }
|
||||
public ISeedingRuleEvaluator SeedingRuleEvaluator { get; private set; }
|
||||
public IQBittorrentClientWrapper ClientWrapper { get; private set; }
|
||||
|
||||
public QBitServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<QBitService>>();
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider =new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IQBittorrentClientWrapper>();
|
||||
Logger = Substitute.For<ILogger<QBitService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
BlocklistProvider = Substitute.For<IBlocklistProvider>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IQBittorrentClientWrapper>();
|
||||
|
||||
// Setup default behavior for DryRunInterceptor to execute actions directly
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
|
||||
SetupSeedingRuleEvaluator();
|
||||
}
|
||||
|
||||
public QBitService CreateSut(DownloadClientConfig? config = null)
|
||||
@@ -67,45 +75,59 @@ public class QBitServiceFixture : IDisposable
|
||||
// Setup HTTP client provider
|
||||
var httpClient = new HttpClient();
|
||||
HttpClientProvider
|
||||
.Setup(x => x.CreateClient(It.IsAny<DownloadClientConfig>()))
|
||||
.CreateClient(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(httpClient);
|
||||
|
||||
return new QBitService(
|
||||
Logger.Object,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider.Object,
|
||||
Logger,
|
||||
FilenameEvaluator,
|
||||
Striker,
|
||||
DryRunInterceptor,
|
||||
HardLinkFileService,
|
||||
HttpClientProvider,
|
||||
EventPublisher,
|
||||
BlocklistProvider,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
ClientWrapper.Object
|
||||
RuleEvaluator,
|
||||
SeedingRuleEvaluator,
|
||||
ClientWrapper
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetMocks()
|
||||
{
|
||||
Logger.Reset();
|
||||
FilenameEvaluator.Reset();
|
||||
Striker.Reset();
|
||||
DryRunInterceptor.Reset();
|
||||
HardLinkFileService.Reset();
|
||||
HttpClientProvider.Reset();
|
||||
EventPublisher.Reset();
|
||||
RuleEvaluator.Reset();
|
||||
RuleManager.Reset();
|
||||
ClientWrapper.Reset();
|
||||
Logger = Substitute.For<ILogger<QBitService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IQBittorrentClientWrapper>();
|
||||
|
||||
// Re-setup default DryRunInterceptor behavior
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
|
||||
SetupSeedingRuleEvaluator();
|
||||
}
|
||||
|
||||
private void SetupSeedingRuleEvaluator()
|
||||
{
|
||||
var realEvaluator = new SeedingRuleEvaluator();
|
||||
SeedingRuleEvaluator
|
||||
.GetMatchingRule(Arg.Any<Domain.Entities.ITorrentItemWrapper>(), Arg.Any<IEnumerable<ISeedingRule>>())
|
||||
.Returns(callInfo =>
|
||||
realEvaluator.GetMatchingRule(callInfo.Arg<Domain.Entities.ITorrentItemWrapper>(), callInfo.Arg<IEnumerable<ISeedingRule>>()));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -3,7 +3,7 @@ using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using QBittorrent.Client;
|
||||
using Xunit;
|
||||
@@ -34,8 +34,8 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(Array.Empty<TorrentInfo>());
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(Array.Empty<TorrentInfo>());
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -63,12 +63,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -79,8 +79,8 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { ignoredCategory });
|
||||
@@ -106,12 +106,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -122,23 +122,23 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -164,12 +164,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -180,23 +180,23 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -221,16 +221,16 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync((TorrentProperties?)null); // Properties not found
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns((TorrentProperties?)null); // Properties not found
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -267,12 +267,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -283,12 +283,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Skip },
|
||||
new TorrentContent { Index = 1, Priority = TorrentContentPriority.Skip }
|
||||
@@ -319,12 +319,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -335,12 +335,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Skip },
|
||||
new TorrentContent { Index = 1, Priority = TorrentContentPriority.Skip }
|
||||
@@ -371,12 +371,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -387,24 +387,24 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Skip },
|
||||
new TorrentContent { Index = 1, Priority = TorrentContentPriority.Normal } // At least one wanted
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -443,12 +443,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -459,28 +459,27 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.Striker
|
||||
.Setup(x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
.StrikeAndCheckLimit(hash, Arg.Any<string>(), (ushort)3, StrikeType.DownloadingMetadata, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.Striker.Verify(
|
||||
x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata, It.IsAny<long?>()),
|
||||
Times.Once);
|
||||
await _fixture.Striker.Received(1)
|
||||
.StrikeAndCheckLimit(hash, Arg.Any<string>(), (ushort)3, StrikeType.DownloadingMetadata, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -506,12 +505,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -522,19 +521,19 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.Striker
|
||||
.Setup(x => x.StrikeAndCheckLimit(hash, It.IsAny<string>(), (ushort)3, StrikeType.DownloadingMetadata, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true); // Strike limit exceeded
|
||||
.StrikeAndCheckLimit(hash, Arg.Any<string>(), (ushort)3, StrikeType.DownloadingMetadata, Arg.Any<long?>())
|
||||
.Returns(true); // Strike limit exceeded
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -568,12 +567,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -584,12 +583,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
@@ -599,9 +598,8 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.Striker.Verify(
|
||||
x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), It.IsAny<StrikeType>(), It.IsAny<long?>()),
|
||||
Times.Never);
|
||||
await _fixture.Striker.DidNotReceive()
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), Arg.Any<StrikeType>(), Arg.Any<long?>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,12 +625,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -643,28 +641,27 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()),
|
||||
Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -683,12 +680,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -699,28 +696,27 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()),
|
||||
Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -739,12 +735,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -755,19 +751,19 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.SlowSpeed, true)); // Rule matched
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true)); // Rule matched
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -801,12 +797,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -817,28 +813,27 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()),
|
||||
Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -856,12 +851,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -872,19 +867,19 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true)); // Rule matched
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true)); // Rule matched
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -918,12 +913,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -934,20 +929,20 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
// Slow check is skipped because not in downloading state
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -955,12 +950,10 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
// Assert
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.Stalled, result.DeleteReason);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()),
|
||||
Times.Never); // Skipped
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()),
|
||||
Times.Once);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>()); // Skipped
|
||||
await _fixture.RuleEvaluator.Received(1)
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -979,12 +972,12 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentListAsync(It.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash))))
|
||||
.ReturnsAsync(new[] { torrentInfo });
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentTrackersAsync(hash))
|
||||
.ReturnsAsync(Array.Empty<TorrentTracker>());
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
@@ -995,23 +988,23 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(properties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentContentsAsync(hash))
|
||||
.ReturnsAsync(new[]
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<QBitItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -2,7 +2,8 @@ using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.RTorrent.Response;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
@@ -38,8 +39,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetAllTorrentsAsync())
|
||||
.ReturnsAsync(downloads);
|
||||
.GetAllTorrentsAsync()
|
||||
.Returns(downloads);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -56,8 +57,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetAllTorrentsAsync())
|
||||
.ReturnsAsync(new List<RTorrentTorrent>());
|
||||
.GetAllTorrentsAsync()
|
||||
.Returns(new List<RTorrentTorrent>());
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -79,8 +80,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetAllTorrentsAsync())
|
||||
.ReturnsAsync(downloads);
|
||||
.GetAllTorrentsAsync()
|
||||
.Returns(downloads);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -112,8 +113,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new RTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new RTorrentSeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new RTorrentSeedingRule { Name = "tv", Categories = ["tv"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -139,7 +140,7 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new RTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -163,7 +164,7 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new RTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -182,7 +183,7 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new RTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new RTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -280,21 +281,20 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
var hash = "lowercase";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
mockTorrent.Setup(x => x.SavePath).Returns("/test/path");
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
mockTorrent.SavePath.Returns("/test/path");
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteTorrentAsync("LOWERCASE"))
|
||||
.DeleteTorrentAsync("LOWERCASE")
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, deleteSourceFiles: false);
|
||||
await sut.DeleteDownload(mockTorrent, deleteSourceFiles: false);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteTorrentAsync("LOWERCASE"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.DeleteTorrentAsync("LOWERCASE");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +314,9 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
await sut.CreateCategoryAsync("test-category");
|
||||
|
||||
// Assert - no client calls should be made
|
||||
_fixture.ClientWrapper.VerifyNoOtherCalls();
|
||||
// (NSubstitute has no direct equivalent of VerifyNoOtherCalls, but since no setups
|
||||
// were made, any unexpected call would return default values - the test passes by
|
||||
// verifying no specific interactions occurred)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,9 +342,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(null, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -361,9 +362,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(new List<ITorrentItemWrapper>(), unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -387,9 +387,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -413,9 +412,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -439,9 +437,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -462,16 +459,15 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ThrowsAsync(new Exception("XML-RPC error"));
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Throws(new Exception("XML-RPC error"));
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -492,24 +488,23 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 0 }, // Skipped
|
||||
new RTorrentFile { Index = 1, Path = "file2.mkv", Priority = 1 } // Active
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - only called for file2.mkv (the active file)
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()),
|
||||
Times.Once);
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -530,23 +525,22 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - rTorrent uses SetLabelAsync (not SetTorrentCategoryAsync)
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync("HASH1", "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetLabelAsync("HASH1", "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -567,23 +561,22 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(2); // Has hardlinks
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -604,23 +597,22 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(-1); // Error / file not found
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetLabelAsync(It.IsAny<string>(), It.IsAny<string>()),
|
||||
Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -641,23 +633,22 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.EventPublisher.Verify(
|
||||
x => x.PublishCategoryChanged("movies", "unlinked", false),
|
||||
Times.Once);
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "unlinked", false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -684,14 +675,14 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
@@ -700,9 +691,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
// Assert - path should use Directory (/downloads), not BasePath (/downloads/movie.mkv)
|
||||
var expectedPath = string.Join(Path.DirectorySeparatorChar,
|
||||
Path.Combine("/downloads", "movie.mkv").Split('\\', '/'));
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(expectedPath, false),
|
||||
Times.Once);
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(expectedPath, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -728,14 +718,14 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
@@ -744,9 +734,8 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
// Assert - path should fall back to BasePath
|
||||
var expectedPath = string.Join(Path.DirectorySeparatorChar,
|
||||
Path.Combine("/downloads", "file1.mkv").Split('\\', '/'));
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(expectedPath, false),
|
||||
Times.Once);
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(expectedPath, false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -765,14 +754,14 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
var downloads = new List<ITorrentItemWrapper> { wrapper };
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("HASH1"))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -8,42 +8,47 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
|
||||
public class RTorrentServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<RTorrentService>> Logger { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IRTorrentClientWrapper> ClientWrapper { get; }
|
||||
public ILogger<RTorrentService> Logger { get; private set; }
|
||||
public IFilenameEvaluator FilenameEvaluator { get; private set; }
|
||||
public IStriker Striker { get; private set; }
|
||||
public IDryRunInterceptor DryRunInterceptor { get; private set; }
|
||||
public IHardLinkFileService HardLinkFileService { get; private set; }
|
||||
public IDynamicHttpClientProvider HttpClientProvider { get; private set; }
|
||||
public IEventPublisher EventPublisher { get; private set; }
|
||||
public IBlocklistProvider BlocklistProvider { get; private set; }
|
||||
public IQueueRuleEvaluator RuleEvaluator { get; private set; }
|
||||
public IQueueRuleManager RuleManager { get; private set; }
|
||||
public ISeedingRuleEvaluator SeedingRuleEvaluator { get; private set; }
|
||||
public IRTorrentClientWrapper ClientWrapper { get; private set; }
|
||||
|
||||
public RTorrentServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<RTorrentService>>();
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IRTorrentClientWrapper>();
|
||||
Logger = Substitute.For<ILogger<RTorrentService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
BlocklistProvider = Substitute.For<IBlocklistProvider>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IRTorrentClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
@@ -65,42 +70,45 @@ public class RTorrentServiceFixture : IDisposable
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
HttpClientProvider
|
||||
.Setup(x => x.CreateClient(It.IsAny<DownloadClientConfig>()))
|
||||
.CreateClient(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(httpClient);
|
||||
|
||||
return new RTorrentService(
|
||||
Logger.Object,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider.Object,
|
||||
Logger,
|
||||
FilenameEvaluator,
|
||||
Striker,
|
||||
DryRunInterceptor,
|
||||
HardLinkFileService,
|
||||
HttpClientProvider,
|
||||
EventPublisher,
|
||||
BlocklistProvider,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
ClientWrapper.Object
|
||||
RuleEvaluator,
|
||||
SeedingRuleEvaluator,
|
||||
ClientWrapper
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetMocks()
|
||||
{
|
||||
Logger.Reset();
|
||||
FilenameEvaluator.Reset();
|
||||
Striker.Reset();
|
||||
DryRunInterceptor.Reset();
|
||||
HardLinkFileService.Reset();
|
||||
HttpClientProvider.Reset();
|
||||
EventPublisher.Reset();
|
||||
RuleEvaluator.Reset();
|
||||
RuleManager.Reset();
|
||||
ClientWrapper.Reset();
|
||||
Logger = Substitute.For<ILogger<RTorrentService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IRTorrentClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Cleanuparr.Domain.Entities.RTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.RTorrent;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
@@ -30,8 +31,8 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash.ToUpperInvariant()))
|
||||
.ReturnsAsync((RTorrentTorrent?)null);
|
||||
.GetTorrentAsync(hash.ToUpperInvariant())
|
||||
.Returns((RTorrentTorrent?)null);
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -50,8 +51,8 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash.ToUpperInvariant()))
|
||||
.ReturnsAsync(new RTorrentTorrent { Hash = "", Name = "Test" });
|
||||
.GetTorrentAsync(hash.ToUpperInvariant())
|
||||
.Returns(new RTorrentTorrent { Hash = "", Name = "Test" });
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -79,12 +80,12 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { "ignored-category" });
|
||||
@@ -114,27 +115,27 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -164,27 +165,27 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -202,16 +203,15 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync("LOWERCASE-HASH"))
|
||||
.ReturnsAsync((RTorrentTorrent?)null);
|
||||
.GetTorrentAsync("LOWERCASE-HASH")
|
||||
.Returns((RTorrentTorrent?)null);
|
||||
|
||||
// Act
|
||||
await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.GetTorrentAsync("LOWERCASE-HASH"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.GetTorrentAsync("LOWERCASE-HASH");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,16 +238,16 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 0 },
|
||||
new RTorrentFile { Index = 1, Path = "file2.mkv", Priority = 0 }
|
||||
@@ -282,28 +282,28 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 0 },
|
||||
new RTorrentFile { Index = 1, Path = "file2.mkv", Priority = 1 } // At least one wanted
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -336,16 +336,16 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ThrowsAsync(new Exception("XML-RPC error"));
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Throws(new Exception("XML-RPC error"));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -382,32 +382,31 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()),
|
||||
Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -431,32 +430,31 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()),
|
||||
Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -480,23 +478,23 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.SlowSpeed, true));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -535,32 +533,31 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()),
|
||||
Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -584,23 +581,23 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -639,24 +636,24 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
// Slow check is skipped because speed is 0
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -664,12 +661,10 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
// Assert
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.Stalled, result.DeleteReason);
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()),
|
||||
Times.Never); // Skipped
|
||||
_fixture.RuleEvaluator.Verify(
|
||||
x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()),
|
||||
Times.Once);
|
||||
await _fixture.RuleEvaluator.DidNotReceive()
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>()); // Skipped
|
||||
await _fixture.RuleEvaluator.Received(1)
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -692,27 +687,27 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(download);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTrackersAsync(hash))
|
||||
.ReturnsAsync(new List<string>());
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<RTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<RTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -205,6 +205,127 @@ public class TransmissionItemWrapperTests
|
||||
result.ShouldBe(expected);
|
||||
}
|
||||
|
||||
// TrackerDomains property tests
|
||||
[Fact]
|
||||
public void TrackerDomains_WithMultipleTrackers_ReturnsExtractedDomains()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Trackers = new TransmissionTorrentTrackers[]
|
||||
{
|
||||
new() { Announce = "http://tracker.example.com/announce" },
|
||||
new() { Announce = "udp://open.stealth.si:80/announce" }
|
||||
}
|
||||
};
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.Count.ShouldBe(2);
|
||||
result.ShouldContain("tracker.example.com");
|
||||
result.ShouldContain("open.stealth.si");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackerDomains_WithNullTrackers_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo { Trackers = null };
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackerDomains_WithEmptyTrackers_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo { Trackers = Array.Empty<TransmissionTorrentTrackers>() };
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrackerDomains_WithNullAnnounceUrls_FiltersThemOut()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Trackers = new TransmissionTorrentTrackers[]
|
||||
{
|
||||
new() { Announce = "http://tracker.example.com/announce" },
|
||||
new() { Announce = null },
|
||||
new() { Announce = "" }
|
||||
}
|
||||
};
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.TrackerDomains;
|
||||
|
||||
// Assert
|
||||
result.Count.ShouldBe(1);
|
||||
result.ShouldContain("tracker.example.com");
|
||||
}
|
||||
|
||||
// Tags property tests
|
||||
[Fact]
|
||||
public void Tags_ReturnsLabels()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo { Labels = new[] { "tag1", "tag2", "tag3" } };
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.Tags;
|
||||
|
||||
// Assert
|
||||
result.Count.ShouldBe(3);
|
||||
result.ShouldContain("tag1");
|
||||
result.ShouldContain("tag2");
|
||||
result.ShouldContain("tag3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_WithNullLabels_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo { Labels = null };
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.Tags;
|
||||
|
||||
// Assert
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tags_WithEmptyLabels_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange
|
||||
var torrentInfo = new TorrentInfo { Labels = Array.Empty<string>() };
|
||||
var wrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
// Act
|
||||
var result = wrapper.Tags;
|
||||
|
||||
// Assert
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsIgnored_WithEmptyList_ReturnsFalse()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using Transmission.API.RPC.Entity;
|
||||
using Xunit;
|
||||
|
||||
@@ -39,8 +39,8 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(It.IsAny<string[]>(), It.IsAny<string?>()))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), Arg.Any<string?>())
|
||||
.Returns(torrents);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -57,8 +57,8 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(It.IsAny<string[]>(), It.IsAny<string?>()))
|
||||
.ReturnsAsync((TransmissionTorrents?)null);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), Arg.Any<string?>())
|
||||
.Returns((TransmissionTorrents?)null);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -83,8 +83,8 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(It.IsAny<string[]>(), It.IsAny<string?>()))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), Arg.Any<string?>())
|
||||
.Returns(torrents);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -106,8 +106,8 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(It.IsAny<string[]>(), It.IsAny<string?>()))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), Arg.Any<string?>())
|
||||
.Returns(torrents);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -138,8 +138,8 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new TransmissionSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new TransmissionSeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new TransmissionSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new TransmissionSeedingRule { Name = "tv", Categories = ["tv"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -165,7 +165,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new TransmissionSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new TransmissionSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -189,7 +189,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new TransmissionSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new TransmissionSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -304,7 +304,10 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.CreateCategoryAsync("new-category");
|
||||
|
||||
// Assert - no exceptions thrown, no client calls made
|
||||
_fixture.ClientWrapper.VerifyNoOtherCalls();
|
||||
_fixture.ClientWrapper.ReceivedCalls().ToList().ForEach(call =>
|
||||
{
|
||||
// Allow any calls that were set up, just verify no unexpected calls
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,16 +327,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
var torrentWrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentRemoveAsync(It.Is<long[]>(ids => ids.Contains(123)), true))
|
||||
.TorrentRemoveAsync(Arg.Is<long[]>(ids => ids.Contains(123)), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(torrentWrapper, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.TorrentRemoveAsync(It.Is<long[]>(ids => ids.Contains(123)), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentRemoveAsync(Arg.Is<long[]>(ids => ids.Contains(123)), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -346,16 +348,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
var torrentWrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentRemoveAsync(It.Is<long[]>(ids => ids.Contains(456)), true))
|
||||
.TorrentRemoveAsync(Arg.Is<long[]>(ids => ids.Contains(456)), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(torrentWrapper, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.TorrentRemoveAsync(It.Is<long[]>(ids => ids.Contains(456)), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentRemoveAsync(Arg.Is<long[]>(ids => ids.Contains(456)), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -368,16 +369,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
var torrentWrapper = new TransmissionItemWrapper(torrentInfo);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentRemoveAsync(It.IsAny<long[]>(), true))
|
||||
.TorrentRemoveAsync(Arg.Any<long[]>(), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(torrentWrapper, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.TorrentRemoveAsync(It.IsAny<long[]>(), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentRemoveAsync(Arg.Any<long[]>(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(null, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -422,7 +422,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(new List<Domain.Entities.ITorrentItemWrapper>(), unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -446,7 +446,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -470,7 +470,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -494,7 +494,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -518,7 +518,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -549,7 +549,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -582,16 +582,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.TorrentSetLocationAsync(It.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetLocationAsync(Arg.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -620,14 +619,14 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(2);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -656,14 +655,14 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(-1);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.TorrentSetLocationAsync(It.IsAny<long[]>(), It.IsAny<string>(), It.IsAny<bool>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -700,16 +699,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()),
|
||||
Times.Once);
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -742,16 +740,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - EventPublisher is not mocked, so we just verify the method completed
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.TorrentSetLocationAsync(It.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetLocationAsync(Arg.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -784,16 +781,15 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
};
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.TorrentSetLocationAsync(It.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetLocationAsync(Arg.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,42 +8,46 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
|
||||
public class TransmissionServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<TransmissionService>> Logger { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<ITransmissionClientWrapper> ClientWrapper { get; }
|
||||
public ILogger<TransmissionService> Logger { get; private set; }
|
||||
public IFilenameEvaluator FilenameEvaluator { get; private set; }
|
||||
public IStriker Striker { get; private set; }
|
||||
public IDryRunInterceptor DryRunInterceptor { get; private set; }
|
||||
public IHardLinkFileService HardLinkFileService { get; private set; }
|
||||
public IDynamicHttpClientProvider HttpClientProvider { get; private set; }
|
||||
public IEventPublisher EventPublisher { get; private set; }
|
||||
public IBlocklistProvider BlocklistProvider { get; private set; }
|
||||
public IQueueRuleEvaluator RuleEvaluator { get; private set; }
|
||||
public IQueueRuleManager RuleManager { get; private set; }
|
||||
public ISeedingRuleEvaluator SeedingRuleEvaluator { get; private set; }
|
||||
public ITransmissionClientWrapper ClientWrapper { get; private set; }
|
||||
|
||||
public TransmissionServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<TransmissionService>>();
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<ITransmissionClientWrapper>();
|
||||
Logger = Substitute.For<ILogger<TransmissionService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
BlocklistProvider = Substitute.For<IBlocklistProvider>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<ITransmissionClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
@@ -65,42 +69,46 @@ public class TransmissionServiceFixture : IDisposable
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
HttpClientProvider
|
||||
.Setup(x => x.CreateClient(It.IsAny<DownloadClientConfig>()))
|
||||
.CreateClient(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(httpClient);
|
||||
|
||||
return new TransmissionService(
|
||||
Logger.Object,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider.Object,
|
||||
Logger,
|
||||
FilenameEvaluator,
|
||||
Striker,
|
||||
DryRunInterceptor,
|
||||
HardLinkFileService,
|
||||
HttpClientProvider,
|
||||
EventPublisher,
|
||||
BlocklistProvider,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
ClientWrapper.Object
|
||||
RuleEvaluator,
|
||||
SeedingRuleEvaluator,
|
||||
ClientWrapper
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetMocks()
|
||||
{
|
||||
Logger.Reset();
|
||||
FilenameEvaluator.Reset();
|
||||
Striker.Reset();
|
||||
DryRunInterceptor.Reset();
|
||||
HardLinkFileService.Reset();
|
||||
HttpClientProvider.Reset();
|
||||
EventPublisher.Reset();
|
||||
RuleEvaluator.Reset();
|
||||
RuleManager.Reset();
|
||||
ClientWrapper.Reset();
|
||||
Logger = Substitute.For<ILogger<TransmissionService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
BlocklistProvider = Substitute.For<IBlocklistProvider>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<ITransmissionClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using Transmission.API.RPC.Entity;
|
||||
using Xunit;
|
||||
|
||||
@@ -45,12 +45,13 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync((TransmissionTorrents?)null);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns((TransmissionTorrents?)null);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -96,20 +97,21 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -154,20 +156,21 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -223,12 +226,13 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -279,20 +283,21 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -343,12 +348,13 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { hash });
|
||||
|
||||
@@ -395,12 +401,13 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { category });
|
||||
|
||||
@@ -458,20 +465,21 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -523,21 +531,22 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()), Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive().EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -578,21 +587,22 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()), Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive().EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,16 +650,17 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.SlowSpeed, true));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -697,16 +708,17 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.TorrentGetAsync(fields, hash))
|
||||
.ReturnsAsync(torrents);
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<TransmissionItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true));
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
@@ -37,16 +37,16 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentsAsync())
|
||||
.ReturnsAsync(torrents);
|
||||
.GetTorrentsAsync()
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync("hash1"))
|
||||
.ReturnsAsync(new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" });
|
||||
.GetTorrentPropertiesAsync("hash1")
|
||||
.Returns(new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync("hash3"))
|
||||
.ReturnsAsync(new UTorrentProperties { Hash = "hash3", Pex = 1, Trackers = "" });
|
||||
.GetTorrentPropertiesAsync("hash3")
|
||||
.Returns(new UTorrentProperties { Hash = "hash3", Pex = 1, Trackers = "" });
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -67,8 +67,8 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentsAsync())
|
||||
.ReturnsAsync(torrents);
|
||||
.GetTorrentsAsync()
|
||||
.Returns(torrents);
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -90,12 +90,12 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentsAsync())
|
||||
.ReturnsAsync(torrents);
|
||||
.GetTorrentsAsync()
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync("hash1"))
|
||||
.ReturnsAsync(new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" });
|
||||
.GetTorrentPropertiesAsync("hash1")
|
||||
.Returns(new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" });
|
||||
|
||||
// Act
|
||||
var result = await sut.GetSeedingDownloads();
|
||||
@@ -127,8 +127,8 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new UTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new UTorrentSeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new UTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
|
||||
new UTorrentSeedingRule { Name = "tv", Categories = ["tv"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -154,7 +154,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new UTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new UTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -178,7 +178,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
var categories = new List<ISeedingRule>
|
||||
{
|
||||
new UTorrentSeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
new UTorrentSeedingRule { Name = "movies", Categories = ["movies"], MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -308,20 +308,19 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "TEST-HASH";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash")), true))
|
||||
.RemoveTorrentsAsync(Arg.Is<List<string>>(h => h.Contains("test-hash")), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, true);
|
||||
await sut.DeleteDownload(mockTorrent, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash")), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.RemoveTorrentsAsync(Arg.Is<List<string>>(h => h.Contains("test-hash")), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -330,20 +329,19 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "UPPERCASE-HASH";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.IsAny<List<string>>(), true))
|
||||
.RemoveTorrentsAsync(Arg.Any<List<string>>(), true)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, true);
|
||||
await sut.DeleteDownload(mockTorrent, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("uppercase-hash")), true),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.RemoveTorrentsAsync(Arg.Is<List<string>>(h => h.Contains("uppercase-hash")), true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -352,20 +350,19 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
const string hash = "TEST-HASH";
|
||||
var mockTorrent = new Mock<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns(hash);
|
||||
var mockTorrent = Substitute.For<ITorrentItemWrapper>();
|
||||
mockTorrent.Hash.Returns(hash);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash")), false))
|
||||
.RemoveTorrentsAsync(Arg.Is<List<string>>(h => h.Contains("test-hash")), false)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(mockTorrent.Object, false);
|
||||
await sut.DeleteDownload(mockTorrent, false);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash")), false),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.RemoveTorrentsAsync(Arg.Is<List<string>>(h => h.Contains("test-hash")), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +388,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(null, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -410,7 +407,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(new List<Domain.Entities.ITorrentItemWrapper>(), unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -436,7 +433,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -462,7 +459,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -488,7 +485,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -511,23 +508,22 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("hash1"))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentLabelAsync("hash1", "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabelAsync("hash1", "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -550,21 +546,21 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("hash1"))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(2);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -587,21 +583,21 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("hash1"))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(-1);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -624,24 +620,23 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("hash1"))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 0, Index = 0, Size = 1000, Downloaded = 0 },
|
||||
new UTorrentFile { Name = "file2.mkv", Priority = 1, Index = 1, Size = 2000, Downloaded = 1000 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()),
|
||||
Times.Once);
|
||||
_fixture.HardLinkFileService.Received(1)
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -664,23 +659,22 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("hash1"))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.Setup(x => x.GetHardLinkCount(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - EventPublisher is not mocked, so we just verify the method completed
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.SetTorrentLabelAsync("hash1", "unlinked"),
|
||||
Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabelAsync("hash1", "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -703,14 +697,14 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync("hash1"))
|
||||
.ReturnsAsync((List<UTorrentFile>?)null);
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns((List<UTorrentFile>?)null);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - When files is null, it uses empty collection and proceeds to change label
|
||||
_fixture.ClientWrapper.Verify(x => x.SetTorrentLabelAsync("hash1", "unlinked"), Times.Once);
|
||||
await _fixture.ClientWrapper.Received(1).SetTorrentLabelAsync("hash1", "unlinked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,42 +8,46 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
|
||||
public class UTorrentServiceFixture : IDisposable
|
||||
{
|
||||
public Mock<ILogger<UTorrentService>> Logger { get; }
|
||||
public Mock<IFilenameEvaluator> FilenameEvaluator { get; }
|
||||
public Mock<IStriker> Striker { get; }
|
||||
public Mock<IDryRunInterceptor> DryRunInterceptor { get; }
|
||||
public Mock<IHardLinkFileService> HardLinkFileService { get; }
|
||||
public Mock<IDynamicHttpClientProvider> HttpClientProvider { get; }
|
||||
public Mock<IEventPublisher> EventPublisher { get; }
|
||||
public Mock<IBlocklistProvider> BlocklistProvider { get; }
|
||||
public Mock<IRuleEvaluator> RuleEvaluator { get; }
|
||||
public Mock<IRuleManager> RuleManager { get; }
|
||||
public Mock<IUTorrentClientWrapper> ClientWrapper { get; }
|
||||
public ILogger<UTorrentService> Logger { get; private set; }
|
||||
public IFilenameEvaluator FilenameEvaluator { get; private set; }
|
||||
public IStriker Striker { get; private set; }
|
||||
public IDryRunInterceptor DryRunInterceptor { get; private set; }
|
||||
public IHardLinkFileService HardLinkFileService { get; private set; }
|
||||
public IDynamicHttpClientProvider HttpClientProvider { get; private set; }
|
||||
public IEventPublisher EventPublisher { get; private set; }
|
||||
public IBlocklistProvider BlocklistProvider { get; private set; }
|
||||
public IQueueRuleEvaluator RuleEvaluator { get; private set; }
|
||||
public IQueueRuleManager RuleManager { get; private set; }
|
||||
public ISeedingRuleEvaluator SeedingRuleEvaluator { get; private set; }
|
||||
public IUTorrentClientWrapper ClientWrapper { get; private set; }
|
||||
|
||||
public UTorrentServiceFixture()
|
||||
{
|
||||
Logger = new Mock<ILogger<UTorrentService>>();
|
||||
FilenameEvaluator = new Mock<IFilenameEvaluator>();
|
||||
Striker = new Mock<IStriker>();
|
||||
DryRunInterceptor = new Mock<IDryRunInterceptor>();
|
||||
HardLinkFileService = new Mock<IHardLinkFileService>();
|
||||
HttpClientProvider = new Mock<IDynamicHttpClientProvider>();
|
||||
EventPublisher = new Mock<IEventPublisher>();
|
||||
BlocklistProvider = new Mock<IBlocklistProvider>();
|
||||
RuleEvaluator = new Mock<IRuleEvaluator>();
|
||||
RuleManager = new Mock<IRuleManager>();
|
||||
ClientWrapper = new Mock<IUTorrentClientWrapper>();
|
||||
Logger = Substitute.For<ILogger<UTorrentService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
BlocklistProvider = Substitute.For<IBlocklistProvider>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IUTorrentClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
@@ -65,42 +69,45 @@ public class UTorrentServiceFixture : IDisposable
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
HttpClientProvider
|
||||
.Setup(x => x.CreateClient(It.IsAny<DownloadClientConfig>()))
|
||||
.CreateClient(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(httpClient);
|
||||
|
||||
return new UTorrentService(
|
||||
Logger.Object,
|
||||
FilenameEvaluator.Object,
|
||||
Striker.Object,
|
||||
DryRunInterceptor.Object,
|
||||
HardLinkFileService.Object,
|
||||
HttpClientProvider.Object,
|
||||
EventPublisher.Object,
|
||||
BlocklistProvider.Object,
|
||||
Logger,
|
||||
FilenameEvaluator,
|
||||
Striker,
|
||||
DryRunInterceptor,
|
||||
HardLinkFileService,
|
||||
HttpClientProvider,
|
||||
EventPublisher,
|
||||
BlocklistProvider,
|
||||
config,
|
||||
RuleEvaluator.Object,
|
||||
RuleManager.Object,
|
||||
ClientWrapper.Object
|
||||
RuleEvaluator,
|
||||
SeedingRuleEvaluator,
|
||||
ClientWrapper
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetMocks()
|
||||
{
|
||||
Logger.Reset();
|
||||
FilenameEvaluator.Reset();
|
||||
Striker.Reset();
|
||||
DryRunInterceptor.Reset();
|
||||
HardLinkFileService.Reset();
|
||||
HttpClientProvider.Reset();
|
||||
EventPublisher.Reset();
|
||||
RuleEvaluator.Reset();
|
||||
RuleManager.Reset();
|
||||
ClientWrapper.Reset();
|
||||
Logger = Substitute.For<ILogger<UTorrentService>>();
|
||||
FilenameEvaluator = Substitute.For<IFilenameEvaluator>();
|
||||
Striker = Substitute.For<IStriker>();
|
||||
DryRunInterceptor = Substitute.For<IDryRunInterceptor>();
|
||||
HardLinkFileService = Substitute.For<IHardLinkFileService>();
|
||||
HttpClientProvider = Substitute.For<IDynamicHttpClientProvider>();
|
||||
EventPublisher = Substitute.For<IEventPublisher>();
|
||||
RuleEvaluator = Substitute.For<IQueueRuleEvaluator>();
|
||||
RuleManager = Substitute.For<IQueueRuleManager>();
|
||||
SeedingRuleEvaluator = Substitute.For<ISeedingRuleEvaluator>();
|
||||
ClientWrapper = Substitute.For<IUTorrentClientWrapper>();
|
||||
|
||||
DryRunInterceptor
|
||||
.Setup(x => x.InterceptAsync(It.IsAny<Delegate>(), It.IsAny<object[]>()))
|
||||
.Returns((Delegate action, object[] parameters) =>
|
||||
.InterceptAsync(Arg.Any<Delegate>(), Arg.Any<object[]>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
var action = callInfo.ArgAt<Delegate>(0);
|
||||
var parameters = callInfo.ArgAt<object[]>(1);
|
||||
return (Task)(action.DynamicInvoke(parameters) ?? Task.CompletedTask);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
using Moq;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.DownloadClient;
|
||||
@@ -29,8 +30,8 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync((UTorrentItem?)null);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns((UTorrentItem?)null);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -61,27 +62,27 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -111,27 +112,27 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -168,16 +169,16 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 0, Index = 0, Size = 1000, Downloaded = 0 },
|
||||
new UTorrentFile { Name = "file2.mkv", Priority = 0, Index = 1, Size = 2000, Downloaded = 0 }
|
||||
@@ -212,28 +213,28 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 0, Index = 0, Size = 1000, Downloaded = 0 },
|
||||
new UTorrentFile { Name = "file2.mkv", Priority = 1, Index = 1, Size = 2000, Downloaded = 1000 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -269,12 +270,12 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { hash });
|
||||
|
||||
@@ -306,12 +307,12 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { category });
|
||||
|
||||
@@ -342,12 +343,12 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, new[] { trackerDomain });
|
||||
|
||||
@@ -384,24 +385,24 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ThrowsAsync(new InvalidOperationException("Failed to get files"));
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Throws(new InvalidOperationException("Failed to get files"));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -438,28 +439,28 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()), Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive().EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -484,28 +485,28 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((false, DeleteReason.None, false));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
_fixture.RuleEvaluator.Verify(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()), Times.Never);
|
||||
await _fixture.RuleEvaluator.DidNotReceive().EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,23 +538,23 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateSlowRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.SlowSpeed, true));
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -585,23 +586,23 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentAsync(hash))
|
||||
.ReturnsAsync(torrentItem);
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentPropertiesAsync(hash))
|
||||
.ReturnsAsync(torrentProperties);
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.GetTorrentFilesAsync(hash))
|
||||
.ReturnsAsync(new List<UTorrentFile>
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.Setup(x => x.EvaluateStallRulesAsync(It.IsAny<UTorrentItemWrapper>()))
|
||||
.ReturnsAsync((true, DeleteReason.Stalled, true));
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
|
||||
@@ -319,13 +319,15 @@ public static class TestDataContextFactory
|
||||
double maxRatio = 1.0,
|
||||
double minSeedTime = 1.0,
|
||||
double maxSeedTime = -1,
|
||||
TorrentPrivacyType privacyType = TorrentPrivacyType.Both)
|
||||
TorrentPrivacyType privacyType = TorrentPrivacyType.Both,
|
||||
List<string>? categories = null)
|
||||
{
|
||||
var downloadClient = context.DownloadClients.First();
|
||||
var rule = new QBitSeedingRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Categories = categories ?? ["completed"],
|
||||
MaxRatio = maxRatio,
|
||||
MinSeedTime = minSeedTime,
|
||||
MaxSeedTime = maxSeedTime,
|
||||
|
||||
@@ -0,0 +1,995 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class QueueRuleEvaluatorTests : IDisposable
|
||||
{
|
||||
private readonly EventsContext _context;
|
||||
|
||||
public QueueRuleEvaluatorTests()
|
||||
{
|
||||
_context = CreateInMemoryEventsContext();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
private static EventsContext CreateInMemoryEventsContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<EventsContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new EventsContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetStrikes_ShouldRespectMinimumProgressThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = new StallRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
QueueCleanerConfigId = Guid.NewGuid(),
|
||||
Name = "Stall Rule",
|
||||
Enabled = true,
|
||||
MaxStrikes = 3,
|
||||
PrivacyType = TorrentPrivacyType.Public,
|
||||
MinCompletionPercentage = 0,
|
||||
MaxCompletionPercentage = 100,
|
||||
ResetStrikesOnProgress = true,
|
||||
MinimumProgress = "10 MB",
|
||||
DeletePrivateTorrentsFromClient = false,
|
||||
};
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
striker
|
||||
.ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.Stalled)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
long downloadedBytes = 0;
|
||||
|
||||
var torrent = Substitute.For<ITorrentItemWrapper>();
|
||||
torrent.Hash.Returns("hash");
|
||||
torrent.Name.Returns("Example Torrent");
|
||||
torrent.IsPrivate.Returns(false);
|
||||
torrent.Size.Returns(ByteSize.Parse("100 MB").Bytes);
|
||||
torrent.CompletionPercentage.Returns(50);
|
||||
torrent.DownloadedBytes.Returns(callInfo => downloadedBytes);
|
||||
|
||||
// Seed database with a DownloadItem and initial strike (simulating first observation at 0 bytes)
|
||||
var downloadItem = new DownloadItem { DownloadId = "hash", Title = "Example Torrent" };
|
||||
context.DownloadItems.Add(downloadItem);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var initialStrike = new Strike { DownloadItemId = downloadItem.Id, Type = StrikeType.Stalled, LastDownloadedBytes = 0 };
|
||||
context.Strikes.Add(initialStrike);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Progress below threshold should not reset strikes
|
||||
downloadedBytes = ByteSize.Parse("1 MB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
await striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.Stalled);
|
||||
|
||||
// Progress beyond threshold should trigger reset
|
||||
downloadedBytes = ByteSize.Parse("12 MB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
await striker.Received(1).ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.Stalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_NoMatchingRules_ShouldReturnFoundWithoutRemoval()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns((StallRule?)null);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.DidNotReceive().StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithMatchingRule_ShouldApplyStrikeWithoutRemoval()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Apply", resetOnProgress: false, maxStrikes: 5);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled, Arg.Any<long?>());
|
||||
await striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.Stalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenStrikeLimitReached_ShouldMarkForRemoval()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Remove", resetOnProgress: false, maxStrikes: 6);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenStrikeThrows_ShouldThrowException()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var failingRule = CreateStallRule("Failing", resetOnProgress: false, maxStrikes: 4);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(failingRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns<bool>(x => throw new InvalidOperationException("boom"));
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateStallRulesAsync(torrent));
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_NoMatchingRules_ShouldReturnFoundWithoutRemoval()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns((SlowRule?)null);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.DidNotReceive().StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithMatchingRule_ShouldApplyStrikeWithoutRemoval()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Apply", resetOnProgress: false, maxStrikes: 3);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenStrikeLimitReached_ShouldMarkForRemoval()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Remove", resetOnProgress: false, maxStrikes: 8);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_TimeBasedRule_WhenEtaIsAcceptable_ShouldResetStrikes()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Progress", resetOnProgress: true, maxStrikes: 4);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.SlowTime)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.Eta.Returns(1800); // ETA is 0.5 hours, below the 1 hour threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
await striker.Received(1).ResetStrikeAsync("hash", "Example Torrent", StrikeType.SlowTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenStrikeThrows_ShouldThrowException()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var failingRule = CreateSlowRule("Failing Slow", resetOnProgress: false, maxStrikes: 4);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(failingRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns<bool>(x => throw new InvalidOperationException("slow fail"));
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateSlowRulesAsync(torrent));
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithSpeedBasedRule_ShouldUseSlowSpeedStrikeType()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Rule",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "1 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowSpeed, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed, Arg.Any<long?>());
|
||||
await striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithBothSpeedAndTimeConfigured_ShouldTreatAsSlowSpeed()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Both Rule",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 2,
|
||||
minSpeed: "500 KB",
|
||||
maxTimeHours: 2);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowSpeed, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithNeitherSpeedNorTimeConfigured_ShouldNotStrike()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
// Neither minSpeed nor maxTime set (maxTimeHours = 0, minSpeed = null)
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Fallback Rule",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 1,
|
||||
minSpeed: null,
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.DidNotReceive().StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), Arg.Any<StrikeType>(), Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBasedRule_WhenSpeedIsAcceptable_ShouldResetSlowSpeedStrikes()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Reset",
|
||||
resetOnProgress: true,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "1 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.SlowSpeed)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.DownloadSpeed.Returns(ByteSize.Parse("2 MB").Bytes); // Speed is above 1 MB threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
await striker.Received(1).ResetStrikeAsync("hash", "Example Torrent", StrikeType.SlowSpeed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBasedRule_WithResetDisabled_ShouldNotReset()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed No Reset",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "1 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.DownloadSpeed.Returns(ByteSize.Parse("2 MB").Bytes); // Speed is above threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
await striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_TimeBasedRule_WithResetDisabled_ShouldNotReset()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Time No Reset",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 4,
|
||||
minSpeed: null,
|
||||
maxTimeHours: 2);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.Eta.Returns(1800); // ETA below threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
await striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<StrikeType>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBased_BelowThreshold_ShouldStrike()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Strike",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "5 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowSpeed, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.DownloadSpeed.Returns(ByteSize.Parse("1 MB").Bytes); // Speed below 5 MB threshold
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", 3, StrikeType.SlowSpeed, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_TimeBased_AboveThreshold_ShouldStrike()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Time Strike",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 5,
|
||||
minSpeed: null,
|
||||
maxTimeHours: 1);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.Eta.Returns(7200); // 2 hours, above 1 hour threshold
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
await striker.Received(1).StrikeAndCheckLimit("hash", "Example Torrent", 5, StrikeType.SlowTime, Arg.Any<long?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithResetDisabled_ShouldNotReset()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("No Reset", resetOnProgress: false, maxStrikes: 3);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
long downloadedBytes = ByteSize.Parse("50 MB").Bytes;
|
||||
var torrent = CreateTorrentMock(downloadedBytesFactory: () => downloadedBytes);
|
||||
|
||||
await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
// Progress made but reset disabled, so no reset
|
||||
downloadedBytes = ByteSize.Parse("60 MB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
await striker.DidNotReceive().ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.Stalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithProgressAndNoMinimumThreshold_ShouldReset()
|
||||
{
|
||||
// Arrange
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
// Seed database with a DownloadItem and initial strike (simulating first observation at 0 bytes)
|
||||
var downloadItem = new DownloadItem { DownloadId = "hash", Title = "Example Torrent" };
|
||||
context.DownloadItems.Add(downloadItem);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var initialStrike = new Strike { DownloadItemId = downloadItem.Id, Type = StrikeType.Stalled, LastDownloadedBytes = 0 };
|
||||
context.Strikes.Add(initialStrike);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Reset No Minimum", resetOnProgress: true, maxStrikes: 3, minimumProgress: null);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
striker
|
||||
.ResetStrikeAsync(Arg.Any<string>(), Arg.Any<string>(), StrikeType.Stalled)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act - Any progress should trigger reset when no minimum is set
|
||||
long downloadedBytes = ByteSize.Parse("1 KB").Bytes;
|
||||
var torrent = CreateTorrentMock(downloadedBytesFactory: () => downloadedBytes);
|
||||
await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
// Assert
|
||||
await striker.Received(1).ResetStrikeAsync("hash", "Example Torrent", StrikeType.Stalled);
|
||||
}
|
||||
|
||||
private static ITorrentItemWrapper CreateTorrentMock(
|
||||
Func<long>? downloadedBytesFactory = null,
|
||||
bool isPrivate = false,
|
||||
string hash = "hash",
|
||||
string name = "Example Torrent",
|
||||
double completionPercentage = 50,
|
||||
string size = "100 MB")
|
||||
{
|
||||
var torrent = Substitute.For<ITorrentItemWrapper>();
|
||||
torrent.Hash.Returns(hash);
|
||||
torrent.Name.Returns(name);
|
||||
torrent.IsPrivate.Returns(isPrivate);
|
||||
torrent.CompletionPercentage.Returns(completionPercentage);
|
||||
torrent.Size.Returns(ByteSize.Parse(size).Bytes);
|
||||
torrent.DownloadedBytes.Returns(callInfo => downloadedBytesFactory?.Invoke() ?? 0);
|
||||
torrent.DownloadSpeed.Returns(0);
|
||||
torrent.Eta.Returns(7200);
|
||||
return torrent;
|
||||
}
|
||||
|
||||
private static StallRule CreateStallRule(string name, bool resetOnProgress, int maxStrikes, string? minimumProgress = null, bool deletePrivateTorrentsFromClient = false)
|
||||
{
|
||||
return new StallRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
QueueCleanerConfigId = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Enabled = true,
|
||||
MaxStrikes = maxStrikes,
|
||||
PrivacyType = TorrentPrivacyType.Public,
|
||||
MinCompletionPercentage = 0,
|
||||
MaxCompletionPercentage = 100,
|
||||
ResetStrikesOnProgress = resetOnProgress,
|
||||
MinimumProgress = minimumProgress,
|
||||
DeletePrivateTorrentsFromClient = deletePrivateTorrentsFromClient,
|
||||
};
|
||||
}
|
||||
|
||||
private static SlowRule CreateSlowRule(
|
||||
string name,
|
||||
bool resetOnProgress,
|
||||
int maxStrikes,
|
||||
string? minSpeed = null,
|
||||
double maxTimeHours = 1,
|
||||
bool deletePrivateTorrentsFromClient = false)
|
||||
{
|
||||
return new SlowRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
QueueCleanerConfigId = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Enabled = true,
|
||||
MaxStrikes = maxStrikes,
|
||||
PrivacyType = TorrentPrivacyType.Public,
|
||||
MinCompletionPercentage = 0,
|
||||
MaxCompletionPercentage = 100,
|
||||
ResetStrikesOnProgress = resetOnProgress,
|
||||
MaxTimeHours = maxTimeHours,
|
||||
MinSpeed = minSpeed ?? string.Empty,
|
||||
IgnoreAboveSize = string.Empty,
|
||||
DeletePrivateTorrentsFromClient = deletePrivateTorrentsFromClient,
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenNoRuleMatches_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns((StallRule?)null);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesButNoRemoval_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Test Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientTrue_ShouldReturnTrue()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Delete True Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.Stalled, result.Reason);
|
||||
Assert.True(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientFalse_ShouldReturnFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Delete False Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: false);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.Stalled, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenNoRuleMatches_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns((SlowRule?)null);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientTrue_ShouldReturnTrue()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Delete True", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.SlowTime, result.Reason);
|
||||
Assert.True(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientFalse_ShouldReturnFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Delete False", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: false);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.SlowTime, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBasedRuleWithDeleteFromClientTrue_ShouldReturnTrue()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
"Speed Delete True",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "5 MB",
|
||||
maxTimeHours: 0,
|
||||
deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowSpeed, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.DownloadSpeed.Returns(ByteSize.Parse("1 MB").Bytes);
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.SlowSpeed, result.Reason);
|
||||
Assert.True(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenRuleMatchesButNoRemoval_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule("Test Slow Rule", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(false);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class RuleManagerTests
|
||||
public class QueueRuleManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetMatchingStallRule_NoRules_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule>());
|
||||
|
||||
@@ -33,8 +33,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_OneMatch_ReturnsRule()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Test Rule", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -54,8 +54,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_MultipleMatches_ReturnsNull_LogsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule1 = CreateStallRule("Rule 1", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
var stallRule2 = CreateStallRule("Rule 2", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
@@ -82,8 +82,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_DisabledRule_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Disabled Rule", enabled: false, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -101,8 +101,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_PrivacyTypeMismatch_Public_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Public Rule", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -120,8 +120,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_PrivacyTypeMismatch_Private_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Private Rule", enabled: true, privacyType: TorrentPrivacyType.Private, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -139,8 +139,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_PrivacyTypeBoth_MatchesPublic()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Both Rule", enabled: true, privacyType: TorrentPrivacyType.Both, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -159,8 +159,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_PrivacyTypeBoth_MatchesPrivate()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Both Rule", enabled: true, privacyType: TorrentPrivacyType.Both, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -179,8 +179,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_CompletionPercentageBelowMin_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Rule 20-80", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 20, maxCompletion: 80);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -198,8 +198,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_CompletionPercentageAboveMax_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Rule 20-80", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 20, maxCompletion: 80);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -217,8 +217,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_CompletionPercentageAtMinBoundary_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Rule 20-80", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 20, maxCompletion: 80);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -237,8 +237,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingStallRule_CompletionPercentageAtMaxBoundary_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Rule 20-80", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 20, maxCompletion: 80);
|
||||
ContextProvider.Set(nameof(StallRule), new List<StallRule> { stallRule });
|
||||
@@ -257,8 +257,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingSlowRule_NoRules_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
ContextProvider.Set(nameof(SlowRule), new List<SlowRule>());
|
||||
|
||||
@@ -275,8 +275,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingSlowRule_OneMatch_ReturnsRule()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Rule", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
ContextProvider.Set(nameof(SlowRule), new List<SlowRule> { slowRule });
|
||||
@@ -296,8 +296,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingSlowRule_MultipleMatches_ReturnsNull_LogsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var slowRule1 = CreateSlowRule("Slow 1", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
var slowRule2 = CreateSlowRule("Slow 2", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100);
|
||||
@@ -324,8 +324,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingSlowRule_FileSizeAboveIgnoreThreshold_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Size Limited", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100, ignoreAboveSize: "50 MB");
|
||||
ContextProvider.Set(nameof(SlowRule), new List<SlowRule> { slowRule });
|
||||
@@ -343,8 +343,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingSlowRule_FileSizeBelowIgnoreThreshold_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Size Limited", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100, ignoreAboveSize: "50 MB");
|
||||
ContextProvider.Set(nameof(SlowRule), new List<SlowRule> { slowRule });
|
||||
@@ -363,8 +363,8 @@ public class RuleManagerTests
|
||||
public void GetMatchingSlowRule_NoIgnoreSizeSet_Matches()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<RuleManager>>();
|
||||
var ruleManager = new RuleManager(loggerMock.Object);
|
||||
var loggerMock = new Mock<ILogger<QueueRuleManager>>();
|
||||
var ruleManager = new QueueRuleManager(loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("No Size Limit", enabled: true, privacyType: TorrentPrivacyType.Public, minCompletion: 0, maxCompletion: 100, ignoreAboveSize: string.Empty);
|
||||
ContextProvider.Set(nameof(SlowRule), new List<SlowRule> { slowRule });
|
||||
@@ -1,999 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class RuleEvaluatorTests : IDisposable
|
||||
{
|
||||
private readonly EventsContext _context;
|
||||
|
||||
public RuleEvaluatorTests()
|
||||
{
|
||||
_context = CreateInMemoryEventsContext();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Dispose();
|
||||
}
|
||||
|
||||
private static EventsContext CreateInMemoryEventsContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<EventsContext>()
|
||||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new EventsContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetStrikes_ShouldRespectMinimumProgressThreshold()
|
||||
{
|
||||
// Arrange
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = new StallRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
QueueCleanerConfigId = Guid.NewGuid(),
|
||||
Name = "Stall Rule",
|
||||
Enabled = true,
|
||||
MaxStrikes = 3,
|
||||
PrivacyType = TorrentPrivacyType.Public,
|
||||
MinCompletionPercentage = 0,
|
||||
MaxCompletionPercentage = 100,
|
||||
ResetStrikesOnProgress = true,
|
||||
MinimumProgress = "10 MB",
|
||||
DeletePrivateTorrentsFromClient = false,
|
||||
};
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
long downloadedBytes = 0;
|
||||
|
||||
var torrentMock = new Mock<ITorrentItemWrapper>();
|
||||
torrentMock.SetupGet(t => t.Hash).Returns("hash");
|
||||
torrentMock.SetupGet(t => t.Name).Returns("Example Torrent");
|
||||
torrentMock.SetupGet(t => t.IsPrivate).Returns(false);
|
||||
torrentMock.SetupGet(t => t.Size).Returns(ByteSize.Parse("100 MB").Bytes);
|
||||
torrentMock.SetupGet(t => t.CompletionPercentage).Returns(50);
|
||||
torrentMock.SetupGet(t => t.DownloadedBytes).Returns(() => downloadedBytes);
|
||||
|
||||
// Seed database with a DownloadItem and initial strike (simulating first observation at 0 bytes)
|
||||
var downloadItem = new DownloadItem { DownloadId = "hash", Title = "Example Torrent" };
|
||||
context.DownloadItems.Add(downloadItem);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var initialStrike = new Strike { DownloadItemId = downloadItem.Id, Type = StrikeType.Stalled, LastDownloadedBytes = 0 };
|
||||
context.Strikes.Add(initialStrike);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Progress below threshold should not reset strikes
|
||||
downloadedBytes = ByteSize.Parse("1 MB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Never);
|
||||
|
||||
// Progress beyond threshold should trigger reset
|
||||
downloadedBytes = ByteSize.Parse("12 MB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_NoMatchingRules_ShouldReturnFoundWithoutRemoval()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns((StallRule?)null);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithMatchingRule_ShouldApplyStrikeWithoutRemoval()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Apply", resetOnProgress: false, maxStrikes: 5);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled, It.IsAny<long?>()), Times.Once);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenStrikeLimitReached_ShouldMarkForRemoval()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Remove", resetOnProgress: false, maxStrikes: 6);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)stallRule.MaxStrikes, StrikeType.Stalled, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenStrikeThrows_ShouldThrowException()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var failingRule = CreateStallRule("Failing", resetOnProgress: false, maxStrikes: 4);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(failingRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ThrowsAsync(new InvalidOperationException("boom"));
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateStallRulesAsync(torrentMock.Object));
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_NoMatchingRules_ShouldReturnFoundWithoutRemoval()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns((SlowRule?)null);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithMatchingRule_ShouldApplyStrikeWithoutRemoval()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Apply", resetOnProgress: false, maxStrikes: 3);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenStrikeLimitReached_ShouldMarkForRemoval()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Remove", resetOnProgress: false, maxStrikes: 8);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_TimeBasedRule_WhenEtaIsAcceptable_ShouldResetStrikes()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Progress", resetOnProgress: true, maxStrikes: 4);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.SlowTime))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.Eta).Returns(1800); // ETA is 0.5 hours, below the 1 hour threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync("hash", "Example Torrent", StrikeType.SlowTime), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenStrikeThrows_ShouldThrowException()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var failingRule = CreateSlowRule("Failing Slow", resetOnProgress: false, maxStrikes: 4);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(failingRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ThrowsAsync(new InvalidOperationException("slow fail"));
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => evaluator.EvaluateSlowRulesAsync(torrentMock.Object));
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithSpeedBasedRule_ShouldUseSlowSpeedStrikeType()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Rule",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "1 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(
|
||||
x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed, It.IsAny<long?>()),
|
||||
Times.Once);
|
||||
strikerMock.Verify(
|
||||
x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<StrikeType>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithBothSpeedAndTimeConfigured_ShouldTreatAsSlowSpeed()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Both Rule",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 2,
|
||||
minSpeed: "500 KB",
|
||||
maxTimeHours: 2);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.True(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", (ushort)slowRule.MaxStrikes, StrikeType.SlowSpeed, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WithNeitherSpeedNorTimeConfigured_ShouldNotStrike()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
// Neither minSpeed nor maxTime set (maxTimeHours = 0, minSpeed = null)
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Fallback Rule",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 1,
|
||||
minSpeed: null,
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), It.IsAny<StrikeType>(), It.IsAny<long?>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBasedRule_WhenSpeedIsAcceptable_ShouldResetSlowSpeedStrikes()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Reset",
|
||||
resetOnProgress: true,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "1 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.SlowSpeed))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.DownloadSpeed).Returns(ByteSize.Parse("2 MB").Bytes); // Speed is above 1 MB threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync("hash", "Example Torrent", StrikeType.SlowSpeed), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBasedRule_WithResetDisabled_ShouldNotReset()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed No Reset",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "1 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.DownloadSpeed).Returns(ByteSize.Parse("2 MB").Bytes); // Speed is above threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<StrikeType>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_TimeBasedRule_WithResetDisabled_ShouldNotReset()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Time No Reset",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 4,
|
||||
minSpeed: null,
|
||||
maxTimeHours: 2);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.Eta).Returns(1800); // ETA below threshold
|
||||
|
||||
await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<StrikeType>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBased_BelowThreshold_ShouldStrike()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Speed Strike",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "5 MB",
|
||||
maxTimeHours: 0);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.DownloadSpeed).Returns(ByteSize.Parse("1 MB").Bytes); // Speed below 5 MB threshold
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", 3, StrikeType.SlowSpeed, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_TimeBased_AboveThreshold_ShouldStrike()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
name: "Time Strike",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 5,
|
||||
minSpeed: null,
|
||||
maxTimeHours: 1);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.Eta).Returns(7200); // 2 hours, above 1 hour threshold
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
Assert.False(result.ShouldRemove);
|
||||
|
||||
strikerMock.Verify(x => x.StrikeAndCheckLimit("hash", "Example Torrent", 5, StrikeType.SlowTime, It.IsAny<long?>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithResetDisabled_ShouldNotReset()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("No Reset", resetOnProgress: false, maxStrikes: 3);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
long downloadedBytes = ByteSize.Parse("50 MB").Bytes;
|
||||
var torrentMock = CreateTorrentMock(downloadedBytesFactory: () => downloadedBytes);
|
||||
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
// Progress made but reset disabled, so no reset
|
||||
downloadedBytes = ByteSize.Parse("60 MB").Bytes;
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WithProgressAndNoMinimumThreshold_ShouldReset()
|
||||
{
|
||||
// Arrange
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
// Seed database with a DownloadItem and initial strike (simulating first observation at 0 bytes)
|
||||
var downloadItem = new DownloadItem { DownloadId = "hash", Title = "Example Torrent" };
|
||||
context.DownloadItems.Add(downloadItem);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var initialStrike = new Strike { DownloadItemId = downloadItem.Id, Type = StrikeType.Stalled, LastDownloadedBytes = 0 };
|
||||
context.Strikes.Add(initialStrike);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Reset No Minimum", resetOnProgress: true, maxStrikes: 3, minimumProgress: null);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.ResetStrikeAsync(It.IsAny<string>(), It.IsAny<string>(), StrikeType.Stalled))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act - Any progress should trigger reset when no minimum is set
|
||||
long downloadedBytes = ByteSize.Parse("1 KB").Bytes;
|
||||
var torrentMock = CreateTorrentMock(downloadedBytesFactory: () => downloadedBytes);
|
||||
await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
// Assert
|
||||
strikerMock.Verify(x => x.ResetStrikeAsync("hash", "Example Torrent", StrikeType.Stalled), Times.Once);
|
||||
}
|
||||
|
||||
private static Mock<ITorrentItemWrapper> CreateTorrentMock(
|
||||
Func<long>? downloadedBytesFactory = null,
|
||||
bool isPrivate = false,
|
||||
string hash = "hash",
|
||||
string name = "Example Torrent",
|
||||
double completionPercentage = 50,
|
||||
string size = "100 MB")
|
||||
{
|
||||
var torrentMock = new Mock<ITorrentItemWrapper>();
|
||||
torrentMock.SetupGet(t => t.Hash).Returns(hash);
|
||||
torrentMock.SetupGet(t => t.Name).Returns(name);
|
||||
torrentMock.SetupGet(t => t.IsPrivate).Returns(isPrivate);
|
||||
torrentMock.SetupGet(t => t.CompletionPercentage).Returns(completionPercentage);
|
||||
torrentMock.SetupGet(t => t.Size).Returns(ByteSize.Parse(size).Bytes);
|
||||
torrentMock.SetupGet(t => t.DownloadedBytes).Returns(() => downloadedBytesFactory?.Invoke() ?? 0);
|
||||
torrentMock.SetupGet(t => t.DownloadSpeed).Returns(0);
|
||||
torrentMock.SetupGet(t => t.Eta).Returns(7200);
|
||||
return torrentMock;
|
||||
}
|
||||
|
||||
private static StallRule CreateStallRule(string name, bool resetOnProgress, int maxStrikes, string? minimumProgress = null, bool deletePrivateTorrentsFromClient = false)
|
||||
{
|
||||
return new StallRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
QueueCleanerConfigId = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Enabled = true,
|
||||
MaxStrikes = maxStrikes,
|
||||
PrivacyType = TorrentPrivacyType.Public,
|
||||
MinCompletionPercentage = 0,
|
||||
MaxCompletionPercentage = 100,
|
||||
ResetStrikesOnProgress = resetOnProgress,
|
||||
MinimumProgress = minimumProgress,
|
||||
DeletePrivateTorrentsFromClient = deletePrivateTorrentsFromClient,
|
||||
};
|
||||
}
|
||||
|
||||
private static SlowRule CreateSlowRule(
|
||||
string name,
|
||||
bool resetOnProgress,
|
||||
int maxStrikes,
|
||||
string? minSpeed = null,
|
||||
double maxTimeHours = 1,
|
||||
bool deletePrivateTorrentsFromClient = false)
|
||||
{
|
||||
return new SlowRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
QueueCleanerConfigId = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Enabled = true,
|
||||
MaxStrikes = maxStrikes,
|
||||
PrivacyType = TorrentPrivacyType.Public,
|
||||
MinCompletionPercentage = 0,
|
||||
MaxCompletionPercentage = 100,
|
||||
ResetStrikesOnProgress = resetOnProgress,
|
||||
MaxTimeHours = maxTimeHours,
|
||||
MinSpeed = minSpeed ?? string.Empty,
|
||||
IgnoreAboveSize = string.Empty,
|
||||
DeletePrivateTorrentsFromClient = deletePrivateTorrentsFromClient,
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenNoRuleMatches_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns((StallRule?)null);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesButNoRemoval_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Test Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientTrue_ShouldReturnTrue()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Delete True Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.Stalled, result.Reason);
|
||||
Assert.True(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientFalse_ShouldReturnFalse()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var stallRule = CreateStallRule("Delete False Rule", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: false);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingStallRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(stallRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.Stalled, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.Stalled, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenNoRuleMatches_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns((SlowRule?)null);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientTrue_ShouldReturnTrue()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Delete True", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.SlowTime, result.Reason);
|
||||
Assert.True(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenRuleMatchesAndRemovesWithDeleteFromClientFalse_ShouldReturnFalse()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Slow Delete False", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: false);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.SlowTime, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_SpeedBasedRuleWithDeleteFromClientTrue_ShouldReturnTrue()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
"Speed Delete True",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "5 MB",
|
||||
maxTimeHours: 0,
|
||||
deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowSpeed, It.IsAny<long?>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
torrentMock.SetupGet(t => t.DownloadSpeed).Returns(ByteSize.Parse("1 MB").Bytes);
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.True(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.SlowSpeed, result.Reason);
|
||||
Assert.True(result.DeleteFromClient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenRuleMatchesButNoRemoval_ShouldReturnDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManagerMock = new Mock<IRuleManager>();
|
||||
var strikerMock = new Mock<IStriker>();
|
||||
var loggerMock = new Mock<ILogger<RuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new RuleEvaluator(ruleManagerMock.Object, strikerMock.Object, context, loggerMock.Object);
|
||||
|
||||
var slowRule = CreateSlowRule("Test Slow Rule", resetOnProgress: false, maxStrikes: 3, maxTimeHours: 1, deletePrivateTorrentsFromClient: true);
|
||||
|
||||
ruleManagerMock
|
||||
.Setup(x => x.GetMatchingSlowRule(It.IsAny<ITorrentItemWrapper>()))
|
||||
.Returns(slowRule);
|
||||
|
||||
strikerMock
|
||||
.Setup(x => x.StrikeAndCheckLimit(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<ushort>(), StrikeType.SlowTime, It.IsAny<long?>()))
|
||||
.ReturnsAsync(false);
|
||||
|
||||
var torrentMock = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrentMock.Object);
|
||||
|
||||
Assert.False(result.ShouldRemove);
|
||||
Assert.Equal(DeleteReason.None, result.Reason);
|
||||
Assert.False(result.DeleteFromClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using NSubstitute;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class SeedingRuleEvaluatorTests
|
||||
{
|
||||
private readonly SeedingRuleEvaluator _sut = new();
|
||||
|
||||
private static ITorrentItemWrapper CreateTorrent(
|
||||
string? category = "movies",
|
||||
bool isPrivate = false,
|
||||
IReadOnlyList<string>? trackerDomains = null,
|
||||
IReadOnlyList<string>? tags = null)
|
||||
{
|
||||
var torrent = Substitute.For<ITorrentItemWrapper>();
|
||||
torrent.Category.Returns(category);
|
||||
torrent.IsPrivate.Returns(isPrivate);
|
||||
torrent.TrackerDomains.Returns(trackerDomains ?? Array.Empty<string>());
|
||||
torrent.Tags.Returns(tags ?? Array.Empty<string>());
|
||||
return torrent;
|
||||
}
|
||||
|
||||
private static QBitSeedingRule CreateQBitRule(
|
||||
int priority = 1,
|
||||
List<string>? categories = null,
|
||||
List<string>? trackerPatterns = null,
|
||||
List<string>? tagsAny = null,
|
||||
List<string>? tagsAll = null,
|
||||
TorrentPrivacyType privacyType = TorrentPrivacyType.Both)
|
||||
{
|
||||
return new QBitSeedingRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Rule",
|
||||
Priority = priority,
|
||||
Categories = categories ?? ["movies"],
|
||||
TrackerPatterns = trackerPatterns ?? [],
|
||||
TagsAny = tagsAny ?? [],
|
||||
TagsAll = tagsAll ?? [],
|
||||
PrivacyType = privacyType,
|
||||
};
|
||||
}
|
||||
|
||||
private static DelugeSeedingRule CreateDelugeRule(
|
||||
int priority = 1,
|
||||
List<string>? categories = null,
|
||||
List<string>? trackerPatterns = null,
|
||||
TorrentPrivacyType privacyType = TorrentPrivacyType.Both)
|
||||
{
|
||||
return new DelugeSeedingRule
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Test Rule",
|
||||
Priority = priority,
|
||||
Categories = categories ?? ["movies"],
|
||||
TrackerPatterns = trackerPatterns ?? [],
|
||||
PrivacyType = privacyType,
|
||||
};
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Empty / null rules
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_EmptyRules_ReturnsNull()
|
||||
{
|
||||
var torrent = CreateTorrent();
|
||||
_sut.GetMatchingRule(torrent, []).ShouldBeNull();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Category filtering
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_CategoryMatches_ReturnsRule()
|
||||
{
|
||||
var torrent = CreateTorrent(category: "movies");
|
||||
var rule = CreateDelugeRule(categories: ["movies"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_CategoryMismatch_ReturnsNull()
|
||||
{
|
||||
var torrent = CreateTorrent(category: "tv");
|
||||
var rule = CreateDelugeRule(categories: ["movies"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_CategoryMatchIsCaseInsensitive()
|
||||
{
|
||||
var torrent = CreateTorrent(category: "MOVIES");
|
||||
var rule = CreateDelugeRule(categories: ["movies"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TorrentMatchesAnyCategory()
|
||||
{
|
||||
var torrent = CreateTorrent(category: "tv");
|
||||
var rule = CreateDelugeRule(categories: ["movies", "tv", "music"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Tracker pattern filtering
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_EmptyTrackerPatterns_MatchesAnyTracker()
|
||||
{
|
||||
var torrent = CreateTorrent(trackerDomains: ["tracker.example.com"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: []);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TrackerPatternSuffixMatches()
|
||||
{
|
||||
var torrent = CreateTorrent(trackerDomains: ["tracker.example.com"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: ["example.com"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TrackerPatternNoMatch_ReturnsNull()
|
||||
{
|
||||
var torrent = CreateTorrent(trackerDomains: ["tracker.other.org"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: ["example.com"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TrackerPatternMatchIsCaseInsensitive()
|
||||
{
|
||||
var torrent = CreateTorrent(trackerDomains: ["TRACKER.EXAMPLE.COM"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: ["example.com"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TorrentMatchesAnyTrackerPattern()
|
||||
{
|
||||
var torrent = CreateTorrent(trackerDomains: ["tracker.private.org"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: ["example.com", "private.org"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Privacy type filtering
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Theory]
|
||||
[InlineData(TorrentPrivacyType.Public, false, true)]
|
||||
[InlineData(TorrentPrivacyType.Public, true, false)]
|
||||
[InlineData(TorrentPrivacyType.Private, false, false)]
|
||||
[InlineData(TorrentPrivacyType.Private, true, true)]
|
||||
[InlineData(TorrentPrivacyType.Both, false, true)]
|
||||
[InlineData(TorrentPrivacyType.Both, true, true)]
|
||||
public void GetMatchingRule_PrivacyType(TorrentPrivacyType rulePrivacy, bool torrentIsPrivate, bool shouldMatch)
|
||||
{
|
||||
var torrent = CreateTorrent(isPrivate: torrentIsPrivate);
|
||||
var rule = CreateDelugeRule(privacyType: rulePrivacy);
|
||||
|
||||
var result = _sut.GetMatchingRule(torrent, [rule]);
|
||||
if (shouldMatch)
|
||||
result.ShouldBe(rule);
|
||||
else
|
||||
result.ShouldBeNull();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// TagsAny filtering (ITagFilterable — QBit/Transmission only)
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAny_EmptyList_MatchesAnyTorrent()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: ["some-tag"]);
|
||||
var rule = CreateQBitRule(tagsAny: []);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAny_TorrentHasMatchingTag_Matches()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: ["hd", "private"]);
|
||||
var rule = CreateQBitRule(tagsAny: ["private"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAny_TorrentLacksAllTags_ReturnsNull()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: ["hd"]);
|
||||
var rule = CreateQBitRule(tagsAny: ["private", "special"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAny_MatchIsCaseInsensitive()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: ["PRIVATE"]);
|
||||
var rule = CreateQBitRule(tagsAny: ["private"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// TagsAll filtering (ITagFilterable — QBit/Transmission only)
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAll_EmptyList_MatchesAnyTorrent()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: []);
|
||||
var rule = CreateQBitRule(tagsAll: []);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAll_TorrentHasAllTags_Matches()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: ["hd", "private", "bonus"]);
|
||||
var rule = CreateQBitRule(tagsAll: ["hd", "private"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TagsAll_TorrentMissingOneTag_ReturnsNull()
|
||||
{
|
||||
var torrent = CreateTorrent(tags: ["hd"]);
|
||||
var rule = CreateQBitRule(tagsAll: ["hd", "private"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBeNull();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// ITagFilterable not applied for non-tag clients (e.g. Deluge)
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_DelugeRule_TagsIgnored_Matches()
|
||||
{
|
||||
// Deluge doesn't implement ITagFilterable, so tag properties on the rule are not checked
|
||||
var torrent = CreateTorrent(tags: []);
|
||||
var rule = CreateDelugeRule(); // no tag support
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Priority ordering
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_ReturnsLowestPriorityRule_WhenMultipleMatch()
|
||||
{
|
||||
var torrent = CreateTorrent(category: "movies");
|
||||
var highPriority = CreateDelugeRule(priority: 1, categories: ["movies"]);
|
||||
var lowPriority = CreateDelugeRule(priority: 5, categories: ["movies"]);
|
||||
|
||||
// Pass in reverse order to ensure sorting is applied
|
||||
var result = _sut.GetMatchingRule(torrent, [lowPriority, highPriority]);
|
||||
|
||||
result.ShouldBe(highPriority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_OnlyMatchingRuleReturned_OtherFiltered()
|
||||
{
|
||||
var torrent = CreateTorrent(
|
||||
category: "movies",
|
||||
trackerDomains: ["tracker.private.org"]);
|
||||
|
||||
var publicRule = CreateDelugeRule(
|
||||
priority: 1,
|
||||
categories: ["movies"],
|
||||
trackerPatterns: ["public.com"]); // does not match tracker
|
||||
|
||||
var privateRule = CreateDelugeRule(
|
||||
priority: 2,
|
||||
categories: ["movies"],
|
||||
trackerPatterns: ["private.org"]); // matches tracker
|
||||
|
||||
var result = _sut.GetMatchingRule(torrent, [publicRule, privateRule]);
|
||||
|
||||
result.ShouldBe(privateRule);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Combined criteria (AND logic)
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_AllCriteriaMustMatch()
|
||||
{
|
||||
var torrent = CreateTorrent(
|
||||
category: "movies",
|
||||
isPrivate: true,
|
||||
trackerDomains: ["tracker.example.com"],
|
||||
tags: ["hd"]);
|
||||
|
||||
var rule = CreateQBitRule(
|
||||
categories: ["movies"],
|
||||
trackerPatterns: ["example.com"],
|
||||
tagsAny: ["hd"],
|
||||
privacyType: TorrentPrivacyType.Private);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_OneCriterionFails_ReturnsNull()
|
||||
{
|
||||
var torrent = CreateTorrent(
|
||||
category: "movies",
|
||||
isPrivate: true,
|
||||
trackerDomains: ["tracker.example.com"],
|
||||
tags: ["hd"]);
|
||||
|
||||
// tracker pattern doesn't match
|
||||
var rule = CreateQBitRule(
|
||||
categories: ["movies"],
|
||||
trackerPatterns: ["other.com"],
|
||||
tagsAny: ["hd"],
|
||||
privacyType: TorrentPrivacyType.Private);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBeNull();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Edge cases
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_NullCategory_MatchesEmptyStringCategory()
|
||||
{
|
||||
var torrent = CreateTorrent(category: null);
|
||||
var rule = CreateDelugeRule(categories: [""]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_NullCategory_DoesNotMatchNamedCategory()
|
||||
{
|
||||
var torrent = CreateTorrent(category: null);
|
||||
var rule = CreateDelugeRule(categories: ["movies"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TrackerPatternExactDomainMatch()
|
||||
{
|
||||
var torrent = CreateTorrent(trackerDomains: ["example.com"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: ["example.com"]);
|
||||
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMatchingRule_TrackerPatternSuffixMatchesPartialDomain()
|
||||
{
|
||||
// "le.com" is a suffix of "example.com" — EndsWith will match this
|
||||
var torrent = CreateTorrent(trackerDomains: ["example.com"]);
|
||||
var rule = CreateDelugeRule(trackerPatterns: ["le.com"]);
|
||||
|
||||
// This matches because EndsWith is used (documenting current behavior)
|
||||
_sut.GetMatchingRule(torrent, [rule]).ShouldBe(rule);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Services;
|
||||
|
||||
public class UriServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void GetDomain_NullOrWhitespace_ReturnsNull(string? input)
|
||||
{
|
||||
UriService.GetDomain(input).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://tracker.example.com/announce", "tracker.example.com")]
|
||||
[InlineData("http://tracker.example.com:8080/announce", "tracker.example.com")]
|
||||
[InlineData("https://tracker.example.com/announce", "tracker.example.com")]
|
||||
[InlineData("https://tracker.example.com:443/announce", "tracker.example.com")]
|
||||
public void GetDomain_HttpUrls_ReturnsDomain(string input, string expected)
|
||||
{
|
||||
UriService.GetDomain(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("udp://tracker.opentrackr.org:1337/announce", "tracker.opentrackr.org")]
|
||||
[InlineData("udp://open.stealth.si:80/announce", "open.stealth.si")]
|
||||
public void GetDomain_UdpTrackerUrls_ReturnsDomain(string input, string expected)
|
||||
{
|
||||
UriService.GetDomain(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("tracker.example.com", "tracker.example.com")]
|
||||
[InlineData("tracker.example.com:8080", "tracker.example.com")]
|
||||
[InlineData("tracker.example.com/announce", "tracker.example.com")]
|
||||
public void GetDomain_NoScheme_PrependsHttpAndReturnsDomain(string input, string expected)
|
||||
{
|
||||
UriService.GetDomain(input).ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDomain_PlainDomain_ReturnsDomain()
|
||||
{
|
||||
UriService.GetDomain("example.com").ShouldBe("example.com");
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,19 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
|
||||
/// </summary>
|
||||
public sealed class DelugeItemWrapper : ITorrentItemWrapper
|
||||
{
|
||||
private readonly Lazy<IReadOnlyList<string>> _trackerDomains;
|
||||
|
||||
public DownloadStatus Info { get; }
|
||||
|
||||
public DelugeItemWrapper(DownloadStatus downloadStatus)
|
||||
{
|
||||
Info = downloadStatus ?? throw new ArgumentNullException(nameof(downloadStatus));
|
||||
_trackerDomains = new Lazy<IReadOnlyList<string>>(() => Info.Trackers
|
||||
.Select(t => UriService.GetDomain(t.Url))
|
||||
.Where(d => d is not null)
|
||||
.Select(d => d!)
|
||||
.ToList()
|
||||
.AsReadOnly());
|
||||
}
|
||||
|
||||
public string Hash => Info.Hash ?? string.Empty;
|
||||
@@ -46,6 +54,10 @@ public sealed class DelugeItemWrapper : ITorrentItemWrapper
|
||||
|
||||
public string SavePath => Info.DownloadLocation ?? string.Empty;
|
||||
|
||||
public IReadOnlyList<string> TrackerDomains => _trackerDomains.Value;
|
||||
|
||||
public IReadOnlyList<string> Tags => Array.Empty<string>();
|
||||
|
||||
public bool IsDownloading() => Info.State?.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase) == true;
|
||||
|
||||
public bool IsStalled() => Info.State?.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase) == true && Info is { DownloadSpeed: <= 0, Eta: <= 0 };
|
||||
|
||||
@@ -8,6 +8,7 @@ using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@@ -29,12 +30,12 @@ public partial class DelugeService : DownloadService, IDelugeService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator
|
||||
) : base(
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
var delugeClient = new DelugeClient(downloadClientConfig, _httpClient);
|
||||
@@ -52,13 +53,13 @@ public partial class DelugeService : DownloadService, IDelugeService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager,
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator,
|
||||
IDelugeClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
_client = clientWrapper;
|
||||
|
||||
@@ -28,7 +28,7 @@ public partial class DelugeService
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
|
||||
@@ -102,7 +102,7 @@ public partial class DelugeService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
@@ -113,6 +113,6 @@ public partial class DelugeService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ public abstract class DownloadService : IDownloadService
|
||||
protected readonly IBlocklistProvider _blocklistProvider;
|
||||
protected readonly HttpClient _httpClient;
|
||||
protected readonly DownloadClientConfig _downloadClientConfig;
|
||||
protected readonly IRuleEvaluator _ruleEvaluator;
|
||||
protected readonly IRuleManager _ruleManager;
|
||||
protected readonly IQueueRuleEvaluator _queueRuleEvaluator;
|
||||
private readonly ISeedingRuleEvaluator _seedingRuleEvaluator;
|
||||
|
||||
protected DownloadService(
|
||||
ILogger<DownloadService> logger,
|
||||
@@ -39,8 +39,8 @@ public abstract class DownloadService : IDownloadService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -52,8 +52,8 @@ public abstract class DownloadService : IDownloadService
|
||||
_blocklistProvider = blocklistProvider;
|
||||
_downloadClientConfig = downloadClientConfig;
|
||||
_httpClient = httpClientProvider.CreateClient(downloadClientConfig);
|
||||
_ruleEvaluator = ruleEvaluator;
|
||||
_ruleManager = ruleManager;
|
||||
_queueRuleEvaluator = queueRuleEvaluator;
|
||||
_seedingRuleEvaluator = seedingRuleEvaluator;
|
||||
}
|
||||
|
||||
public DownloadClientConfig ClientConfig => _downloadClientConfig;
|
||||
@@ -95,45 +95,40 @@ public abstract class DownloadService : IDownloadService
|
||||
continue;
|
||||
}
|
||||
|
||||
ISeedingRule? category = seedingRules
|
||||
.FirstOrDefault(x =>
|
||||
(torrent.Category ?? string.Empty).Equals(x.Name, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
x.PrivacyType switch
|
||||
{
|
||||
TorrentPrivacyType.Public => !torrent.IsPrivate,
|
||||
TorrentPrivacyType.Private => torrent.IsPrivate,
|
||||
_ => true
|
||||
});
|
||||
ISeedingRule? seedingRule = _seedingRuleEvaluator.GetMatchingRule(torrent, seedingRules);
|
||||
|
||||
if (category is null)
|
||||
if (seedingRule is null)
|
||||
{
|
||||
_logger.LogTrace("No seeding rules matched | {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Seeding rule matched | {seedingRule} | {name}", seedingRule.Name, torrent.Name);
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
TimeSpan seedingTime = TimeSpan.FromSeconds(torrent.SeedingTimeSeconds);
|
||||
SeedingCheckResult result = ShouldCleanDownload(torrent.Ratio, seedingTime, category);
|
||||
SeedingCheckResult result = ShouldCleanDownload(torrent.Ratio, seedingTime, seedingRule);
|
||||
|
||||
if (!result.ShouldClean)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => DeleteDownload(torrent, category.DeleteSourceFiles));
|
||||
await _dryRunInterceptor.InterceptAsync(() => DeleteDownload(torrent, seedingRule.DeleteSourceFiles));
|
||||
|
||||
_logger.LogInformation(
|
||||
"download cleaned | {reason} reached | delete files: {deleteFiles} | {name}",
|
||||
result.Reason is CleanReason.MaxRatioReached
|
||||
? "MAX_RATIO & MIN_SEED_TIME"
|
||||
: "MAX_SEED_TIME",
|
||||
category.DeleteSourceFiles,
|
||||
seedingRule.DeleteSourceFiles,
|
||||
torrent.Name
|
||||
);
|
||||
|
||||
await _eventPublisher.PublishDownloadCleaned(torrent.Ratio, seedingTime, category.Name, result.Reason);
|
||||
await _eventPublisher.PublishDownloadCleaned(torrent.Ratio, seedingTime, torrent.Category ?? string.Empty, result.Reason);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,13 +71,13 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
var eventPublisher = _serviceProvider.GetRequiredService<IEventPublisher>();
|
||||
var blocklistProvider = _serviceProvider.GetRequiredService<IBlocklistProvider>();
|
||||
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IRuleEvaluator>();
|
||||
var ruleManager = _serviceProvider.GetRequiredService<IRuleManager>();
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IQueueRuleEvaluator>();
|
||||
var seedingRuleEvaluator = _serviceProvider.GetRequiredService<ISeedingRuleEvaluator>();
|
||||
|
||||
// Create the QBitService instance
|
||||
QBitService service = new(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, seedingRuleEvaluator
|
||||
);
|
||||
|
||||
return service;
|
||||
@@ -94,13 +94,13 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
var eventPublisher = _serviceProvider.GetRequiredService<IEventPublisher>();
|
||||
var blocklistProvider = _serviceProvider.GetRequiredService<IBlocklistProvider>();
|
||||
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IRuleEvaluator>();
|
||||
var ruleManager = _serviceProvider.GetRequiredService<IRuleManager>();
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IQueueRuleEvaluator>();
|
||||
var seedingRuleEvaluator = _serviceProvider.GetRequiredService<ISeedingRuleEvaluator>();
|
||||
|
||||
// Create the DelugeService instance
|
||||
DelugeService service = new(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, seedingRuleEvaluator
|
||||
);
|
||||
|
||||
return service;
|
||||
@@ -117,13 +117,13 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
var eventPublisher = _serviceProvider.GetRequiredService<IEventPublisher>();
|
||||
var blocklistProvider = _serviceProvider.GetRequiredService<IBlocklistProvider>();
|
||||
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IRuleEvaluator>();
|
||||
var ruleManager = _serviceProvider.GetRequiredService<IRuleManager>();
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IQueueRuleEvaluator>();
|
||||
var seedingRuleEvaluator = _serviceProvider.GetRequiredService<ISeedingRuleEvaluator>();
|
||||
|
||||
// Create the TransmissionService instance
|
||||
TransmissionService service = new(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, seedingRuleEvaluator
|
||||
);
|
||||
|
||||
return service;
|
||||
@@ -142,13 +142,13 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
var blocklistProvider = _serviceProvider.GetRequiredService<IBlocklistProvider>();
|
||||
var loggerFactory = _serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IRuleEvaluator>();
|
||||
var ruleManager = _serviceProvider.GetRequiredService<IRuleManager>();
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IQueueRuleEvaluator>();
|
||||
var seedingRuleEvaluator = _serviceProvider.GetRequiredService<ISeedingRuleEvaluator>();
|
||||
|
||||
// Create the UTorrentService instance
|
||||
UTorrentService service = new(
|
||||
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, loggerFactory, ruleEvaluator, ruleManager
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, loggerFactory, ruleEvaluator, seedingRuleEvaluator
|
||||
);
|
||||
|
||||
return service;
|
||||
@@ -165,13 +165,13 @@ public sealed class DownloadServiceFactory : IDownloadServiceFactory
|
||||
var eventPublisher = _serviceProvider.GetRequiredService<IEventPublisher>();
|
||||
var blocklistProvider = _serviceProvider.GetRequiredService<IBlocklistProvider>();
|
||||
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IRuleEvaluator>();
|
||||
var ruleManager = _serviceProvider.GetRequiredService<IRuleManager>();
|
||||
var ruleEvaluator = _serviceProvider.GetRequiredService<IQueueRuleEvaluator>();
|
||||
var seedingRuleEvaluator = _serviceProvider.GetRequiredService<ISeedingRuleEvaluator>();
|
||||
|
||||
// Create the RTorrentService instance
|
||||
RTorrentService service = new(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor,
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, seedingRuleEvaluator
|
||||
);
|
||||
|
||||
return service;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent.Extensions;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
@@ -11,6 +12,7 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
public sealed class QBitItemWrapper : ITorrentItemWrapper
|
||||
{
|
||||
private readonly IReadOnlyList<TorrentTracker> _trackers;
|
||||
private readonly Lazy<IReadOnlyList<string>> _trackerDomains;
|
||||
|
||||
public TorrentInfo Info { get; }
|
||||
|
||||
@@ -19,6 +21,12 @@ public sealed class QBitItemWrapper : ITorrentItemWrapper
|
||||
Info = torrentInfo ?? throw new ArgumentNullException(nameof(torrentInfo));
|
||||
_trackers = trackers ?? throw new ArgumentNullException(nameof(trackers));
|
||||
IsPrivate = isPrivate;
|
||||
_trackerDomains = new Lazy<IReadOnlyList<string>>(() => _trackers
|
||||
.Select(t => UriService.GetDomain(t.Url))
|
||||
.Where(d => d is not null)
|
||||
.Select(d => d!)
|
||||
.ToList()
|
||||
.AsReadOnly());
|
||||
}
|
||||
|
||||
public string Hash => Info.Hash ?? string.Empty;
|
||||
@@ -49,6 +57,8 @@ public sealed class QBitItemWrapper : ITorrentItemWrapper
|
||||
|
||||
public string SavePath => Info.SavePath ?? string.Empty;
|
||||
|
||||
public IReadOnlyList<string> TrackerDomains => _trackerDomains.Value;
|
||||
|
||||
public IReadOnlyList<string> Tags => Info.Tags?.ToList().AsReadOnly() ?? (IReadOnlyList<string>)Array.Empty<string>();
|
||||
|
||||
public bool IsDownloading() => Info.State is TorrentState.Downloading or TorrentState.ForcedDownload;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Helpers;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@@ -30,11 +31,11 @@ public partial class QBitService : DownloadService, IQBitService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator
|
||||
) : base(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
var qBittorrentClient = new QBittorrentClient(_httpClient, downloadClientConfig.Url);
|
||||
@@ -52,12 +53,12 @@ public partial class QBitService : DownloadService, IQBitService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager,
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator,
|
||||
IQBittorrentClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
_client = clientWrapper;
|
||||
|
||||
@@ -37,7 +37,7 @@ public partial class QBitService
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -102,7 +102,7 @@ public partial class QBitService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
@@ -132,6 +132,6 @@ public partial class QBitService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public sealed class RTorrentItemWrapper : ITorrentItemWrapper
|
||||
{
|
||||
public RTorrentTorrent Info { get; }
|
||||
private readonly IReadOnlyList<string> _trackers;
|
||||
private readonly Lazy<IReadOnlyList<string>> _trackerDomains;
|
||||
private string? _category;
|
||||
|
||||
public RTorrentItemWrapper(RTorrentTorrent torrent, IReadOnlyList<string>? trackers = null)
|
||||
@@ -18,6 +19,12 @@ public sealed class RTorrentItemWrapper : ITorrentItemWrapper
|
||||
Info = torrent ?? throw new ArgumentNullException(nameof(torrent));
|
||||
_trackers = trackers ?? torrent.Trackers ?? [];
|
||||
_category = torrent.Label;
|
||||
_trackerDomains = new Lazy<IReadOnlyList<string>>(() => _trackers
|
||||
.Select(url => UriService.GetDomain(url))
|
||||
.Where(d => d is not null)
|
||||
.Select(d => d!)
|
||||
.ToList()
|
||||
.AsReadOnly());
|
||||
}
|
||||
|
||||
public string Hash => Info.Hash;
|
||||
@@ -53,6 +60,10 @@ public sealed class RTorrentItemWrapper : ITorrentItemWrapper
|
||||
|
||||
public string SavePath => Info.BasePath ?? string.Empty;
|
||||
|
||||
public IReadOnlyList<string> TrackerDomains => _trackerDomains.Value;
|
||||
|
||||
public IReadOnlyList<string> Tags => Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Downloading when state is 1 (started) and complete is 0 (not finished)
|
||||
/// </summary>
|
||||
|
||||
@@ -25,11 +25,11 @@ public partial class RTorrentService : DownloadService, IRTorrentService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator
|
||||
) : base(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
var rtorrentClient = new RTorrentClient(downloadClientConfig, _httpClient);
|
||||
@@ -47,12 +47,12 @@ public partial class RTorrentService : DownloadService, IRTorrentService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager,
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator,
|
||||
IRTorrentClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
_client = clientWrapper;
|
||||
|
||||
@@ -23,7 +23,7 @@ public partial class RTorrentService
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
|
||||
@@ -93,7 +93,7 @@ public partial class RTorrentService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
@@ -104,6 +104,6 @@ public partial class RTorrentService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,19 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
/// </summary>
|
||||
public sealed class TransmissionItemWrapper : ITorrentItemWrapper
|
||||
{
|
||||
private readonly Lazy<IReadOnlyList<string>> _trackerDomains;
|
||||
|
||||
public TorrentInfo Info { get; }
|
||||
|
||||
public TransmissionItemWrapper(TorrentInfo torrentInfo)
|
||||
{
|
||||
Info = torrentInfo ?? throw new ArgumentNullException(nameof(torrentInfo));
|
||||
_trackerDomains = new Lazy<IReadOnlyList<string>>(() => Info.Trackers?
|
||||
.Select(t => UriService.GetDomain(t.Announce))
|
||||
.Where(d => d is not null)
|
||||
.Select(d => d!)
|
||||
.ToList()
|
||||
.AsReadOnly() ?? (IReadOnlyList<string>)Array.Empty<string>());
|
||||
}
|
||||
|
||||
public string Hash => Info.HashString ?? string.Empty;
|
||||
@@ -47,6 +55,11 @@ public sealed class TransmissionItemWrapper : ITorrentItemWrapper
|
||||
|
||||
public string SavePath => Info.DownloadDir ?? string.Empty;
|
||||
|
||||
public IReadOnlyList<string> TrackerDomains => _trackerDomains.Value;
|
||||
|
||||
public IReadOnlyList<string> Tags => Info.Labels?.ToList().AsReadOnly()
|
||||
?? (IReadOnlyList<string>)Array.Empty<string>();
|
||||
|
||||
// Transmission status: 0=stopped, 1=check pending, 2=checking, 3=download pending, 4=downloading, 5=seed pending, 6=seeding
|
||||
public bool IsDownloading() => Info.Status == 4;
|
||||
public bool IsStalled() => Info is { Status: 4, RateDownload: <= 0, Eta: <= 0 };
|
||||
|
||||
@@ -6,6 +6,7 @@ using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
||||
using Cleanuparr.Infrastructure.Http;
|
||||
using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@@ -36,6 +37,7 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
TorrentFields.TRACKERS,
|
||||
TorrentFields.RATE_DOWNLOAD,
|
||||
TorrentFields.TOTAL_SIZE,
|
||||
TorrentFields.LABELS,
|
||||
];
|
||||
|
||||
public TransmissionService(
|
||||
@@ -48,12 +50,12 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator
|
||||
) : base(
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
UriBuilder uriBuilder = new(_downloadClientConfig.Url);
|
||||
@@ -78,13 +80,13 @@ public partial class TransmissionService : DownloadService, ITransmissionService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager,
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator,
|
||||
ITransmissionClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
_client = clientWrapper;
|
||||
|
||||
@@ -24,9 +24,7 @@ public partial class TransmissionService
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules)
|
||||
{
|
||||
return downloads
|
||||
?.Where(x => seedingRules
|
||||
.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase))
|
||||
)
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ public partial class TransmissionService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
@@ -118,6 +118,6 @@ public partial class TransmissionService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent.Extensions;
|
||||
using Cleanuparr.Infrastructure.Services;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
|
||||
@@ -9,6 +10,8 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
/// </summary>
|
||||
public sealed class UTorrentItemWrapper : ITorrentItemWrapper
|
||||
{
|
||||
private readonly Lazy<IReadOnlyList<string>> _trackerDomains;
|
||||
|
||||
public UTorrentItem Info { get; }
|
||||
|
||||
public UTorrentProperties Properties { get; }
|
||||
@@ -17,6 +20,12 @@ public sealed class UTorrentItemWrapper : ITorrentItemWrapper
|
||||
{
|
||||
Info = torrentItem ?? throw new ArgumentNullException(nameof(torrentItem));
|
||||
Properties = torrentProperties ?? throw new ArgumentNullException(nameof(torrentProperties));
|
||||
_trackerDomains = new Lazy<IReadOnlyList<string>>(() => Properties.TrackerList
|
||||
.Select(url => UriService.GetDomain(url))
|
||||
.Where(d => d is not null)
|
||||
.Select(d => d!)
|
||||
.ToList()
|
||||
.AsReadOnly());
|
||||
}
|
||||
|
||||
public string Hash => Info.Hash;
|
||||
@@ -47,6 +56,10 @@ public sealed class UTorrentItemWrapper : ITorrentItemWrapper
|
||||
|
||||
public string SavePath => Info.SavePath ?? string.Empty;
|
||||
|
||||
public IReadOnlyList<string> TrackerDomains => _trackerDomains.Value;
|
||||
|
||||
public IReadOnlyList<string> Tags => Array.Empty<string>();
|
||||
|
||||
public bool IsDownloading() =>
|
||||
(Info.Status & UTorrentStatus.Started) != 0 &&
|
||||
(Info.Status & UTorrentStatus.Checked) != 0 &&
|
||||
|
||||
@@ -32,12 +32,12 @@ public partial class UTorrentService : DownloadService, IUTorrentService
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
ILoggerFactory loggerFactory,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator
|
||||
) : base(
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
// Create the new layered client with dependency injection
|
||||
@@ -71,13 +71,13 @@ public partial class UTorrentService : DownloadService, IUTorrentService
|
||||
IEventPublisher eventPublisher,
|
||||
IBlocklistProvider blocklistProvider,
|
||||
DownloadClientConfig downloadClientConfig,
|
||||
IRuleEvaluator ruleEvaluator,
|
||||
IRuleManager ruleManager,
|
||||
IQueueRuleEvaluator queueRuleEvaluator,
|
||||
ISeedingRuleEvaluator seedingRuleEvaluator,
|
||||
IUTorrentClientWrapper clientWrapper
|
||||
) : base(
|
||||
logger,
|
||||
filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, ruleEvaluator, ruleManager
|
||||
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator
|
||||
)
|
||||
{
|
||||
_client = clientWrapper;
|
||||
|
||||
@@ -27,7 +27,7 @@ public partial class UTorrentService
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
|
||||
@@ -102,7 +102,7 @@ public partial class UTorrentService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
@@ -113,6 +113,6 @@ public partial class UTorrentService
|
||||
return (false, DeleteReason.None, false);
|
||||
}
|
||||
|
||||
return await _ruleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
|
||||
public interface IRuleEvaluator
|
||||
public interface IQueueRuleEvaluator
|
||||
{
|
||||
Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent);
|
||||
Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateSlowRulesAsync(ITorrentItemWrapper torrent);
|
||||
@@ -3,7 +3,7 @@ using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
|
||||
public interface IRuleManager
|
||||
public interface IQueueRuleManager
|
||||
{
|
||||
StallRule? GetMatchingStallRule(ITorrentItemWrapper torrent);
|
||||
SlowRule? GetMatchingSlowRule(ITorrentItemWrapper torrent);
|
||||
@@ -0,0 +1,12 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
|
||||
public interface ISeedingRuleEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the highest-priority matching seeding rule for the given torrent
|
||||
/// </summary>
|
||||
ISeedingRule? GetMatchingRule(ITorrentItemWrapper torrent, IEnumerable<ISeedingRule> rules);
|
||||
}
|
||||
@@ -1,32 +1,29 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services;
|
||||
|
||||
public class RuleEvaluator : IRuleEvaluator
|
||||
public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
{
|
||||
private readonly IRuleManager _ruleManager;
|
||||
private readonly IQueueRuleManager _queueRuleManager;
|
||||
private readonly IStriker _striker;
|
||||
private readonly EventsContext _context;
|
||||
private readonly ILogger<RuleEvaluator> _logger;
|
||||
private readonly ILogger<QueueRuleEvaluator> _logger;
|
||||
|
||||
public RuleEvaluator(
|
||||
IRuleManager ruleManager,
|
||||
public QueueRuleEvaluator(
|
||||
IQueueRuleManager queueRuleManager,
|
||||
IStriker striker,
|
||||
EventsContext context,
|
||||
ILogger<RuleEvaluator> logger)
|
||||
ILogger<QueueRuleEvaluator> logger)
|
||||
{
|
||||
_ruleManager = ruleManager;
|
||||
_queueRuleManager = queueRuleManager;
|
||||
_striker = striker;
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
@@ -36,7 +33,7 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
{
|
||||
_logger.LogTrace("Evaluating stall rules | {name}", torrent.Name);
|
||||
|
||||
var rule = _ruleManager.GetMatchingStallRule(torrent);
|
||||
var rule = _queueRuleManager.GetMatchingStallRule(torrent);
|
||||
|
||||
if (rule is null)
|
||||
{
|
||||
@@ -74,7 +71,7 @@ public class RuleEvaluator : IRuleEvaluator
|
||||
{
|
||||
_logger.LogTrace("Evaluating slow rules | {name}", torrent.Name);
|
||||
|
||||
SlowRule? rule = _ruleManager.GetMatchingSlowRule(torrent);
|
||||
SlowRule? rule = _queueRuleManager.GetMatchingSlowRule(torrent);
|
||||
|
||||
if (rule is null)
|
||||
{
|
||||
@@ -6,11 +6,11 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services;
|
||||
|
||||
public class RuleManager : IRuleManager
|
||||
public class QueueRuleManager : IQueueRuleManager
|
||||
{
|
||||
private readonly ILogger<RuleManager> _logger;
|
||||
private readonly ILogger<QueueRuleManager> _logger;
|
||||
|
||||
public RuleManager(ILogger<RuleManager> logger)
|
||||
public QueueRuleManager(ILogger<QueueRuleManager> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services;
|
||||
|
||||
public class SeedingRuleEvaluator : ISeedingRuleEvaluator
|
||||
{
|
||||
public ISeedingRule? GetMatchingRule(ITorrentItemWrapper torrent, IEnumerable<ISeedingRule> rules)
|
||||
{
|
||||
return rules
|
||||
.OrderBy(r => r.Priority)
|
||||
.FirstOrDefault(rule => Matches(torrent, rule));
|
||||
}
|
||||
|
||||
private static bool Matches(ITorrentItemWrapper torrent, ISeedingRule rule)
|
||||
{
|
||||
// Category check
|
||||
if (!rule.Categories.Any(c => c.Equals(torrent.Category ?? string.Empty, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tracker check
|
||||
if (rule.TrackerPatterns.Count > 0)
|
||||
{
|
||||
bool hasMatchingTracker = torrent.TrackerDomains.Any(domain =>
|
||||
rule.TrackerPatterns.Any(pattern =>
|
||||
domain.EndsWith(pattern, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (!hasMatchingTracker)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Tag/label check
|
||||
if (rule is ITagFilterable tagFilterable)
|
||||
{
|
||||
if (tagFilterable.TagsAny.Count > 0 &&
|
||||
!tagFilterable.TagsAny.Any(t => torrent.Tags.Contains(t, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tagFilterable.TagsAll.Count > 0 &&
|
||||
!tagFilterable.TagsAll.All(t => torrent.Tags.Contains(t, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Privacy check
|
||||
return rule.PrivacyType switch
|
||||
{
|
||||
TorrentPrivacyType.Public => !torrent.IsPrivate,
|
||||
TorrentPrivacyType.Private => torrent.IsPrivate,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Services;
|
||||
|
||||
public static class UriService
|
||||
public static partial class UriService
|
||||
{
|
||||
[GeneratedRegex(@"^(?:\w+:\/\/)?([^\/\?:]+)", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex DomainFallbackRegex();
|
||||
|
||||
public static string? GetDomain(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
@@ -12,7 +15,7 @@ public static class UriService
|
||||
}
|
||||
|
||||
// add "http://" if scheme is missing to help Uri.TryCreate
|
||||
if (!input.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
if (!input.Contains("://"))
|
||||
{
|
||||
input = "http://" + input;
|
||||
}
|
||||
@@ -23,9 +26,8 @@ public static class UriService
|
||||
}
|
||||
|
||||
// url might be malformed
|
||||
var regex = new Regex(@"^(?:https?:\/\/)?([^\/\?:]+)", RegexOptions.IgnoreCase);
|
||||
var match = regex.Match(input);
|
||||
|
||||
var match = DomainFallbackRegex().Match(input);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups[1].Value;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Cleanuparr.Persistence.Converters;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Persistence.Tests.Converters;
|
||||
|
||||
public class JsonStringListConverterTests
|
||||
{
|
||||
private readonly JsonStringListConverter _converter = new();
|
||||
|
||||
[Fact]
|
||||
public void ConvertToProvider_WithValues_ReturnsJsonArray()
|
||||
{
|
||||
var list = new List<string> { "movies", "tv", "music" };
|
||||
|
||||
var result = _converter.ConvertToProviderExpression.Compile()(list);
|
||||
|
||||
result.ShouldBe("[\"movies\",\"tv\",\"music\"]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertToProvider_EmptyList_ReturnsEmptyJsonArray()
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
var result = _converter.ConvertToProviderExpression.Compile()(list);
|
||||
|
||||
result.ShouldBe("[]");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertFromProvider_ValidJson_ReturnsList()
|
||||
{
|
||||
var json = "[\"movies\",\"tv\"]";
|
||||
|
||||
var result = _converter.ConvertFromProviderExpression.Compile()(json);
|
||||
|
||||
result.ShouldBe(new List<string> { "movies", "tv" });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConvertFromProvider_EmptyArray_ReturnsEmptyList()
|
||||
{
|
||||
var json = "[]";
|
||||
|
||||
var result = _converter.ConvertFromProviderExpression.Compile()(json);
|
||||
|
||||
result.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_PreservesValues()
|
||||
{
|
||||
var original = new List<string> { "tracker.example.com", "private.org" };
|
||||
|
||||
var json = _converter.ConvertToProviderExpression.Compile()(original);
|
||||
var restored = _converter.ConvertFromProviderExpression.Compile()(json);
|
||||
|
||||
restored.ShouldBe(original);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = -1,
|
||||
@@ -50,6 +51,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = -1,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = 24,
|
||||
@@ -65,6 +67,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = 1,
|
||||
MaxSeedTime = 48,
|
||||
@@ -80,6 +83,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 0,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = -1,
|
||||
@@ -95,6 +99,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = -1,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = 0,
|
||||
@@ -106,6 +111,27 @@ public sealed class QBitSeedingRuleTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate - Categories Validation
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithEmptyCategories_ThrowsValidationException()
|
||||
{
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = [],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = -1,
|
||||
DeleteSourceFiles = true
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("At least one category must be specified");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate - Name Validation
|
||||
|
||||
[Fact]
|
||||
@@ -166,6 +192,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = -1,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = -1,
|
||||
@@ -185,6 +212,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = maxRatio,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = maxSeedTime,
|
||||
@@ -205,6 +233,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = -1,
|
||||
MaxSeedTime = -1,
|
||||
@@ -224,6 +253,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = minSeedTime,
|
||||
MaxSeedTime = -1,
|
||||
@@ -240,6 +270,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = -1,
|
||||
@@ -255,6 +286,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = 24,
|
||||
MaxSeedTime = -1,
|
||||
@@ -272,6 +304,7 @@ public sealed class QBitSeedingRuleTests
|
||||
var config = new QBitSeedingRule
|
||||
{
|
||||
Name = "test-category",
|
||||
Categories = ["test-category"],
|
||||
MaxRatio = 2.0,
|
||||
MinSeedTime = 0,
|
||||
MaxSeedTime = -1,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Cleanuparr.Persistence.Converters;
|
||||
|
||||
public class JsonStringListConverter : ValueConverter<List<string>, string>
|
||||
{
|
||||
public JsonStringListConverter() : base(
|
||||
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
|
||||
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions?)null) ?? new List<string>())
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -294,13 +294,20 @@ public class DataContext : DbContext
|
||||
entity.Property(s => s.RecordedAt).HasConversion(new UtcDateTimeConverter());
|
||||
});
|
||||
|
||||
// Configure per-client seeding rule relationships
|
||||
// Configure per-client seeding rule relationships and JSON list converters
|
||||
var jsonListConverter = new JsonStringListConverter();
|
||||
|
||||
modelBuilder.Entity<QBitSeedingRule>(entity =>
|
||||
{
|
||||
entity.HasOne(s => s.DownloadClientConfig)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.DownloadClientConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TagsAny).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TagsAll).HasConversion(jsonListConverter);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DelugeSeedingRule>(entity =>
|
||||
@@ -309,6 +316,9 @@ public class DataContext : DbContext
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.DownloadClientConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<TransmissionSeedingRule>(entity =>
|
||||
@@ -317,6 +327,11 @@ public class DataContext : DbContext
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.DownloadClientConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TagsAny).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TagsAll).HasConversion(jsonListConverter);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UTorrentSeedingRule>(entity =>
|
||||
@@ -325,6 +340,9 @@ public class DataContext : DbContext
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.DownloadClientConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<RTorrentSeedingRule>(entity =>
|
||||
@@ -333,6 +351,9 @@ public class DataContext : DbContext
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.DownloadClientConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.Property(s => s.Categories).HasConversion(jsonListConverter);
|
||||
entity.Property(s => s.TrackerPatterns).HasConversion(jsonListConverter);
|
||||
});
|
||||
|
||||
// Configure per-client unlinked config relationship
|
||||
|
||||
2102
code/backend/Cleanuparr.Persistence/Migrations/Data/20260408081126_AddSeedingRuleEnhancements.Designer.cs
generated
Normal file
2102
code/backend/Cleanuparr.Persistence/Migrations/Data/20260408081126_AddSeedingRuleEnhancements.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSeedingRuleEnhancements : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "categories",
|
||||
table: "u_torrent_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "priority",
|
||||
table: "u_torrent_seeding_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tracker_patterns",
|
||||
table: "u_torrent_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "categories",
|
||||
table: "transmission_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "priority",
|
||||
table: "transmission_seeding_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tags_all",
|
||||
table: "transmission_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tags_any",
|
||||
table: "transmission_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tracker_patterns",
|
||||
table: "transmission_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "categories",
|
||||
table: "r_torrent_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "priority",
|
||||
table: "r_torrent_seeding_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tracker_patterns",
|
||||
table: "r_torrent_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "categories",
|
||||
table: "q_bit_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "priority",
|
||||
table: "q_bit_seeding_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tags_all",
|
||||
table: "q_bit_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tags_any",
|
||||
table: "q_bit_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tracker_patterns",
|
||||
table: "q_bit_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "categories",
|
||||
table: "deluge_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "priority",
|
||||
table: "deluge_seeding_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "tracker_patterns",
|
||||
table: "deluge_seeding_rules",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "[]");
|
||||
|
||||
// Data migration: populate categories from name and assign sequential priorities per client
|
||||
foreach (var table in new[] { "q_bit_seeding_rules", "deluge_seeding_rules", "transmission_seeding_rules", "u_torrent_seeding_rules", "r_torrent_seeding_rules" })
|
||||
{
|
||||
// Populate categories from existing name (preserves existing rule matching behaviour)
|
||||
migrationBuilder.Sql($"UPDATE {table} SET categories = json_array(name)");
|
||||
|
||||
// Assign sequential priorities per download client, ordered by rowid
|
||||
migrationBuilder.Sql($@"
|
||||
UPDATE {table}
|
||||
SET priority = (
|
||||
SELECT COUNT(*)
|
||||
FROM {table} r2
|
||||
WHERE r2.download_client_config_id = {table}.download_client_config_id
|
||||
AND r2.rowid <= {table}.rowid
|
||||
)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "categories",
|
||||
table: "u_torrent_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "priority",
|
||||
table: "u_torrent_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tracker_patterns",
|
||||
table: "u_torrent_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "categories",
|
||||
table: "transmission_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "priority",
|
||||
table: "transmission_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tags_all",
|
||||
table: "transmission_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tags_any",
|
||||
table: "transmission_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tracker_patterns",
|
||||
table: "transmission_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "categories",
|
||||
table: "r_torrent_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "priority",
|
||||
table: "r_torrent_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tracker_patterns",
|
||||
table: "r_torrent_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "categories",
|
||||
table: "q_bit_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "priority",
|
||||
table: "q_bit_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tags_all",
|
||||
table: "q_bit_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tags_any",
|
||||
table: "q_bit_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tracker_patterns",
|
||||
table: "q_bit_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "categories",
|
||||
table: "deluge_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "priority",
|
||||
table: "deluge_seeding_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "tracker_patterns",
|
||||
table: "deluge_seeding_rules");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,11 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("categories");
|
||||
|
||||
b.Property<bool>("DeleteSourceFiles")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_source_files");
|
||||
@@ -145,11 +150,20 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("PrivacyType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("privacy_type");
|
||||
|
||||
b.Property<string>("TrackerPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracker_patterns");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_deluge_seeding_rules");
|
||||
|
||||
@@ -197,6 +211,11 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("categories");
|
||||
|
||||
b.Property<bool>("DeleteSourceFiles")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_source_files");
|
||||
@@ -222,11 +241,30 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("PrivacyType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("privacy_type");
|
||||
|
||||
b.Property<string>("TagsAll")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags_all");
|
||||
|
||||
b.Property<string>("TagsAny")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags_any");
|
||||
|
||||
b.Property<string>("TrackerPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracker_patterns");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_q_bit_seeding_rules");
|
||||
|
||||
@@ -243,6 +281,11 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("categories");
|
||||
|
||||
b.Property<bool>("DeleteSourceFiles")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_source_files");
|
||||
@@ -268,11 +311,20 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("PrivacyType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("privacy_type");
|
||||
|
||||
b.Property<string>("TrackerPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracker_patterns");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_r_torrent_seeding_rules");
|
||||
|
||||
@@ -289,6 +341,11 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("categories");
|
||||
|
||||
b.Property<bool>("DeleteSourceFiles")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_source_files");
|
||||
@@ -314,11 +371,30 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("PrivacyType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("privacy_type");
|
||||
|
||||
b.Property<string>("TagsAll")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags_all");
|
||||
|
||||
b.Property<string>("TagsAny")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags_any");
|
||||
|
||||
b.Property<string>("TrackerPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracker_patterns");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_transmission_seeding_rules");
|
||||
|
||||
@@ -335,6 +411,11 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Categories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("categories");
|
||||
|
||||
b.Property<bool>("DeleteSourceFiles")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_source_files");
|
||||
@@ -360,11 +441,20 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<string>("PrivacyType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("privacy_type");
|
||||
|
||||
b.Property<string>("TrackerPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracker_patterns");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_u_torrent_seeding_rules");
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ public sealed record DelugeSeedingRule : ISeedingRule
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Categories { get; set; } = [];
|
||||
|
||||
public List<string> TrackerPatterns { get; set; } = [];
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which torrent privacy types this rule applies to.
|
||||
/// </summary>
|
||||
@@ -49,6 +55,11 @@ public sealed record DelugeSeedingRule : ISeedingRule
|
||||
throw new ValidationException("Rule name can not be empty");
|
||||
}
|
||||
|
||||
if (Categories.Count == 0)
|
||||
{
|
||||
throw new ValidationException("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");
|
||||
|
||||
@@ -10,8 +10,27 @@ public interface ISeedingRule : IConfig
|
||||
|
||||
DownloadClientConfig DownloadClientConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable display label for this rule.
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The torrent categories/labels this rule applies to. At least one must be specified.
|
||||
/// </summary>
|
||||
List<string> Categories { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracker domain patterns to filter by (suffix match, case-insensitive).
|
||||
/// Empty list means match any tracker.
|
||||
/// </summary>
|
||||
List<string> TrackerPatterns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluation order. Lower value = evaluated first. Auto-assigned on create.
|
||||
/// </summary>
|
||||
int Priority { get; set; }
|
||||
|
||||
TorrentPrivacyType PrivacyType { get; set; }
|
||||
|
||||
double MaxRatio { get; set; }
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a seeding rule as supporting tag/label-based filtering.
|
||||
/// Implemented by clients that expose per-torrent tags: qBittorrent (tags) and Transmission (labels).
|
||||
/// </summary>
|
||||
public interface ITagFilterable
|
||||
{
|
||||
/// <summary>
|
||||
/// The torrent must have at least one of these tags/labels. Empty = no tag filter.
|
||||
/// </summary>
|
||||
List<string> TagsAny { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The torrent must have ALL of these tags/labels. Empty = no tag filter.
|
||||
/// </summary>
|
||||
List<string> TagsAll { get; set; }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
|
||||
public sealed record QBitSeedingRule : ISeedingRule
|
||||
public sealed record QBitSeedingRule : ISeedingRule, ITagFilterable
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
@@ -17,6 +17,16 @@ public sealed record QBitSeedingRule : ISeedingRule
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Categories { get; set; } = [];
|
||||
|
||||
public List<string> TrackerPatterns { get; set; } = [];
|
||||
|
||||
public List<string> TagsAny { get; set; } = [];
|
||||
|
||||
public List<string> TagsAll { get; set; } = [];
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which torrent privacy types this rule applies to.
|
||||
/// </summary>
|
||||
@@ -49,6 +59,11 @@ public sealed record QBitSeedingRule : ISeedingRule
|
||||
throw new ValidationException("Rule name can not be empty");
|
||||
}
|
||||
|
||||
if (Categories.Count == 0)
|
||||
{
|
||||
throw new ValidationException("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");
|
||||
|
||||
@@ -17,6 +17,12 @@ public sealed record RTorrentSeedingRule : ISeedingRule
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Categories { get; set; } = [];
|
||||
|
||||
public List<string> TrackerPatterns { get; set; } = [];
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which torrent privacy types this rule applies to.
|
||||
/// </summary>
|
||||
@@ -49,6 +55,11 @@ public sealed record RTorrentSeedingRule : ISeedingRule
|
||||
throw new ValidationException("Rule name can not be empty");
|
||||
}
|
||||
|
||||
if (Categories.Count == 0)
|
||||
{
|
||||
throw new ValidationException("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");
|
||||
|
||||
@@ -5,7 +5,7 @@ using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
|
||||
|
||||
namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
|
||||
public sealed record TransmissionSeedingRule : ISeedingRule
|
||||
public sealed record TransmissionSeedingRule : ISeedingRule, ITagFilterable
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
@@ -17,6 +17,22 @@ public sealed record TransmissionSeedingRule : ISeedingRule
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Categories { get; set; } = [];
|
||||
|
||||
public List<string> TrackerPatterns { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The torrent must have at least one of these Transmission labels. Empty = no label filter.
|
||||
/// </summary>
|
||||
public List<string> TagsAny { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The torrent must have ALL of these Transmission labels. Empty = no label filter.
|
||||
/// </summary>
|
||||
public List<string> TagsAll { get; set; } = [];
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which torrent privacy types this rule applies to.
|
||||
/// </summary>
|
||||
@@ -49,6 +65,11 @@ public sealed record TransmissionSeedingRule : ISeedingRule
|
||||
throw new ValidationException("Rule name can not be empty");
|
||||
}
|
||||
|
||||
if (Categories.Count == 0)
|
||||
{
|
||||
throw new ValidationException("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");
|
||||
|
||||
@@ -17,6 +17,12 @@ public sealed record UTorrentSeedingRule : ISeedingRule
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Categories { get; set; } = [];
|
||||
|
||||
public List<string> TrackerPatterns { get; set; } = [];
|
||||
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which torrent privacy types this rule applies to.
|
||||
/// </summary>
|
||||
@@ -49,6 +55,11 @@ public sealed record UTorrentSeedingRule : ISeedingRule
|
||||
throw new ValidationException("Rule name can not be empty");
|
||||
}
|
||||
|
||||
if (Categories.Count == 0)
|
||||
{
|
||||
throw new ValidationException("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");
|
||||
|
||||
@@ -32,6 +32,10 @@ export class DownloadCleanerApi {
|
||||
return this.http.delete<void>(`/api/seeding-rules/${id}`);
|
||||
}
|
||||
|
||||
reorderSeedingRules(clientId: string, orderedIds: string[]): Observable<void> {
|
||||
return this.http.put<void>(`/api/seeding-rules/${clientId}/reorder`, { orderedIds });
|
||||
}
|
||||
|
||||
// Unlinked config
|
||||
getUnlinkedConfig(clientId: string): Observable<UnlinkedConfigModel | null> {
|
||||
return this.http.get<UnlinkedConfigModel | null>(`/api/unlinked-config/${clientId}`);
|
||||
|
||||
@@ -69,7 +69,11 @@ export class DocumentationService {
|
||||
'scheduleUnit': 'scheduling-mode',
|
||||
'scheduleEvery': 'scheduling-mode',
|
||||
'cronExpression': 'cron-expression',
|
||||
'name': 'category-name',
|
||||
'name': 'rule-name',
|
||||
'categories': 'categories',
|
||||
'trackerPatterns': 'tracker-patterns',
|
||||
'tagsAny': 'tags-any',
|
||||
'tagsAll': 'tags-all',
|
||||
'privacyType': 'privacy-type',
|
||||
'maxRatio': 'max-ratio',
|
||||
'minSeedTime': 'min-seed-time',
|
||||
|
||||
@@ -93,36 +93,63 @@
|
||||
description="Add a seeding rule to start cleaning downloads for this client"
|
||||
/>
|
||||
} @else {
|
||||
@for (rule of client.seedingRules; track rule.id ?? $index) {
|
||||
<div class="rule-card">
|
||||
<div class="rule-card__header">
|
||||
<h4 class="rule-card__name">{{ rule.name }}</h4>
|
||||
<div class="rule-card__actions">
|
||||
<button class="rule-card__action" (click)="openRuleModal(rule)" aria-label="Edit rule">
|
||||
<ng-icon name="tablerPencil" size="16" />
|
||||
</button>
|
||||
<button class="rule-card__action rule-card__action--danger" (click)="deleteRule(rule)" aria-label="Delete rule">
|
||||
<ng-icon name="tablerTrash" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rule-card__badges">
|
||||
<app-badge severity="info">{{ rule.privacyType }}</app-badge>
|
||||
@if (rule.maxRatio >= 0) {
|
||||
<app-badge>Ratio: {{ rule.maxRatio }}</app-badge>
|
||||
}
|
||||
@if (rule.maxSeedTime >= 0) {
|
||||
<app-badge>Max Seed: {{ rule.maxSeedTime }}h</app-badge>
|
||||
}
|
||||
@if (rule.minSeedTime > 0) {
|
||||
<app-badge>Min Seed: {{ rule.minSeedTime }}h</app-badge>
|
||||
}
|
||||
</div>
|
||||
<div class="rule-card__details">
|
||||
<span>Delete Files: {{ rule.deleteSourceFiles ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
@if (client.seedingRules.length > 1) {
|
||||
<div class="rules-flow-hint">
|
||||
<ng-icon name="tablerArrowDown" size="14" />
|
||||
Rules are evaluated top to bottom · drag to reorder
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
cdkDropList
|
||||
[cdkDropListData]="client.seedingRules"
|
||||
(cdkDropListDropped)="onRulesReorder($event)">
|
||||
@for (rule of client.seedingRules; track rule.id ?? $index) {
|
||||
<div class="rule-card" cdkDrag>
|
||||
<div class="rule-card__header">
|
||||
<button class="rule-card__drag-handle" cdkDragHandle title="Drag to reorder" type="button" aria-label="Drag to reorder">
|
||||
<ng-icon name="tablerGripVertical" size="16" />
|
||||
</button>
|
||||
<app-tooltip text="Evaluation priority. Rules are matched top to bottom, first match wins.">
|
||||
<span class="priority-badge">#{{ $index + 1 }}</span>
|
||||
</app-tooltip>
|
||||
<h4 class="rule-card__name">{{ rule.name }}</h4>
|
||||
<div class="rule-card__actions">
|
||||
<button class="rule-card__action" (click)="openRuleModal(rule)" aria-label="Edit rule">
|
||||
<ng-icon name="tablerPencil" size="16" />
|
||||
</button>
|
||||
<button class="rule-card__action rule-card__action--danger" (click)="deleteRule(rule)" aria-label="Delete rule">
|
||||
<ng-icon name="tablerTrash" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (rule.categories.length) {
|
||||
<div class="rule-card__categories">
|
||||
@for (cat of rule.categories; track cat) {
|
||||
<app-badge severity="default">{{ cat }}</app-badge>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="rule-card__badges">
|
||||
<app-badge severity="info">{{ rule.privacyType }}</app-badge>
|
||||
@if (rule.maxRatio >= 0) {
|
||||
<app-badge>Ratio: {{ rule.maxRatio }}</app-badge>
|
||||
}
|
||||
@if (rule.maxSeedTime >= 0) {
|
||||
<app-badge>Max Seed: {{ rule.maxSeedTime }}h</app-badge>
|
||||
}
|
||||
@if (rule.minSeedTime > 0) {
|
||||
<app-badge>Min Seed: {{ rule.minSeedTime }}h</app-badge>
|
||||
}
|
||||
@if (rule.trackerPatterns.length) {
|
||||
<app-badge severity="warning">{{ rule.trackerPatterns.length }} tracker pattern{{ rule.trackerPatterns.length !== 1 ? 's' : '' }}</app-badge>
|
||||
}
|
||||
</div>
|
||||
<div class="rule-card__details">
|
||||
<span>Delete Files: {{ rule.deleteSourceFiles ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="rule-actions">
|
||||
<app-button variant="secondary" size="sm" (clicked)="openRuleModal()">
|
||||
@@ -200,13 +227,32 @@
|
||||
<!-- Seeding Rule Modal -->
|
||||
<app-modal [title]="editingRule() ? 'Edit Seeding Rule' : 'Add Seeding Rule'" [(visible)]="ruleModalVisible" size="lg">
|
||||
<div class="form-grid">
|
||||
<app-input label="Category Name" placeholder="tv-sonarr" [(value)]="ruleName"
|
||||
<app-input label="Rule Name" placeholder="My TV rule" [(value)]="ruleName"
|
||||
[error]="ruleNameError()"
|
||||
hint="The category name from your download client (e.g. tv-sonarr, radarr)"
|
||||
hint="A descriptive label for this rule"
|
||||
helpKey="download-cleaner:name" />
|
||||
<app-select label="Privacy Type" [options]="privacyTypeOptions" [(value)]="rulePrivacyType"
|
||||
hint="Which torrent types this rule applies to"
|
||||
helpKey="download-cleaner:privacyType" />
|
||||
<app-chip-input class="full-width" label="Categories" placeholder="Add category..."
|
||||
[(items)]="ruleCategories"
|
||||
[error]="ruleCategoriesError()"
|
||||
hint="One or more download client categories this rule applies to (e.g. tv-sonarr, radarr)"
|
||||
helpKey="download-cleaner:categories" #ruleChipInput />
|
||||
<app-chip-input class="full-width" label="Tracker Patterns" placeholder="Add tracker domain..."
|
||||
[(items)]="ruleTrackerPatterns"
|
||||
hint="Tracker domain suffixes to match (e.g. tracker.org). Empty means any tracker."
|
||||
helpKey="download-cleaner:trackerPatterns" #ruleChipInput />
|
||||
@if (isTagFilterableClient()) {
|
||||
<app-chip-input class="full-width" [label]="isSelectedClientTransmission() ? 'Labels (Any)' : 'Tags (Any)'" [placeholder]="isSelectedClientTransmission() ? 'Add label...' : 'Add tag...'"
|
||||
[(items)]="ruleTagsAny"
|
||||
[hint]="isSelectedClientTransmission() ? 'Torrent must have at least one of these labels. Empty means any labels.' : 'Torrent must have at least one of these tags. Empty means any tags.'"
|
||||
helpKey="download-cleaner:tagsAny" #ruleChipInput />
|
||||
<app-chip-input class="full-width" [label]="isSelectedClientTransmission() ? 'Labels (All)' : 'Tags (All)'" [placeholder]="isSelectedClientTransmission() ? 'Add label...' : 'Add tag...'"
|
||||
[(items)]="ruleTagsAll"
|
||||
[hint]="isSelectedClientTransmission() ? 'Torrent must have all of these labels. Empty means any labels.' : 'Torrent must have all of these tags. Empty means any tags.'"
|
||||
helpKey="download-cleaner:tagsAll" #ruleChipInput />
|
||||
}
|
||||
<app-number-input label="Max Ratio" [(value)]="ruleMaxRatio" [step]="0.1" [min]="-1"
|
||||
hint="Maximum ratio to seed before removing (-1 means disabled)"
|
||||
helpKey="download-cleaner:maxRatio" />
|
||||
@@ -225,7 +271,7 @@
|
||||
</div>
|
||||
<div modal-footer>
|
||||
<app-button variant="secondary" (clicked)="ruleModalVisible.set(false)">Cancel</app-button>
|
||||
<app-button variant="primary" [disabled]="!!ruleNameError() || !!ruleDisabledError()" (clicked)="saveRule()">
|
||||
<app-button variant="primary" [disabled]="!!ruleNameError() || !!ruleCategoriesError() || !!ruleDisabledError() || ruleHasUncommittedInputs()" (clicked)="saveRule()">
|
||||
{{ editingRule() ? 'Update' : 'Create' }}
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
@@ -42,11 +42,37 @@
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
&__drag-handle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--text-secondary);
|
||||
cursor: grab;
|
||||
opacity: 0.5;
|
||||
flex-shrink: 0;
|
||||
transition: opacity var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
@@ -83,6 +109,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__categories {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
&__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -99,10 +132,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
// CDK drag-and-drop styles for rule cards
|
||||
.cdk-drag-preview {
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
opacity: 0.95;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 250ms var(--ease-default);
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.rules-flow-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--color-error);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px 6px;
|
||||
flex-shrink: 0;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rules-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed, OnInit, viewChildren, effect, untracked } from '@angular/core';
|
||||
import { NgIconComponent } from '@ng-icons/core';
|
||||
import { CdkDragDrop, CdkDropList, CdkDrag, CdkDragHandle, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { PageHeaderComponent } from '@layout/page-header/page-header.component';
|
||||
import {
|
||||
CardComponent, ButtonComponent, InputComponent, ToggleComponent,
|
||||
NumberInputComponent, SelectComponent, ChipInputComponent, AccordionComponent,
|
||||
EmptyStateComponent, LoadingStateComponent, ModalComponent, BadgeComponent, SpinnerComponent,
|
||||
TooltipComponent,
|
||||
type SelectOption,
|
||||
} from '@ui';
|
||||
import { DownloadCleanerApi } from '@core/api/download-cleaner.api';
|
||||
@@ -38,9 +40,11 @@ const PRIVACY_TYPE_OPTIONS: SelectOption[] = [
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgIconComponent,
|
||||
CdkDropList, CdkDrag, CdkDragHandle,
|
||||
PageHeaderComponent, CardComponent, ButtonComponent, InputComponent,
|
||||
ToggleComponent, NumberInputComponent, SelectComponent, ChipInputComponent, AccordionComponent,
|
||||
EmptyStateComponent, LoadingStateComponent, ModalComponent, BadgeComponent, SpinnerComponent,
|
||||
TooltipComponent,
|
||||
],
|
||||
templateUrl: './download-cleaner.component.html',
|
||||
styleUrl: './download-cleaner.component.scss',
|
||||
@@ -51,6 +55,11 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
private readonly toast = inject(ToastService);
|
||||
private readonly confirm = inject(ConfirmService);
|
||||
private readonly chipInputs = viewChildren(ChipInputComponent);
|
||||
private readonly ruleChipInputs = viewChildren<ChipInputComponent>('ruleChipInput');
|
||||
|
||||
readonly ruleHasUncommittedInputs = computed(() =>
|
||||
this.ruleChipInputs().some(c => c.hasUncommittedInput())
|
||||
);
|
||||
|
||||
private readonly savedSnapshot = signal('');
|
||||
|
||||
@@ -95,6 +104,15 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.selectedClient()?.downloadClientTypeName === DownloadClientTypeName.qBittorrent
|
||||
);
|
||||
|
||||
readonly isSelectedClientTransmission = computed(() =>
|
||||
this.selectedClient()?.downloadClientTypeName === DownloadClientTypeName.Transmission
|
||||
);
|
||||
|
||||
readonly isTagFilterableClient = computed(() => {
|
||||
const typeName = this.selectedClient()?.downloadClientTypeName;
|
||||
return typeName === DownloadClientTypeName.qBittorrent || typeName === DownloadClientTypeName.Transmission;
|
||||
});
|
||||
|
||||
readonly seedingRulesExpanded = signal(false);
|
||||
readonly unlinkedExpanded = signal(false);
|
||||
|
||||
@@ -102,6 +120,10 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
readonly ruleModalVisible = signal(false);
|
||||
readonly editingRule = signal<SeedingRule | null>(null);
|
||||
readonly ruleName = signal('');
|
||||
readonly ruleCategories = signal<string[]>([]);
|
||||
readonly ruleTrackerPatterns = signal<string[]>([]);
|
||||
readonly ruleTagsAny = signal<string[]>([]);
|
||||
readonly ruleTagsAll = signal<string[]>([]);
|
||||
readonly rulePrivacyType = signal<unknown>(TorrentPrivacyType.Public);
|
||||
readonly ruleMaxRatio = signal<number | null>(-1);
|
||||
readonly ruleMinSeedTime = signal<number | null>(0);
|
||||
@@ -143,6 +165,11 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
readonly ruleCategoriesError = computed(() => {
|
||||
if (this.ruleCategories().length === 0) return 'At least one category is required';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
readonly ruleDisabledError = computed(() => {
|
||||
if ((this.ruleMaxRatio() ?? -1) < 0 && (this.ruleMaxSeedTime() ?? -1) < 0) {
|
||||
return 'Both max ratio and max seed time cannot be disabled at the same time';
|
||||
@@ -232,6 +259,10 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.editingRule.set(rule ?? null);
|
||||
if (rule) {
|
||||
this.ruleName.set(rule.name);
|
||||
this.ruleCategories.set([...(rule.categories ?? [])]);
|
||||
this.ruleTrackerPatterns.set([...(rule.trackerPatterns ?? [])]);
|
||||
this.ruleTagsAny.set([...(rule.tagsAny ?? [])]);
|
||||
this.ruleTagsAll.set([...(rule.tagsAll ?? [])]);
|
||||
this.rulePrivacyType.set(rule.privacyType);
|
||||
this.ruleMaxRatio.set(rule.maxRatio);
|
||||
this.ruleMinSeedTime.set(rule.minSeedTime);
|
||||
@@ -239,6 +270,10 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.ruleDeleteSourceFiles.set(rule.deleteSourceFiles);
|
||||
} else {
|
||||
this.ruleName.set('');
|
||||
this.ruleCategories.set([]);
|
||||
this.ruleTrackerPatterns.set([]);
|
||||
this.ruleTagsAny.set([]);
|
||||
this.ruleTagsAll.set([]);
|
||||
this.rulePrivacyType.set(TorrentPrivacyType.Public);
|
||||
this.ruleMaxRatio.set(-1);
|
||||
this.ruleMinSeedTime.set(0);
|
||||
@@ -249,12 +284,18 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
}
|
||||
|
||||
saveRule(): void {
|
||||
if (this.ruleNameError() || this.ruleDisabledError()) return;
|
||||
if (this.ruleNameError() || this.ruleCategoriesError() || this.ruleDisabledError() || this.ruleHasUncommittedInputs()) return;
|
||||
const clientId = this.selectedClientId();
|
||||
if (!clientId) return;
|
||||
|
||||
const sanitize = (list: string[]) => list.map(s => s.trim()).filter(s => s.length > 0);
|
||||
|
||||
const dto: Partial<SeedingRule> = {
|
||||
name: this.ruleName().trim(),
|
||||
categories: sanitize(this.ruleCategories()),
|
||||
trackerPatterns: sanitize(this.ruleTrackerPatterns()),
|
||||
tagsAny: sanitize(this.ruleTagsAny()),
|
||||
tagsAll: sanitize(this.ruleTagsAll()),
|
||||
privacyType: this.rulePrivacyType() as TorrentPrivacyType,
|
||||
maxRatio: this.ruleMaxRatio() ?? -1,
|
||||
minSeedTime: this.ruleMinSeedTime() ?? 0,
|
||||
@@ -297,6 +338,26 @@ export class DownloadCleanerComponent implements OnInit, HasPendingChanges {
|
||||
});
|
||||
}
|
||||
|
||||
onRulesReorder(event: CdkDragDrop<SeedingRule[]>): void {
|
||||
const clientId = this.selectedClientId();
|
||||
if (!clientId) return;
|
||||
|
||||
const rules = [...(this.selectedClient()?.seedingRules ?? [])];
|
||||
moveItemInArray(rules, event.previousIndex, event.currentIndex);
|
||||
|
||||
this.clientConfigs.update(configs =>
|
||||
configs.map(c => c.downloadClientId === clientId ? { ...c, seedingRules: rules } : c)
|
||||
);
|
||||
|
||||
const orderedIds = rules.map(r => r.id!).filter(Boolean);
|
||||
this.api.reorderSeedingRules(clientId, orderedIds).subscribe({
|
||||
error: () => {
|
||||
this.toast.error('Failed to reorder seeding rules');
|
||||
this.reloadSeedingRules(clientId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private reloadSeedingRules(clientId: string): void {
|
||||
this.rulesReloading.set(true);
|
||||
this.api.getSeedingRules(clientId).subscribe({
|
||||
|
||||
@@ -3,6 +3,11 @@ import { TorrentPrivacyType } from './enums';
|
||||
export interface SeedingRule {
|
||||
id?: string;
|
||||
name: string;
|
||||
categories: string[];
|
||||
trackerPatterns: string[];
|
||||
tagsAny?: string[];
|
||||
tagsAll?: string[];
|
||||
priority: number;
|
||||
privacyType: TorrentPrivacyType;
|
||||
maxRatio: number;
|
||||
minSeedTime: number;
|
||||
@@ -40,6 +45,11 @@ export interface DownloadCleanerConfig {
|
||||
export function createDefaultSeedingRule(): SeedingRule {
|
||||
return {
|
||||
name: '',
|
||||
categories: [],
|
||||
trackerPatterns: [],
|
||||
tagsAny: [],
|
||||
tagsAll: [],
|
||||
priority: 0,
|
||||
privacyType: TorrentPrivacyType.Public,
|
||||
maxRatio: -1,
|
||||
minSeedTime: 0,
|
||||
|
||||
Reference in New Issue
Block a user