Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m51s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Add GET /api/media-sources/collections-scan-status (MediaSourcesController →
GetCollectionsScanStatus handler) reporting which media-source families
(plex/jellyfin/emby) currently hold their external-collections scan lock,
reading IEntityLocker.Are{X}CollectionsLocked(). The lock is family-global
(no source id) and boolean (no percent), so the DTO carries just {family} and
returns only active families — the counterpart to GET /api/libraries/scan-status.
SPA: useCollectionsScan now polls this endpoint and reconciles optimistic
pending against the active-family set (seeding on mount so an in-progress scan
disables buttons immediately), using the same grace-tick helper as library
scans (now generic over the pending key type). Drops COLLECTIONS_PENDING_TIMEOUT_MS
— a long deep scan no longer re-enables the button early, and a fast scan no
longer wedges it disabled for the full timeout. A row shows Scanning when its
family is active or it has an in-grace optimistic pending key.
Tests: handler (3), controller route+delegation (2), SPA api fn + hook reconcile
(mount-seed / 202-promote / 409-keeps-disabled / 404-error). OpenAPI + TS types
regenerated. Docs: api-conventions §3b, blazor-route-parity §5, decisions.md.
Unblocks #91b (arc item 4): Libraries.razor's collections-scan affordance now
has full authoritative parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
61 lines
2.1 KiB
C#
61 lines
2.1 KiB
C#
using ErsatzTV.Application.MediaSources;
|
|
using ErsatzTV.Core.Api.MediaSources;
|
|
using ErsatzTV.Core.Interfaces.Locking;
|
|
using NSubstitute;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Application.MediaSources;
|
|
|
|
[TestFixture]
|
|
public class GetCollectionsScanStatusHandlerTests
|
|
{
|
|
[Test]
|
|
public async Task Handle_Should_Return_Empty_When_No_Family_Is_Locked()
|
|
{
|
|
var locker = Substitute.For<IEntityLocker>();
|
|
locker.ArePlexCollectionsLocked().Returns(false);
|
|
locker.AreJellyfinCollectionsLocked().Returns(false);
|
|
locker.AreEmbyCollectionsLocked().Returns(false);
|
|
|
|
var handler = new GetCollectionsScanStatusHandler(locker);
|
|
|
|
List<CollectionsScanStatusResponseModel> result =
|
|
await handler.Handle(new GetCollectionsScanStatus(), CancellationToken.None);
|
|
|
|
result.ShouldBeEmpty();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Return_Only_Locked_Families()
|
|
{
|
|
var locker = Substitute.For<IEntityLocker>();
|
|
locker.ArePlexCollectionsLocked().Returns(true);
|
|
locker.AreJellyfinCollectionsLocked().Returns(false);
|
|
locker.AreEmbyCollectionsLocked().Returns(true);
|
|
|
|
var handler = new GetCollectionsScanStatusHandler(locker);
|
|
|
|
List<CollectionsScanStatusResponseModel> result =
|
|
await handler.Handle(new GetCollectionsScanStatus(), CancellationToken.None);
|
|
|
|
result.Select(r => r.Family).ShouldBe(["plex", "emby"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Return_All_Families_When_All_Locked()
|
|
{
|
|
var locker = Substitute.For<IEntityLocker>();
|
|
locker.ArePlexCollectionsLocked().Returns(true);
|
|
locker.AreJellyfinCollectionsLocked().Returns(true);
|
|
locker.AreEmbyCollectionsLocked().Returns(true);
|
|
|
|
var handler = new GetCollectionsScanStatusHandler(locker);
|
|
|
|
List<CollectionsScanStatusResponseModel> result =
|
|
await handler.Handle(new GetCollectionsScanStatus(), CancellationToken.None);
|
|
|
|
result.Select(r => r.Family).ShouldBe(["plex", "jellyfin", "emby"]);
|
|
}
|
|
}
|