#![cfg(feature = "vault")]
use base64::{engine::general_purpose::STANDARD as B64, Engine};
use secure_data::kms::KeyProvider;
use secure_data::providers::vault::VaultKeyProvider;
use std::io::{Read, Write};
use std::net::TcpListener;
const TEST_DEK_BYTES: [u8; 32] = [0x42u8; 32];
const TEST_CIPHERTEXT: &str = "vault:v1:test-wrapped-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
fn test_dek_b64() -> String {
B64.encode(TEST_DEK_BYTES)
}
fn make_generate_response() -> String {
let dek_b64 = test_dek_b64();
let body = format!(
r#"{{"data":{{"plaintext":"{dek_b64}","ciphertext":"{TEST_CIPHERTEXT}","key_version":1}}}}"#
);
format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
)
}
fn make_decrypt_response() -> String {
let dek_b64 = test_dek_b64();
let body = format!(r#"{{"data":{{"plaintext":"{dek_b64}"}}}}"#);
format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
)
}
fn make_auth_error_response() -> String {
let body = r#"{"errors":["permission denied"]}"#;
format!(
"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
)
}
fn start_mock_server(responses: Vec<String>) -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
let port = listener.local_addr().unwrap().port();
std::thread::spawn(move || {
for response in responses {
match listener.accept() {
Ok((mut stream, _)) => {
let mut buf = [0u8; 8192];
let _ = stream.read(&mut buf);
let _ = stream.write_all(response.as_bytes());
}
Err(_) => break,
}
}
});
port
}
#[tokio::test]
async fn test_vault_generate_data_key_happy_path() {
let port = start_mock_server(vec![make_generate_response()]);
let provider = VaultKeyProvider::new(format!("http://127.0.0.1:{port}"), "test-token")
.expect("provider creation must succeed");
let result = provider.generate_data_key("my-key").await;
let (dek, wrapped, version) = result.expect("generate_data_key must succeed");
assert_eq!(
dek.as_slice(),
&TEST_DEK_BYTES,
"DEK must match mock response"
);
assert!(!wrapped.is_empty(), "wrapped key must not be empty");
assert!(!version.is_empty(), "version must not be empty");
}
#[tokio::test]
async fn test_vault_unwrap_data_key_happy_path() {
let port = start_mock_server(vec![make_decrypt_response()]);
let provider = VaultKeyProvider::new(format!("http://127.0.0.1:{port}"), "test-token")
.expect("provider creation must succeed");
let wrapped = TEST_CIPHERTEXT.as_bytes().to_vec();
let result = provider.unwrap_data_key(&wrapped, "my-key", "v1").await;
let dek = result.expect("unwrap_data_key must succeed");
assert_eq!(dek.as_slice(), &TEST_DEK_BYTES, "unwrapped DEK must match");
}
#[tokio::test]
async fn test_vault_generate_data_key_provider_unavailable() {
use secure_data::error::DataError;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let provider = VaultKeyProvider::new(format!("http://127.0.0.1:{port}"), "test-token")
.expect("provider creation must succeed");
let result = provider.generate_data_key("my-key").await;
assert!(
matches!(result, Err(DataError::ProviderUnavailable { .. })),
"expected ProviderUnavailable, got: {result:?}"
);
}
#[tokio::test]
async fn test_vault_invalid_auth_token() {
use secure_data::error::DataError;
let port = start_mock_server(vec![make_auth_error_response()]);
let provider = VaultKeyProvider::new(format!("http://127.0.0.1:{port}"), "expired-token")
.expect("provider creation must succeed");
let result = provider.generate_data_key("my-key").await;
assert!(
matches!(result, Err(DataError::ProviderAuthError { .. })),
"expected ProviderAuthError, got: {result:?}"
);
}
#[tokio::test]
async fn test_vault_encrypt_decrypt_roundtrip() {
use secure_data::envelope::{decrypt_for_use, encrypt_for_storage};
let port_gen = start_mock_server(vec![make_generate_response()]);
let port_dec = start_mock_server(vec![make_decrypt_response()]);
let provider_enc = VaultKeyProvider::new(format!("http://127.0.0.1:{port_gen}"), "test-token")
.expect("provider for encryption must succeed");
let provider_dec = VaultKeyProvider::new(format!("http://127.0.0.1:{port_dec}"), "test-token")
.expect("provider for decryption must succeed");
let plaintext = b"secret data for vault roundtrip test";
let envelope = encrypt_for_storage(plaintext, "my-key", &provider_enc)
.await
.expect("encryption must succeed");
let recovered = decrypt_for_use(&envelope, &provider_dec)
.await
.expect("decryption must succeed");
assert_eq!(
recovered, plaintext,
"recovered plaintext must match original"
);
}