use crate::testing_prelude::*;
use rogue_logging::{TimeFormat, Verbosity};
use std::fs::read_to_string;
#[test]
fn batch_options_default_values() {
let result = BatchOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn file_options_default_values() {
let result = FileOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn target_options_default_values() {
let result = TargetOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn spectrogram_options_default_values() {
let result = SpectrogramOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn upload_options_default_values() {
let result = UploadOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn verify_options_default_values() {
let result = VerifyOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn cache_options_default_values() {
let resolved = CacheOptionsPartial::default().resolve_without_validation();
if is_docker() {
assert_eq!(resolved.cache, PathBuf::from("/cache"));
} else {
assert!(
resolved.cache.ends_with("caesura"),
"expected cache path to end with 'caesura', got: {:?}",
resolved.cache
);
}
}
#[test]
fn copy_options_default_values() {
let result = CopyOptionsPartial::default().resolve_without_validation();
assert_yaml_snapshot!(result);
}
#[test]
fn shared_options_calculates_indexer_from_red_announce_url() {
let resolved = SharedOptionsPartial {
announce_url: Some("https://flacsfor.me/abc123/announce".to_owned()),
..SharedOptionsPartial::default()
}
.resolve_without_validation();
assert_eq!(resolved.indexer, "red");
assert_eq!(resolved.indexer_url, "https://redacted.sh");
}
#[test]
fn shared_options_calculates_indexer_from_ops_announce_url() {
let resolved = SharedOptionsPartial {
announce_url: Some("https://home.opsfet.ch/abc123/announce".to_owned()),
..SharedOptionsPartial::default()
}
.resolve_without_validation();
assert_eq!(resolved.indexer, "ops");
assert_eq!(resolved.indexer_url, "https://orpheus.network");
}
#[test]
fn shared_options_does_not_override_explicit_indexer() {
let resolved = SharedOptionsPartial {
announce_url: Some("https://flacsfor.me/abc123/announce".to_owned()),
indexer: Some("custom".to_owned()),
..SharedOptionsPartial::default()
}
.resolve_without_validation();
assert_eq!(resolved.indexer, "custom");
assert_eq!(resolved.indexer_url, "");
}
#[test]
fn shared_options_does_not_override_explicit_indexer_url() {
let resolved = SharedOptionsPartial {
announce_url: Some("https://flacsfor.me/abc123/announce".to_owned()),
indexer_url: Some("https://custom.example.com".to_owned()),
..SharedOptionsPartial::default()
}
.resolve_without_validation();
assert_eq!(resolved.indexer, "red");
assert_eq!(resolved.indexer_url, "https://custom.example.com");
}
#[test]
fn shared_options_unknown_announce_url_leaves_indexer_empty() {
let resolved = SharedOptionsPartial {
announce_url: Some("https://unknown.tracker.com/announce".to_owned()),
..SharedOptionsPartial::default()
}
.resolve_without_validation();
assert_eq!(resolved.indexer, "");
assert_eq!(resolved.indexer_url, "");
}
#[test]
fn shared_options_explicit_empty_indexer_bypasses_required() {
let result = SharedOptionsPartial {
announce_url: Some("https://flacsfor.me/abc/announce".to_owned()),
api_key: Some("key".to_owned()),
indexer: Some(String::new()), content: Some(vec![PathBuf::from(".")]),
output: Some(PathBuf::from(".")),
..SharedOptionsPartial::default()
}
.resolve();
let errors = result.expect_err("should fail due to indexer_url");
assert!(
errors
.iter()
.any(|e| matches!(e, OptionRule::NotSet(name) if name == "Indexer url"))
);
}
#[test]
fn batch_options_rejects_upload_without_transcode() {
let result = BatchOptionsPartial {
upload: Some(true),
transcode: None,
..BatchOptionsPartial::default()
}
.resolve();
let errors = result.expect_err("should reject upload without transcode");
assert!(
errors
.iter()
.any(|e| matches!(e, OptionRule::Dependent(_, _)))
);
}
#[test]
fn batch_options_accepts_upload_with_transcode() {
let result = BatchOptionsPartial {
upload: Some(true),
transcode: Some(true),
..BatchOptionsPartial::default()
}
.resolve();
assert!(result.is_ok());
}
#[test]
fn batch_options_rejects_invalid_wait_duration() {
let result = BatchOptionsPartial {
wait_before_upload: Some("invalid".to_owned()),
..BatchOptionsPartial::default()
}
.resolve();
let errors = result.expect_err("should reject invalid duration");
assert!(
errors
.iter()
.any(|e| matches!(e, OptionRule::DurationInvalid(_, _)))
);
}
#[test]
fn batch_options_accepts_valid_wait_duration() {
let result = BatchOptionsPartial {
wait_before_upload: Some("5m30s".to_owned()),
..BatchOptionsPartial::default()
}
.resolve();
assert!(result.is_ok());
}
#[test]
fn target_options_rejects_empty_target_list() {
let result = TargetOptionsPartial {
target: Some(vec![]),
..TargetOptionsPartial::default()
}
.resolve();
let errors = result.expect_err("should reject empty target list");
assert!(errors.iter().any(|e| matches!(e, OptionRule::IsEmpty(_))));
}
#[test]
fn spectrogram_options_rejects_empty_size_list() {
let result = SpectrogramOptionsPartial {
spectrogram_size: Some(vec![]),
}
.resolve();
let errors = result.expect_err("should reject empty size list");
assert!(errors.iter().any(|e| matches!(e, OptionRule::IsEmpty(_))));
}
#[test]
fn batch_options_yaml_round_trip() {
let original = BatchOptionsPartial {
spectrogram: Some(true),
transcode: Some(true),
upload: Some(true),
limit: Some(5),
no_limit: Some(false),
wait_before_upload: Some("10m".to_owned()),
..BatchOptionsPartial::default()
};
let yaml = serde_yaml::to_string(&original).expect("should serialize");
let parsed: BatchOptionsPartial = serde_yaml::from_str(&yaml).expect("should deserialize");
assert_eq!(original.spectrogram, parsed.spectrogram);
assert_eq!(original.transcode, parsed.transcode);
assert_eq!(original.upload, parsed.upload);
assert_eq!(original.limit, parsed.limit);
assert_eq!(original.wait_before_upload, parsed.wait_before_upload);
}
#[test]
fn target_options_yaml_round_trip() {
let original = TargetOptionsPartial {
target: Some(vec![TargetFormat::Flac, TargetFormat::V0]),
allow_existing: Some(true),
sox_random_dither: Some(true),
exclude_vorbis_comments: Some(TargetOptions::default_exclude_vorbis_comments()),
};
let yaml = serde_yaml::to_string(&original).expect("should serialize");
let parsed: TargetOptionsPartial = serde_yaml::from_str(&yaml).expect("should deserialize");
assert_eq!(original.target, parsed.target);
assert_eq!(original.allow_existing, parsed.allow_existing);
assert_eq!(original.sox_random_dither, parsed.sox_random_dither);
assert_eq!(
original.exclude_vorbis_comments,
parsed.exclude_vorbis_comments
);
}
#[test]
fn shared_options_yaml_round_trip() {
let original = SharedOptionsPartial {
announce_url: Some("https://example.com/announce".to_owned()),
api_key: Some("secret_key".to_owned()),
indexer: Some("red".to_owned()),
indexer_url: Some("https://redacted.sh".to_owned()),
content: Some(vec![PathBuf::from("/data/music")]),
output: Some(PathBuf::from("/data/output")),
verbosity: Some(Verbosity::Debug),
log_time: Some(TimeFormat::Elapsed),
};
let yaml = serde_yaml::to_string(&original).expect("should serialize");
let parsed: SharedOptionsPartial = serde_yaml::from_str(&yaml).expect("should deserialize");
assert_eq!(original.announce_url, parsed.announce_url);
assert_eq!(original.api_key, parsed.api_key);
assert_eq!(original.indexer, parsed.indexer);
assert_eq!(original.content, parsed.content);
assert_eq!(original.verbosity, parsed.verbosity);
}
#[test]
fn merge_cli_overrides_yaml() {
let mut cli = BatchOptionsPartial {
limit: Some(10),
..BatchOptionsPartial::default()
};
let yaml = BatchOptionsPartial {
limit: Some(5),
spectrogram: Some(true),
transcode: Some(true),
..BatchOptionsPartial::default()
};
cli.merge(yaml);
assert_eq!(cli.limit, Some(10));
assert_eq!(cli.spectrogram, Some(true));
assert_eq!(cli.transcode, Some(true));
}
#[test]
fn merge_yaml_fills_none_values() {
let mut cli = BatchOptionsPartial::default();
let yaml = BatchOptionsPartial {
limit: Some(5),
spectrogram: Some(true),
..BatchOptionsPartial::default()
};
cli.merge(yaml);
assert_eq!(cli.limit, Some(5));
assert_eq!(cli.spectrogram, Some(true));
}
#[test]
fn merge_does_not_override_set_values() {
let mut cli = SharedOptionsPartial {
indexer: Some("custom".to_owned()),
verbosity: Some(Verbosity::Trace),
..SharedOptionsPartial::default()
};
let yaml = SharedOptionsPartial {
indexer: Some("red".to_owned()),
verbosity: Some(Verbosity::Info),
api_key: Some("from_yaml".to_owned()),
..SharedOptionsPartial::default()
};
cli.merge(yaml);
assert_eq!(cli.indexer, Some("custom".to_owned()));
assert_eq!(cli.verbosity, Some(Verbosity::Trace));
assert_eq!(cli.api_key, Some("from_yaml".to_owned()));
}
#[test]
fn with_options_overrides_default_registration() {
use crate::hosting::HostBuilder;
let mut builder = HostBuilder::new();
let custom_output = PathBuf::from("/custom/output/path");
let host = builder
.with_options(SharedOptions {
output: custom_output.clone(),
..SharedOptions::default()
})
.expect_build();
let options = host.services.get_required::<SharedOptions>();
assert_eq!(options.output, custom_output);
}
#[test]
fn shared_options_validate_missing_fields() {
let result = SharedOptionsPartial {
content: Some(vec![PathBuf::from("./nonexistent-content")]),
output: Some(PathBuf::from("./nonexistent-output")),
..SharedOptionsPartial::default()
}
.resolve();
let errors = result.expect_err("should reject missing required fields");
assert_yaml_snapshot!(errors);
}
#[test]
fn shared_options_validate_invalid_urls() {
let result = SharedOptionsPartial {
api_key: Some("key".to_owned()),
indexer: Some("red".to_owned()),
indexer_url: Some("not-a-url".to_owned()),
announce_url: Some("https://example.com/announce/".to_owned()),
content: Some(vec![PathBuf::from(".")]),
output: Some(PathBuf::from(".")),
..SharedOptionsPartial::default()
}
.resolve();
let errors = result.expect_err("should reject invalid URLs");
assert_yaml_snapshot!(errors);
}
#[test]
fn shared_options_validate_no_errors_when_valid() {
let result = SharedOptionsPartial {
api_key: Some("key".to_owned()),
indexer: Some("red".to_owned()),
indexer_url: Some("https://redacted.sh".to_owned()),
announce_url: Some("https://flacsfor.me/abc/announce".to_owned()),
content: Some(vec![PathBuf::from(".")]),
output: Some(PathBuf::from(".")),
..SharedOptionsPartial::default()
}
.resolve();
assert!(result.is_ok());
}
#[test]
fn queue_rm_args_rejects_invalid_hash() {
let result = QueueRemoveArgsPartial {
queue_rm_hash: Some("not-a-valid-hash".to_owned()),
}
.resolve();
let errors = result.expect_err("should reject invalid hash");
assert!(
errors
.iter()
.any(|e| matches!(e, OptionRule::HashInvalid(_, _)))
);
}
#[test]
fn queue_rm_args_rejects_missing_hash() {
let result = QueueRemoveArgsPartial {
queue_rm_hash: None,
}
.resolve();
let errors = result.expect_err("should reject missing hash");
assert!(
errors
.iter()
.any(|e| matches!(e, OptionRule::HashInvalid(_, _)))
);
}
#[test]
fn queue_rm_args_accepts_valid_hash() {
let result = QueueRemoveArgsPartial {
queue_rm_hash: Some("a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2".to_owned()),
}
.resolve();
assert!(result.is_ok());
}
#[test]
fn cache_options_rejects_dollar_home() {
let path = "$HOME/.cache/caesura";
let errors = CacheOptionsPartial {
cache: Some(PathBuf::from(path)),
}
.resolve()
.expect_err("should reject");
assert_eq!(
errors,
vec![DoesNotExist(CACHE_DIR_LABEL.to_owned(), path.to_owned())]
);
}
#[test]
fn cache_options_expands_tilde() {
let result = CacheOptionsPartial {
cache: Some(PathBuf::from("~")),
}
.resolve();
assert!(result.is_ok(), "~ should expand to home directory");
}
#[test]
fn shared_options_rejects_dollar_home_output() {
let path = "$HOME/.local/share/caesura/output";
let errors = valid_shared_options_with_output(path)
.resolve()
.expect_err("should reject");
assert_eq!(
errors,
vec![DoesNotExist(OUTPUT_DIR_LABEL.to_owned(), path.to_owned())]
);
}
#[test]
fn shared_options_expands_tilde_output() {
let result = valid_shared_options_with_output("~").resolve();
assert!(result.is_ok(), "~ should expand to home directory");
}
#[test]
fn shared_options_rejects_dollar_home_content() {
let path = "$HOME/music";
let errors = valid_shared_options_with_content(path)
.resolve()
.expect_err("should reject");
assert_eq!(
errors,
vec![DoesNotExist(CONTENT_DIR_LABEL.to_owned(), path.to_owned())]
);
}
#[test]
fn shared_options_expands_tilde_content() {
let result = valid_shared_options_with_content("~").resolve();
assert!(result.is_ok(), "~ should expand to home directory");
}
#[test]
fn config_read_to_string_fails_on_tilde() {
assert!(read_to_string("~/.config/caesura/config.yml").is_err());
}
#[test]
fn config_read_to_string_fails_on_dollar_home() {
assert!(read_to_string("$HOME/.config/caesura/config.yml").is_err());
}
#[test]
fn config_options_rejects_dollar_home() {
let path = "$HOME/.config/caesura/config.yml";
let errors = ConfigOptionsPartial {
config: Some(PathBuf::from(path)),
}
.resolve()
.expect_err("should reject");
assert_eq!(
errors,
vec![DoesNotExist(CONFIG_FILE_LABEL.to_owned(), path.to_owned())]
);
}
#[test]
fn config_options_expands_tilde() {
let partial = ConfigOptionsPartial {
config: Some(PathBuf::from("~/.config/caesura/nonexistent.yml")),
};
let errors = partial.resolve().expect_err("file should not exist");
assert_eq!(errors.len(), 1);
assert!(matches!(
errors.first(),
Some(DoesNotExist(_, path)) if !path.starts_with('~')
));
}
fn valid_shared_options_with_output(output: &str) -> SharedOptionsPartial {
SharedOptionsPartial {
announce_url: Some("https://flacsfor.me/abc/announce".to_owned()),
api_key: Some("key".to_owned()),
content: Some(vec![PathBuf::from(".")]),
output: Some(PathBuf::from(output)),
..SharedOptionsPartial::default()
}
}
fn valid_shared_options_with_content(content: &str) -> SharedOptionsPartial {
SharedOptionsPartial {
announce_url: Some("https://flacsfor.me/abc/announce".to_owned()),
api_key: Some("key".to_owned()),
content: Some(vec![PathBuf::from(content)]),
output: Some(PathBuf::from(".")),
..SharedOptionsPartial::default()
}
}
#[test]
fn inspect_arg_rejects_nonexistent_path() {
let result = InspectArgPartial {
inspect_path: Some(PathBuf::from("/nonexistent/path/that/does/not/exist")),
}
.resolve();
let errors = result.expect_err("should reject nonexistent path");
assert!(
errors
.iter()
.any(|e| matches!(e, OptionRule::DoesNotExist(_, _)))
);
}