Files
Cleanuparr/code/backend/Cleanuparr.Api.Tests/Features/Seeker/SearchStatsControllerTests.cs

610 lines
24 KiB
C#

using System.Text.Json;
using Cleanuparr.Api.Features.Seeker.Contracts.Responses;
using Cleanuparr.Api.Features.Seeker.Controllers;
using Cleanuparr.Api.Tests.Features.Seeker.TestHelpers;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.Seeker;
using Cleanuparr.Persistence.Models.Events;
using Cleanuparr.Persistence.Models.State;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Time.Testing;
using Shouldly;
namespace Cleanuparr.Api.Tests.Features.Seeker;
public class SearchStatsControllerTests : IDisposable
{
private static readonly DateTimeOffset Now = new(2026, 1, 1, 12, 0, 0, TimeSpan.Zero);
private readonly DataContext _dataContext;
private readonly EventsContext _eventsContext;
private readonly SearchStatsController _controller;
public SearchStatsControllerTests()
{
_dataContext = SeekerTestDataFactory.CreateDataContext();
_eventsContext = SeekerTestDataFactory.CreateEventsContext();
_controller = new SearchStatsController(_dataContext, _eventsContext, new FakeTimeProvider(Now));
}
public void Dispose()
{
_dataContext.Dispose();
_eventsContext.Dispose();
GC.SuppressFinalize(this);
}
private static SearchStatsSummaryResponse Summary(IActionResult result) =>
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<SearchStatsSummaryResponse>();
private static JsonElement GetResponseBody(IActionResult result)
{
var okResult = result.ShouldBeOfType<OkObjectResult>();
var json = JsonSerializer.Serialize(okResult.Value);
return JsonDocument.Parse(json).RootElement;
}
#region GetSummary
[Fact]
public async Task GetSummary_CountsSearchesInsideTheSevenAndThirtyDayWindows()
{
AddSearchEvent(timestamp: Now.UtcDateTime.AddDays(-1));
AddSearchEvent(timestamp: Now.UtcDateTime.AddDays(-10));
AddSearchEvent(timestamp: Now.UtcDateTime.AddDays(-40));
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
summary.TotalSearchesAllTime.ShouldBe(3);
summary.SearchesLast7Days.ShouldBe(1);
summary.SearchesLast30Days.ShouldBe(2);
}
[Fact]
public async Task GetSummary_ExcludesSearchesOnTheFarSideOfEachWindow()
{
AddSearchEvent(timestamp: Now.UtcDateTime.AddDays(-7).AddMinutes(-1));
AddSearchEvent(timestamp: Now.UtcDateTime.AddDays(-30).AddMinutes(-1));
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
summary.TotalSearchesAllTime.ShouldBe(2);
summary.SearchesLast7Days.ShouldBe(0);
summary.SearchesLast30Days.ShouldBe(1);
}
#endregion
#region GetSummary Funnel
[Fact]
public async Task GetSummary_Funnel_IsNullUntilARunCountedIt()
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-1));
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
summary.PerInstanceStats.ShouldHaveSingleItem().Funnel.ShouldBeNull();
}
[Fact]
public async Task GetSummary_Funnel_MapsTheCycleColumns()
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-1),
funnelCountedAt: Now.AddMinutes(-5), queuedUnits: 2, recentlyGrabbedUnits: 1);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceFunnelStat funnel = summary.PerInstanceStats.ShouldHaveSingleItem().Funnel.ShouldNotBeNull();
funnel.LibraryItems.ShouldBe(50);
funnel.ReleasedItems.ShouldBe(40);
funnel.MonitoredItems.ShouldBe(30);
funnel.TagFilteredItems.ShouldBe(20);
funnel.EligibleUnits.ShouldBe(10);
funnel.QueuedUnits.ShouldBe(2);
funnel.RecentlyGrabbedUnits.ShouldBe(1);
funnel.RemainingUnits.ShouldBe(4);
funnel.SearchedUnits.ShouldBe(3);
funnel.CountedAt.ShouldBe(Now.AddMinutes(-5));
}
[Fact]
public async Task GetSummary_Funnel_SearchedUnitsSubtractsRestingUnits()
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-1),
funnelCountedAt: Now, queuedUnits: 1, recentlyGrabbedUnits: 1, restingUnits: 2);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceFunnelStat funnel = summary.PerInstanceStats.ShouldHaveSingleItem().Funnel.ShouldNotBeNull();
funnel.RestingUnits.ShouldBe(2);
funnel.SearchedUnits.ShouldBe(2);
}
[Fact]
public async Task GetSummary_Funnel_SearchedUnitsNeverGoesNegative()
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 3, remainingUnits: 2, startedAt: Now.AddDays(-1),
funnelCountedAt: Now, queuedUnits: 2, recentlyGrabbedUnits: 1);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
summary.PerInstanceStats.ShouldHaveSingleItem().Funnel.ShouldNotBeNull().SearchedUnits.ShouldBe(0);
}
#endregion
#region GetSummary PerInstanceStats
[Fact]
public async Task GetSummary_PerInstanceStats_UsesTheCurrentCycleRow()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-2), searchedUnits: 3);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.CycleItemsTotal.ShouldBe(10);
stat.CycleItemsRemaining.ShouldBe(4);
stat.CycleItemsSearched.ShouldBe(3);
stat.CycleStartedAt.ShouldBe(Now.AddDays(-2));
stat.CycleCompletedAt.ShouldBeNull();
}
[Fact]
public async Task GetSummary_PerInstanceStats_CycleItemsSearched_ExcludesQueuedItems()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
// 5 eligible, 2 remaining, 1 actually searched: 2 are sitting in the download queue
AddCycle(currentCycleId, radarr.Id, eligibleNow: 5, remainingUnits: 2, startedAt: Now.AddDays(-1), searchedUnits: 1);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.CycleItemsSearched.ShouldBe(1);
}
[Fact]
public async Task GetSummary_PerInstanceStats_DryRunOn_NoDryCycle_ValuesAreZeroOrNull()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
(await _dataContext.GeneralConfigs.FirstAsync()).DryRun = true;
await _dataContext.SaveChangesAsync();
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-2), isDryRun: false);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.CycleItemsSearched.ShouldBe(0);
stat.CycleItemsTotal.ShouldBe(0);
stat.CycleItemsRemaining.ShouldBe(0);
stat.CycleStartedAt.ShouldBeNull();
stat.CycleCompletedAt.ShouldBeNull();
stat.CurrentCycleId.ShouldBeNull();
}
[Fact]
public async Task GetSummary_PerInstanceStats_LastCycle_IsTheNewestCompletedOtherCycle()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 5, remainingUnits: 5, startedAt: Now);
Guid olderCompletedId = Guid.NewGuid();
Guid newerCompletedId = Guid.NewGuid();
AddCycle(olderCompletedId, radarr.Id, eligibleNow: 3, remainingUnits: 0, startedAt: Now.AddDays(-20), completedAt: Now.AddDays(-19));
AddCycle(newerCompletedId, radarr.Id, eligibleNow: 3, remainingUnits: 0, startedAt: Now.AddDays(-10), completedAt: Now.AddDays(-9));
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.LastCycleStartedAt.ShouldBe(Now.AddDays(-10));
stat.LastCycleCompletedAt.ShouldBe(Now.AddDays(-9));
}
[Fact]
public async Task GetSummary_PerInstanceStats_DryRunOn_ShowsNewestDryCycleAndIgnoresLiveCycles()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
(await _dataContext.GeneralConfigs.FirstAsync()).DryRun = true;
await _dataContext.SaveChangesAsync();
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 99, remainingUnits: 99, startedAt: Now.AddDays(-30), isDryRun: false);
Guid dryCycleId = Guid.NewGuid();
AddCycle(dryCycleId, radarr.Id, eligibleNow: 7, remainingUnits: 2, startedAt: Now.AddHours(-1), isDryRun: true);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.CurrentCycleId.ShouldBe(dryCycleId);
stat.CycleItemsTotal.ShouldBe(7);
stat.CycleItemsRemaining.ShouldBe(2);
}
[Fact]
public async Task GetSummary_PerInstanceStats_NoCycleRow_ValuesAreZeroOrNull()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
AddInstanceConfig(radarr.Id, Guid.NewGuid());
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.CycleItemsSearched.ShouldBe(0);
stat.CycleItemsTotal.ShouldBe(0);
stat.CycleItemsRemaining.ShouldBe(0);
stat.CycleStartedAt.ShouldBeNull();
stat.CycleCompletedAt.ShouldBeNull();
stat.LastCycleStartedAt.ShouldBeNull();
stat.LastCycleCompletedAt.ShouldBeNull();
}
[Fact]
public async Task GetSummary_PerInstanceStats_NoCurrentCycleRow_LastCycleIsTheNewestCompleted()
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
AddInstanceConfig(radarr.Id, Guid.NewGuid());
AddCycle(Guid.NewGuid(), radarr.Id, eligibleNow: 3, remainingUnits: 0, startedAt: Now.AddDays(-10), completedAt: Now.AddDays(-9));
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
stat.CycleStartedAt.ShouldBeNull();
stat.LastCycleStartedAt.ShouldBe(Now.AddDays(-10));
stat.LastCycleCompletedAt.ShouldBe(Now.AddDays(-9));
}
private void AddInstanceConfig(Guid arrInstanceId, Guid currentCycleId)
{
_dataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
{
ArrInstanceId = arrInstanceId,
Enabled = true,
CurrentCycleId = currentCycleId,
});
_dataContext.SaveChanges();
}
private void AddCycle(
Guid id,
Guid arrInstanceId,
int eligibleNow,
int remainingUnits,
DateTimeOffset startedAt,
DateTimeOffset? completedAt = null,
bool isDryRun = false,
int searchedUnits = 0,
DateTimeOffset? funnelCountedAt = null,
int queuedUnits = 0,
int recentlyGrabbedUnits = 0,
int restingUnits = 0)
{
_eventsContext.SeekerCycles.Add(new SeekerCycle
{
Id = id,
ArrInstanceId = arrInstanceId,
StartedAt = startedAt,
CompletedAt = completedAt,
CompletionReason = completedAt is null ? null : SeekerCycleCompletionReason.Completed,
EligibleAtStart = eligibleNow,
EligibleNow = eligibleNow,
RemainingUnits = remainingUnits,
SearchedUnits = searchedUnits,
IsDryRun = isDryRun,
LibraryItems = 50,
ReleasedItems = 40,
MonitoredItems = 30,
TagFilteredItems = 20,
QueuedUnits = queuedUnits,
RecentlyGrabbedUnits = recentlyGrabbedUnits,
RestingUnits = restingUnits,
FunnelCountedAt = funnelCountedAt,
});
_eventsContext.SaveChanges();
}
#endregion
#region GetEvents with SearchEventData
[Fact]
public async Task GetEvents_WithNoSearchEventData_ReturnsUnknownDefaults()
{
AddSearchEvent();
var result = await _controller.GetEvents();
var body = GetResponseBody(result);
var item = body.GetProperty("Items")[0];
item.GetProperty("ItemTitle").GetString().ShouldBe("Unknown");
}
[Fact]
public async Task GetEvents_WithSearchEventData_ReturnsAllFields()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
AddSearchEvent(
arrInstanceId: radarr.Id,
itemTitle: "Movie A",
searchType: SeekerSearchType.Proactive,
searchReason: SeekerSearchReason.Missing,
grabbedItems: ["Movie A (2024)"]);
var result = await _controller.GetEvents();
var body = GetResponseBody(result);
var item = body.GetProperty("Items")[0];
item.GetProperty("ArrInstanceId").GetString().ShouldBe(radarr.Id.ToString());
item.GetProperty("InstanceType").GetString().ShouldBe(nameof(InstanceType.Radarr));
item.GetProperty("ItemTitle").GetString().ShouldBe("Movie A");
item.GetProperty("SearchType").GetString().ShouldBe(nameof(SeekerSearchType.Proactive));
item.GetProperty("SearchReason").GetString().ShouldBe(nameof(SeekerSearchReason.Missing));
item.GetProperty("GrabbedItems")[0].GetString().ShouldBe("Movie A (2024)");
}
[Fact]
public async Task GetEvents_WithReplacementSearchType_ParsesCorrectEnum()
{
AddSearchEvent(
itemTitle: "Series A",
searchType: SeekerSearchType.Replacement,
searchReason: SeekerSearchReason.Replacement);
var result = await _controller.GetEvents();
var body = GetResponseBody(result);
var item = body.GetProperty("Items")[0];
item.GetProperty("SearchType").GetString().ShouldBe(nameof(SeekerSearchType.Replacement));
item.GetProperty("SearchReason").GetString().ShouldBe(nameof(SeekerSearchReason.Replacement));
}
#endregion
#region GetEvents Filtering
[Fact]
public async Task GetEvents_WithInstanceIdFilter_FiltersByArrInstanceId()
{
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
var sonarr = SeekerTestDataFactory.AddSonarrInstance(_dataContext);
AddSearchEvent(arrInstanceId: radarr.Id, itemTitle: "Radarr Movie");
AddSearchEvent(arrInstanceId: sonarr.Id, itemTitle: "Sonarr Series");
var result = await _controller.GetEvents(instanceId: radarr.Id);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ArrInstanceId").GetString().ShouldBe(radarr.Id.ToString());
}
[Fact]
public async Task GetEvents_WithCycleIdFilter_ReturnsOnlyMatchingCycle()
{
var cycleA = Guid.NewGuid();
var cycleB = Guid.NewGuid();
AddSearchEvent(cycleId: cycleA, itemTitle: "Cycle A Movie");
AddSearchEvent(cycleId: cycleB, itemTitle: "Cycle B Movie");
var result = await _controller.GetEvents(cycleId: cycleA);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ItemTitle").GetString().ShouldBe("Cycle A Movie");
}
[Fact]
public async Task GetEvents_WithSearchFilter_FiltersOnItemTitle()
{
AddSearchEvent(itemTitle: "The Matrix");
AddSearchEvent(itemTitle: "Breaking Bad");
var result = await _controller.GetEvents(search: "matrix");
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
}
[Fact]
public async Task GetEvents_WithPagination_ReturnsCorrectPageAndCount()
{
for (int i = 0; i < 5; i++)
{
AddSearchEvent(itemTitle: $"Event {i}");
}
var result = await _controller.GetEvents(page: 2, pageSize: 2);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(5);
body.GetProperty("TotalPages").GetInt32().ShouldBe(3);
body.GetProperty("Page").GetInt32().ShouldBe(2);
body.GetProperty("Items").GetArrayLength().ShouldBe(2);
}
[Fact]
public async Task GetEvents_WithSortByTitleAscending_OrdersAlphabetically()
{
AddSearchEvent(itemTitle: "Charlie");
AddSearchEvent(itemTitle: "Alpha");
AddSearchEvent(itemTitle: "Bravo");
var result = await _controller.GetEvents(
sortBy: SearchEventsSortBy.Title,
sortDirection: Cleanuparr.Domain.Enums.SortDirection.Asc);
var body = GetResponseBody(result);
var items = body.GetProperty("Items");
items[0].GetProperty("ItemTitle").GetString().ShouldBe("Alpha");
items[1].GetProperty("ItemTitle").GetString().ShouldBe("Bravo");
items[2].GetProperty("ItemTitle").GetString().ShouldBe("Charlie");
}
[Fact]
public async Task GetEvents_WithSortByTimestampAscending_OldestFirst()
{
AddSearchEvent(itemTitle: "Newest", timestamp: Now.UtcDateTime);
AddSearchEvent(itemTitle: "Oldest", timestamp: Now.UtcDateTime.AddHours(-2));
AddSearchEvent(itemTitle: "Middle", timestamp: Now.UtcDateTime.AddHours(-1));
var result = await _controller.GetEvents(sortDirection: Cleanuparr.Domain.Enums.SortDirection.Asc);
var body = GetResponseBody(result);
var items = body.GetProperty("Items");
items[0].GetProperty("ItemTitle").GetString().ShouldBe("Oldest");
items[2].GetProperty("ItemTitle").GetString().ShouldBe("Newest");
}
[Fact]
public async Task GetEvents_WithSearchStatusFilter_ReturnsOnlyMatchingStatuses()
{
AddSearchEvent(itemTitle: "A", searchStatus: SearchCommandStatus.Completed);
AddSearchEvent(itemTitle: "B", searchStatus: SearchCommandStatus.Failed);
AddSearchEvent(itemTitle: "C", searchStatus: SearchCommandStatus.TimedOut);
var result = await _controller.GetEvents(
searchStatus: [SearchCommandStatus.Completed, SearchCommandStatus.Failed]);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(2);
}
[Fact]
public async Task GetEvents_WithSearchTypeFilter_ReturnsOnlyMatchingType()
{
AddSearchEvent(itemTitle: "Proactive Movie", searchType: SeekerSearchType.Proactive);
AddSearchEvent(itemTitle: "Replacement Movie", searchType: SeekerSearchType.Replacement);
var result = await _controller.GetEvents(searchType: SeekerSearchType.Replacement);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ItemTitle").GetString().ShouldBe("Replacement Movie");
}
[Fact]
public async Task GetEvents_WithSearchReasonFilter_ReturnsOnlyMatchingReason()
{
AddSearchEvent(itemTitle: "Missing", searchReason: SeekerSearchReason.Missing);
AddSearchEvent(itemTitle: "Cutoff", searchReason: SeekerSearchReason.QualityCutoffNotMet);
var result = await _controller.GetEvents(searchReason: SeekerSearchReason.QualityCutoffNotMet);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ItemTitle").GetString().ShouldBe("Cutoff");
}
[Fact]
public async Task GetEvents_WithContentKindFilter_ReturnsOnlyMatchingKind()
{
AddSearchEvent(itemTitle: "Show S01", contentKind: SeekerContentKind.Standard);
AddSearchEvent(itemTitle: "Show S00E01", contentKind: SeekerContentKind.Special);
IActionResult result = await _controller.GetEvents(contentKind: SeekerContentKind.Special);
JsonElement body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ItemTitle").GetString().ShouldBe("Show S00E01");
body.GetProperty("Items")[0].GetProperty("ContentKind").GetString().ShouldBe("Special");
}
[Fact]
public async Task GetEvents_WithGrabbedTrue_KeepsOnlyEventsWithGrabbedItems()
{
AddSearchEvent(itemTitle: "With Grabs", grabbedItems: ["movie (2024)"]);
AddSearchEvent(itemTitle: "No Grabs", grabbedItems: []);
var result = await _controller.GetEvents(grabbed: true);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ItemTitle").GetString().ShouldBe("With Grabs");
}
[Fact]
public async Task GetEvents_WithGrabbedFalse_KeepsOnlyEventsWithoutGrabbedItems()
{
AddSearchEvent(itemTitle: "With Grabs", grabbedItems: ["movie (2024)"]);
AddSearchEvent(itemTitle: "No Grabs", grabbedItems: []);
var result = await _controller.GetEvents(grabbed: false);
var body = GetResponseBody(result);
body.GetProperty("TotalCount").GetInt32().ShouldBe(1);
body.GetProperty("Items")[0].GetProperty("ItemTitle").GetString().ShouldBe("No Grabs");
}
#endregion
#region Helpers
private void AddSearchEvent(
string? itemTitle = null,
SeekerSearchType searchType = SeekerSearchType.Proactive,
SeekerSearchReason searchReason = SeekerSearchReason.Missing,
SeekerContentKind? contentKind = null,
List<string>? grabbedItems = null,
Guid? arrInstanceId = null,
Guid? cycleId = null,
SearchCommandStatus? searchStatus = null,
DateTime? timestamp = null)
{
var appEvent = new AppEvent
{
EventType = EventType.SearchTriggered,
Message = "Search triggered",
Severity = EventSeverity.Information,
ArrInstanceId = arrInstanceId,
CycleId = cycleId,
SearchStatus = searchStatus,
Timestamp = timestamp ?? Now.UtcDateTime
};
if (itemTitle is not null)
{
appEvent.ItemTitle = itemTitle;
appEvent.SearchType = searchType;
appEvent.SearchReason = searchReason;
appEvent.ContentKind = contentKind;
appEvent.GrabbedItems = grabbedItems ?? [];
}
_eventsContext.Events.Add(appEvent);
_eventsContext.SaveChanges();
}
#endregion
}