diff --git a/ErsatzTV.Tests/Controllers/PlexMediaSourcesControllerTests.cs b/ErsatzTV.Tests/Controllers/PlexMediaSourcesControllerTests.cs index 6b7431d15..4c66dacd1 100644 --- a/ErsatzTV.Tests/Controllers/PlexMediaSourcesControllerTests.cs +++ b/ErsatzTV.Tests/Controllers/PlexMediaSourcesControllerTests.cs @@ -259,6 +259,27 @@ public class PlexMediaSourcesControllerTests await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } + [Test] + public async Task ReplaceLibraries_Should_Return_422_When_An_Owned_Library_Is_Missing() + { + SourceExists(true); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(new List + { + new(5, "Movies", LibraryMediaKind.Movies, true), + new(6, "Shows", LibraryMediaKind.Shows, true) + }); + + // request omits owned id 6 -> 422: the PUT is a complete flag document (§C4a), matching Jellyfin/Emby + IActionResult result = await _controller.ReplaceLibraryPreferences( + 3, + new ReplaceRemoteLibraryPreferencesRequest([new RemoteLibraryPreferenceRequest(5, true)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + [Test] public async Task ReplaceLibraries_Should_Save_Enqueue_Ordered_Pair_And_Return_Reloaded() { diff --git a/ErsatzTV/Controllers/Api/PlexMediaSourcesController.cs b/ErsatzTV/Controllers/Api/PlexMediaSourcesController.cs index c47d61e6f..e1cc451d6 100644 --- a/ErsatzTV/Controllers/Api/PlexMediaSourcesController.cs +++ b/ErsatzTV/Controllers/Api/PlexMediaSourcesController.cs @@ -164,6 +164,18 @@ public class PlexMediaSourcesController( .ToErrorResult(); } + // Require the complete set of this source's libraries (§C4a; matches Jellyfin/Emby): the PUT is a + // full flag document, so an omitted owned id would silently keep its old sync state. + var incomingIds = libraries.Map(l => l.Id).ToList(); + var missingIds = ownedIds.Where(ownedId => !incomingIds.Contains(ownedId)).ToList(); + if (missingIds.Count > 0) + { + return BaseError.New( + $"The request must include every library for Plex media source {id} " + + $"(missing id(s) {string.Join(", ", missingIds)})") + .ToErrorResult(); + } + Either result = await mediator.Send(request.ToPlexCommand(), cancellationToken); if (result.IsLeft) diff --git a/web/src/screens/RemoteLibrariesEditScreen.tsx b/web/src/screens/RemoteLibrariesEditScreen.tsx index 8e304f1df..ead8afbbc 100644 Binary files a/web/src/screens/RemoteLibrariesEditScreen.tsx and b/web/src/screens/RemoteLibrariesEditScreen.tsx differ