use chrono::Utc; use sea_orm::ActiveValue::Set; use shanty_db::entities::wanted_item::{ItemType, WantedStatus}; use shanty_db::{Database, queries}; use shanty_tag::error::TagResult; use shanty_tag::provider::*; use shanty_watch::{add_album, add_artist, add_track, library_summary, list_items, remove_item}; async fn test_db() -> Database { Database::new("sqlite::memory:") .await .expect("failed to create test database") } /// Insert a fake track into the DB to simulate an indexed library. async fn insert_track(db: &Database, artist: &str, title: &str, album: &str) { let now = Utc::now().naive_utc(); let artist_rec = queries::artists::upsert(db.conn(), artist, None) .await .unwrap(); let album_rec = queries::albums::upsert(db.conn(), album, artist, None, Some(artist_rec.id)) .await .unwrap(); let active = shanty_db::entities::track::ActiveModel { file_path: Set(format!("/music/{artist}/{album}/{title}.mp3")), title: Set(Some(title.to_string())), artist: Set(Some(artist.to_string())), album: Set(Some(album.to_string())), album_artist: Set(Some(artist.to_string())), file_size: Set(1_000_000), artist_id: Set(Some(artist_rec.id)), album_id: Set(Some(album_rec.id)), added_at: Set(now), updated_at: Set(now), ..Default::default() }; queries::tracks::upsert(db.conn(), active).await.unwrap(); } /// Mock provider that returns a tracklist for known releases. struct MockProvider; impl MetadataProvider for MockProvider { async fn search_recording( &self, _artist: &str, _title: &str, ) -> TagResult> { Ok(vec![]) } async fn search_release(&self, _artist: &str, _album: &str) -> TagResult> { Ok(vec![ReleaseMatch { mbid: "release-123".into(), title: "Test Album".into(), artist: "Test Artist".into(), artist_mbid: Some("artist-456".into()), date: Some("2024".into()), track_count: Some(2), score: 100, }]) } async fn get_recording(&self, _mbid: &str) -> TagResult { Err(shanty_tag::TagError::Other("not found".into())) } async fn search_artist(&self, _query: &str, _limit: u32) -> TagResult> { Ok(vec![ArtistSearchResult { mbid: "artist-456".into(), name: "Test Artist".into(), disambiguation: None, country: None, artist_type: None, score: 100, }]) } async fn get_artist_releases( &self, _mbid: &str, _limit: u32, ) -> TagResult> { Ok(vec![DiscographyEntry { mbid: "release-123".into(), title: "Test Album".into(), date: Some("2024".into()), release_type: Some("Album".into()), track_count: Some(2), }]) } async fn get_release_tracks(&self, _release_mbid: &str) -> TagResult> { Ok(vec![ ReleaseTrack { recording_mbid: "rec-1".into(), title: "Track One".into(), track_number: Some(1), disc_number: Some(1), duration_ms: Some(180_000), }, ReleaseTrack { recording_mbid: "rec-2".into(), title: "Track Two".into(), track_number: Some(2), disc_number: Some(1), duration_ms: Some(200_000), }, ]) } async fn get_artist_release_groups( &self, _artist_mbid: &str, ) -> TagResult> { Ok(vec![]) } } #[tokio::test] async fn test_add_track_wanted() { let db = test_db().await; let provider = MockProvider; let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider, None) .await .unwrap(); assert_eq!(entry.item_type, ItemType::Track); assert_eq!(entry.name, "Creep"); assert_eq!(entry.status, WantedStatus::Wanted); } #[tokio::test] async fn test_add_track_auto_owned() { let db = test_db().await; let provider = MockProvider; insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; let entry = add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, &provider, None) .await .unwrap(); assert_eq!(entry.status, WantedStatus::Owned); } #[tokio::test] async fn test_add_album_expands_to_tracks() { let db = test_db().await; let provider = MockProvider; let summary = add_album( db.conn(), Some("Test Artist"), Some("Test Album"), None, &provider, None, ) .await .unwrap(); assert_eq!(summary.tracks_added, 2); let items = list_items(db.conn(), Some(WantedStatus::Wanted), None, None) .await .unwrap(); assert_eq!(items.len(), 2); assert_eq!(items[0].item_type, ItemType::Track); } #[tokio::test] async fn test_add_artist_expands_to_tracks() { let db = test_db().await; let provider = MockProvider; let summary = add_artist(db.conn(), Some("Test Artist"), None, &provider, None) .await .unwrap(); assert_eq!(summary.tracks_added, 2); let items = list_items(db.conn(), None, None, None).await.unwrap(); assert_eq!(items.len(), 2); } #[tokio::test] async fn test_list_items_with_filters() { let db = test_db().await; let provider = MockProvider; add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider, None) .await .unwrap(); add_track(db.conn(), Some("Tool"), Some("Lateralus"), None, &provider, None) .await .unwrap(); let all = list_items(db.conn(), None, None, None).await.unwrap(); assert_eq!(all.len(), 2); let wanted = list_items(db.conn(), Some(WantedStatus::Wanted), None, None) .await .unwrap(); assert_eq!(wanted.len(), 2); let radiohead = list_items(db.conn(), None, Some("Radiohead"), None) .await .unwrap(); assert_eq!(radiohead.len(), 1); } #[tokio::test] async fn test_remove_item() { let db = test_db().await; let provider = MockProvider; let entry = add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider, None) .await .unwrap(); remove_item(db.conn(), entry.id).await.unwrap(); let all = list_items(db.conn(), None, None, None).await.unwrap(); assert!(all.is_empty()); } #[tokio::test] async fn test_library_summary() { let db = test_db().await; let provider = MockProvider; insert_track(&db, "Pink Floyd", "Time", "DSOTM").await; add_track(db.conn(), Some("Radiohead"), Some("Creep"), None, &provider, None) .await .unwrap(); add_track(db.conn(), Some("Pink Floyd"), Some("Time"), None, &provider, None) .await .unwrap(); let summary = library_summary(db.conn()).await.unwrap(); assert_eq!(summary.total_items, 2); assert_eq!(summary.wanted, 1); assert_eq!(summary.owned, 1); }