//! Error types for the SyncKit client SDK. use thiserror::Error; /// All errors that can occur in the SyncKit client. #[derive(Debug, Error)] pub enum SyncKitError { /// Network-level failure: connection refused, timeout, DNS resolution, TLS handshake. #[error("HTTP request failed: {0}")] Http(#[from] reqwest::Error), /// Server returned a non-success HTTP status (4xx or 5xx). Status and message /// extracted from response. #[error("Server returned {status}: {message}")] Server { status: u16, message: String, /// Parsed `Retry-After` header value in seconds (429 responses only). /// Hidden from public API — used internally by the retry loop. #[doc(hidden)] retry_after_secs: Option, }, /// Response body could not be parsed as expected JSON type. #[error("JSON serialization error: {0}")] Json(#[from] serde_json::Error), /// Encryption method called before `setup_encryption_new` or /// `setup_encryption_existing`. #[error("Encryption not initialized — call setup_encryption first")] NoMasterKey, /// `unwrap_master_key` or `decrypt_data`/`decrypt_bytes` failed. Wrong /// password or corrupted ciphertext. #[error("Wrong password or corrupted key envelope")] DecryptionFailed, /// Key envelope JSON has an unrecognized version or missing fields. #[error("Invalid key envelope: {0}")] InvalidEnvelope(String), /// Argon2 key derivation or AEAD encryption/decryption failed (corrupt /// data, wrong parameters). #[error("Encryption error: {0}")] Crypto(String), /// Base64 decoding of encrypted payloads failed. #[error("Base64 decode error: {0}")] Base64(#[from] base64::DecodeError), /// API method called before `authenticate` or `restore_session`. #[error("Not authenticated — call authenticate first")] NotAuthenticated, /// JWT `exp` claim is within 30 seconds of current time. Caller should /// re-authenticate. #[error("Token expired — re-authenticate to continue syncing")] TokenExpired, /// Internal error that should not occur in normal operation. #[error("Internal error: {0}")] Internal(String), /// OS keychain operation failed (store, load, or delete). Platform-specific. #[cfg(feature = "keychain")] #[error("Keychain error: {0}")] Keychain(String), } #[cfg(feature = "keychain")] impl From for SyncKitError { fn from(e: keyring::Error) -> Self { SyncKitError::Keychain(e.to_string()) } } /// Convenience alias. pub type Result = std::result::Result; #[cfg(test)] mod tests { use super::*; use std::error::Error; #[test] fn error_is_send_and_sync() { fn assert_send_sync() {} assert_send_sync::(); } #[test] fn display_all_variants() { let cases: Vec<(SyncKitError, &str)> = vec![ ( SyncKitError::Server { status: 500, message: "boom".into(), retry_after_secs: None }, "Server returned 500: boom", ), (SyncKitError::NoMasterKey, "Encryption not initialized"), (SyncKitError::DecryptionFailed, "Wrong password"), (SyncKitError::InvalidEnvelope("bad".into()), "Invalid key envelope: bad"), (SyncKitError::Crypto("aead".into()), "Encryption error: aead"), (SyncKitError::NotAuthenticated, "Not authenticated"), (SyncKitError::TokenExpired, "Token expired"), (SyncKitError::Internal("oops".into()), "Internal error: oops"), ]; for (err, expected_substring) in cases { let msg = err.to_string(); assert!( msg.contains(expected_substring), "Expected '{expected_substring}' in '{msg}'" ); } } #[test] fn debug_format_no_panic() { let variants: Vec = vec![ SyncKitError::Server { status: 500, message: "err".into(), retry_after_secs: None }, SyncKitError::NoMasterKey, SyncKitError::DecryptionFailed, SyncKitError::InvalidEnvelope("v".into()), SyncKitError::Crypto("c".into()), SyncKitError::NotAuthenticated, SyncKitError::TokenExpired, SyncKitError::Internal("i".into()), ]; for v in variants { let debug = format!("{v:?}"); assert!(!debug.is_empty()); } } #[test] fn source_json_error() { let inner = serde_json::from_str::("bad").unwrap_err(); let err = SyncKitError::Json(inner); assert!(err.source().is_some(), "Json variant should chain source"); } #[test] fn source_base64_error() { use base64::Engine; let inner = base64::engine::general_purpose::STANDARD .decode("!!!invalid!!!") .unwrap_err(); let err = SyncKitError::Base64(inner); assert!(err.source().is_some(), "Base64 variant should chain source"); } #[test] fn source_none_for_leaf_variants() { assert!(SyncKitError::NoMasterKey.source().is_none()); assert!(SyncKitError::DecryptionFailed.source().is_none()); assert!(SyncKitError::NotAuthenticated.source().is_none()); assert!(SyncKitError::TokenExpired.source().is_none()); assert!(SyncKitError::InvalidEnvelope("x".into()).source().is_none()); assert!(SyncKitError::Crypto("x".into()).source().is_none()); assert!(SyncKitError::Internal("x".into()).source().is_none()); let server = SyncKitError::Server { status: 500, message: "x".into(), retry_after_secs: None }; assert!(server.source().is_none()); } #[test] fn server_error_empty_message() { let err = SyncKitError::Server { status: 503, message: String::new(), retry_after_secs: None }; let msg = err.to_string(); assert!(msg.contains("503")); assert!(msg.contains(": "), "Should have colon separator even with empty message"); } #[test] fn server_error_very_long_message() { let long = "x".repeat(1_000_000); let err = SyncKitError::Server { status: 500, message: long, retry_after_secs: None }; let msg = err.to_string(); assert!(msg.contains("500")); assert!(msg.len() > 1_000_000); } #[test] fn invalid_envelope_preserves_detail() { let detail = "unsupported version 99"; let err = SyncKitError::InvalidEnvelope(detail.into()); assert!(err.to_string().contains(detail)); } #[test] fn internal_preserves_detail() { let detail = "unexpected state in push handler"; let err = SyncKitError::Internal(detail.into()); assert!(err.to_string().contains(detail)); } }