Initial commit

This commit is contained in:
Connor Johnstone
2026-03-17 15:31:29 -04:00
commit d5641493b9
11 changed files with 1458 additions and 0 deletions

239
tests/integration.rs Normal file
View File

@@ -0,0 +1,239 @@
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<Vec<SearchResult>> {
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<DownloadResult> {
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, 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);
}