use serde::{Deserialize, Serialize};
use crate::client::GuacamoleClient;
use crate::error::Result;
use crate::validation::{validate_query_param, validate_sort_order};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HistoryEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_date: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_date: Option<i64>,
}
impl GuacamoleClient {
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(¶ms).send().await?;
Self::parse_response(response, "connection history").await
}
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(¶ms).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"));
}
}