use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("missing authentication token: set OP_SERVICE_ACCOUNT_TOKEN or use from_token()")]
MissingAuthToken,
#[error("invalid authentication token format")]
InvalidToken,
#[error("authentication failed: {message}")]
AuthenticationFailed {
message: String,
},
#[error("session error: {message}")]
SessionError {
message: String,
},
#[error("invalid secret reference '{reference}': {reason}")]
InvalidReference {
reference: String,
reason: String,
},
#[error("secret not found: {reference}")]
SecretNotFound {
reference: String,
},
#[error("access denied to vault: {vault}")]
AccessDenied {
vault: String,
},
#[error("network error: {message}")]
NetworkError {
message: String,
},
#[error("SDK error: {message}")]
SdkError {
message: String,
},
#[error("failed to load native library: {message}")]
LibraryLoadError {
message: String,
},
#[error("JSON error: {message}")]
JsonError {
message: String,
},
}
impl Error {
pub fn is_retriable(&self) -> bool {
matches!(
self,
Error::NetworkError { .. } | Error::SessionError { .. }
)
}
pub fn is_auth_error(&self) -> bool {
matches!(
self,
Error::MissingAuthToken
| Error::InvalidToken
| Error::AuthenticationFailed { .. }
| Error::AccessDenied { .. }
)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::JsonError {
message: err.to_string(),
}
}
}
impl From<libloading::Error> for Error {
fn from(err: libloading::Error) -> Self {
Error::LibraryLoadError {
message: err.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Error>();
}
#[test]
fn test_error_display_no_secrets() {
let error = Error::AuthenticationFailed {
message: "token expired".to_string(),
};
let display = error.to_string();
assert!(!display.contains("ops_"));
}
#[test]
fn test_is_retriable_network_error() {
let error = Error::NetworkError {
message: "connection reset".to_string(),
};
assert!(error.is_retriable());
}
#[test]
fn test_is_retriable_session_error() {
let error = Error::SessionError {
message: "session expired".to_string(),
};
assert!(error.is_retriable());
}
#[test]
fn test_is_retriable_non_retriable_errors() {
let non_retriable = vec![
Error::MissingAuthToken,
Error::InvalidToken,
Error::AuthenticationFailed {
message: "bad token".to_string(),
},
Error::InvalidReference {
reference: "op://x".to_string(),
reason: "too short".to_string(),
},
Error::SecretNotFound {
reference: "op://vault/item/field".to_string(),
},
Error::AccessDenied {
vault: "private".to_string(),
},
Error::SdkError {
message: "internal".to_string(),
},
Error::LibraryLoadError {
message: "not found".to_string(),
},
Error::JsonError {
message: "parse error".to_string(),
},
];
for error in non_retriable {
assert!(!error.is_retriable(), "{error:?} should not be retriable");
}
}
#[test]
fn test_is_auth_error_missing_token() {
assert!(Error::MissingAuthToken.is_auth_error());
}
#[test]
fn test_is_auth_error_invalid_token() {
assert!(Error::InvalidToken.is_auth_error());
}
#[test]
fn test_is_auth_error_auth_failed() {
let error = Error::AuthenticationFailed {
message: "token expired".to_string(),
};
assert!(error.is_auth_error());
}
#[test]
fn test_is_auth_error_access_denied() {
let error = Error::AccessDenied {
vault: "private-vault".to_string(),
};
assert!(error.is_auth_error());
}
#[test]
fn test_is_auth_error_non_auth_errors() {
let non_auth = vec![
Error::SessionError {
message: "session expired".to_string(),
},
Error::InvalidReference {
reference: "op://x".to_string(),
reason: "too short".to_string(),
},
Error::SecretNotFound {
reference: "op://vault/item/field".to_string(),
},
Error::NetworkError {
message: "timeout".to_string(),
},
Error::SdkError {
message: "internal".to_string(),
},
Error::LibraryLoadError {
message: "not found".to_string(),
},
Error::JsonError {
message: "parse error".to_string(),
},
];
for error in non_auth {
assert!(!error.is_auth_error(), "{error:?} should not be auth error");
}
}
#[test]
fn test_from_serde_json_error() {
let json_err = serde_json::from_str::<String>("not valid json").unwrap_err();
let error: Error = json_err.into();
assert!(matches!(error, Error::JsonError { .. }));
let display = error.to_string();
assert!(display.contains("JSON error"));
}
#[test]
fn test_from_libloading_error() {
let lib_err =
unsafe { libloading::Library::new("/nonexistent/path/to/lib.so") }.unwrap_err();
let error: Error = lib_err.into();
assert!(matches!(error, Error::LibraryLoadError { .. }));
let display = error.to_string();
assert!(display.contains("native library"));
}
#[test]
fn test_error_display_missing_auth_token() {
let error = Error::MissingAuthToken;
let display = error.to_string();
assert!(display.contains("missing authentication token"));
assert!(display.contains("OP_SERVICE_ACCOUNT_TOKEN"));
}
#[test]
fn test_error_display_invalid_token() {
let error = Error::InvalidToken;
let display = error.to_string();
assert!(display.contains("invalid authentication token format"));
}
#[test]
fn test_error_display_authentication_failed() {
let error = Error::AuthenticationFailed {
message: "token expired".to_string(),
};
let display = error.to_string();
assert!(display.contains("authentication failed"));
assert!(display.contains("token expired"));
}
#[test]
fn test_error_display_session_error() {
let error = Error::SessionError {
message: "connection lost".to_string(),
};
let display = error.to_string();
assert!(display.contains("session error"));
assert!(display.contains("connection lost"));
}
#[test]
fn test_error_display_invalid_reference() {
let error = Error::InvalidReference {
reference: "op://vault".to_string(),
reason: "missing item and field".to_string(),
};
let display = error.to_string();
assert!(display.contains("invalid secret reference"));
assert!(display.contains("op://vault"));
assert!(display.contains("missing item and field"));
}
#[test]
fn test_error_display_secret_not_found() {
let error = Error::SecretNotFound {
reference: "op://vault/item/field".to_string(),
};
let display = error.to_string();
assert!(display.contains("secret not found"));
assert!(display.contains("op://vault/item/field"));
}
#[test]
fn test_error_display_access_denied() {
let error = Error::AccessDenied {
vault: "private-vault".to_string(),
};
let display = error.to_string();
assert!(display.contains("access denied"));
assert!(display.contains("private-vault"));
}
#[test]
fn test_error_display_network_error() {
let error = Error::NetworkError {
message: "connection timed out".to_string(),
};
let display = error.to_string();
assert!(display.contains("network error"));
assert!(display.contains("connection timed out"));
}
#[test]
fn test_error_display_sdk_error() {
let error = Error::SdkError {
message: "internal SDK failure".to_string(),
};
let display = error.to_string();
assert!(display.contains("SDK error"));
assert!(display.contains("internal SDK failure"));
}
#[test]
fn test_error_display_library_load_error() {
let error = Error::LibraryLoadError {
message: "library not found".to_string(),
};
let display = error.to_string();
assert!(display.contains("native library"));
assert!(display.contains("library not found"));
}
#[test]
fn test_error_display_json_error() {
let error = Error::JsonError {
message: "unexpected token".to_string(),
};
let display = error.to_string();
assert!(display.contains("JSON error"));
assert!(display.contains("unexpected token"));
}
#[test]
fn test_error_debug_impl() {
let error = Error::SdkError {
message: "test".to_string(),
};
let debug_str = format!("{error:?}");
assert!(debug_str.contains("SdkError"));
}
}