pub mod migrations; use rusqlite::params; use rusqlite::Connection as SqliteConnection; use std::collections::HashMap; use std::sync::Mutex; use crate::models::{Connection, ConnectionInput, Folder, FolderInput, Settings, Tag, TagInput}; pub struct Store { conn: Mutex, } impl Store { pub fn from_connection(conn: SqliteConnection) -> Self { Self { conn: Mutex::new(conn), } } pub fn open(path: &str) -> Result { let conn = SqliteConnection::open(path).map_err(|e| e.to_string())?; migrations::run_migrations(&conn).map_err(|e| e.to_string())?; Ok(Self::from_connection(conn)) } fn now() -> String { chrono::Utc::now().to_rfc3339() } pub fn get_folders(&self) -> Result, String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare("SELECT id, name, parent_id, created_at, updated_at FROM folders ORDER BY name") .map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { Ok(Folder { id: row.get(0)?, name: row.get(1)?, parent_id: row.get(2)?, tag_ids: vec![], created_at: row.get(3)?, updated_at: row.get(4)?, }) }) .map_err(|e| e.to_string())?; let mut folders: Vec = rows.filter_map(|r| r.ok()).collect(); // Load tags for each folder for f in folders.iter_mut() { let mut tag_stmt = conn .prepare("SELECT tag_id FROM folder_tags WHERE folder_id = ?1") .map_err(|e| e.to_string())?; let tag_rows = tag_stmt .query_map(params![f.id], |row| row.get::<_, String>(0)) .map_err(|e| e.to_string())?; f.tag_ids = tag_rows.filter_map(|r| r.ok()).collect(); } Ok(folders) } pub fn create_folder(&self, input: FolderInput) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let id = uuid::Uuid::new_v4().to_string(); let now = Self::now(); conn.execute( "INSERT INTO folders (id, name, parent_id, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5)", params![id, input.name, input.parent_id, now, now], ) .map_err(|e| e.to_string())?; let tag_ids = input.tag_ids.unwrap_or_default(); for tag_id in &tag_ids { conn.execute( "INSERT OR IGNORE INTO folder_tags (folder_id, tag_id) VALUES (?1, ?2)", params![id, tag_id], ) .map_err(|e| e.to_string())?; } Ok(Folder { id, name: input.name, parent_id: input.parent_id, tag_ids, created_at: now.clone(), updated_at: now, }) } pub fn update_folder(&self, id: &str, input: FolderInput) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let now = Self::now(); conn.execute( "UPDATE folders SET name = ?1, parent_id = ?2, updated_at = ?3 WHERE id = ?4", params![input.name, input.parent_id, now, id], ) .map_err(|e| e.to_string())?; // Replace all tags: clear existing, insert new conn.execute("DELETE FROM folder_tags WHERE folder_id = ?1", params![id]) .map_err(|e| e.to_string())?; let tag_ids = input.tag_ids.unwrap_or_default(); for tag_id in &tag_ids { conn.execute( "INSERT OR IGNORE INTO folder_tags (folder_id, tag_id) VALUES (?1, ?2)", params![id, tag_id], ) .map_err(|e| e.to_string())?; } Ok(Folder { id: id.to_string(), name: input.name, parent_id: input.parent_id, tag_ids, created_at: now.clone(), updated_at: now, }) } pub fn delete_folder(&self, id: &str) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; // Get the folder's parent_id to reparent children let parent_id: Option = conn .query_row( "SELECT parent_id FROM folders WHERE id = ?1", params![id], |row| row.get(0), ) .map_err(|e| e.to_string())?; // Move child folders to the parent conn.execute( "UPDATE folders SET parent_id = ?1 WHERE parent_id = ?2", params![parent_id, id], ) .map_err(|e| e.to_string())?; // Move child connections to the parent conn.execute( "UPDATE connections SET folder_id = ?1 WHERE folder_id = ?2", params![parent_id, id], ) .map_err(|e| e.to_string())?; // Delete the folder conn.execute("DELETE FROM folders WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } pub fn add_folder_tags(&self, folder_id: &str, tag_ids: &[String]) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; for tag_id in tag_ids { conn.execute( "INSERT OR IGNORE INTO folder_tags (folder_id, tag_id) VALUES (?1, ?2)", params![folder_id, tag_id], ) .map_err(|e| e.to_string())?; } Ok(()) } pub fn add_connection_tags(&self, conn_id: &str, tag_ids: &[String]) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; for tag_id in tag_ids { conn.execute( "INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?1, ?2)", params![conn_id, tag_id], ) .map_err(|e| e.to_string())?; } Ok(()) } pub fn get_tags(&self) -> Result, String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare("SELECT id, name, color, created_at FROM tags ORDER BY name") .map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { Ok(Tag { id: row.get(0)?, name: row.get(1)?, color: row.get(2)?, created_at: row.get(3)?, }) }) .map_err(|e| e.to_string())?; Ok(rows.filter_map(|r| r.ok()).collect()) } pub fn create_tag(&self, input: TagInput) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let id = uuid::Uuid::new_v4().to_string(); let now = Self::now(); conn.execute( "INSERT INTO tags (id, name, color, created_at) VALUES (?1, ?2, ?3, ?4)", params![id, input.name, input.color, now], ) .map_err(|e| e.to_string())?; Ok(Tag { id, name: input.name, color: input.color, created_at: now, }) } pub fn delete_tag(&self, id: &str) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM tags WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } pub fn update_tag(&self, id: &str, input: TagInput) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let now = Self::now(); conn.execute( "UPDATE tags SET name = ?1, color = ?2 WHERE id = ?3", params![input.name, input.color, id], ) .map_err(|e| e.to_string())?; Ok(Tag { id: id.to_string(), name: input.name, color: input.color, created_at: now, }) } pub fn get_connections(&self) -> Result, String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( "SELECT id, name, db_type, host, port, username, database, folder_id, keychain_ref, ssh_host, ssh_port, ssh_user, ssh_auth_method, ssh_private_key_path, ssl_mode, ssl_ca_path, ssl_cert_path, ssl_key_path, environment, created_at, updated_at FROM connections ORDER BY name", ) .map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { Ok(Connection { id: row.get(0)?, name: row.get(1)?, db_type: row.get(2)?, host: row.get(3)?, port: row.get(4)?, username: row.get(5)?, database: row.get(6)?, folder_id: row.get(7)?, keychain_ref: row.get(8)?, ssh_host: row.get(9)?, ssh_port: row.get(10)?, ssh_user: row.get(11)?, ssh_auth_method: row.get(12)?, ssh_private_key_path: row.get(13)?, ssl_mode: row.get(14)?, ssl_ca_path: row.get(15)?, ssl_cert_path: row.get(16)?, ssl_key_path: row.get(17)?, environment: row.get(18)?, tag_ids: vec![], created_at: row.get(19)?, updated_at: row.get(20)?, }) }) .map_err(|e| e.to_string())?; let mut conns: Vec = rows.filter_map(|r| r.ok()).collect(); // Load tags for each connection for c in conns.iter_mut() { let mut tag_stmt = conn .prepare("SELECT tag_id FROM connection_tags WHERE connection_id = ?1") .map_err(|e| e.to_string())?; let tag_rows = tag_stmt .query_map(params![c.id], |row| row.get::<_, String>(0)) .map_err(|e| e.to_string())?; c.tag_ids = tag_rows.filter_map(|r| r.ok()).collect(); } Ok(conns) } pub fn create_connection(&self, input: ConnectionInput) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let id = uuid::Uuid::new_v4().to_string(); let now = Self::now(); conn.execute( "INSERT INTO connections (id, name, db_type, host, port, username, database, folder_id, keychain_ref, ssh_host, ssh_port, ssh_user, ssh_auth_method, ssh_private_key_path, ssl_mode, ssl_ca_path, ssl_cert_path, ssl_key_path, environment, created_at, updated_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, NULL, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", params![id, input.name, input.db_type, input.host, input.port, input.username, input.database, input.folder_id, input.ssh_host, input.ssh_port, input.ssh_user, input.ssh_auth_method, input.ssh_private_key_path, input.ssl_mode, input.ssl_ca_path, input.ssl_cert_path, input.ssl_key_path, input.environment, now, now], ) .map_err(|e| e.to_string())?; for tag_id in &input.tag_ids { conn.execute( "INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?1, ?2)", params![id, tag_id], ) .map_err(|e| e.to_string())?; } Ok(Connection { id, name: input.name, db_type: input.db_type, host: input.host, port: input.port, username: input.username, folder_id: input.folder_id, database: input.database, keychain_ref: None, environment: input.environment, ssh_host: input.ssh_host, ssh_port: input.ssh_port, ssh_user: input.ssh_user, ssh_auth_method: input.ssh_auth_method, ssh_private_key_path: input.ssh_private_key_path, ssl_mode: input.ssl_mode, ssl_ca_path: input.ssl_ca_path, ssl_cert_path: input.ssl_cert_path, ssl_key_path: input.ssl_key_path, tag_ids: input.tag_ids, created_at: now.clone(), updated_at: now, }) } pub fn delete_connection(&self, id: &str) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; conn.execute("DELETE FROM connections WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } pub fn update_connection(&self, id: &str, input: ConnectionInput) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let now = Self::now(); conn.execute( "UPDATE connections SET name=?1, db_type=?2, host=?3, port=?4, username=?5, database=?6, folder_id=?7, ssh_host=?8, ssh_port=?9, ssh_user=?10, ssh_auth_method=?11, ssh_private_key_path=?12, ssl_mode=?13, ssl_ca_path=?14, ssl_cert_path=?15, ssl_key_path=?16, environment=?17, updated_at=?18 WHERE id=?19", params![ input.name, input.db_type, input.host, input.port, input.username, input.database, input.folder_id, input.ssh_host, input.ssh_port, input.ssh_user, input.ssh_auth_method, input.ssh_private_key_path, input.ssl_mode, input.ssl_ca_path, input.ssl_cert_path, input.ssl_key_path, input.environment, now, id ], ).map_err(|e| e.to_string())?; // Update tags conn.execute("DELETE FROM connection_tags WHERE connection_id = ?1", params![id]) .map_err(|e| e.to_string())?; for tag_id in &input.tag_ids { conn.execute( "INSERT OR IGNORE INTO connection_tags (connection_id, tag_id) VALUES (?1, ?2)", params![id, tag_id], ).map_err(|e| e.to_string())?; } Ok(Connection { id: id.to_string(), name: input.name, db_type: input.db_type, host: input.host, port: input.port, username: input.username, database: input.database, folder_id: input.folder_id, keychain_ref: None, environment: input.environment, ssh_host: input.ssh_host, ssh_port: input.ssh_port, ssh_user: input.ssh_user, ssh_auth_method: input.ssh_auth_method, ssh_private_key_path: input.ssh_private_key_path, ssl_mode: input.ssl_mode, ssl_ca_path: input.ssl_ca_path, ssl_cert_path: input.ssl_cert_path, ssl_key_path: input.ssl_key_path, tag_ids: input.tag_ids.clone(), created_at: String::new(), // not updated updated_at: now, }) } pub fn get_settings(&self) -> Result { let conn = self.conn.lock().map_err(|e| e.to_string())?; let mut map: HashMap = HashMap::new(); let mut stmt = conn .prepare("SELECT key, value FROM settings") .map_err(|e| e.to_string())?; let rows = stmt .query_map([], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, String>(1)?, )) }) .map_err(|e| e.to_string())?; for r in rows.filter_map(|r| r.ok()) { map.insert(r.0, r.1); } let theme = map .get("theme") .cloned() .unwrap_or_else(|| "system".to_string()); let font_size = map .get("font_size") .cloned() .unwrap_or_else(|| "medium".to_string()); let confirm = map .get("confirm_before_delete") .map(|v| v == "true") .unwrap_or(true); let default_folder_id = map .get("default_folder_id") .filter(|v| v.as_str() != "null") .cloned(); let mut default_ports = HashMap::new(); default_ports.insert("postgresql".to_string(), Some(5432i64)); default_ports.insert("mysql".to_string(), Some(3306i64)); default_ports.insert("redis".to_string(), Some(6379i64)); default_ports.insert("sqlite".to_string(), None); if let Some(ports_json) = map.get("default_ports") { if let Ok(parsed) = serde_json::from_str::>>(ports_json) { default_ports = parsed; } } Ok(Settings { confirm_before_delete: confirm, default_folder_id, theme, font_size, default_ports, tag_order: map.get("tag_order").cloned(), table_refresh_rate: map .get("table_refresh_rate") .and_then(|v| v.parse().ok()) .unwrap_or(0), table_page_size: map .get("table_page_size") .and_then(|v| v.parse().ok()) .unwrap_or(50), shortcuts: map .get("shortcuts") .and_then(|v| serde_json::from_str(v).ok()) .unwrap_or_default(), }) } pub fn update_setting(&self, key: &str, value: &str) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO settings (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value = excluded.value", params![key, value], ) .map_err(|e| e.to_string())?; Ok(()) } /// Insert a row into the `query_history` table. pub fn insert_query_history( &self, id: &str, connection_id: &str, query_text: &str, execution_time_ms: Option, row_count: Option, status: &str, error_message: Option<&str>, ) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; let now = Self::now(); conn.execute( "INSERT INTO query_history (id, connection_id, query_text, execution_time_ms, row_count, status, error_message, executed_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![id, connection_id, query_text, execution_time_ms, row_count, status, error_message, now], ) .map_err(|e| e.to_string())?; Ok(()) } /// Fetch query history rows, optionally filtered by `connection_id`. /// Returns results ordered by `executed_at DESC`. pub fn get_query_history( &self, connection_id: Option<&str>, limit: i64, offset: i64, ) -> Result, String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; let (sql, params): (String, Vec>) = if let Some(cid) = connection_id { ( "SELECT id, connection_id, query_text, execution_time_ms, row_count, status, error_message, executed_at FROM query_history WHERE connection_id = ?1 ORDER BY executed_at DESC LIMIT ?2 OFFSET ?3".to_string(), vec![Box::new(cid.to_string()), Box::new(limit), Box::new(offset)], ) } else { ( "SELECT id, connection_id, query_text, execution_time_ms, row_count, status, error_message, executed_at FROM query_history ORDER BY executed_at DESC LIMIT ?1 OFFSET ?2".to_string(), vec![Box::new(limit), Box::new(offset)], ) }; let mut stmt = conn.prepare(&sql).map_err(|e| e.to_string())?; let refs: Vec<&dyn rusqlite::types::ToSql> = params.iter().map(|p| p.as_ref()).collect(); let rows = stmt .query_map(rusqlite::params_from_iter(&refs), |row| { Ok(crate::commands::query::QueryHistoryEntry { id: row.get(0)?, connection_id: row.get(1)?, query_text: row.get(2)?, execution_time_ms: row.get(3)?, row_count: row.get(4)?, status: row.get(5)?, error_message: row.get(6)?, executed_at: row.get(7)?, }) }) .map_err(|e| e.to_string())?; rows.collect::, _>>().map_err(|e| e.to_string()) } /// Delete all query history rows, optionally filtered by `connection_id`. pub fn clear_query_history(&self, connection_id: Option<&str>) -> Result<(), String> { let conn = self.conn.lock().map_err(|e| e.to_string())?; if let Some(cid) = connection_id { conn.execute( "DELETE FROM query_history WHERE connection_id = ?1", params![cid], ) .map_err(|e| e.to_string())?; } else { conn.execute("DELETE FROM query_history", []) .map_err(|e| e.to_string())?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::models::{ConnectionInput, FolderInput, TagInput}; fn fresh_store() -> Store { let conn = rusqlite::Connection::open_in_memory().unwrap(); crate::store::migrations::run_migrations(&conn).unwrap(); Store::from_connection(conn) } #[test] fn create_and_get_folder() { let store = fresh_store(); let folder = store .create_folder(FolderInput { tag_ids: None, name: "Work".into(), parent_id: None, }) .unwrap(); let got = store.get_folders().unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].name, "Work"); assert_eq!(got[0].id, folder.id); assert!(got[0].parent_id.is_none()); } #[test] fn create_nested_folders() { let store = fresh_store(); let parent = store .create_folder(FolderInput { tag_ids: None, name: "root".into(), parent_id: None, }) .unwrap(); let child = store .create_folder(FolderInput { tag_ids: None, name: "child".into(), parent_id: Some(parent.id.clone()), }) .unwrap(); assert_eq!(child.parent_id, Some(parent.id)); } #[test] fn create_and_get_tag() { let store = fresh_store(); let tag = store .create_tag(TagInput { name: "production".into(), color: "#ef4444".into(), }) .unwrap(); let got = store.get_tags().unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].name, "production"); assert_eq!(got[0].color, "#ef4444"); assert_eq!(got[0].id, tag.id); } #[test] fn create_and_get_connection() { let store = fresh_store(); let conn = store .create_connection(ConnectionInput { name: "Prod".into(), db_type: "postgresql".into(), host: "prod.example.com".into(), port: Some(5432), username: Some("admin".into()), folder_id: None, password: None, database: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, environment: None, tag_ids: vec![], }) .unwrap(); let got = store.get_connections().unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].name, "Prod"); assert_eq!(got[0].port, Some(5432)); assert!(got[0].tag_ids.is_empty()); assert_eq!(got[0].id, conn.id); } #[test] fn connection_with_tags_persists_join() { let store = fresh_store(); let t1 = store .create_tag(TagInput { name: "prod".into(), color: "#ef4444".into(), }) .unwrap(); let t2 = store .create_tag(TagInput { name: "primary".into(), color: "#3b82f6".into(), }) .unwrap(); store .create_connection(ConnectionInput { name: "Prod".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, password: None, database: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, environment: None, tag_ids: vec![t1.id.clone(), t2.id.clone()], }) .unwrap(); let got = store.get_connections().unwrap(); assert_eq!(got[0].tag_ids.len(), 2); assert!(got[0].tag_ids.contains(&t1.id)); assert!(got[0].tag_ids.contains(&t2.id)); } #[test] fn delete_folder_sets_connection_folder_null() { let store = fresh_store(); let folder = store .create_folder(FolderInput { tag_ids: None, name: "f".into(), parent_id: None, }) .unwrap(); store .create_connection(ConnectionInput { name: "C".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: Some(folder.id.clone()), password: None, database: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, environment: None, tag_ids: vec![], }) .unwrap(); store.delete_folder(&folder.id).unwrap(); let conns = store.get_connections().unwrap(); assert!(conns[0].folder_id.is_none()); } #[test] fn delete_tag_removes_from_connection() { let store = fresh_store(); let tag = store .create_tag(TagInput { name: "prod".into(), color: "#ef4444".into(), }) .unwrap(); store .create_connection(ConnectionInput { name: "C".into(), db_type: "postgresql".into(), host: "h".into(), port: Some(5432), username: None, folder_id: None, password: None, database: None, ssh_host: None, ssh_port: None, ssh_user: None, ssh_auth_method: None, ssh_private_key_path: None, ssh_passphrase: None, ssl_mode: None, ssl_ca_path: None, ssl_cert_path: None, ssl_key_path: None, environment: None, tag_ids: vec![tag.id.clone()], }) .unwrap(); store.delete_tag(&tag.id).unwrap(); let conns = store.get_connections().unwrap(); assert!(conns[0].tag_ids.is_empty()); } #[test] fn settings_get_returns_defaults_when_empty() { let store = fresh_store(); let settings = store.get_settings().unwrap(); assert_eq!(settings.theme, "system"); assert_eq!(settings.font_size, "medium"); assert!(settings.confirm_before_delete); assert_eq!( settings.default_ports.get("postgresql"), Some(&Some(5432)) ); } #[test] fn settings_update_persists() { let store = fresh_store(); store.update_setting("theme", "light").unwrap(); let settings = store.get_settings().unwrap(); assert_eq!(settings.theme, "light"); } #[test] fn ssh_ssl_fields_persist_and_retrieve() { let store = fresh_store(); let _conn = store .create_connection(ConnectionInput { name: "SSH-Tunnel-DB".into(), db_type: "postgresql".into(), host: "localhost".into(), port: Some(5432), username: Some("dbuser".into()), folder_id: None, password: None, database: Some("analytics".into()), ssh_host: Some("jumphost.example.com".into()), ssh_port: Some(2222), ssh_user: Some("tunneluser".into()), ssh_auth_method: Some("Key".into()), ssh_private_key_path: Some("/home/user/.ssh/id_rsa".into()), ssh_passphrase: None, ssl_mode: Some("verify-full".into()), ssl_ca_path: Some("/etc/ssl/certs/ca.pem".into()), ssl_cert_path: Some("/etc/ssl/certs/client-cert.pem".into()), ssl_key_path: Some("/etc/ssl/private/client-key.pem".into()), environment: None, tag_ids: vec![], }) .unwrap(); let got = store.get_connections().unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].database.as_deref(), Some("analytics")); assert_eq!(got[0].ssh_host.as_deref(), Some("jumphost.example.com")); assert_eq!(got[0].ssh_port, Some(2222)); assert_eq!(got[0].ssh_user.as_deref(), Some("tunneluser")); assert_eq!(got[0].ssh_auth_method.as_deref(), Some("Key")); assert_eq!( got[0].ssh_private_key_path.as_deref(), Some("/home/user/.ssh/id_rsa") ); assert_eq!(got[0].ssl_mode.as_deref(), Some("verify-full")); assert_eq!( got[0].ssl_ca_path.as_deref(), Some("/etc/ssl/certs/ca.pem") ); assert_eq!( got[0].ssl_cert_path.as_deref(), Some("/etc/ssl/certs/client-cert.pem") ); assert_eq!( got[0].ssl_key_path.as_deref(), Some("/etc/ssl/private/client-key.pem") ); } }