diff --git a/src/backends/aws_secrets.rs b/src/backends/aws_secrets.rs index afb94b9..2ac158c 100644 --- a/src/backends/aws_secrets.rs +++ b/src/backends/aws_secrets.rs @@ -9,9 +9,9 @@ use super::secret_backend::{SecretBackend, SecretData}; /// AWS Secrets Manager client pub struct AwsSecretsClient { - client: SecretsManagerClient, + pub client: SecretsManagerClient, #[allow(dead_code)] // Kept for potential future use (logging, debugging) - region: String, + pub region: String, } impl AwsSecretsClient { @@ -36,7 +36,7 @@ impl AwsSecretsClient { } /// Convert AWS tags to metadata HashMap - fn tags_to_metadata(&self, tags: &[Tag]) -> HashMap { + pub fn tags_to_metadata(&self, tags: &[Tag]) -> HashMap { tags.iter() .filter_map(|tag| { tag.key() @@ -46,7 +46,7 @@ impl AwsSecretsClient { } /// Convert metadata HashMap to AWS tags - fn metadata_to_tags(&self, metadata: &HashMap) -> Vec { + pub fn metadata_to_tags(&self, metadata: &HashMap) -> Vec { metadata .iter() .map(|(k, v)| Tag::builder().key(k).value(v).build()) @@ -249,115 +249,18 @@ impl SecretBackend for AwsSecretsClient { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tags_to_metadata() { - let client = AwsSecretsClient { - client: create_test_client(), - region: "us-east-1".to_string(), - }; - - let tags = vec![ - Tag::builder().key("rotation_enabled").value("true").build(), - Tag::builder() - .key("last_rotated") - .value("2023-01-01T00:00:00Z") - .build(), - Tag::builder() - .key("target_username") - .value("testuser") - .build(), - ]; - - let metadata = client.tags_to_metadata(&tags); - assert_eq!(metadata.get("rotation_enabled"), Some(&"true".to_string())); - assert_eq!( - metadata.get("last_rotated"), - Some(&"2023-01-01T00:00:00Z".to_string()) - ); - assert_eq!( - metadata.get("target_username"), - Some(&"testuser".to_string()) - ); - } - - #[test] - fn test_tags_to_metadata_empty() { - let client = AwsSecretsClient { - client: create_test_client(), - region: "us-east-1".to_string(), - }; - - let tags = vec![]; - let metadata = client.tags_to_metadata(&tags); - assert!(metadata.is_empty()); - } - - #[test] - fn test_metadata_to_tags() { - let client = AwsSecretsClient { - client: create_test_client(), - region: "us-east-1".to_string(), - }; - - let mut metadata = HashMap::new(); - metadata.insert("rotation_enabled".to_string(), "true".to_string()); - metadata.insert( - "last_rotated".to_string(), - "2023-01-01T00:00:00Z".to_string(), - ); - metadata.insert("target_username".to_string(), "testuser".to_string()); - - let tags = client.metadata_to_tags(&metadata); - assert_eq!(tags.len(), 3); - - // Verify tag values - let tag_map: HashMap = tags - .iter() - .filter_map(|tag| { - tag.key() - .and_then(|k| tag.value().map(|v| (k.to_string(), v.to_string()))) - }) - .collect(); - - assert_eq!(tag_map.get("rotation_enabled"), Some(&"true".to_string())); - assert_eq!( - tag_map.get("last_rotated"), - Some(&"2023-01-01T00:00:00Z".to_string()) - ); - assert_eq!( - tag_map.get("target_username"), - Some(&"testuser".to_string()) - ); - } - - #[test] - fn test_metadata_to_tags_empty() { - let client = AwsSecretsClient { - client: create_test_client(), - region: "us-east-1".to_string(), - }; - - let metadata = HashMap::new(); - let tags = client.metadata_to_tags(&metadata); - assert!(tags.is_empty()); - } - - // Helper function to create a test client - // Note: This creates a real client but tests don't actually call AWS APIs - // In a real scenario, you'd use a mock client - fn create_test_client() -> SecretsManagerClient { - // Use tokio runtime for async initialization - let rt = tokio::runtime::Runtime::new().unwrap(); - let config = rt.block_on(async { - aws_config::defaults(aws_config::BehaviorVersion::latest()) - .region(Region::new("us-east-1")) - .load() - .await - }); - SecretsManagerClient::new(&config) - } +// Helper function to create a test client +// Note: This creates a real client but tests don't actually call AWS APIs +// In a real scenario, you'd use a mock client +#[allow(dead_code)] // Used in tests +pub fn create_test_client() -> SecretsManagerClient { + // Use tokio runtime for async initialization + let rt = tokio::runtime::Runtime::new().unwrap(); + let config = rt.block_on(async { + aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(Region::new("us-east-1")) + .load() + .await + }); + SecretsManagerClient::new(&config) } diff --git a/src/backends/file.rs b/src/backends/file.rs index acb1b7f..c5ce0a1 100644 --- a/src/backends/file.rs +++ b/src/backends/file.rs @@ -53,7 +53,7 @@ impl FileBackend { } /// Parse a key:value line from the secret file - fn parse_line(line: &str) -> Option<(String, String)> { + pub fn parse_line(line: &str) -> Option<(String, String)> { let line = line.trim(); if line.is_empty() || line.starts_with('#') { return None; @@ -235,79 +235,3 @@ impl SecretBackend for FileBackend { "file" } } - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[tokio::test] - async fn test_write_and_read_secret() -> Result<()> { - let temp_dir = TempDir::new()?; - let backend = FileBackend::new(temp_dir.path())?; - - let mut data = HashMap::new(); - data.insert("password".to_string(), "test123".to_string()); - data.insert("username".to_string(), "admin".to_string()); - - backend.write_secret("test/secret", data.clone()).await?; - - let secret = backend.read_secret("test/secret").await?; - assert_eq!(secret.data, data); - - Ok(()) - } - - #[tokio::test] - async fn test_metadata() -> Result<()> { - let temp_dir = TempDir::new()?; - let backend = FileBackend::new(temp_dir.path())?; - - let mut metadata = HashMap::new(); - metadata.insert("rotation_enabled".to_string(), "true".to_string()); - metadata.insert("last_rotated".to_string(), "2024-01-01".to_string()); - - backend - .update_metadata("test/secret", metadata.clone()) - .await?; - - let read_meta = backend.read_metadata("test/secret").await?; - assert_eq!(read_meta, metadata); - - Ok(()) - } - - #[tokio::test] - async fn test_list_secrets() -> Result<()> { - let temp_dir = TempDir::new()?; - let backend = FileBackend::new(temp_dir.path())?; - - let mut data1 = HashMap::new(); - data1.insert("password".to_string(), "pass1".to_string()); - backend.write_secret("app/db", data1).await?; - - let mut data2 = HashMap::new(); - data2.insert("token".to_string(), "token1".to_string()); - backend.write_secret("app/api", data2).await?; - - let secrets = backend.list_secrets("").await?; - assert!(secrets.contains(&"app/db".to_string())); - assert!(secrets.contains(&"app/api".to_string())); - - Ok(()) - } - - #[test] - fn test_parse_line() { - assert_eq!( - FileBackend::parse_line("password:test123"), - Some(("password".to_string(), "test123".to_string())) - ); - assert_eq!( - FileBackend::parse_line(" key : value "), - Some(("key".to_string(), "value".to_string())) - ); - assert_eq!(FileBackend::parse_line("# comment"), None); - assert_eq!(FileBackend::parse_line(""), None); - } -} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 2d3d371..e4f8cd5 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -7,10 +7,12 @@ mod file; mod secret_backend; mod vault; -pub use aws_secrets::AwsSecretsClient; +#[allow(unused_imports)] // Used in tests +pub use aws_secrets::{AwsSecretsClient, create_test_client}; pub use file::FileBackend; pub use secret_backend::SecretBackend; -pub use vault::{VaultBackend, VaultClient}; +#[allow(unused_imports)] // Used in tests +pub use vault::{VaultBackend, VaultClient, SecretMetadata, VaultSecretData, VaultWriteRequest}; /// Backend type enumeration #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/backends/vault.rs b/src/backends/vault.rs index dd8ed76..8d4aa90 100644 --- a/src/backends/vault.rs +++ b/src/backends/vault.rs @@ -10,7 +10,7 @@ use super::secret_backend::{SecretBackend, SecretData}; #[derive(Clone)] pub struct VaultClient { client: Client, - address: String, + pub address: String, token: String, } @@ -31,10 +31,10 @@ struct VaultResponse { } #[derive(Debug, Serialize, Deserialize)] -struct VaultWriteRequest { - data: HashMap, +pub struct VaultWriteRequest { + pub data: HashMap, #[serde(skip_serializing_if = "Option::is_none")] - options: Option>, + pub options: Option>, } impl VaultClient { @@ -262,103 +262,3 @@ impl SecretBackend for VaultBackend { "HashiCorp Vault" } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vault_client_new() { - let client = VaultClient::new( - "http://localhost:8200".to_string(), - "test-token".to_string(), - ); - assert!(client.is_ok()); - } - - #[test] - fn test_vault_url_construction() { - let client = VaultClient::new( - "http://localhost:8200".to_string(), - "test-token".to_string(), - ) - .unwrap(); - - // Test read URL - let read_url = format!("{}/v1/{}/data/{}", client.address, "secret", "myapp/db"); - assert_eq!(read_url, "http://localhost:8200/v1/secret/data/myapp/db"); - - // Test write URL - let write_url = format!("{}/v1/{}/data/{}", client.address, "secret", "myapp/db"); - assert_eq!(write_url, "http://localhost:8200/v1/secret/data/myapp/db"); - - // Test metadata URL - let meta_url = format!("{}/v1/{}/metadata/{}", client.address, "secret", "myapp/db"); - assert_eq!( - meta_url, - "http://localhost:8200/v1/secret/metadata/myapp/db" - ); - } - - #[test] - fn test_vault_secret_metadata_parsing() { - let mut custom_meta = HashMap::new(); - custom_meta.insert("rotation_enabled".to_string(), "true".to_string()); - custom_meta.insert( - "last_rotated".to_string(), - "2023-01-01T00:00:00Z".to_string(), - ); - - let metadata = SecretMetadata { - custom_metadata: Some(custom_meta.clone()), - }; - - assert_eq!( - metadata - .custom_metadata - .as_ref() - .unwrap() - .get("rotation_enabled"), - Some(&"true".to_string()) - ); - } - - #[test] - fn test_vault_secret_data_structure() { - let mut data = HashMap::new(); - data.insert("password".to_string(), "secret123".to_string()); - data.insert("username".to_string(), "admin".to_string()); - - let mut custom_meta = HashMap::new(); - custom_meta.insert("rotation_enabled".to_string(), "true".to_string()); - - let secret_data = VaultSecretData { - data: data.clone(), - metadata: Some(SecretMetadata { - custom_metadata: Some(custom_meta), - }), - }; - - assert_eq!( - secret_data.data.get("password"), - Some(&"secret123".to_string()) - ); - assert_eq!(secret_data.data.get("username"), Some(&"admin".to_string())); - assert!(secret_data.metadata.is_some()); - } - - #[test] - fn test_vault_write_request_serialization() { - let mut data = HashMap::new(); - data.insert("password".to_string(), "newpass".to_string()); - - let request = VaultWriteRequest { - data: data.clone(), - options: None, - }; - - // Verify structure - assert_eq!(request.data.get("password"), Some(&"newpass".to_string())); - assert!(request.options.is_none()); - } -} diff --git a/src/config.rs b/src/config.rs index 3f54081..9806d86 100644 --- a/src/config.rs +++ b/src/config.rs @@ -293,217 +293,3 @@ impl Config { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - #[test] - fn test_default_rotation_config() { - let config = RotationConfig::default(); - assert_eq!(config.period_months, 6); - assert_eq!(config.secret_length, 32); - } - - #[test] - fn test_config_from_file() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -backend = "vault" -[vault] -address = "http://localhost:8200" -token = "test-token" -mount = "secret" - -[rotation] -period_months = 12 -secret_length = 64 -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - assert_eq!(config.backend, "vault"); - assert_eq!( - config.vault.as_ref().unwrap().address, - "http://localhost:8200" - ); - assert_eq!(config.rotation.period_months, 12); - assert_eq!(config.rotation.secret_length, 64); - } - - #[test] - fn test_config_from_file_with_aws() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -backend = "aws" -[aws] -region = "us-west-2" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - assert_eq!(config.backend, "aws"); - assert_eq!(config.aws.as_ref().unwrap().region, "us-west-2"); - } - - #[test] - fn test_config_from_file_with_file_backend() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -backend = "file" -[file] -directory = "/tmp/test-secrets" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - assert_eq!(config.backend, "file"); - assert_eq!(config.file.as_ref().unwrap().directory, "/tmp/test-secrets"); - } - - #[test] - fn test_config_from_file_with_targets() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -backend = "vault" -[vault] -address = "http://localhost:8200" -token = "test-token" - -[targets.postgres] -host = "localhost" -port = 5432 -database = "testdb" -username = "admin" -password_path = "admin/password" -ssl_mode = "require" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - assert!(config.targets.is_some()); - let postgres = config.targets.as_ref().unwrap().postgres.as_ref().unwrap(); - assert_eq!(postgres.host, "localhost"); - assert_eq!(postgres.port, 5432); - assert_eq!(postgres.database, "testdb"); - assert_eq!(postgres.username, "admin"); - assert_eq!(postgres.password_path.as_ref().unwrap(), "admin/password"); - assert_eq!(postgres.ssl_mode, "require"); - } - - #[test] - fn test_config_from_file_with_api_target() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -backend = "vault" -[vault] -address = "http://localhost:8200" -token = "test-token" - -[targets.api] -base_url = "https://api.example.com" -endpoint = "/users/{username}/password" -method = "PUT" -password_field = "new_password" -username_field = "user" -timeout_seconds = 60 -auth_header = "Bearer token123" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - let api = config.targets.as_ref().unwrap().api.as_ref().unwrap(); - assert_eq!(api.base_url, "https://api.example.com"); - assert_eq!(api.endpoint, "/users/{username}/password"); - assert_eq!(api.method, "PUT"); - assert_eq!(api.password_field, "new_password"); - assert_eq!(api.username_field.as_ref().unwrap(), "user"); - assert_eq!(api.timeout_seconds, 60); - assert_eq!(api.auth_header.as_ref().unwrap(), "Bearer token123"); - } - - #[test] - fn test_config_defaults() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -backend = "vault" -[vault] -address = "http://localhost:8200" -token = "test-token" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - // Test defaults - assert_eq!(config.vault.as_ref().unwrap().mount, "secret"); - assert_eq!(config.rotation.period_months, 6); - assert_eq!(config.rotation.secret_length, 32); - } - - #[test] - fn test_config_create_sample() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("sample.toml"); - - Config::create_sample(&config_path).unwrap(); - - assert!(config_path.exists()); - let config = Config::from_file(&config_path).unwrap(); - assert_eq!(config.backend, "vault"); - assert!(config.vault.is_some()); - assert!(config.aws.is_some()); - assert!(config.file.is_some()); - } - - #[test] - fn test_postgres_config_defaults() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -[targets.postgres] -host = "localhost" -database = "testdb" -username = "admin" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - let postgres = config.targets.as_ref().unwrap().postgres.as_ref().unwrap(); - assert_eq!(postgres.port, 5432); // default port - assert_eq!(postgres.ssl_mode, "prefer"); // default ssl_mode - } - - #[test] - fn test_api_config_defaults() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -[targets.api] -base_url = "https://api.example.com" -endpoint = "/password" -"#; - fs::write(&config_path, config_content).unwrap(); - - let config = Config::from_file(&config_path).unwrap(); - let api = config.targets.as_ref().unwrap().api.as_ref().unwrap(); - assert_eq!(api.method, "POST"); // default method - assert_eq!(api.password_field, "password"); // default password_field - assert_eq!(api.timeout_seconds, 30); // default timeout - } -} diff --git a/src/env_updater.rs b/src/env_updater.rs index 5a15d6a..4e6bd3c 100644 --- a/src/env_updater.rs +++ b/src/env_updater.rs @@ -125,7 +125,7 @@ impl EnvUpdater { /// /// Note: This is primarily used for testing. For production use, consider /// manually editing shell config files or using standard shell utilities. - #[cfg(test)] + #[allow(dead_code)] // Used in tests pub fn remove_env_var(&self, var_name: &str) -> Result<()> { info!("Removing environment variable: {}", var_name); @@ -143,7 +143,7 @@ impl EnvUpdater { } /// Remove environment variable from a specific file - #[cfg(test)] + #[allow(dead_code)] // Used in tests fn remove_from_file(&self, path: &Path, var_name: &str) -> Result<()> { let content = fs::read_to_string(path) .with_context(|| format!("Failed to read {}", path.display()))?; @@ -193,57 +193,3 @@ impl EnvUpdater { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - #[test] - fn test_update_new_variable() -> Result<()> { - let temp_dir = TempDir::new()?; - let bashrc = temp_dir.path().join(".bashrc"); - fs::write(&bashrc, "# existing config\n")?; - - let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); - updater.update_env_var("MY_SECRET", "new_value")?; - - let content = fs::read_to_string(&bashrc)?; - assert!(content.contains("export MY_SECRET=\"new_value\"")); - - Ok(()) - } - - #[test] - fn test_update_existing_variable() -> Result<()> { - let temp_dir = TempDir::new()?; - let bashrc = temp_dir.path().join(".bashrc"); - fs::write(&bashrc, "export MY_SECRET=\"old_value\"\n")?; - - let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); - updater.update_env_var("MY_SECRET", "new_value")?; - - let content = fs::read_to_string(&bashrc)?; - assert!(content.contains("export MY_SECRET=\"new_value\"")); - assert!(!content.contains("old_value")); - - Ok(()) - } - - #[test] - fn test_remove_variable() -> Result<()> { - let temp_dir = TempDir::new()?; - let bashrc = temp_dir.path().join(".bashrc"); - fs::write(&bashrc, "export MY_SECRET=\"value\"\n# other config\n")?; - - let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); - updater.remove_env_var("MY_SECRET")?; - - let content = fs::read_to_string(&bashrc)?; - assert!(!content.contains("MY_SECRET")); - assert!(content.contains("# other config")); - - Ok(()) - } -} diff --git a/src/rotation.rs b/src/rotation.rs index 68bc020..f38c726 100644 --- a/src/rotation.rs +++ b/src/rotation.rs @@ -231,53 +231,3 @@ pub async fn scan_for_rotation( Ok(needs_rotation_list) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generate_secret() { - let secret = generate_secret(32); - assert_eq!(secret.len(), 32); - - let secret2 = generate_secret(32); - assert_ne!(secret, secret2); // Should be different each time - } - - #[test] - fn test_needs_rotation_no_metadata() { - assert!(!needs_rotation(&None, 6)); - } - - #[test] - fn test_needs_rotation_not_enabled() { - let mut meta = HashMap::new(); - meta.insert("rotation_enabled".to_string(), "false".to_string()); - assert!(!needs_rotation(&Some(meta), 6)); - } - - #[test] - fn test_needs_rotation_no_date() { - let mut meta = HashMap::new(); - meta.insert("rotation_enabled".to_string(), "true".to_string()); - assert!(needs_rotation(&Some(meta), 6)); - } - - #[test] - fn test_needs_rotation_recent() { - let mut meta = HashMap::new(); - meta.insert("rotation_enabled".to_string(), "true".to_string()); - meta.insert("last_rotated".to_string(), Utc::now().to_rfc3339()); - assert!(!needs_rotation(&Some(meta), 6)); - } - - #[test] - fn test_needs_rotation_old() { - let mut meta = HashMap::new(); - meta.insert("rotation_enabled".to_string(), "true".to_string()); - let old_date = Utc::now() - Duration::days(200); - meta.insert("last_rotated".to_string(), old_date.to_rfc3339()); - assert!(needs_rotation(&Some(meta), 6)); - } -} diff --git a/src/targets/api.rs b/src/targets/api.rs index 53583f0..da9e6bd 100644 --- a/src/targets/api.rs +++ b/src/targets/api.rs @@ -30,7 +30,7 @@ impl ApiTarget { } /// Build the full URL for password update endpoint - pub(crate) fn build_url(&self, username: &str) -> String { + pub fn build_url(&self, username: &str) -> String { // Replace {username} placeholder if present let url = self.config.endpoint.replace("{username}", username); @@ -133,93 +133,3 @@ impl Target for ApiTarget { "api" } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::ApiTargetConfig; - - #[test] - fn test_build_url_with_placeholder() { - let config = ApiTargetConfig { - base_url: "https://api.example.com".to_string(), - endpoint: "/users/{username}/password".to_string(), - method: "POST".to_string(), - password_field: "password".to_string(), - username_field: Some("username".to_string()), - additional_fields: None, - auth_header: None, - headers: None, - timeout_seconds: 30, - }; - - let rt = tokio::runtime::Runtime::new().unwrap(); - let target = rt.block_on(ApiTarget::new(&config)).unwrap(); - - let url = target.build_url("testuser"); - assert_eq!(url, "https://api.example.com/users/testuser/password"); - } - - #[test] - fn test_build_url_with_full_url() { - let config = ApiTargetConfig { - base_url: "https://api.example.com".to_string(), - endpoint: "https://other.com/api/password".to_string(), - method: "POST".to_string(), - password_field: "password".to_string(), - username_field: None, - additional_fields: None, - auth_header: None, - headers: None, - timeout_seconds: 30, - }; - - let rt = tokio::runtime::Runtime::new().unwrap(); - let target = rt.block_on(ApiTarget::new(&config)).unwrap(); - - let url = target.build_url("testuser"); - assert_eq!(url, "https://other.com/api/password"); - } - - #[test] - fn test_build_url_with_relative_path() { - let config = ApiTargetConfig { - base_url: "https://api.example.com".to_string(), - endpoint: "password".to_string(), - method: "POST".to_string(), - password_field: "password".to_string(), - username_field: None, - additional_fields: None, - auth_header: None, - headers: None, - timeout_seconds: 30, - }; - - let rt = tokio::runtime::Runtime::new().unwrap(); - let target = rt.block_on(ApiTarget::new(&config)).unwrap(); - - let url = target.build_url("testuser"); - assert_eq!(url, "https://api.example.com/password"); - } - - #[test] - fn test_build_url_with_trailing_slash() { - let config = ApiTargetConfig { - base_url: "https://api.example.com/".to_string(), - endpoint: "/password".to_string(), - method: "POST".to_string(), - password_field: "password".to_string(), - username_field: None, - additional_fields: None, - auth_header: None, - headers: None, - timeout_seconds: 30, - }; - - let rt = tokio::runtime::Runtime::new().unwrap(); - let target = rt.block_on(ApiTarget::new(&config)).unwrap(); - - let url = target.build_url("testuser"); - assert_eq!(url, "https://api.example.com/password"); - } -} diff --git a/src/targets/postgres.rs b/src/targets/postgres.rs index 1ec1374..dfa67d9 100644 --- a/src/targets/postgres.rs +++ b/src/targets/postgres.rs @@ -55,7 +55,7 @@ impl PostgresTarget { } /// Build PostgreSQL connection string - fn build_connection_string( + pub fn build_connection_string( host: &str, port: u16, username: &str, @@ -70,7 +70,7 @@ impl PostgresTarget { } /// Quote PostgreSQL identifier to prevent SQL injection - fn quote_identifier(identifier: &str) -> String { + pub fn quote_identifier(identifier: &str) -> String { // PostgreSQL identifiers are case-insensitive unless quoted // We'll quote them to be safe and preserve case format!("\"{}\"", identifier.replace("\"", "\"\"")) @@ -143,38 +143,3 @@ impl Target for PostgresTarget { "postgres" } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_quote_identifier() { - assert_eq!( - PostgresTarget::quote_identifier("test_user"), - "\"test_user\"" - ); - assert_eq!( - PostgresTarget::quote_identifier("user\"name"), - "\"user\"\"name\"" - ); - } - - #[test] - fn test_build_connection_string() { - let conn_str = PostgresTarget::build_connection_string( - "localhost", - 5432, - "postgres", - "password", - "postgres", - "prefer", - ); - assert!(conn_str.contains("host=localhost")); - assert!(conn_str.contains("port=5432")); - assert!(conn_str.contains("user=postgres")); - assert!(conn_str.contains("password=password")); - assert!(conn_str.contains("dbname=postgres")); - assert!(conn_str.contains("sslmode=prefer")); - } -} diff --git a/tests/api_target_tests.rs b/tests/api_target_tests.rs new file mode 100644 index 0000000..46004e4 --- /dev/null +++ b/tests/api_target_tests.rs @@ -0,0 +1,86 @@ +use secret_rotator::targets::ApiTarget; +use secret_rotator::config::ApiTargetConfig; + +#[test] +fn test_build_url_with_placeholder() { + let config = ApiTargetConfig { + base_url: "https://api.example.com".to_string(), + endpoint: "/users/{username}/password".to_string(), + method: "POST".to_string(), + password_field: "password".to_string(), + username_field: Some("username".to_string()), + additional_fields: None, + auth_header: None, + headers: None, + timeout_seconds: 30, + }; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let target = rt.block_on(ApiTarget::new(&config)).unwrap(); + + let url = target.build_url("testuser"); + assert_eq!(url, "https://api.example.com/users/testuser/password"); +} + +#[test] +fn test_build_url_with_full_url() { + let config = ApiTargetConfig { + base_url: "https://api.example.com".to_string(), + endpoint: "https://other.com/api/password".to_string(), + method: "POST".to_string(), + password_field: "password".to_string(), + username_field: None, + additional_fields: None, + auth_header: None, + headers: None, + timeout_seconds: 30, + }; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let target = rt.block_on(ApiTarget::new(&config)).unwrap(); + + let url = target.build_url("testuser"); + assert_eq!(url, "https://other.com/api/password"); +} + +#[test] +fn test_build_url_with_relative_path() { + let config = ApiTargetConfig { + base_url: "https://api.example.com".to_string(), + endpoint: "password".to_string(), + method: "POST".to_string(), + password_field: "password".to_string(), + username_field: None, + additional_fields: None, + auth_header: None, + headers: None, + timeout_seconds: 30, + }; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let target = rt.block_on(ApiTarget::new(&config)).unwrap(); + + let url = target.build_url("testuser"); + assert_eq!(url, "https://api.example.com/password"); +} + +#[test] +fn test_build_url_with_trailing_slash() { + let config = ApiTargetConfig { + base_url: "https://api.example.com/".to_string(), + endpoint: "/password".to_string(), + method: "POST".to_string(), + password_field: "password".to_string(), + username_field: None, + additional_fields: None, + auth_header: None, + headers: None, + timeout_seconds: 30, + }; + + let rt = tokio::runtime::Runtime::new().unwrap(); + let target = rt.block_on(ApiTarget::new(&config)).unwrap(); + + let url = target.build_url("testuser"); + assert_eq!(url, "https://api.example.com/password"); +} diff --git a/tests/aws_secrets_tests.rs b/tests/aws_secrets_tests.rs new file mode 100644 index 0000000..84f08d4 --- /dev/null +++ b/tests/aws_secrets_tests.rs @@ -0,0 +1,97 @@ +use secret_rotator::backends::{AwsSecretsClient, create_test_client}; +use aws_sdk_secretsmanager::types::Tag; +use std::collections::HashMap; + +#[test] +fn test_tags_to_metadata() { + let client = AwsSecretsClient { + client: create_test_client(), + region: "us-east-1".to_string(), + }; + + let tags = vec![ + Tag::builder().key("rotation_enabled").value("true").build(), + Tag::builder() + .key("last_rotated") + .value("2023-01-01T00:00:00Z") + .build(), + Tag::builder() + .key("target_username") + .value("testuser") + .build(), + ]; + + let metadata = client.tags_to_metadata(&tags); + assert_eq!(metadata.get("rotation_enabled"), Some(&"true".to_string())); + assert_eq!( + metadata.get("last_rotated"), + Some(&"2023-01-01T00:00:00Z".to_string()) + ); + assert_eq!( + metadata.get("target_username"), + Some(&"testuser".to_string()) + ); +} + +#[test] +fn test_tags_to_metadata_empty() { + let client = AwsSecretsClient { + client: create_test_client(), + region: "us-east-1".to_string(), + }; + + let tags = vec![]; + let metadata = client.tags_to_metadata(&tags); + assert!(metadata.is_empty()); +} + +#[test] +fn test_metadata_to_tags() { + let client = AwsSecretsClient { + client: create_test_client(), + region: "us-east-1".to_string(), + }; + + let mut metadata = HashMap::new(); + metadata.insert("rotation_enabled".to_string(), "true".to_string()); + metadata.insert( + "last_rotated".to_string(), + "2023-01-01T00:00:00Z".to_string(), + ); + metadata.insert("target_username".to_string(), "testuser".to_string()); + + let tags = client.metadata_to_tags(&metadata); + assert_eq!(tags.len(), 3); + + // Verify tag values + let tag_map: HashMap = tags + .iter() + .filter_map(|tag| { + tag.key() + .and_then(|k| tag.value().map(|v| (k.to_string(), v.to_string()))) + }) + .collect(); + + assert_eq!(tag_map.get("rotation_enabled"), Some(&"true".to_string())); + assert_eq!( + tag_map.get("last_rotated"), + Some(&"2023-01-01T00:00:00Z".to_string()) + ); + assert_eq!( + tag_map.get("target_username"), + Some(&"testuser".to_string()) + ); +} + +#[test] +fn test_metadata_to_tags_empty() { + let client = AwsSecretsClient { + client: create_test_client(), + region: "us-east-1".to_string(), + }; + + let metadata = HashMap::new(); + let tags = client.metadata_to_tags(&metadata); + assert!(tags.is_empty()); +} + diff --git a/tests/config_tests.rs b/tests/config_tests.rs new file mode 100644 index 0000000..af7b9d5 --- /dev/null +++ b/tests/config_tests.rs @@ -0,0 +1,210 @@ +use secret_rotator::config::{Config, RotationConfig}; +use std::fs; +use tempfile::TempDir; + +#[test] +fn test_default_rotation_config() { + let config = RotationConfig::default(); + assert_eq!(config.period_months, 6); + assert_eq!(config.secret_length, 32); +} + +#[test] +fn test_config_from_file() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +backend = "vault" +[vault] +address = "http://localhost:8200" +token = "test-token" +mount = "secret" + +[rotation] +period_months = 12 +secret_length = 64 +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + assert_eq!(config.backend, "vault"); + assert_eq!( + config.vault.as_ref().unwrap().address, + "http://localhost:8200" + ); + assert_eq!(config.rotation.period_months, 12); + assert_eq!(config.rotation.secret_length, 64); +} + +#[test] +fn test_config_from_file_with_aws() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +backend = "aws" +[aws] +region = "us-west-2" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + assert_eq!(config.backend, "aws"); + assert_eq!(config.aws.as_ref().unwrap().region, "us-west-2"); +} + +#[test] +fn test_config_from_file_with_file_backend() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +backend = "file" +[file] +directory = "/tmp/test-secrets" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + assert_eq!(config.backend, "file"); + assert_eq!(config.file.as_ref().unwrap().directory, "/tmp/test-secrets"); +} + +#[test] +fn test_config_from_file_with_targets() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +backend = "vault" +[vault] +address = "http://localhost:8200" +token = "test-token" + +[targets.postgres] +host = "localhost" +port = 5432 +database = "testdb" +username = "admin" +password_path = "admin/password" +ssl_mode = "require" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + assert!(config.targets.is_some()); + let postgres = config.targets.as_ref().unwrap().postgres.as_ref().unwrap(); + assert_eq!(postgres.host, "localhost"); + assert_eq!(postgres.port, 5432); + assert_eq!(postgres.database, "testdb"); + assert_eq!(postgres.username, "admin"); + assert_eq!(postgres.password_path.as_ref().unwrap(), "admin/password"); + assert_eq!(postgres.ssl_mode, "require"); +} + +#[test] +fn test_config_from_file_with_api_target() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +backend = "vault" +[vault] +address = "http://localhost:8200" +token = "test-token" + +[targets.api] +base_url = "https://api.example.com" +endpoint = "/users/{username}/password" +method = "PUT" +password_field = "new_password" +username_field = "user" +timeout_seconds = 60 +auth_header = "Bearer token123" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + let api = config.targets.as_ref().unwrap().api.as_ref().unwrap(); + assert_eq!(api.base_url, "https://api.example.com"); + assert_eq!(api.endpoint, "/users/{username}/password"); + assert_eq!(api.method, "PUT"); + assert_eq!(api.password_field, "new_password"); + assert_eq!(api.username_field.as_ref().unwrap(), "user"); + assert_eq!(api.timeout_seconds, 60); + assert_eq!(api.auth_header.as_ref().unwrap(), "Bearer token123"); +} + +#[test] +fn test_config_defaults() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +backend = "vault" +[vault] +address = "http://localhost:8200" +token = "test-token" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + // Test defaults + assert_eq!(config.vault.as_ref().unwrap().mount, "secret"); + assert_eq!(config.rotation.period_months, 6); + assert_eq!(config.rotation.secret_length, 32); +} + +#[test] +fn test_config_create_sample() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("sample.toml"); + + Config::create_sample(&config_path).unwrap(); + + assert!(config_path.exists()); + let config = Config::from_file(&config_path).unwrap(); + assert_eq!(config.backend, "vault"); + assert!(config.vault.is_some()); + assert!(config.aws.is_some()); + assert!(config.file.is_some()); +} + +#[test] +fn test_postgres_config_defaults() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +[targets.postgres] +host = "localhost" +database = "testdb" +username = "admin" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + let postgres = config.targets.as_ref().unwrap().postgres.as_ref().unwrap(); + assert_eq!(postgres.port, 5432); // default port + assert_eq!(postgres.ssl_mode, "prefer"); // default ssl_mode +} + +#[test] +fn test_api_config_defaults() { + let temp_dir = TempDir::new().unwrap(); + let config_path = temp_dir.path().join("config.toml"); + + let config_content = r#" +[targets.api] +base_url = "https://api.example.com" +endpoint = "/password" +"#; + fs::write(&config_path, config_content).unwrap(); + + let config = Config::from_file(&config_path).unwrap(); + let api = config.targets.as_ref().unwrap().api.as_ref().unwrap(); + assert_eq!(api.method, "POST"); // default method + assert_eq!(api.password_field, "password"); // default password_field + assert_eq!(api.timeout_seconds, 30); // default timeout +} diff --git a/tests/env_updater_tests.rs b/tests/env_updater_tests.rs new file mode 100644 index 0000000..cbc52be --- /dev/null +++ b/tests/env_updater_tests.rs @@ -0,0 +1,51 @@ +use secret_rotator::env_updater::EnvUpdater; +use anyhow::Result; +use std::fs; +use tempfile::TempDir; + +#[test] +fn test_update_new_variable() -> Result<()> { + let temp_dir = TempDir::new()?; + let bashrc = temp_dir.path().join(".bashrc"); + fs::write(&bashrc, "# existing config\n")?; + + let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); + updater.update_env_var("MY_SECRET", "new_value")?; + + let content = fs::read_to_string(&bashrc)?; + assert!(content.contains("export MY_SECRET=\"new_value\"")); + + Ok(()) +} + +#[test] +fn test_update_existing_variable() -> Result<()> { + let temp_dir = TempDir::new()?; + let bashrc = temp_dir.path().join(".bashrc"); + fs::write(&bashrc, "export MY_SECRET=\"old_value\"\n")?; + + let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); + updater.update_env_var("MY_SECRET", "new_value")?; + + let content = fs::read_to_string(&bashrc)?; + assert!(content.contains("export MY_SECRET=\"new_value\"")); + assert!(!content.contains("old_value")); + + Ok(()) +} + +#[test] +fn test_remove_variable() -> Result<()> { + let temp_dir = TempDir::new()?; + let bashrc = temp_dir.path().join(".bashrc"); + fs::write(&bashrc, "export MY_SECRET=\"value\"\n# other config\n")?; + + let updater = EnvUpdater::with_home_dir(temp_dir.path().to_path_buf()); + updater.remove_env_var("MY_SECRET")?; + + let content = fs::read_to_string(&bashrc)?; + assert!(!content.contains("MY_SECRET")); + assert!(content.contains("# other config")); + + Ok(()) +} diff --git a/tests/file_backend_tests.rs b/tests/file_backend_tests.rs new file mode 100644 index 0000000..a623adf --- /dev/null +++ b/tests/file_backend_tests.rs @@ -0,0 +1,74 @@ +use secret_rotator::backends::{FileBackend, SecretBackend}; +use anyhow::Result; +use std::collections::HashMap; +use tempfile::TempDir; + +#[tokio::test] +async fn test_write_and_read_secret() -> Result<()> { + let temp_dir = TempDir::new()?; + let backend = FileBackend::new(temp_dir.path())?; + + let mut data = HashMap::new(); + data.insert("password".to_string(), "test123".to_string()); + data.insert("username".to_string(), "admin".to_string()); + + backend.write_secret("test/secret", data.clone()).await?; + + let secret = backend.read_secret("test/secret").await?; + assert_eq!(secret.data, data); + + Ok(()) +} + +#[tokio::test] +async fn test_metadata() -> Result<()> { + let temp_dir = TempDir::new()?; + let backend = FileBackend::new(temp_dir.path())?; + + let mut metadata = HashMap::new(); + metadata.insert("rotation_enabled".to_string(), "true".to_string()); + metadata.insert("last_rotated".to_string(), "2024-01-01".to_string()); + + backend + .update_metadata("test/secret", metadata.clone()) + .await?; + + let read_meta = backend.read_metadata("test/secret").await?; + assert_eq!(read_meta, metadata); + + Ok(()) +} + +#[tokio::test] +async fn test_list_secrets() -> Result<()> { + let temp_dir = TempDir::new()?; + let backend = FileBackend::new(temp_dir.path())?; + + let mut data1 = HashMap::new(); + data1.insert("password".to_string(), "pass1".to_string()); + backend.write_secret("app/db", data1).await?; + + let mut data2 = HashMap::new(); + data2.insert("token".to_string(), "token1".to_string()); + backend.write_secret("app/api", data2).await?; + + let secrets = backend.list_secrets("").await?; + assert!(secrets.contains(&"app/db".to_string())); + assert!(secrets.contains(&"app/api".to_string())); + + Ok(()) +} + +#[test] +fn test_parse_line() { + assert_eq!( + FileBackend::parse_line("password:test123"), + Some(("password".to_string(), "test123".to_string())) + ); + assert_eq!( + FileBackend::parse_line(" key : value "), + Some(("key".to_string(), "value".to_string())) + ); + assert_eq!(FileBackend::parse_line("# comment"), None); + assert_eq!(FileBackend::parse_line(""), None); +} diff --git a/tests/postgres_target_tests.rs b/tests/postgres_target_tests.rs new file mode 100644 index 0000000..ce064b5 --- /dev/null +++ b/tests/postgres_target_tests.rs @@ -0,0 +1,31 @@ +use secret_rotator::targets::PostgresTarget; + +#[test] +fn test_quote_identifier() { + assert_eq!( + PostgresTarget::quote_identifier("test_user"), + "\"test_user\"" + ); + assert_eq!( + PostgresTarget::quote_identifier("user\"name"), + "\"user\"\"name\"" + ); +} + +#[test] +fn test_build_connection_string() { + let conn_str = PostgresTarget::build_connection_string( + "localhost", + 5432, + "postgres", + "password", + "postgres", + "prefer", + ); + assert!(conn_str.contains("host=localhost")); + assert!(conn_str.contains("port=5432")); + assert!(conn_str.contains("user=postgres")); + assert!(conn_str.contains("password=password")); + assert!(conn_str.contains("dbname=postgres")); + assert!(conn_str.contains("sslmode=prefer")); +} diff --git a/tests/rotation_tests.rs b/tests/rotation_tests.rs new file mode 100644 index 0000000..8f8f872 --- /dev/null +++ b/tests/rotation_tests.rs @@ -0,0 +1,48 @@ +use secret_rotator::rotation::{generate_secret, needs_rotation}; +use std::collections::HashMap; +use chrono::{Duration, Utc}; + +#[test] +fn test_generate_secret() { + let secret = generate_secret(32); + assert_eq!(secret.len(), 32); + + let secret2 = generate_secret(32); + assert_ne!(secret, secret2); // Should be different each time +} + +#[test] +fn test_needs_rotation_no_metadata() { + assert!(!needs_rotation(&None, 6)); +} + +#[test] +fn test_needs_rotation_not_enabled() { + let mut meta = HashMap::new(); + meta.insert("rotation_enabled".to_string(), "false".to_string()); + assert!(!needs_rotation(&Some(meta), 6)); +} + +#[test] +fn test_needs_rotation_no_date() { + let mut meta = HashMap::new(); + meta.insert("rotation_enabled".to_string(), "true".to_string()); + assert!(needs_rotation(&Some(meta), 6)); +} + +#[test] +fn test_needs_rotation_recent() { + let mut meta = HashMap::new(); + meta.insert("rotation_enabled".to_string(), "true".to_string()); + meta.insert("last_rotated".to_string(), Utc::now().to_rfc3339()); + assert!(!needs_rotation(&Some(meta), 6)); +} + +#[test] +fn test_needs_rotation_old() { + let mut meta = HashMap::new(); + meta.insert("rotation_enabled".to_string(), "true".to_string()); + let old_date = Utc::now() - Duration::days(200); + meta.insert("last_rotated".to_string(), old_date.to_rfc3339()); + assert!(needs_rotation(&Some(meta), 6)); +} diff --git a/tests/vault_tests.rs b/tests/vault_tests.rs new file mode 100644 index 0000000..93b5819 --- /dev/null +++ b/tests/vault_tests.rs @@ -0,0 +1,97 @@ +use secret_rotator::backends::{VaultClient, SecretMetadata, VaultSecretData, VaultWriteRequest}; +use std::collections::HashMap; + +#[test] +fn test_vault_client_new() { + let client = VaultClient::new( + "http://localhost:8200".to_string(), + "test-token".to_string(), + ); + assert!(client.is_ok()); +} + +#[test] +fn test_vault_url_construction() { + let client = VaultClient::new( + "http://localhost:8200".to_string(), + "test-token".to_string(), + ) + .unwrap(); + + // Test read URL + let read_url = format!("{}/v1/{}/data/{}", client.address, "secret", "myapp/db"); + assert_eq!(read_url, "http://localhost:8200/v1/secret/data/myapp/db"); + + // Test write URL + let write_url = format!("{}/v1/{}/data/{}", client.address, "secret", "myapp/db"); + assert_eq!(write_url, "http://localhost:8200/v1/secret/data/myapp/db"); + + // Test metadata URL + let meta_url = format!("{}/v1/{}/metadata/{}", client.address, "secret", "myapp/db"); + assert_eq!( + meta_url, + "http://localhost:8200/v1/secret/metadata/myapp/db" + ); +} + +#[test] +fn test_vault_secret_metadata_parsing() { + let mut custom_meta = HashMap::new(); + custom_meta.insert("rotation_enabled".to_string(), "true".to_string()); + custom_meta.insert( + "last_rotated".to_string(), + "2023-01-01T00:00:00Z".to_string(), + ); + + let metadata = SecretMetadata { + custom_metadata: Some(custom_meta.clone()), + }; + + assert_eq!( + metadata + .custom_metadata + .as_ref() + .unwrap() + .get("rotation_enabled"), + Some(&"true".to_string()) + ); +} + +#[test] +fn test_vault_secret_data_structure() { + let mut data = HashMap::new(); + data.insert("password".to_string(), "secret123".to_string()); + data.insert("username".to_string(), "admin".to_string()); + + let mut custom_meta = HashMap::new(); + custom_meta.insert("rotation_enabled".to_string(), "true".to_string()); + + let secret_data = VaultSecretData { + data: data.clone(), + metadata: Some(SecretMetadata { + custom_metadata: Some(custom_meta), + }), + }; + + assert_eq!( + secret_data.data.get("password"), + Some(&"secret123".to_string()) + ); + assert_eq!(secret_data.data.get("username"), Some(&"admin".to_string())); + assert!(secret_data.metadata.is_some()); +} + +#[test] +fn test_vault_write_request_serialization() { + let mut data = HashMap::new(); + data.insert("password".to_string(), "newpass".to_string()); + + let request = VaultWriteRequest { + data: data.clone(), + options: None, + }; + + // Verify structure + assert_eq!(request.data.get("password"), Some(&"newpass".to_string())); + assert!(request.options.is_none()); +}