This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using SeerrAutoApprove.Options;
|
||||
using SeerrAutoApprove.Processing;
|
||||
using SeerrAutoApprove.Quota;
|
||||
using SeerrAutoApprove.Storage;
|
||||
|
||||
namespace SeerrAutoApprove.Tests;
|
||||
|
||||
public sealed class RequestProcessorTests : IDisposable
|
||||
{
|
||||
private readonly TempDatabase _db = new();
|
||||
private readonly SqliteApprovalStore _store;
|
||||
private readonly FakeSeerrClient _seerr = new();
|
||||
private readonly FakeTimeProvider _time = new(new DateTimeOffset(2026, 10, 15, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
public RequestProcessorTests()
|
||||
{
|
||||
_store = _db.CreateStore();
|
||||
}
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
|
||||
private RequestProcessor CreateProcessor(AutoApproveOptions options) =>
|
||||
new(_seerr, _store, new StaticOptionsMonitor<AutoApproveOptions>(options), _time, NullLogger<RequestProcessor>.Instance);
|
||||
|
||||
[Fact]
|
||||
public async Task Approves_movies_up_to_limit_and_leaves_rest_pending()
|
||||
{
|
||||
var requests = Enumerable.Range(0, 4).Select(i => Requests.Movie(userId: 1, createdAt: _time.GetUtcNow().AddMinutes(i))).ToList();
|
||||
_seerr.Pending.AddRange(requests);
|
||||
|
||||
var result = await CreateProcessor(new AutoApproveOptions { MovieLimit = 2 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([requests[0].Id, requests[1].Id], _seerr.Approved);
|
||||
Assert.Equal(2, result.Approved);
|
||||
Assert.Equal(2, result.Skipped);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approves_oldest_requests_first()
|
||||
{
|
||||
var newer = Requests.Movie(1, createdAt: _time.GetUtcNow());
|
||||
var older = Requests.Movie(1, createdAt: _time.GetUtcNow().AddHours(-1));
|
||||
_seerr.Pending.AddRange([newer, older]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([older.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Quotas_are_tracked_per_user()
|
||||
{
|
||||
var a1 = Requests.Movie(1);
|
||||
var a2 = Requests.Movie(1);
|
||||
var b1 = Requests.Movie(2);
|
||||
_seerr.Pending.AddRange([a1, a2, b1]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([a1.Id, b1.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Movie_and_series_quotas_are_independent()
|
||||
{
|
||||
var movie = Requests.Movie(1);
|
||||
var series = Requests.Series(1, seasons: 1);
|
||||
_seerr.Pending.AddRange([movie, series]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1, SeriesLimit = 1 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([movie.Id, series.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Series_count_per_season_by_default()
|
||||
{
|
||||
var threeSeasons = Requests.Series(1, seasons: 3);
|
||||
var twoSeasons = Requests.Series(1, seasons: 2);
|
||||
_seerr.Pending.AddRange([threeSeasons, twoSeasons]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { SeriesLimit = 2 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([twoSeasons.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Series_count_per_request_when_configured()
|
||||
{
|
||||
var threeSeasons = Requests.Series(1, seasons: 3);
|
||||
var twoSeasons = Requests.Series(1, seasons: 2);
|
||||
_seerr.Pending.AddRange([threeSeasons, twoSeasons]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { SeriesLimit = 2, SeriesCountMode = SeriesCountMode.PerRequest })
|
||||
.ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([threeSeasons.Id, twoSeasons.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FourK_requests_use_separate_limits()
|
||||
{
|
||||
var movie = Requests.Movie(1);
|
||||
var movie4k = Requests.Movie(1, is4k: true);
|
||||
var series4k = Requests.Series(1, seasons: 1, is4k: true);
|
||||
_seerr.Pending.AddRange([movie, movie4k, series4k]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1, Movie4kLimit = 1, Series4kLimit = 0 })
|
||||
.ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([movie.Id, movie4k.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Limit_zero_never_approves()
|
||||
{
|
||||
_seerr.Pending.Add(Requests.Movie(1));
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 0 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Empty(_seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Limit_minus_one_is_unlimited()
|
||||
{
|
||||
_seerr.Pending.AddRange(Enumerable.Range(0, 25).Select(_ => Requests.Movie(1)));
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = -1 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(25, _seerr.Approved.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Previous_approvals_in_period_count_towards_quota()
|
||||
{
|
||||
await _store.RecordApprovalAsync(new ApprovalRecord(1, 1, QuotaCategory.Movie, 1, _time.GetUtcNow().AddDays(-5)), CancellationToken.None);
|
||||
_seerr.Pending.AddRange([Requests.Movie(1), Requests.Movie(1)]);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 2, RollingDays = 30 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Single(_seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approvals_outside_rolling_window_do_not_count()
|
||||
{
|
||||
await _store.RecordApprovalAsync(new ApprovalRecord(1, 1, QuotaCategory.Movie, 1, _time.GetUtcNow().AddDays(-31)), CancellationToken.None);
|
||||
_seerr.Pending.Add(Requests.Movie(1));
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1, RollingDays = 30 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Single(_seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Approvals_in_previous_calendar_period_do_not_count()
|
||||
{
|
||||
// now = 2026-10-15; previous approval in September
|
||||
await _store.RecordApprovalAsync(new ApprovalRecord(1, 1, QuotaCategory.Movie, 1, new DateTimeOffset(2026, 9, 30, 23, 0, 0, TimeSpan.Zero)), CancellationToken.None);
|
||||
_seerr.Pending.Add(Requests.Movie(1));
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1, PeriodType = PeriodType.Calendar, CalendarPeriod = CalendarPeriod.Month })
|
||||
.ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Single(_seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Dry_run_does_not_approve_or_record()
|
||||
{
|
||||
_seerr.Pending.Add(Requests.Movie(1));
|
||||
|
||||
var result = await CreateProcessor(new AutoApproveOptions { MovieLimit = 1, DryRun = true }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, result.Approved);
|
||||
Assert.Empty(_seerr.Approved);
|
||||
Assert.Equal(0, await _store.GetUsedUnitsAsync(1, QuotaCategory.Movie, DateTimeOffset.MinValue, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Failed_approval_is_not_recorded()
|
||||
{
|
||||
var failing = Requests.Movie(1);
|
||||
var ok = Requests.Movie(1);
|
||||
_seerr.Pending.AddRange([failing, ok]);
|
||||
_seerr.FailingApprovals.Add(failing.Id);
|
||||
|
||||
await CreateProcessor(new AutoApproveOptions { MovieLimit = 1 }).ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([ok.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Skipped_requests_are_approved_later_when_reevaluation_enabled()
|
||||
{
|
||||
var first = Requests.Movie(1);
|
||||
var second = Requests.Movie(1);
|
||||
_seerr.Pending.AddRange([first, second]);
|
||||
var processor = CreateProcessor(new AutoApproveOptions { MovieLimit = 1, RollingDays = 7, ReevaluateSkippedRequests = true });
|
||||
|
||||
await processor.ProcessAsync(CancellationToken.None);
|
||||
Assert.Equal([first.Id], _seerr.Approved);
|
||||
|
||||
_time.Advance(TimeSpan.FromDays(8));
|
||||
await processor.ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([first.Id, second.Id], _seerr.Approved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Skipped_requests_stay_pending_when_reevaluation_disabled()
|
||||
{
|
||||
var first = Requests.Movie(1);
|
||||
var second = Requests.Movie(1);
|
||||
_seerr.Pending.AddRange([first, second]);
|
||||
var processor = CreateProcessor(new AutoApproveOptions { MovieLimit = 1, RollingDays = 7, ReevaluateSkippedRequests = false });
|
||||
|
||||
await processor.ProcessAsync(CancellationToken.None);
|
||||
_time.Advance(TimeSpan.FromDays(8));
|
||||
var third = Requests.Movie(1);
|
||||
_seerr.Pending.Add(third);
|
||||
await processor.ProcessAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal([first.Id, third.Id], _seerr.Approved);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user