diff --git a/docs/requirements.md b/docs/requirements.md index 3b094aa65..0cb04646b 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -819,6 +819,7 @@ Internal architecture, components, and application logic. | UT-246 | Opening a PiP window disarms background audio, and a background signal arriving with the native PiP flag false is still treated as PiP while the frontend's latch says the window is open — without resurrecting one it has already seen close | DR-266 | Done | | UT-247 | A library whose `collection_type` has no mapping — Books, Photos, a mixed library — does not return the server's films, albums and shows from cache | DR-277 | Done | | UT-248 | Narrowing the library clause does not starve the libraries that do have landing pages: music, movies and TV each still list their own media and none of the others | DR-277 | Done | +| UT-249 | Opening an individual collection still lists its own children: a BoxSet's members are matched by the stored `parent_id`, not by the library clause, so narrowing that clause did not empty collections | DR-277 | Done | ### Integration Tests | Test ID | Test Description | Traces To | Status | diff --git a/src-tauri/src/repository/offline.rs b/src-tauri/src/repository/offline.rs index 037754217..e493dd171 100644 --- a/src-tauri/src/repository/offline.rs +++ b/src-tauri/src/repository/offline.rs @@ -5037,15 +5037,67 @@ mod tests { #[tokio::test] async fn test_get_items_unknown_library_type_does_not_return_whole_server() { let db = create_test_db(); - seed_library(&db, "books-lib", "books").await; insert_item(&db, "movie-1", "Movie", None, None, None).await; insert_item(&db, "album-1", "MusicAlbum", None, None, None).await; insert_item(&db, "series-1", "Series", None, None, None).await; + // Every library kind the app has no landing page for, including the + // empty `collection_type` Jellyfin sends for a mixed library. + for collection_type in ["books", "boxsets", "photos", "homevideos", ""] { + let lib = format!("lib-{collection_type}"); + seed_library(&db, &lib, collection_type).await; + + let repo = make_repo(&db); + let ids: Vec = repo + .get_items(&lib, None) + .await + .unwrap() + .items + .iter() + .map(|i| i.id.clone()) + .collect(); + + assert!( + ids.is_empty(), + "a '{collection_type}' library must not serve the server's films, \ + albums and shows; got {:?}", + ids + ); + } + } + + /// Opening an individual collection is a different path and must keep + /// working: a BoxSet's children carry `parent_id`, which the cache does + /// store, so they are matched by the ordinary parent link rather than by + /// the library clause this fix narrowed. + /// + /// Worth pinning separately — narrowing the library clause could plausibly + /// have taken collections with it, and "Collections is empty" would look + /// identical to the bug it was meant to fix. + /// + /// TRACES: UR-007 | DR-277 | UT-249 + #[tokio::test] + async fn test_get_items_collection_lists_its_own_children() { + let db = create_test_db(); + seed_library(&db, "boxset-lib", "boxsets").await; + + insert_item(&db, "boxset-1", "BoxSet", None, None, None).await; + insert_item(&db, "outsider", "Movie", None, None, None).await; + + // A film inside the collection: linked by parent_id, which is what a + // BoxSet's children actually carry. + db.execute(Query::with_params( + "INSERT INTO items (id, server_id, name, item_type, parent_id, synced_at) \ + VALUES ('in-set', 'test-server', 'In The Set', 'Movie', ?1, '2024-01-01')", + vec![QueryParam::String("boxset-1".to_string())], + )) + .await + .unwrap(); + let repo = make_repo(&db); let ids: Vec = repo - .get_items("books-lib", None) + .get_items("boxset-1", None) .await .unwrap() .items @@ -5053,11 +5105,10 @@ mod tests { .map(|i| i.id.clone()) .collect(); - assert!( - !ids.contains(&"movie-1".to_string()) - && !ids.contains(&"album-1".to_string()) - && !ids.contains(&"series-1".to_string()), - "a Books library must not serve the server's films, albums and shows; got {:?}", + assert_eq!( + ids, + vec!["in-set".to_string()], + "a collection lists its own children and nothing else; got {:?}", ids ); }