use crate::testing_prelude::*;
use gazelle_api::{Format, Media, MockGazelleClient, Quality};
use qbittorrent_api::mock::MockQBittorrentClient;
use std::fs;
#[tokio::test]
async fn upload_command_succeeds_with_valid_source() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let host = build_upload_test_host(&transcode, &test_dir).await;
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
assert!(status.formats.is_some(), "should have format statuses");
let formats = status.formats.as_ref().expect("checked above");
assert!(!formats.is_empty(), "should have at least one format");
Ok(())
}
#[tokio::test]
async fn upload_command_dry_run_skips_api_call() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let host = HostBuilder::new()
.with_mock_api(transcode.album.clone())
.with_test_options(&test_dir)
.await
.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
..TargetOptions::default()
})
.with_options(UploadOptions {
dry_run: true,
..UploadOptions::default()
})
.expect_build();
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "dry run should succeed");
assert!(
status.formats.unwrap_or_default().is_empty(),
"dry run should not record formats"
);
assert!(status.errors.is_none(), "dry run should have no errors");
Ok(())
}
#[tokio::test]
async fn upload_command_torrent_client_injection_succeeds() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let mut builder = HostBuilder::new();
let _ = builder
.with_mock_api(transcode.album.clone())
.with_test_options(&test_dir)
.await
.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
..TargetOptions::default()
})
.with_options(QbitOptions::mock())
.with_options(QbitUploadOptions::mock())
.with_mock_torrent_client(MockQBittorrentClient::default());
let host = builder.expect_build();
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
assert!(
status.errors.is_none(),
"injection should succeed with mock client"
);
Ok(())
}
#[tokio::test]
async fn upload_command_skips_injection_when_not_configured() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let host = build_upload_test_host(&transcode, &test_dir).await;
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
assert!(
status.errors.is_none(),
"no warnings when injection not configured"
);
Ok(())
}
#[tokio::test]
async fn upload_command_missing_transcode_returns_error() -> Result<(), TestError> {
init_logger();
let album = AlbumProvider::get(SampleFormat::default()).await;
let test_dir = TestDirectory::new();
let host = HostBuilder::new()
.with_mock_api(album)
.with_test_options(&test_dir)
.await
.with_options(UploadOptions {
copy_torrent_to: Some(PathBuf::from("/nonexistent/path")),
..UploadOptions::default()
})
.with_options(SourceArg {
source: AlbumConfig::TORRENT_ID.to_string(),
})
.expect_build();
let command = host.services.get_required::<UploadCommand>();
let result = command.execute_cli().await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn upload_command_missing_torrent_fails() -> Result<(), TestError> {
init_logger();
let album = AlbumProvider::get(SampleFormat::default()).await;
let test_dir = TestDirectory::new();
let host = HostBuilder::new()
.with_mock_api(album)
.with_test_options(&test_dir)
.await
.expect_build();
let provider = host.services.get_required::<SourceProvider>();
let transcoder = host.services.get_required::<TranscodeCommand>();
let source = provider
.get(AlbumConfig::TORRENT_ID)
.await
.expect("should not fail")
.expect("should find source");
transcoder
.execute(&source)
.await
.expect("transcode should succeed");
let output_dir = test_dir.output();
delete_torrent_files(&output_dir);
let command = host.services.get_required::<UploadCommand>();
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(!status.success, "upload should fail with missing torrent");
assert!(status.errors.is_some(), "should have error messages");
Ok(())
}
fn delete_torrent_files(dir: &PathBuf) {
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "torrent") {
let _ = fs::remove_file(&path);
}
if path.is_dir() {
delete_torrent_files(&path);
}
}
}
}
#[tokio::test]
async fn upload_command_copies_to_content_dir() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let copy_target = TempDirectory::create("content_copy_target");
let host = HostBuilder::new()
.with_mock_api(transcode.album.clone())
.with_test_options(&test_dir)
.await
.with_options(SharedOptions {
content: vec![copy_target.to_path_buf(), SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
allow_existing: true, ..TargetOptions::default()
})
.with_options(UploadOptions {
copy_transcode_to_content_dir: true,
..UploadOptions::default()
})
.expect_build();
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
let entries: Vec<_> = fs::read_dir(©_target).expect("read dir").collect();
assert!(!entries.is_empty(), "should have copied transcodes");
Ok(())
}
#[tokio::test]
async fn upload_command_copies_to_custom_dir() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let copy_target = TempDirectory::create("custom_copy_target");
let host = HostBuilder::new()
.with_mock_api(transcode.album.clone())
.with_test_options(&test_dir)
.await
.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
..TargetOptions::default()
})
.with_options(UploadOptions {
copy_transcode_to: Some(copy_target.to_path_buf()),
..UploadOptions::default()
})
.expect_build();
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
let entries: Vec<_> = fs::read_dir(©_target).expect("read dir").collect();
assert!(!entries.is_empty(), "should have copied transcodes");
Ok(())
}
#[tokio::test]
async fn upload_command_copies_torrent_file() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let torrent_target = TempDirectory::create("torrent_copy_target");
let host = HostBuilder::new()
.with_mock_api(transcode.album.clone())
.with_test_options(&test_dir)
.await
.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
..TargetOptions::default()
})
.with_options(UploadOptions {
copy_torrent_to: Some(torrent_target.to_path_buf()),
..UploadOptions::default()
})
.expect_build();
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
let torrent_files: Vec<_> = fs::read_dir(&*torrent_target)
.expect("read dir")
.filter_map(Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "torrent"))
.collect();
assert!(!torrent_files.is_empty(), "should have copied torrents");
Ok(())
}
#[tokio::test]
async fn upload_command_api_failure_sets_error() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let torrent_path = SAMPLE_SOURCES_DIR.join(transcode.album.torrent_filename());
let torrent_bytes = fs::read(torrent_path).expect("torrent file should exist");
let mock = MockGazelleClient::new()
.with_get_torrent(Ok(gazelle_api::TorrentResponse {
group: gazelle_api::Group {
id: 123,
name: transcode.album.album.to_owned(),
year: transcode.album.year,
category_name: "Music".to_owned(),
music_info: Some(gazelle_api::Credits {
artists: vec![gazelle_api::Credit {
id: 1,
name: transcode.album.artist.to_owned(),
}],
..gazelle_api::Credits::default()
}),
..gazelle_api::Group::default()
},
torrent: gazelle_api::Torrent {
id: AlbumConfig::TORRENT_ID,
format: Format::FLAC,
encoding: Quality::Lossless,
media: Media::WEB,
..gazelle_api::Torrent::default()
},
}))
.with_get_torrent_group(Ok(gazelle_api::GroupResponse {
group: gazelle_api::Group {
id: 123,
name: transcode.album.album.to_owned(),
year: transcode.album.year,
category_name: "Music".to_owned(),
..gazelle_api::Group::default()
},
torrents: vec![gazelle_api::Torrent {
id: AlbumConfig::TORRENT_ID,
format: Format::FLAC,
encoding: Quality::Lossless,
media: Media::WEB,
..gazelle_api::Torrent::default()
}],
}))
.with_download_torrent(Ok(torrent_bytes))
.with_upload_torrent(Err(gazelle_api::GazelleError {
operation: gazelle_api::GazelleOperation::ReadFile,
source: gazelle_api::ErrorSource::Io(IoError::new(
ErrorKind::NotFound,
"Upload failed",
)),
}));
let mut builder = HostBuilder::new();
#[allow(clippy::as_conversions)]
let client: di::Ref<Box<dyn gazelle_api::GazelleClientTrait + Send + Sync>> =
di::Ref::new(Box::new(mock) as Box<dyn gazelle_api::GazelleClientTrait + Send + Sync>);
builder
.services
.add(di::singleton_as_self().from(move |_| client.clone()));
let _ = builder
.with_test_options(&test_dir)
.await
.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
..TargetOptions::default()
});
let host = builder.expect_build();
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(!status.success, "upload should fail on API error");
assert!(status.errors.is_some(), "should have errors");
Ok(())
}
#[tokio::test]
async fn upload_command_captures_response_ids() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let host = build_upload_test_host(&transcode, &test_dir).await;
let (source, command) = get_source_and_command(&host).await;
let result = command.execute(&source).await;
let status = UploadStatus::new(result);
assert!(status.success, "upload should succeed");
assert!(status.formats.is_some(), "should have format statuses");
for format_status in status.formats.as_ref().expect("checked") {
assert!(format_status.id > 0, "should have valid ID");
}
Ok(())
}
#[tokio::test]
async fn upload_command_skip_existing_copy_succeeds() -> Result<(), TestError> {
init_logger();
let transcode = TranscodeProvider::get(SampleFormat::default(), TargetFormat::_320).await;
let test_dir = TestDirectory::new();
let host = build_upload_test_host(&transcode, &test_dir).await;
let (source, command) = get_source_and_command(&host).await;
let result1 = command.execute(&source).await;
let status1 = UploadStatus::new(result1);
assert!(status1.success, "first upload should succeed");
let result2 = command.execute(&source).await;
let status2 = UploadStatus::new(result2);
assert!(status2.success, "second upload should succeed");
Ok(())
}
async fn build_upload_test_host(transcode: &TranscodeConfig, test_dir: &TestDirectory) -> Host {
HostBuilder::new()
.with_mock_api(transcode.album.clone())
.with_test_options(test_dir)
.await
.with_options(SharedOptions {
content: vec![SAMPLE_SOURCES_DIR.clone()],
output: SAMPLE_TRANSCODES_DIR.clone(),
..SharedOptions::mock()
})
.with_options(TargetOptions {
target: vec![transcode.target],
..TargetOptions::default()
})
.expect_build()
}
async fn get_source_and_command(host: &Host) -> (Source, di::Ref<UploadCommand>) {
let provider = host.services.get_required::<SourceProvider>();
let source = provider
.get(AlbumConfig::TORRENT_ID)
.await
.expect("should not fail")
.expect("should find source");
let command = host.services.get_required::<UploadCommand>();
(source, command)
}