guacamole-client 0.5.1

Rust client library for the Guacamole REST API
Documentation
use serde::{Deserialize, Serialize};

use crate::client::GuacamoleClient;
use crate::error::Result;
use crate::validation::{validate_query_param, validate_sort_order};

/// A single history entry for a connection or user event.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HistoryEntry {
    /// Remote host that initiated the connection.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub remote_host: Option<String>,

    /// Username of the user who used the connection.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub username: Option<String>,

    /// Name of the connection.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub connection_name: Option<String>,

    /// Start timestamp in milliseconds since epoch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub start_date: Option<i64>,

    /// End timestamp in milliseconds since epoch.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub end_date: Option<i64>,
}

impl GuacamoleClient {
    /// Returns global connection history, optionally filtered.
    ///
    /// - `contains`: filter history entries whose connection name contains this substring.
    /// - `order`: sort order (e.g. `"asc"` or `"desc"`).
    pub async fn list_connection_history(
        &self,
        data_source: Option<&str>,
        contains: Option<&str>,
        order: Option<&str>,
    ) -> Result<Vec<HistoryEntry>> {
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/history/connections"))?;

        let mut params = Vec::new();
        if let Some(c) = contains {
            validate_query_param("contains", c)?;
            params.push(("contains", c));
        }
        if let Some(o) = order {
            validate_sort_order(o)?;
            params.push(("order", o));
        }

        let response = self.http.get(&url).query(&params).send().await?;
        Self::parse_response(response, "connection history").await
    }

    /// Returns global user history, optionally ordered.
    pub async fn list_user_history(
        &self,
        data_source: Option<&str>,
        order: Option<&str>,
    ) -> Result<Vec<HistoryEntry>> {
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/history/users"))?;

        let mut params = Vec::new();
        if let Some(o) = order {
            validate_sort_order(o)?;
            params.push(("order", o));
        }

        let response = self.http.get(&url).query(&params).send().await?;
        Self::parse_response(response, "user history").await
    }
}

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

    #[test]
    fn history_entry_serde_roundtrip() {
        let entry = HistoryEntry {
            remote_host: Some("10.0.0.1".to_string()),
            username: Some("guacadmin".to_string()),
            connection_name: Some("my-server".to_string()),
            start_date: Some(1_700_000_000_000),
            end_date: Some(1_700_001_000_000),
        };
        let json = serde_json::to_string(&entry).unwrap();
        let deserialized: HistoryEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(entry, deserialized);
    }

    #[test]
    fn history_entry_camel_case_keys() {
        let entry = HistoryEntry {
            remote_host: Some("10.0.0.1".to_string()),
            connection_name: Some("srv".to_string()),
            start_date: Some(1),
            end_date: Some(2),
            ..Default::default()
        };
        let json = serde_json::to_value(&entry).unwrap();
        assert!(json.get("remoteHost").is_some());
        assert!(json.get("connectionName").is_some());
        assert!(json.get("startDate").is_some());
        assert!(json.get("endDate").is_some());
    }

    #[test]
    fn history_entry_skip_none_fields() {
        let entry = HistoryEntry::default();
        let json = serde_json::to_value(&entry).unwrap();
        let obj = json.as_object().unwrap();
        assert!(obj.is_empty());
    }

    #[test]
    fn deserialize_from_api_json() {
        let json = r#"{
            "remoteHost": "192.168.1.100",
            "username": "admin",
            "connectionName": "production-ssh",
            "startDate": 1700000000000,
            "endDate": 1700001000000
        }"#;
        let entry: HistoryEntry = serde_json::from_str(json).unwrap();
        assert_eq!(entry.remote_host.as_deref(), Some("192.168.1.100"));
        assert_eq!(entry.username.as_deref(), Some("admin"));
        assert_eq!(entry.connection_name.as_deref(), Some("production-ssh"));
        assert_eq!(entry.start_date, Some(1_700_000_000_000));
        assert_eq!(entry.end_date, Some(1_700_001_000_000));
    }

    #[test]
    fn deserialize_unknown_fields_ignored() {
        let json = r#"{"remoteHost": "10.0.0.1", "unknownField": true}"#;
        let entry: HistoryEntry = serde_json::from_str(json).unwrap();
        assert_eq!(entry.remote_host.as_deref(), Some("10.0.0.1"));
    }
}