Ctrl K
ComunidadGuías…

src/icloud/error.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ICloudError {
    #[error("Could not connect to iCloud: {0}")]
    Connection(String),
    #[error(
        "iCloud Photos is not available through Apple's web API ({code}): {reason}\n\n\
         This usually means one of:\n  \
         1. Advanced Data Protection (ADP) is enabled, which blocks third-party iCloud access.\n     \
            To fix, change both settings on your iPhone/iPad:\n     \
            - Disable ADP: Settings > Apple ID > iCloud > Advanced Data Protection\n     \
            - Enable web access: Settings > Apple ID > iCloud > Access iCloud Data on the Web\n  \
         2. iCloud setup is incomplete.\n     \
            Log in to https://icloud.com/ and finish setting up iCloud Photos."
    )]
    ServiceNotActivated { code: String, reason: String },
    /// CloudKit rejected the request with an auth-class HTTP status. Typically
    /// 401 (stale session), 403 (rotated routing cookie or an ADP edge case
    /// not caught earlier), or - rarely - another 4xx that maps to the same
    /// recovery path. The caller should invalidate any cached session data
    /// and re-authenticate with SRP before retrying.
    #[error("Your iCloud session expired (CloudKit HTTP {status})")]
    SessionExpired { status: u16 },
    /// CloudKit returned HTTP 421 Misdirected Request. The HTTP/2 connection
    /// was routed to the wrong CloudKit partition; the caller should reset
    /// the connection pool and retry on a fresh connection.
    #[error("Apple routed the iCloud request to the wrong CloudKit partition (HTTP 421)")]
    MisdirectedRequest,
    #[error(transparent)]
    Http(Box<reqwest::Error>),
    #[error(transparent)]
    Io(Box<std::io::Error>),
    #[error(transparent)]
    Json(Box<serde_json::Error>),
}

impl ICloudError {
    /// True if the error means kei should invalidate the session cache, force
    /// SRP re-authentication, and retry. Both `SessionExpired` (CloudKit
    /// 401/403) and `MisdirectedRequest` (persistent CloudKit 421) typically
    /// indicate stale session routing state that only SRP can re-mint.
    pub fn is_session_error(&self) -> bool {
        matches!(self, Self::SessionExpired { .. } | Self::MisdirectedRequest)
    }
}

impl From<reqwest::Error> for ICloudError {
    fn from(e: reqwest::Error) -> Self {
        Self::Http(Box::new(e))
    }
}

impl From<std::io::Error> for ICloudError {
    fn from(e: std::io::Error) -> Self {
        Self::Io(Box::new(e))
    }
}

impl From<serde_json::Error> for ICloudError {
    fn from(e: serde_json::Error) -> Self {
        Self::Json(Box::new(e))
    }
}

const _: () = assert!(std::mem::size_of::<ICloudError>() <= 80);

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn connection_display_contains_message() {
        let err = ICloudError::Connection("timeout reached".into());
        let display = err.to_string();
        assert!(
            display.contains("timeout reached"),
            "expected display to contain the message, got: {display}"
        );
    }

    #[test]
    fn service_not_activated_display_mentions_code_reason_and_adp() {
        let err = ICloudError::ServiceNotActivated {
            code: "ZONE_NOT_FOUND".into(),
            reason: "service unavailable".into(),
        };
        let display = err.to_string();
        assert!(
            display.contains("ZONE_NOT_FOUND"),
            "expected display to contain the code, got: {display}"
        );
        assert!(
            display.contains("service unavailable"),
            "expected display to contain the reason, got: {display}"
        );
        assert!(
            display.contains("Advanced Data Protection"),
            "expected display to mention Advanced Data Protection, got: {display}"
        );
    }

    #[test]
    fn from_io_error_creates_io_variant() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
        let err: ICloudError = io_err.into();
        assert!(
            matches!(err, ICloudError::Io(_)),
            "expected Io variant, got: {err:?}"
        );
    }

    #[test]
    fn from_serde_json_error_creates_json_variant() {
        let json_err = serde_json::from_str::<serde_json::Value>("not valid json").unwrap_err();
        let err: ICloudError = json_err.into();
        assert!(
            matches!(err, ICloudError::Json(_)),
            "expected Json variant, got: {err:?}"
        );
    }

    #[test]
    fn misdirected_request_is_distinct_variant() {
        let err = ICloudError::MisdirectedRequest;
        assert!(
            matches!(err, ICloudError::MisdirectedRequest),
            "dedicated variant so callers can reset pool and retry"
        );
        let display = err.to_string();
        assert!(display.contains("421"), "display mentions 421: {display}");
    }

    #[test]
    fn session_expired_is_distinct_variant() {
        let err = ICloudError::SessionExpired { status: 401 };
        assert!(
            matches!(err, ICloudError::SessionExpired { .. }),
            "dedicated variant so callers can trigger SRP re-auth"
        );
        let display = err.to_string();
        assert!(display.contains("401"), "display mentions 401: {display}");
    }

    #[test]
    fn session_expired_display_renders_actual_status() {
        // A 403 that maps to SessionExpired (e.g. bare CloudKit 403) must
        // surface as "HTTP 403" so the diagnostic matches the on-wire status.
        let err = ICloudError::SessionExpired { status: 403 };
        assert!(
            err.to_string().contains("HTTP 403"),
            "403 must render as HTTP 403, not HTTP 401: {err}"
        );
    }

    #[test]
    fn is_session_error_true_for_session_expired_and_misdirected() {
        assert!(ICloudError::SessionExpired { status: 401 }.is_session_error());
        assert!(ICloudError::SessionExpired { status: 403 }.is_session_error());
        assert!(ICloudError::MisdirectedRequest.is_session_error());
    }

    #[test]
    fn is_session_error_false_for_other_variants() {
        assert!(!ICloudError::Connection("x".into()).is_session_error());
        assert!(
            !ICloudError::ServiceNotActivated {
                code: "ADP".into(),
                reason: "y".into()
            }
            .is_session_error()
        );
    }
}