guacamole-client 0.5.1

Rust client library for the Guacamole REST API
Documentation
use std::time::Duration;

use reqwest::StatusCode;

/// Errors returned by the Guacamole client.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// An underlying HTTP request failed.
    #[error("HTTP request failed: {0}")]
    Request(#[from] reqwest::Error),

    /// No authentication token is available; call `login()` first.
    #[error("not authenticated — call login() first")]
    NotAuthenticated,

    /// The supplied data source name is invalid.
    #[error("invalid data source: {0}")]
    InvalidDataSource(String),

    /// The supplied username is invalid.
    #[error("invalid username: {0}")]
    InvalidUsername(String),

    /// The supplied connection ID is invalid.
    #[error("invalid connection ID: {0}")]
    InvalidConnectionId(String),

    /// The supplied sharing profile ID is invalid.
    #[error("invalid sharing profile ID: {0}")]
    InvalidSharingProfileId(String),

    /// The supplied user group ID is invalid.
    #[error("invalid user group ID: {0}")]
    InvalidUserGroupId(String),

    /// The supplied connection group ID is invalid.
    #[error("invalid connection group ID: {0}")]
    InvalidConnectionGroupId(String),

    /// The supplied tunnel ID is invalid.
    #[error("invalid tunnel ID: {0}")]
    InvalidTunnelId(String),

    /// The authentication token is invalid (contains unsafe characters).
    #[error("invalid auth token: {0}")]
    InvalidToken(String),

    /// A query parameter value is invalid (contains unsafe characters).
    #[error("invalid query parameter `{name}`: {reason}")]
    InvalidQueryParam {
        /// Name of the query parameter.
        name: String,
        /// Reason the value was rejected.
        reason: String,
    },

    /// The API returned `401 Unauthorized`.
    #[error("authentication failed (401)")]
    Unauthorized {
        /// Response body from the server.
        body: String,
    },

    /// The API returned `403 Forbidden`.
    #[error("access denied (403)")]
    Forbidden {
        /// Response body from the server.
        body: String,
    },

    /// The requested resource was not found (`404`).
    #[error("resource not found (404): {resource}")]
    NotFound {
        /// Name of the resource that was not found.
        resource: String,
        /// Response body from the server.
        body: String,
    },

    /// The API returned `429 Too Many Requests`.
    #[error("rate limited (429)")]
    RateLimited {
        /// Value of the `Retry-After` header, if present.
        retry_after: Option<Duration>,
    },

    /// Any other non-success HTTP status code.
    #[error("API error (HTTP {status}): {body}")]
    Api {
        /// HTTP status code.
        status: StatusCode,
        /// Response body from the server.
        body: String,
    },
}

/// A specialized [`Result`](std::result::Result) type for Guacamole client operations.
pub type Result<T> = std::result::Result<T, Error>;

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

    #[test]
    fn error_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}
        assert_send_sync::<Error>();
    }

    #[test]
    fn error_implements_std_error() {
        fn assert_std_error<T: std::error::Error>() {}
        assert_std_error::<Error>();
    }

    #[test]
    fn display_not_authenticated() {
        let err = Error::NotAuthenticated;
        let msg = err.to_string();
        assert!(msg.contains("not authenticated"), "got: {msg}");
    }

    #[test]
    fn display_invalid_data_source() {
        let err = Error::InvalidDataSource("bad/ds".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid data source"), "got: {msg}");
        assert!(msg.contains("bad/ds"), "got: {msg}");
    }

    #[test]
    fn display_invalid_username() {
        let err = Error::InvalidUsername("bad\0user".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid username"), "got: {msg}");
    }

    #[test]
    fn display_invalid_connection_id() {
        let err = Error::InvalidConnectionId("abc".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid connection ID"), "got: {msg}");
        assert!(msg.contains("abc"), "got: {msg}");
    }

    #[test]
    fn display_invalid_sharing_profile_id() {
        let err = Error::InvalidSharingProfileId("bad".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid sharing profile ID"), "got: {msg}");
    }

    #[test]
    fn display_invalid_user_group_id() {
        let err = Error::InvalidUserGroupId("bad".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid user group ID"), "got: {msg}");
    }

    #[test]
    fn display_invalid_connection_group_id() {
        let err = Error::InvalidConnectionGroupId("bad".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid connection group ID"), "got: {msg}");
        assert!(msg.contains("bad"), "got: {msg}");
    }

    #[test]
    fn display_invalid_tunnel_id() {
        let err = Error::InvalidTunnelId("bad".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid tunnel ID"), "got: {msg}");
        assert!(msg.contains("bad"), "got: {msg}");
    }

    #[test]
    fn display_invalid_token() {
        let err = Error::InvalidToken("a&b".to_string());
        let msg = err.to_string();
        assert!(msg.contains("invalid auth token"), "got: {msg}");
        assert!(msg.contains("a&b"), "got: {msg}");
    }

    #[test]
    fn display_invalid_query_param() {
        let err = Error::InvalidQueryParam {
            name: "order".to_string(),
            reason: "contains unsafe characters".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("invalid query parameter"), "got: {msg}");
        assert!(msg.contains("order"), "got: {msg}");
    }

    #[test]
    fn display_unauthorized() {
        let err = Error::Unauthorized {
            body: "nope".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("401"), "got: {msg}");
        assert!(!msg.contains("nope"), "body should not appear in Display: {msg}");
    }

    #[test]
    fn display_forbidden() {
        let err = Error::Forbidden {
            body: "nope".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("403"), "got: {msg}");
        assert!(!msg.contains("nope"), "body should not appear in Display: {msg}");
    }

    #[test]
    fn display_not_found() {
        let err = Error::NotFound {
            resource: "user admin".to_string(),
            body: "not here".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("404"), "got: {msg}");
        assert!(msg.contains("user admin"), "got: {msg}");
    }

    #[test]
    fn display_rate_limited() {
        let err = Error::RateLimited {
            retry_after: Some(Duration::from_secs(60)),
        };
        let msg = err.to_string();
        assert!(msg.contains("429"), "got: {msg}");
    }

    #[test]
    fn display_api_error() {
        let err = Error::Api {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            body: "kaboom".to_string(),
        };
        let msg = err.to_string();
        assert!(msg.contains("500"), "got: {msg}");
        assert!(msg.contains("kaboom"), "got: {msg}");
    }

    #[test]
    fn display_rate_limited_no_retry_after() {
        let err = Error::RateLimited { retry_after: None };
        let msg = err.to_string();
        assert!(msg.contains("429"), "got: {msg}");
    }
}