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,
|
||||
|
||||
@@ -86,7 +86,7 @@ mytracker.com
|
||||
<SectionTitle>Seeding Rules</SectionTitle>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Categories define the cleanup rules for different types of downloads. Each category specifies when downloads should be removed based on ratio and time limits.
|
||||
Seeding rules define the cleanup criteria for downloads in your download client. Each rule specifies which downloads it applies to (by category, tracker, tags, and privacy type) and when they should be removed based on ratio and time limits. Rules are evaluated in priority order — the first matching rule wins.
|
||||
</p>
|
||||
|
||||
<Note>
|
||||
@@ -98,10 +98,23 @@ Both Max Ratio and Max Seed Time cannot be disabled (-1) at the same time. At le
|
||||
</Important>
|
||||
|
||||
<ConfigSection
|
||||
title="Category Name"
|
||||
title="Rule Name"
|
||||
>
|
||||
|
||||
The name of the download client category to apply these rules to. Must match the category name exactly as configured in your download client.
|
||||
A human-readable label for this seeding rule. This is a display name only and does not affect rule matching.
|
||||
|
||||
**Examples:**
|
||||
- `TV shows`
|
||||
- `Movies - private trackers`
|
||||
- `Music long seed`
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Categories"
|
||||
>
|
||||
|
||||
One or more download client categories this rule applies to. A torrent matches if its category matches **any** entry in the list (case-insensitive). At least one category must be specified.
|
||||
|
||||
**Examples:**
|
||||
- `tv-sonarr`
|
||||
@@ -110,6 +123,38 @@ The name of the download client category to apply these rules to. Must match the
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Tracker Patterns"
|
||||
>
|
||||
|
||||
Tracker domain suffixes used to filter which torrents this rule applies to. A torrent matches if **any** of its tracker domains end with one of these patterns (case-insensitive). Leave empty to match torrents from any tracker.
|
||||
|
||||
**Examples:**
|
||||
- `tracker.example.com` — matches `https://tracker.example.com/announce`
|
||||
- `private.org` — matches any tracker whose domain ends with `private.org`
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Tags (Any)"
|
||||
>
|
||||
|
||||
Torrent must have **at least one** of these tags or labels. Leave empty to skip tag filtering.
|
||||
|
||||
Supported by **qBittorrent** tags and **Transmission** labels.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Tags (All)"
|
||||
>
|
||||
|
||||
Torrent must have **all** of these tags or labels. Leave empty to skip tag filtering.
|
||||
|
||||
Supported by **qBittorrent** tags and **Transmission** labels.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Privacy Type"
|
||||
>
|
||||
|
||||
272
e2e/tests/12-seeding-rules-api.spec.ts
Normal file
272
e2e/tests/12-seeding-rules-api.spec.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
loginAndGetToken,
|
||||
createDownloadClient,
|
||||
deleteDownloadClient,
|
||||
getSeedingRules,
|
||||
createSeedingRule,
|
||||
updateSeedingRule,
|
||||
deleteSeedingRule,
|
||||
reorderSeedingRules,
|
||||
} from './helpers/app-api';
|
||||
|
||||
test.describe.serial('Seeding Rules API', () => {
|
||||
let token: string;
|
||||
let downloadClientId: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
token = await loginAndGetToken();
|
||||
|
||||
// Create a qBittorrent download client for testing seeding rules
|
||||
const res = await createDownloadClient(token, {
|
||||
enabled: false,
|
||||
name: 'e2e-test-qbit',
|
||||
typeName: 'qBittorrent',
|
||||
type: 'Torrent',
|
||||
host: 'http://localhost:9999',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
const client = await res.json();
|
||||
downloadClientId = client.id;
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (downloadClientId) {
|
||||
await deleteDownloadClient(token, downloadClientId);
|
||||
}
|
||||
});
|
||||
|
||||
test('should return empty seeding rules for new client', async () => {
|
||||
const res = await getSeedingRules(token, downloadClientId);
|
||||
expect(res.status).toBe(200);
|
||||
const rules = await res.json();
|
||||
expect(Array.isArray(rules)).toBe(true);
|
||||
expect(rules).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should create a seeding rule with new fields', async () => {
|
||||
const res = await createSeedingRule(token, downloadClientId, {
|
||||
name: 'Movies Rule',
|
||||
categories: ['movies', 'films'],
|
||||
trackerPatterns: ['tracker.example.com'],
|
||||
tagsAny: ['hd'],
|
||||
tagsAll: [],
|
||||
privacyType: 'Both',
|
||||
maxRatio: 2.0,
|
||||
minSeedTime: 0,
|
||||
maxSeedTime: -1,
|
||||
deleteSourceFiles: true,
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
|
||||
const rule = await res.json();
|
||||
expect(rule.name).toBe('Movies Rule');
|
||||
expect(rule.categories).toEqual(['movies', 'films']);
|
||||
expect(rule.trackerPatterns).toEqual(['tracker.example.com']);
|
||||
expect(rule.tagsAny).toEqual(['hd']);
|
||||
expect(rule.tagsAll).toEqual([]);
|
||||
expect(rule.priority).toBe(1);
|
||||
});
|
||||
|
||||
test('should auto-assign sequential priorities', async () => {
|
||||
const res2 = await createSeedingRule(token, downloadClientId, {
|
||||
name: 'TV Rule',
|
||||
categories: ['tv'],
|
||||
privacyType: 'Both',
|
||||
maxRatio: -1,
|
||||
minSeedTime: 0,
|
||||
maxSeedTime: 48,
|
||||
deleteSourceFiles: true,
|
||||
});
|
||||
expect(res2.status).toBe(201);
|
||||
const rule2 = await res2.json();
|
||||
expect(rule2.priority).toBe(2);
|
||||
|
||||
const res3 = await createSeedingRule(token, downloadClientId, {
|
||||
name: 'Music Rule',
|
||||
categories: ['music'],
|
||||
privacyType: 'Both',
|
||||
maxRatio: 3.0,
|
||||
minSeedTime: 0,
|
||||
maxSeedTime: -1,
|
||||
deleteSourceFiles: false,
|
||||
});
|
||||
expect(res3.status).toBe(201);
|
||||
const rule3 = await res3.json();
|
||||
expect(rule3.priority).toBe(3);
|
||||
});
|
||||
|
||||
test('should round-trip new fields through GET', async () => {
|
||||
const res = await getSeedingRules(token, downloadClientId);
|
||||
expect(res.status).toBe(200);
|
||||
const rules = await res.json();
|
||||
expect(rules).toHaveLength(3);
|
||||
|
||||
const moviesRule = rules.find((r: { name: string }) => r.name === 'Movies Rule');
|
||||
expect(moviesRule).toBeDefined();
|
||||
expect(moviesRule.categories).toEqual(['movies', 'films']);
|
||||
expect(moviesRule.trackerPatterns).toEqual(['tracker.example.com']);
|
||||
expect(moviesRule.priority).toBe(1);
|
||||
});
|
||||
|
||||
test('should reorder seeding rules', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
expect(rules).toHaveLength(3);
|
||||
|
||||
// Reverse the order
|
||||
const reversedIds = rules.map((r: { id: string }) => r.id).reverse();
|
||||
const reorderRes = await reorderSeedingRules(token, downloadClientId, reversedIds);
|
||||
expect(reorderRes.status).toBe(204);
|
||||
|
||||
// Verify new order
|
||||
const verifyRes = await getSeedingRules(token, downloadClientId);
|
||||
const reordered = await verifyRes.json();
|
||||
expect(reordered[0].priority).toBe(1);
|
||||
expect(reordered[0].id).toBe(reversedIds[0]);
|
||||
expect(reordered[1].priority).toBe(2);
|
||||
expect(reordered[1].id).toBe(reversedIds[1]);
|
||||
expect(reordered[2].priority).toBe(3);
|
||||
expect(reordered[2].id).toBe(reversedIds[2]);
|
||||
});
|
||||
|
||||
test('should reject reorder with missing rule IDs', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
|
||||
// Only send 2 of 3 IDs
|
||||
const partialIds = rules.slice(0, 2).map((r: { id: string }) => r.id);
|
||||
const res = await reorderSeedingRules(token, downloadClientId, partialIds);
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('should reject reorder with duplicate IDs', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
|
||||
const firstId = rules[0].id;
|
||||
const res = await reorderSeedingRules(token, downloadClientId, [firstId, firstId, rules[1].id]);
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('should reject reorder with invalid rule ID', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
|
||||
const ids = rules.map((r: { id: string }) => r.id);
|
||||
ids[0] = '00000000-0000-0000-0000-000000000000';
|
||||
const res = await reorderSeedingRules(token, downloadClientId, ids);
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('should not change priority on update', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
const rule = rules[0];
|
||||
|
||||
const updateRes = await updateSeedingRule(token, rule.id, {
|
||||
name: 'Updated Name',
|
||||
categories: rule.categories,
|
||||
trackerPatterns: rule.trackerPatterns,
|
||||
privacyType: rule.privacyType,
|
||||
maxRatio: rule.maxRatio,
|
||||
minSeedTime: rule.minSeedTime,
|
||||
maxSeedTime: rule.maxSeedTime,
|
||||
deleteSourceFiles: rule.deleteSourceFiles,
|
||||
});
|
||||
expect(updateRes.status).toBe(200);
|
||||
const updated = await updateRes.json();
|
||||
expect(updated.priority).toBe(rule.priority);
|
||||
});
|
||||
|
||||
test('should update tags and persist them', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
const rule = rules[0];
|
||||
|
||||
const updateRes = await updateSeedingRule(token, rule.id, {
|
||||
name: rule.name,
|
||||
categories: rule.categories,
|
||||
trackerPatterns: rule.trackerPatterns,
|
||||
tagsAny: ['updated-tag-1', 'updated-tag-2'],
|
||||
tagsAll: ['required-tag'],
|
||||
privacyType: rule.privacyType,
|
||||
maxRatio: rule.maxRatio,
|
||||
minSeedTime: rule.minSeedTime,
|
||||
maxSeedTime: rule.maxSeedTime,
|
||||
deleteSourceFiles: rule.deleteSourceFiles,
|
||||
});
|
||||
expect(updateRes.status).toBe(200);
|
||||
|
||||
// Verify tags persisted via GET
|
||||
const verifyRes = await getSeedingRules(token, downloadClientId);
|
||||
const updated = await verifyRes.json();
|
||||
const updatedRule = updated.find((r: { id: string }) => r.id === rule.id);
|
||||
expect(updatedRule.tagsAny).toEqual(['updated-tag-1', 'updated-tag-2']);
|
||||
expect(updatedRule.tagsAll).toEqual(['required-tag']);
|
||||
});
|
||||
|
||||
test('should reject empty categories', async () => {
|
||||
const res = await createSeedingRule(token, downloadClientId, {
|
||||
name: 'Bad Rule',
|
||||
categories: [],
|
||||
privacyType: 'Both',
|
||||
maxRatio: -1,
|
||||
minSeedTime: 0,
|
||||
maxSeedTime: -1,
|
||||
deleteSourceFiles: true,
|
||||
});
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('should reject negative priority', async () => {
|
||||
const res = await createSeedingRule(token, downloadClientId, {
|
||||
name: 'Bad Priority Rule',
|
||||
categories: ['test'],
|
||||
priority: -1,
|
||||
privacyType: 'Both',
|
||||
maxRatio: -1,
|
||||
minSeedTime: 0,
|
||||
maxSeedTime: -1,
|
||||
deleteSourceFiles: true,
|
||||
});
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
test('should strip empty tracker patterns', async () => {
|
||||
const res = await createSeedingRule(token, downloadClientId, {
|
||||
name: 'Whitespace Test',
|
||||
categories: ['test'],
|
||||
trackerPatterns: ['', ' ', 'valid.com'],
|
||||
privacyType: 'Both',
|
||||
maxRatio: 2.0,
|
||||
minSeedTime: 0,
|
||||
maxSeedTime: -1,
|
||||
deleteSourceFiles: true,
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
const rule = await res.json();
|
||||
expect(rule.trackerPatterns).toEqual(['valid.com']);
|
||||
|
||||
// Clean up
|
||||
await deleteSeedingRule(token, rule.id);
|
||||
});
|
||||
|
||||
test('should delete a seeding rule', async () => {
|
||||
const getRes = await getSeedingRules(token, downloadClientId);
|
||||
const rules = await getRes.json();
|
||||
const lastRule = rules[rules.length - 1];
|
||||
|
||||
const delRes = await deleteSeedingRule(token, lastRule.id);
|
||||
expect(delRes.status).toBe(204);
|
||||
|
||||
const verifyRes = await getSeedingRules(token, downloadClientId);
|
||||
const remaining = await verifyRes.json();
|
||||
expect(remaining).toHaveLength(rules.length - 1);
|
||||
});
|
||||
|
||||
test('should return 404 for non-existent download client', async () => {
|
||||
const res = await getSeedingRules(token, '00000000-0000-0000-0000-000000000000');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -238,6 +238,44 @@ export async function updateUnlinkedConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function reorderSeedingRules(
|
||||
accessToken: string,
|
||||
downloadClientId: string,
|
||||
orderedIds: string[],
|
||||
): Promise<Response> {
|
||||
return fetch(`${API}/api/seeding-rules/${downloadClientId}/reorder`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ orderedIds }),
|
||||
});
|
||||
}
|
||||
|
||||
// --- Download Client helpers ---
|
||||
|
||||
export async function createDownloadClient(
|
||||
accessToken: string,
|
||||
client: Record<string, unknown>,
|
||||
): Promise<Response> {
|
||||
return fetch(`${API}/api/configuration/download_client`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(client),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteDownloadClient(accessToken: string, clientId: string): Promise<Response> {
|
||||
return fetch(`${API}/api/configuration/download_client/${clientId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
}
|
||||
|
||||
export async function configureOidc(accessToken: string): Promise<void> {
|
||||
const putRes = await fetch(`${API}/api/account/oidc`, {
|
||||
method: 'PUT',
|
||||
|
||||
Reference in New Issue
Block a user