From e060ee237bbcb48a7a52d20794c9738db007cfd7 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Fri, 23 Oct 2026 14:17:07 +0300 Subject: [PATCH] added quiet hours to seeker config api --- .../Seeker/SeekerConfigControllerTests.cs | 77 +++++++++++++++++++ .../Requests/UpdateSeekerConfigRequest.cs | 9 +++ .../Responses/SeekerConfigResponse.cs | 6 ++ .../Controllers/SeekerConfigController.cs | 3 + 4 files changed, 95 insertions(+) diff --git a/code/backend/Cleanuparr.Api.Tests/Features/Seeker/SeekerConfigControllerTests.cs b/code/backend/Cleanuparr.Api.Tests/Features/Seeker/SeekerConfigControllerTests.cs index f516f630..63090380 100644 --- a/code/backend/Cleanuparr.Api.Tests/Features/Seeker/SeekerConfigControllerTests.cs +++ b/code/backend/Cleanuparr.Api.Tests/Features/Seeker/SeekerConfigControllerTests.cs @@ -1,6 +1,8 @@ +using System.Text.Json; using Cleanuparr.Api.Features.Seeker.Contracts.Requests; using Cleanuparr.Api.Features.Seeker.Contracts.Responses; using Cleanuparr.Api.Features.Seeker.Controllers; +using Cleanuparr.Api.Json; using Cleanuparr.Api.Tests.Features.Seeker.TestHelpers; using Cleanuparr.Domain.Enums; using Cleanuparr.Domain.Exceptions; @@ -158,6 +160,25 @@ public class SeekerConfigControllerTests : IDisposable response.Instances.ShouldHaveSingleItem().SonarrSearchMode.ShouldBe(SonarrSearchMode.Episode); } + [Fact] + public async Task GetSeekerConfig_WithQuietHours_ReturnsStoredValues() + { + SeekerTestDataFactory.AddRadarrInstance(_dataContext); + SeekerConfig config = await _dataContext.SeekerConfigs.FirstAsync(); + config.QuietHoursEnabled = true; + config.QuietHoursStart = new TimeOnly(22, 30); + config.QuietHoursEnd = new TimeOnly(6, 15); + await _dataContext.SaveChangesAsync(); + + IActionResult result = await _controller.GetSeekerConfig(); + OkObjectResult okResult = result.ShouldBeOfType(); + SeekerConfigResponse response = okResult.Value.ShouldBeOfType(); + + response.QuietHoursEnabled.ShouldBeTrue(); + response.QuietHoursStart.ShouldBe(new TimeOnly(22, 30)); + response.QuietHoursEnd.ShouldBe(new TimeOnly(6, 15)); + } + [Fact] public async Task GetSeekerConfig_OnlyReturnsSonarrAndRadarrInstances() { @@ -201,6 +222,62 @@ public class SeekerConfigControllerTests : IDisposable await Should.ThrowAsync(() => _controller.UpdateSeekerConfig(request)); } + [Fact] + public async Task UpdateSeekerConfig_WithQuietHours_PersistsValues() + { + ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext); + UpdateSeekerConfigRequest request = new() + { + SearchEnabled = true, + SearchInterval = 5, + QuietHoursEnabled = true, + QuietHoursStart = new TimeOnly(21, 0), + QuietHoursEnd = new TimeOnly(8, 45), + Instances = [new UpdateSeekerInstanceConfigRequest { ArrInstanceId = radarr.Id, Enabled = true }] + }; + + await _controller.UpdateSeekerConfig(request); + + SeekerConfig saved = await _dataContext.SeekerConfigs.AsNoTracking().FirstAsync(); + saved.QuietHoursEnabled.ShouldBeTrue(); + saved.QuietHoursStart.ShouldBe(new TimeOnly(21, 0)); + saved.QuietHoursEnd.ShouldBe(new TimeOnly(8, 45)); + } + + [Fact] + public async Task UpdateSeekerConfig_WithEqualQuietHours_ThrowsValidationException() + { + ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext); + UpdateSeekerConfigRequest request = new() + { + SearchEnabled = true, + SearchInterval = 5, + QuietHoursEnabled = true, + QuietHoursStart = new TimeOnly(7, 0), + QuietHoursEnd = new TimeOnly(7, 0), + Instances = [new UpdateSeekerInstanceConfigRequest { ArrInstanceId = radarr.Id, Enabled = true }] + }; + + await Should.ThrowAsync(() => _controller.UpdateSeekerConfig(request)); + } + + [Theory] + [InlineData("23:00")] + [InlineData("23:00:00")] + public void UpdateSeekerConfigRequest_DeserializesQuietHoursWithOrWithoutSeconds(string time) + { + JsonSerializerOptions options = new(); + CleanuparrJsonConfiguration.ConfigureApiInbound(options); + + UpdateSeekerConfigRequest? request = JsonSerializer.Deserialize( + $"{{\"quietHoursEnabled\":true,\"quietHoursStart\":\"{time}\",\"quietHoursEnd\":\"07:00\"}}", + options); + + request.ShouldNotBeNull(); + request.QuietHoursStart.ShouldBe(new TimeOnly(23, 0)); + request.QuietHoursEnd.ShouldBe(new TimeOnly(7, 0)); + } + [Fact] public async Task UpdateSeekerConfig_WhenIntervalChanges_ReschedulesSeeker() { diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Requests/UpdateSeekerConfigRequest.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Requests/UpdateSeekerConfigRequest.cs index e4b3b27f..9c8e0cc2 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Requests/UpdateSeekerConfigRequest.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Requests/UpdateSeekerConfigRequest.cs @@ -17,6 +17,12 @@ public sealed record UpdateSeekerConfigRequest public int PostReleaseGraceHours { get; init; } = 6; + public bool QuietHoursEnabled { get; init; } + + public TimeOnly QuietHoursStart { get; init; } = new(23, 0); + + public TimeOnly QuietHoursEnd { get; init; } = new(7, 0); + public List Instances { get; init; } = []; public SeekerConfig ApplyTo(SeekerConfig config) @@ -27,6 +33,9 @@ public sealed record UpdateSeekerConfigRequest config.SelectionStrategy = SelectionStrategy; config.UseRoundRobin = UseRoundRobin; config.PostReleaseGraceHours = PostReleaseGraceHours; + config.QuietHoursEnabled = QuietHoursEnabled; + config.QuietHoursStart = QuietHoursStart; + config.QuietHoursEnd = QuietHoursEnd; return config; } diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SeekerConfigResponse.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SeekerConfigResponse.cs index df0e1969..f313596f 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SeekerConfigResponse.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Contracts/Responses/SeekerConfigResponse.cs @@ -16,5 +16,11 @@ public sealed record SeekerConfigResponse public int PostReleaseGraceHours { get; init; } + public bool QuietHoursEnabled { get; init; } + + public TimeOnly QuietHoursStart { get; init; } + + public TimeOnly QuietHoursEnd { get; init; } + public List Instances { get; init; } = []; } diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SeekerConfigController.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SeekerConfigController.cs index 2aee3a52..d1229509 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SeekerConfigController.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/SeekerConfigController.cs @@ -85,6 +85,9 @@ public sealed class SeekerConfigController : ControllerBase SelectionStrategy = config.SelectionStrategy, UseRoundRobin = config.UseRoundRobin, PostReleaseGraceHours = config.PostReleaseGraceHours, + QuietHoursEnabled = config.QuietHoursEnabled, + QuietHoursStart = config.QuietHoursStart, + QuietHoursEnd = config.QuietHoursEnd, Instances = instanceResponses, };