use std::io::Error as IoError;
use {reqwest::Error as ReqwestError, thiserror::Error};
#[derive(Error, Debug)]
pub enum QobuzApiError {
#[error("Authentication failed: {message}")]
AuthenticationError {
message: String,
},
#[error("HTTP request failed: {0}")]
HttpError(#[from] ReqwestError),
#[error("I/O error: {0}")]
IoError(#[from] IoError),
#[error("API error {code}: {message}")]
ApiErrorResponse {
code: i32,
message: String,
status: String,
},
#[error("Failed to parse API response: {content}")]
ApiResponseParseError {
content: String,
source_info: Option<String>,
},
#[error("Service initialization failed: {message}")]
InitializationError {
message: String,
},
#[error("Invalid credentials: {message}")]
CredentialsError {
message: String,
},
#[error("Download failed: {message}")]
DownloadError {
message: String,
},
#[error("Metadata error: {0}")]
MetadataError(String),
#[error("{resource_type} not found: {resource_id}")]
ResourceNotFoundError {
resource_type: String,
resource_id: String,
},
#[error("Rate limited: {message}")]
RateLimitError {
message: String,
},
#[error("Invalid parameter: {message}")]
InvalidParameterError {
message: String,
},
#[error("Unexpected API response: {message}")]
UnexpectedApiResponseError {
message: String,
},
#[error("Download cancelled")]
Canceled,
}
#[cfg(test)]
mod tests {
use std::io::{Error, ErrorKind};
use {reqwest::Client, tokio::runtime::Runtime};
use crate::errors::QobuzApiError::{
self, ApiErrorResponse, ApiResponseParseError, AuthenticationError, Canceled,
CredentialsError, DownloadError, HttpError, InitializationError, InvalidParameterError,
IoError, MetadataError, RateLimitError, ResourceNotFoundError, UnexpectedApiResponseError,
};
fn assert_send_sync_static<T: Send + Sync + 'static>() {}
#[test]
fn all_variants_satisfy_send_sync_static() {
assert_send_sync_static::<QobuzApiError>();
}
#[test]
fn authentication_error_display() {
let err = AuthenticationError {
message: "bad token".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Authentication failed"));
assert!(msg.contains("bad token"));
}
#[test]
fn http_error_display() {
let Ok(rt) = Runtime::new() else {
return;
};
let result = rt.block_on(async { Client::new().get("http://localhost:1").send().await });
let Err(req_err) = result else { return };
let err = HttpError(req_err);
let msg = format!("{err}");
assert!(msg.contains("HTTP request failed"));
}
#[test]
fn io_error_display() {
let err = IoError(Error::new(ErrorKind::NotFound, "file missing"));
let msg = format!("{err}");
assert!(msg.contains("I/O error"));
}
#[test]
fn api_error_response_display() {
let err = ApiErrorResponse {
code: 403,
message: "Forbidden".into(),
status: "403".into(),
};
let msg = format!("{err}");
assert!(msg.contains("API error 403"));
assert!(msg.contains("Forbidden"));
}
#[test]
fn api_response_parse_error_display() {
let err = ApiResponseParseError {
content: "not json".into(),
source_info: Some("expected `{`".into()),
};
let msg = format!("{err}");
assert!(msg.contains("Failed to parse API response"));
assert!(msg.contains("not json"));
}
#[test]
fn initialization_error_display() {
let err = InitializationError {
message: "no app_id".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Service initialization failed"));
}
#[test]
fn credentials_error_display() {
let err = CredentialsError {
message: "missing email".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Invalid credentials"));
}
#[test]
fn download_error_display() {
let err = DownloadError {
message: "disk full".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Download failed"));
}
#[test]
fn metadata_error_display() {
let err = MetadataError("tag write failed".into());
let msg = format!("{err}");
assert!(msg.contains("Metadata error"));
}
#[test]
fn resource_not_found_error_display() {
let err = ResourceNotFoundError {
resource_type: "album".into(),
resource_id: "123".into(),
};
let msg = format!("{err}");
assert!(msg.contains("album not found"));
assert!(msg.contains("123"));
}
#[test]
fn rate_limit_error_display() {
let err = RateLimitError {
message: "too many requests".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Rate limited"));
}
#[test]
fn invalid_parameter_error_display() {
let err = InvalidParameterError {
message: "negative id".into(),
};
let msg = format!("{err}");
assert!(msg.contains("Invalid parameter"));
}
#[test]
fn unexpected_api_response_error_has_remediation() {
let err = UnexpectedApiResponseError {
message: "missing field".into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("unexpected") || msg.contains("response") || msg.contains("missing"));
}
#[test]
fn canceled_error_display() {
let err = Canceled;
let msg = format!("{err}");
assert_eq!(msg, "Download cancelled");
}
#[test]
fn sc006_authentication_error_has_remediation() {
let err = AuthenticationError {
message: "Invalid credentials. Check your QOBUZ_EMAIL and QOBUZ_PASSWORD environment \
variables"
.into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("credentials") || msg.contains("check") || msg.contains("verify"));
}
#[test]
fn sc006_credentials_error_has_remediation() {
let err = CredentialsError {
message: "Failed to extract credentials from web player. Configure QOBUZ_APP_ID and \
QOBUZ_APP_SECRET manually"
.into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("configure") || msg.contains("manual") || msg.contains("set"));
}
#[test]
fn sc006_resource_not_found_error_has_remediation() {
let err = ResourceNotFoundError {
resource_type: "album".into(),
resource_id: "xyz".into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("not found"));
}
#[test]
fn sc006_download_error_has_remediation() {
let err = DownloadError {
message: "Failed to write file. Check disk space and permissions".into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("check") || msg.contains("disk") || msg.contains("permission"));
}
#[test]
fn sc006_initialization_error_has_remediation() {
let err = InitializationError {
message: "app_id and app_secret must be non-empty. Verify your credentials \
configuration"
.into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("verify") || msg.contains("check") || msg.contains("must"));
}
#[test]
fn sc006_rate_limit_error_has_remediation() {
let err = RateLimitError {
message: "Rate limited, retry 3/3. Wait before retrying".into(),
};
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("wait") || msg.contains("retry") || msg.contains("limited"));
}
#[test]
fn sc006_metadata_error_has_remediation() {
let err = MetadataError(
"Probe open failed. Verify the file exists and is a valid audio format".into(),
);
let msg = format!("{err}").to_lowercase();
assert!(msg.contains("verify") || msg.contains("valid") || msg.contains("check"));
}
}