#![forbid(unsafe_code)] use secrecy::{ExposeSecret, Secret}; use xand_secrets::{ReadSecretError, SecretKeyValueStore}; use xand_secrets_vault::{VaultConfiguration, VaultSecretKeyValueStore}; const TEST_TOKEN: &str = "token123"; const TOKEN_HEADER: &str = "X-Vault-Token"; fn create_basic_kv_store() -> VaultSecretKeyValueStore { VaultSecretKeyValueStore::create_from_config(VaultConfiguration { http_endpoint: mockito::server_url(), token: Secret::new(String::from(TEST_TOKEN)), additional_https_root_certificate_files: None, }) .unwrap() } #[tokio::test] async fn test_valid_key_access() -> Result<(), ReadSecretError> { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(200) .with_header("Content-Type", "text/json") .with_body("{\"data\": {\"mykey\": \"mysecret\"}}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await?; mock_kv_endpoint.assert(); println!("{}", result.expose_secret()); assert_eq!("mysecret", result.expose_secret()); Ok(()) } #[tokio::test] async fn test_path_not_found() { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(404) .with_header("Content-Type", "text/json") .with_body("{\"errors\":[]}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await; mock_kv_endpoint.assert(); if let Err(ReadSecretError::KeyNotFound { key }) = result { assert_eq!(key, "secrets/myverysecretdata/mykey"); } else { panic!("Expected KeyNotFoundError, got {:?}", result); } } #[tokio::test] async fn test_key_not_found() { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(200) .with_header("Content-Type", "text/json") .with_body("{\"data\": {\"someotherkey\": \"mysecret\"}}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await; mock_kv_endpoint.assert(); if let Err(ReadSecretError::KeyNotFound { key }) = result { assert_eq!(key, "secrets/myverysecretdata/mykey"); } else { panic!("Expected KeyNotFound error, got {:?}", result); } } #[tokio::test] async fn test_key_has_no_slashes_found() { let kv_store = create_basic_kv_store(); let result = kv_store.read("secretsmyverysecretdatamykey").await; if let Err(ReadSecretError::KeyNotFound { key }) = result { assert_eq!(key, "secretsmyverysecretdatamykey"); } else { panic!("Expected KeyNotFound error, got {:?}", result); } } #[tokio::test] async fn test_invalid_json_missing_brace() { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(200) .with_header("Content-Type", "text/json") // Missing close brace .with_body("{\"data\": {\"mykey\": \"mysecret\"}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await; mock_kv_endpoint.assert(); if let Err(ReadSecretError::Request { internal_error: e }) = result { assert_eq!( "error decoding response body: EOF while parsing an object at line 1 column 30", format!("{}", e) ); } else { panic!("Expected Request error, got {:?}", result); } } #[tokio::test] async fn test_invalid_json_missing_value() { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(200) .with_header("Content-Type", "text/json") .with_body("{\"data\": {\"mykey\":}}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await; mock_kv_endpoint.assert(); if let Err(ReadSecretError::Request { internal_error: e }) = result { assert_eq!( "error decoding response body: expected value at line 1 column 19", format!("{}", e) ); } else { panic!("Expected Request error, got {:?}", result); } } #[tokio::test] async fn test_permission_denied() { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(403) .with_header("Content-Type", "text/json") .with_body("{\"errors\": [\"permission denied\"]}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await; mock_kv_endpoint.assert(); if let Err(ReadSecretError::Authentication { internal_error: e }) = result { assert_eq!( "\ An error code was returned while making an HTTP request to path \"/v1/secrets/myverysecretdata\". \ Status code: 403. Vault returned errors: [\"permission denied\"]\ ", format!("{}", e) ); } else { panic!("Expected Authentication error, got {:?}", result); } } #[tokio::test] async fn test_json_missing_data_attribute() { let kv_store = create_basic_kv_store(); let mock_kv_endpoint = mockito::mock("GET", "/v1/secrets/myverysecretdata") .with_status(200) .with_header("Content-Type", "text/json") .with_body("{\"lease_duration\":36000}") .match_header(TOKEN_HEADER, TEST_TOKEN) .create(); let result = kv_store.read("secrets/myverysecretdata/mykey").await; mock_kv_endpoint.assert(); if let Err(ReadSecretError::Request { internal_error: e }) = result { assert_eq!( "error decoding response body: missing field `data` at line 1 column 24", format!("{}", e) ); } else { panic!("Expected Request error, got {:?}", result); } }