added response contract tests for queue cleaner

This commit is contained in:
Flaminel committed 2026-09-13 02:47:21 +03:00
1 parent 488ccf136c
commit 7a511dfb45
2 files changed
+253

No files matched your search

@@ -0,0 +1,192 @@
using Cleanuparr.Api.Features.QueueCleaner.Contracts.Requests;
using Cleanuparr.Api.Features.QueueCleaner.Controllers;
using Cleanuparr.Api.Tests.TestHelpers;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Models;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Shouldly;
using Xunit;
namespace Cleanuparr.Api.Tests.Features.QueueCleaner;
/// <summary>
/// Records today's wire shape, leaks included.
/// </summary>
public class QueueCleanerResponseContractTests : IDisposable
{
private readonly DataContext _dataContext;
private readonly QueueCleanerConfigController _configController;
private readonly QueueRulesController _rulesController;
public QueueCleanerResponseContractTests()
{
_dataContext = ConfigControllerTestDataFactory.CreateDataContext();
_configController = new QueueCleanerConfigController(
Substitute.For<ILogger<QueueCleanerConfigController>>(),
_dataContext,
Substitute.For<IJobManagementService>());
ConfigControllerTestDataFactory.ConfigureProblemDetails(_configController);
IRuleIntervalValidator validator = Substitute.For<IRuleIntervalValidator>();
validator.ValidateStallRuleIntervals(Arg.Any<StallRule>(), Arg.Any<List<StallRule>>())
.Returns(ValidationResult.Success());
validator.ValidateSlowRuleIntervals(Arg.Any<SlowRule>(), Arg.Any<List<SlowRule>>())
.Returns(ValidationResult.Success());
_rulesController = new QueueRulesController(
Substitute.For<ILogger<QueueRulesController>>(),
_dataContext,
validator);
ConfigControllerTestDataFactory.ConfigureProblemDetails(_rulesController);
}
public void Dispose()
{
_dataContext.Dispose();
GC.SuppressFinalize(this);
}
[Fact]
public async Task GetQueueCleanerConfig_ReturnsTheDocumentedKeys()
{
IActionResult result = await _configController.GetQueueCleanerConfig();
ResponseContract.Keys(result).ShouldBe(
[
"cronExpression",
"downloadingMetadataMaxStrikes",
"enabled",
"failedImport",
"id",
"ignoredDownloads",
"processNoContentId",
"slowRules",
"stallRules",
"useAdvancedScheduling",
]);
}
[Fact]
public async Task GetStallRules_ReturnsTheDocumentedKeys()
{
await _rulesController.CreateStallRule(NewStallDto("contract"));
IActionResult result = await _rulesController.GetStallRules();
ResponseContract.FirstItemKeys(result).ShouldBe(StallRuleKeys);
}
[Fact]
public async Task CreateStallRule_ReturnsTheDocumentedKeys()
{
IActionResult result = await _rulesController.CreateStallRule(NewStallDto("contract"));
ResponseContract.Keys(result).ShouldBe(StallRuleKeys);
}
[Fact]
public async Task UpdateStallRule_ReturnsTheDocumentedKeys()
{
IActionResult created = await _rulesController.CreateStallRule(NewStallDto("contract"));
Guid id = created.ShouldBeOfType<CreatedAtActionResult>().Value.ShouldBeOfType<StallRule>().Id;
IActionResult result = await _rulesController.UpdateStallRule(id, NewStallDto("renamed"));
ResponseContract.Keys(result).ShouldBe(StallRuleKeys);
}
[Fact]
public async Task GetSlowRules_ReturnsTheDocumentedKeys()
{
await _rulesController.CreateSlowRule(NewSlowDto("contract"));
IActionResult result = await _rulesController.GetSlowRules();
ResponseContract.FirstItemKeys(result).ShouldBe(SlowRuleKeys);
}
[Fact]
public async Task CreateSlowRule_ReturnsTheDocumentedKeys()
{
IActionResult result = await _rulesController.CreateSlowRule(NewSlowDto("contract"));
ResponseContract.Keys(result).ShouldBe(SlowRuleKeys);
}
[Fact]
public async Task UpdateSlowRule_ReturnsTheDocumentedKeys()
{
IActionResult created = await _rulesController.CreateSlowRule(NewSlowDto("contract"));
Guid id = created.ShouldBeOfType<CreatedAtActionResult>().Value.ShouldBeOfType<SlowRule>().Id;
IActionResult result = await _rulesController.UpdateSlowRule(id, NewSlowDto("renamed"));
ResponseContract.Keys(result).ShouldBe(SlowRuleKeys);
}
private static readonly string[] StallRuleKeys =
[
"changeCategory",
"deletePrivateTorrentsFromClient",
"enabled",
"id",
"maxCompletionPercentage",
"maxStrikes",
"minCompletionPercentage",
"minimumProgress",
"name",
"privacyType",
"queueCleanerConfig",
"queueCleanerConfigId",
"resetStrikesOnProgress",
];
private static readonly string[] SlowRuleKeys =
[
"changeCategory",
"deletePrivateTorrentsFromClient",
"enabled",
"id",
"ignoreAboveSize",
"ignoreWhileAltSpeedActive",
"maxCompletionPercentage",
"maxStrikes",
"maxTimeHours",
"minCompletionPercentage",
"minSpeed",
"name",
"privacyType",
"queueCleanerConfig",
"queueCleanerConfigId",
"resetStrikesOnProgress",
];
private static StallRuleDto NewStallDto(string name) => new()
{
Name = name,
Enabled = true,
MaxStrikes = 3,
PrivacyType = TorrentPrivacyType.Public,
MinCompletionPercentage = 0,
MaxCompletionPercentage = 100,
ResetStrikesOnProgress = true,
};
private static SlowRuleDto NewSlowDto(string name) => new()
{
Name = name,
Enabled = true,
MaxStrikes = 3,
PrivacyType = TorrentPrivacyType.Public,
MinCompletionPercentage = 0,
MaxCompletionPercentage = 100,
ResetStrikesOnProgress = true,
MinSpeed = "1MB",
};
}
@@ -0,0 +1,61 @@
using System.Text.Json;
using Cleanuparr.Api.Json;
using Microsoft.AspNetCore.Mvc;
namespace Cleanuparr.Api.Tests.TestHelpers;
/// <summary>
/// Serializes a controller result through the API's own JSON options.
/// </summary>
public static class ResponseContract
{
private static readonly JsonSerializerOptions Options = BuildOptions();
private static JsonSerializerOptions BuildOptions()
{
JsonSerializerOptions options = new(JsonSerializerDefaults.Web);
CleanuparrJsonConfiguration.ConfigureApiInbound(options);
return options;
}
/// <summary>
/// Serializes the value carried by an <see cref="ObjectResult"/> and parses it back.
/// </summary>
public static JsonElement Body(IActionResult result)
{
if (result is not ObjectResult objectResult)
{
throw new InvalidOperationException($"Expected an ObjectResult, got {result.GetType().Name}.");
}
if (objectResult.Value is null)
{
throw new InvalidOperationException("The result carries no value.");
}
string json = JsonSerializer.Serialize(objectResult.Value, objectResult.Value.GetType(), Options);
return JsonDocument.Parse(json).RootElement.Clone();
}
/// <summary>
/// Top-level property names, sorted so assertions read in a stable order.
/// </summary>
public static IReadOnlyList<string> Keys(IActionResult result) => Keys(Body(result));
/// <inheritdoc cref="Keys(IActionResult)" />
public static IReadOnlyList<string> Keys(JsonElement element) =>
element.EnumerateObject()
.Select(property => property.Name)
.OrderBy(name => name, StringComparer.Ordinal)
.ToList();
/// <summary>
/// Property names of the first array element, empty when the array is empty.
/// </summary>
public static IReadOnlyList<string> FirstItemKeys(IActionResult result)
{
JsonElement body = Body(result);
JsonElement? first = body.EnumerateArray().Cast<JsonElement?>().FirstOrDefault();
return first is null ? [] : Keys(first.Value);
}
}