use shanty_db::entities::download_queue::DownloadStatus; use shanty_db::{Database, queries}; use shanty_dl::backend::{AudioFormat, BackendConfig, DownloadBackend, DownloadResult, DownloadTarget, SearchResult}; use shanty_dl::error::DlResult; use shanty_dl::queue::{run_queue, download_single}; use tempfile::TempDir; /// Mock backend for testing without yt-dlp. struct MockBackend { /// If true, downloads will fail. should_fail: bool, } impl MockBackend { fn new(should_fail: bool) -> Self { Self { should_fail } } } impl DownloadBackend for MockBackend { async fn check_available(&self) -> DlResult<()> { Ok(()) } async fn search(&self, query: &str) -> DlResult> { Ok(vec![SearchResult { url: format!("https://example.com/{}", query.replace(' ', "_")), title: query.to_string(), artist: Some("Test Artist".to_string()), duration: Some(180.0), source: "mock".to_string(), }]) } async fn download( &self, target: &DownloadTarget, config: &BackendConfig, ) -> DlResult { if self.should_fail { return Err(shanty_dl::DlError::DownloadFailed("mock failure".into())); } let title = match target { DownloadTarget::Url(u) => u.clone(), DownloadTarget::Query(q) => q.clone(), }; let file_name = format!("{}.{}", title.replace(' ', "_"), config.format); let file_path = config.output_dir.join(&file_name); std::fs::write(&file_path, b"fake audio data").unwrap(); Ok(DownloadResult { file_path, title, artist: Some("Test Artist".into()), duration: Some(180.0), source_url: "https://example.com/test".into(), }) } } async fn test_db() -> Database { Database::new("sqlite::memory:") .await .expect("failed to create test database") } #[tokio::test] async fn test_queue_process_success() { let db = test_db().await; let dir = TempDir::new().unwrap(); let backend = MockBackend::new(false); let config = BackendConfig { output_dir: dir.path().to_owned(), format: AudioFormat::Opus, cookies_path: None, }; // Enqueue an item queries::downloads::enqueue(db.conn(), "Test Song", None, "mock") .await .unwrap(); // Process queue let stats = run_queue(db.conn(), &backend, &config, false).await.unwrap(); assert_eq!(stats.downloads_attempted, 1); assert_eq!(stats.downloads_completed, 1); assert_eq!(stats.downloads_failed, 0); // Verify status in DB let items = queries::downloads::list(db.conn(), Some(DownloadStatus::Completed)) .await .unwrap(); assert_eq!(items.len(), 1); } #[tokio::test] async fn test_queue_process_failure() { let db = test_db().await; let dir = TempDir::new().unwrap(); let backend = MockBackend::new(true); let config = BackendConfig { output_dir: dir.path().to_owned(), format: AudioFormat::Opus, cookies_path: None, }; queries::downloads::enqueue(db.conn(), "Failing Song", None, "mock") .await .unwrap(); let stats = run_queue(db.conn(), &backend, &config, false).await.unwrap(); assert_eq!(stats.downloads_attempted, 1); assert_eq!(stats.downloads_failed, 1); // Check it's marked as failed let items = queries::downloads::list(db.conn(), Some(DownloadStatus::Failed)) .await .unwrap(); assert_eq!(items.len(), 1); assert!(items[0].error_message.is_some()); } #[tokio::test] async fn test_queue_dry_run() { let db = test_db().await; let dir = TempDir::new().unwrap(); let backend = MockBackend::new(false); let config = BackendConfig { output_dir: dir.path().to_owned(), format: AudioFormat::Opus, cookies_path: None, }; queries::downloads::enqueue(db.conn(), "Dry Run Song", None, "mock") .await .unwrap(); let stats = run_queue(db.conn(), &backend, &config, true).await.unwrap(); assert_eq!(stats.downloads_attempted, 1); assert_eq!(stats.downloads_skipped, 1); assert_eq!(stats.downloads_completed, 0); // No files should exist let entries: Vec<_> = std::fs::read_dir(dir.path()).unwrap().collect(); assert!(entries.is_empty()); } #[tokio::test] async fn test_download_single_success() { let dir = TempDir::new().unwrap(); let backend = MockBackend::new(false); let config = BackendConfig { output_dir: dir.path().to_owned(), format: AudioFormat::Mp3, cookies_path: None, }; download_single( &backend, DownloadTarget::Query("Test Song".into()), &config, false, ) .await .unwrap(); // File should exist let entries: Vec<_> = std::fs::read_dir(dir.path()) .unwrap() .filter_map(|e| e.ok()) .collect(); assert_eq!(entries.len(), 1); } #[tokio::test] async fn test_queue_retry() { let db = test_db().await; // Enqueue and manually fail an item let item = queries::downloads::enqueue(db.conn(), "Retry Song", None, "mock") .await .unwrap(); queries::downloads::update_status(db.conn(), item.id, DownloadStatus::Failed, Some("oops")) .await .unwrap(); // Retry it queries::downloads::retry_failed(db.conn(), item.id) .await .unwrap(); // Should be pending again with retry_count = 1 let pending = queries::downloads::list(db.conn(), Some(DownloadStatus::Pending)) .await .unwrap(); assert_eq!(pending.len(), 1); assert_eq!(pending[0].retry_count, 1); } #[tokio::test] async fn test_wanted_item_status_updated_on_download() { use shanty_db::entities::wanted_item::{ItemType, WantedStatus}; let db = test_db().await; let dir = TempDir::new().unwrap(); let backend = MockBackend::new(false); let config = BackendConfig { output_dir: dir.path().to_owned(), format: AudioFormat::Opus, cookies_path: None, }; // Create a wanted item let wanted = queries::wanted::add(db.conn(), ItemType::Track, "Wanted Song", None, None, None, None) .await .unwrap(); assert_eq!(wanted.status, WantedStatus::Wanted); // Enqueue download linked to the wanted item queries::downloads::enqueue(db.conn(), "Wanted Song", Some(wanted.id), "mock") .await .unwrap(); // Process queue run_queue(db.conn(), &backend, &config, false).await.unwrap(); // Wanted item should now be Downloaded let updated = queries::wanted::get_by_id(db.conn(), wanted.id) .await .unwrap(); assert_eq!(updated.status, WantedStatus::Downloaded); }