use chrono::Utc; use sea_orm::*; use shanty_db::entities::download_queue::DownloadStatus; use shanty_db::entities::wanted_item::{ItemType, WantedStatus}; use shanty_db::{Database, queries}; async fn test_db() -> Database { Database::new("sqlite::memory:").await.expect("failed to create test database") } #[tokio::test] async fn test_database_creation_and_migrations() { let _db = test_db().await; } #[tokio::test] async fn test_artist_crud() { let db = test_db().await; let conn = db.conn(); // Create let artist = queries::artists::upsert(conn, "Pink Floyd", None) .await .unwrap(); assert_eq!(artist.name, "Pink Floyd"); assert!(artist.musicbrainz_id.is_none()); // Read let found = queries::artists::get_by_id(conn, artist.id).await.unwrap(); assert_eq!(found.name, "Pink Floyd"); // Find by name let found = queries::artists::find_by_name(conn, "Pink Floyd") .await .unwrap(); assert!(found.is_some()); // Upsert (same name, should return existing) let same = queries::artists::upsert(conn, "Pink Floyd", None) .await .unwrap(); assert_eq!(same.id, artist.id); // Upsert with musicbrainz_id updates existing let updated = queries::artists::upsert(conn, "Pink Floyd", Some("mb-123")) .await .unwrap(); assert_eq!(updated.id, artist.id); assert_eq!(updated.musicbrainz_id.as_deref(), Some("mb-123")); // List let all = queries::artists::list(conn, 100, 0).await.unwrap(); assert_eq!(all.len(), 1); // Update top songs let updated = queries::artists::update_top_songs(conn, artist.id, r#"["Time","Money"]"#) .await .unwrap(); assert_eq!(updated.top_songs, r#"["Time","Money"]"#); // Delete queries::artists::delete(conn, artist.id).await.unwrap(); let all = queries::artists::list(conn, 100, 0).await.unwrap(); assert!(all.is_empty()); } #[tokio::test] async fn test_album_crud() { let db = test_db().await; let conn = db.conn(); let artist = queries::artists::upsert(conn, "Pink Floyd", None) .await .unwrap(); // Create album let album = queries::albums::upsert( conn, "The Dark Side of the Moon", "Pink Floyd", None, Some(artist.id), ) .await .unwrap(); assert_eq!(album.name, "The Dark Side of the Moon"); assert_eq!(album.artist_id, Some(artist.id)); // Upsert same album returns existing let same = queries::albums::upsert( conn, "The Dark Side of the Moon", "Pink Floyd", None, Some(artist.id), ) .await .unwrap(); assert_eq!(same.id, album.id); // Get by artist let albums = queries::albums::get_by_artist(conn, artist.id) .await .unwrap(); assert_eq!(albums.len(), 1); } #[tokio::test] async fn test_track_upsert_and_search() { let db = test_db().await; let conn = db.conn(); let now = Utc::now().naive_utc(); // Insert a track with partial metadata let active = shanty_db::entities::track::ActiveModel { file_path: Set("/music/time.flac".to_string()), title: Set(Some("Time".to_string())), artist: Set(Some("Pink Floyd".to_string())), album: Set(Some("The Dark Side of the Moon".to_string())), file_size: Set(42_000_000), added_at: Set(now), updated_at: Set(now), ..Default::default() }; let track = queries::tracks::upsert(conn, active).await.unwrap(); assert_eq!(track.title.as_deref(), Some("Time")); assert!(track.artist_id.is_none()); // no FK yet // Upsert same file_path should update, not duplicate let active2 = shanty_db::entities::track::ActiveModel { file_path: Set("/music/time.flac".to_string()), title: Set(Some("Time".to_string())), artist: Set(Some("Pink Floyd".to_string())), album: Set(Some("The Dark Side of the Moon".to_string())), file_size: Set(42_000_000), bitrate: Set(Some(1411)), ..Default::default() }; let updated = queries::tracks::upsert(conn, active2).await.unwrap(); assert_eq!(updated.id, track.id); assert_eq!(updated.bitrate, Some(1411)); // Search let results = queries::tracks::search(conn, "Time").await.unwrap(); assert_eq!(results.len(), 1); let results = queries::tracks::search(conn, "Pink Floyd").await.unwrap(); assert_eq!(results.len(), 1); // Untagged (no musicbrainz_id) let untagged = queries::tracks::get_untagged(conn).await.unwrap(); assert_eq!(untagged.len(), 1); } #[tokio::test] async fn test_wanted_items_lifecycle() { let db = test_db().await; let conn = db.conn(); let artist = queries::artists::upsert(conn, "Radiohead", None) .await .unwrap(); // Add wanted item let item = queries::wanted::add(conn, ItemType::Artist, Some(artist.id), None, None) .await .unwrap(); assert_eq!(item.status, WantedStatus::Wanted); assert_eq!(item.item_type, ItemType::Artist); // List with filter let wanted = queries::wanted::list(conn, Some(WantedStatus::Wanted)) .await .unwrap(); assert_eq!(wanted.len(), 1); let downloaded = queries::wanted::list(conn, Some(WantedStatus::Downloaded)) .await .unwrap(); assert!(downloaded.is_empty()); // Update status let updated = queries::wanted::update_status(conn, item.id, WantedStatus::Downloaded) .await .unwrap(); assert_eq!(updated.status, WantedStatus::Downloaded); // Remove queries::wanted::remove(conn, item.id).await.unwrap(); let all = queries::wanted::list(conn, None).await.unwrap(); assert!(all.is_empty()); } #[tokio::test] async fn test_download_queue_lifecycle() { let db = test_db().await; let conn = db.conn(); // Enqueue let item = queries::downloads::enqueue(conn, "Pink Floyd Time", None, "ytdlp") .await .unwrap(); assert_eq!(item.status, DownloadStatus::Pending); assert_eq!(item.query, "Pink Floyd Time"); // Get next pending let next = queries::downloads::get_next_pending(conn).await.unwrap(); assert!(next.is_some()); assert_eq!(next.unwrap().id, item.id); // Update to downloading queries::downloads::update_status(conn, item.id, DownloadStatus::Downloading, None) .await .unwrap(); // No more pending let next = queries::downloads::get_next_pending(conn).await.unwrap(); assert!(next.is_none()); // Fail it queries::downloads::update_status( conn, item.id, DownloadStatus::Failed, Some("network error"), ) .await .unwrap(); // List failed let failed = queries::downloads::list(conn, Some(DownloadStatus::Failed)) .await .unwrap(); assert_eq!(failed.len(), 1); assert_eq!(failed[0].error_message.as_deref(), Some("network error")); // Retry queries::downloads::retry_failed(conn, item.id).await.unwrap(); let pending = queries::downloads::list(conn, Some(DownloadStatus::Pending)) .await .unwrap(); assert_eq!(pending.len(), 1); assert_eq!(pending[0].retry_count, 1); } #[tokio::test] async fn test_search_cache_ttl() { let db = test_db().await; let conn = db.conn(); // Set cache with long TTL queries::cache::set(conn, "test_query", "musicbrainz", r#"{"results":[]}"#, 3600) .await .unwrap(); // Get should return the value let result = queries::cache::get(conn, "test_query").await.unwrap(); assert_eq!(result.as_deref(), Some(r#"{"results":[]}"#)); // Non-existent key let result = queries::cache::get(conn, "nonexistent").await.unwrap(); assert!(result.is_none()); // Set cache with 0-second TTL (already expired) queries::cache::set(conn, "expired_query", "musicbrainz", r#"{"old":true}"#, 0) .await .unwrap(); // Should not return expired entry let result = queries::cache::get(conn, "expired_query").await.unwrap(); assert!(result.is_none()); // Purge expired let purged = queries::cache::purge_expired(conn).await.unwrap(); assert_eq!(purged, 1); }