#[cfg(test)]
mod feature_validation_tests {
use keeper_secrets_manager_core::config_keys::ConfigKeys;
use keeper_secrets_manager_core::core::{ClientOptions, SecretsManager};
use keeper_secrets_manager_core::crypto::CryptoUtils;
use keeper_secrets_manager_core::custom_error::KSMRError;
use keeper_secrets_manager_core::dto::payload::{UpdateOptions, UpdateTransactionType};
use keeper_secrets_manager_core::dto::{
EncryptedPayload, KsmHttpResponse, QueryOptions, TransmissionKey,
};
use keeper_secrets_manager_core::enums::KvStoreType;
use keeper_secrets_manager_core::storage::{InMemoryKeyValueStorage, KeyValueStorage};
use serde_json::json;
use std::collections::HashMap;
fn create_test_storage() -> Result<KvStoreType, KSMRError> {
let storage = InMemoryKeyValueStorage::new(None)?;
let mut kv_store = KvStoreType::InMemory(storage);
let private_key = CryptoUtils::generate_private_key_ecc()?;
let private_key_der = CryptoUtils::generate_private_key_der()?;
let private_key_base64 =
keeper_secrets_manager_core::utils::bytes_to_base64(&private_key_der);
let public_key_bytes = CryptoUtils::public_key_ecc(&private_key);
let public_key_base64 =
keeper_secrets_manager_core::utils::bytes_to_base64(&public_key_bytes);
kv_store.set(ConfigKeys::KeyClientId, "TEST_CLIENT_ID".to_string())?;
kv_store.set(
ConfigKeys::KeyAppKey,
"dGVzdF9hcHBfa2V5X2Jhc2U2NF9lbmNvZGVkX3ZhbHVlAAAAAAAAAAAA".to_string(),
)?;
kv_store.set(ConfigKeys::KeyServerPublicKeyId, "10".to_string())?;
kv_store.set(
ConfigKeys::KeyHostname,
"fake.keepersecurity.com".to_string(),
)?;
kv_store.set(ConfigKeys::KeyPrivateKey, private_key_base64)?;
kv_store.set(ConfigKeys::KeyOwnerPublicKey, public_key_base64)?;
Ok(kv_store)
}
fn mock_success_response(
_url: String,
transmission_key: TransmissionKey,
_encrypted_payload: EncryptedPayload,
) -> Result<KsmHttpResponse, KSMRError> {
let response = json!({"status": "success"});
let response_bytes = response.to_string().into_bytes();
let encrypted_response =
CryptoUtils::encrypt_aes_gcm(&response_bytes, &transmission_key.key, None)?;
Ok(KsmHttpResponse {
status_code: 200,
data: encrypted_response,
http_response: None,
})
}
#[test]
fn test_update_options_struct_creation() {
let opts1 = UpdateOptions::new(
UpdateTransactionType::Rotation,
vec!["link1".to_string(), "link2".to_string()],
);
assert_eq!(opts1.transaction_type, UpdateTransactionType::Rotation);
assert_eq!(opts1.links_to_remove.len(), 2);
let opts2 = UpdateOptions::with_transaction_type(UpdateTransactionType::General);
assert_eq!(opts2.transaction_type, UpdateTransactionType::General);
assert_eq!(opts2.links_to_remove.len(), 0);
let opts3 = UpdateOptions::with_links_removal(vec!["link1".to_string()]);
assert_eq!(opts3.transaction_type, UpdateTransactionType::General);
assert_eq!(opts3.links_to_remove.len(), 1);
let opts4 = UpdateOptions::default();
assert_eq!(opts4.transaction_type, UpdateTransactionType::General);
assert_eq!(opts4.links_to_remove.len(), 0);
}
#[test]
fn test_update_secret_with_options_link_removal() {
let storage = create_test_storage().expect("Failed to create storage");
let mut client_options = ClientOptions::new_client_options(storage);
client_options.set_custom_post_function(mock_success_response);
let mut secrets_manager =
SecretsManager::new(client_options).expect("Failed to create SecretsManager");
let mut record_dict = HashMap::new();
record_dict.insert("title".to_string(), json!("Test Record"));
record_dict.insert("type".to_string(), json!("login"));
record_dict.insert("fields".to_string(), json!([]));
let record = keeper_secrets_manager_core::dto::Record {
uid: "test-uid".to_string(),
title: "Test Record".to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict,
password: None,
revision: Some(1),
is_editable: true,
folder_uid: "folder-uid".to_string(),
inner_folder_uid: None,
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![],
};
let update_options = UpdateOptions::new(
UpdateTransactionType::General,
vec!["file-uid-1".to_string(), "file-uid-2".to_string()],
);
let result = secrets_manager.update_secret_with_options(record, update_options);
assert!(
result.is_ok(),
"update_secret_with_options should succeed: {:?}",
result
);
}
#[test]
fn test_get_secrets_by_title() {
let mut records = vec![];
for i in 1..=5 {
let title = if i <= 2 { "Production DB" } else { "Dev DB" };
let mut record_dict = HashMap::new();
record_dict.insert("title".to_string(), json!(title));
record_dict.insert("type".to_string(), json!("login"));
record_dict.insert("fields".to_string(), json!([]));
records.push(keeper_secrets_manager_core::dto::Record {
uid: format!("uid-{}", i),
title: title.to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict,
password: None,
revision: Some(1),
is_editable: true,
folder_uid: "folder-uid".to_string(),
inner_folder_uid: None,
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![],
});
}
#[allow(dead_code)]
fn mock_get_all(
_url: String,
transmission_key: TransmissionKey,
_encrypted_payload: EncryptedPayload,
) -> Result<KsmHttpResponse, KSMRError> {
let encrypted_response =
CryptoUtils::encrypt_aes_gcm(&[], &transmission_key.key, None)?;
Ok(KsmHttpResponse {
status_code: 200,
data: encrypted_response,
http_response: None,
})
}
}
#[test]
fn test_query_options_with_request_links() {
let query1 = QueryOptions::new(vec![], vec![]);
assert!(query1.request_links.is_none());
let query2 = QueryOptions::with_links(vec![], vec![], true);
assert_eq!(query2.request_links, Some(true));
let query3 =
QueryOptions::with_links(vec!["uid1".to_string()], vec!["folder1".to_string()], false);
assert_eq!(query3.request_links, Some(false));
assert_eq!(query3.records_filter.len(), 1);
assert_eq!(query3.folders_filter.len(), 1);
}
#[test]
fn test_record_links_field() {
let mut record_dict = HashMap::new();
record_dict.insert("title".to_string(), json!("Test"));
record_dict.insert("type".to_string(), json!("login"));
record_dict.insert("fields".to_string(), json!([]));
record_dict.insert(
"links".to_string(),
json!([
{"recordUid": "linked-record-1", "data": "encrypted-data-1"},
{"recordUid": "linked-record-2", "data": "encrypted-data-2"}
]),
);
let record = keeper_secrets_manager_core::dto::Record {
uid: "test-uid".to_string(),
title: "Test".to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict,
password: None,
revision: Some(1),
is_editable: true,
folder_uid: "folder-uid".to_string(),
inner_folder_uid: None,
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![
[("recordUid".to_string(), json!("linked-record-1"))].into(),
[("recordUid".to_string(), json!("linked-record-2"))].into(),
],
};
assert_eq!(record.links.len(), 2);
assert!(record.links[0].contains_key("recordUid"));
}
#[test]
fn test_keeper_file_url_fields() {
let _file_dict: HashMap<String, serde_json::Value> = [
("fileUid".to_string(), json!("file-123")),
("url".to_string(), json!("https://example.com/file")),
(
"thumbnailUrl".to_string(),
json!("https://example.com/thumb"),
),
("data".to_string(), json!("encrypted-metadata")),
("fileKey".to_string(), json!("encrypted-key")),
]
.iter()
.cloned()
.collect();
let mut metadata = HashMap::new();
metadata.insert("title".to_string(), json!("Test File"));
metadata.insert("name".to_string(), json!("test.txt"));
metadata.insert("type".to_string(), json!("text/plain"));
}
#[test]
fn test_update_payload_links_to_remove() {
use keeper_secrets_manager_core::dto::UpdatePayload;
let mut payload = UpdatePayload::new(
"v16.7.0".to_string(),
"client-id".to_string(),
"record-uid".to_string(),
5,
"encrypted-data".to_string(),
);
assert!(payload.links2_remove.is_none());
payload.set_links_to_remove(vec!["link1".to_string(), "link2".to_string()]);
assert!(payload.links2_remove.is_some());
assert_eq!(payload.links2_remove.as_ref().unwrap().len(), 2);
payload.set_links_to_remove(vec![]);
assert!(payload.links2_remove.is_none());
}
#[test]
fn test_caching_post_function_fallback() {
use keeper_secrets_manager_core::caching;
use std::sync::atomic::{AtomicUsize, Ordering};
caching::clear_cache().ok();
let storage = create_test_storage().expect("Failed to create storage");
let mut client_options = ClientOptions::new_client_options(storage);
thread_local! {
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
}
fn failing_mock(
_url: String,
transmission_key: TransmissionKey,
_encrypted_payload: EncryptedPayload,
) -> Result<KsmHttpResponse, KSMRError> {
let count = CALL_COUNT.with(|c| c.fetch_add(1, Ordering::SeqCst));
if count == 0 {
let response = json!({"status": "success"});
let response_bytes = response.to_string().into_bytes();
let encrypted_response =
CryptoUtils::encrypt_aes_gcm(&response_bytes, &transmission_key.key, None)?;
let mut cache_data = transmission_key.key.clone();
cache_data.extend_from_slice(&encrypted_response);
keeper_secrets_manager_core::caching::save_cache(&cache_data).ok();
Ok(KsmHttpResponse {
status_code: 200,
data: encrypted_response,
http_response: None,
})
} else {
Err(KSMRError::HTTPError("Network error".to_string()))
}
}
client_options.set_custom_post_function(failing_mock);
}
#[test]
fn test_record_has_is_editable_field() {
let mut record_dict = HashMap::new();
record_dict.insert("title".to_string(), json!("Test"));
record_dict.insert("type".to_string(), json!("login"));
record_dict.insert("fields".to_string(), json!([]));
let record1 = keeper_secrets_manager_core::dto::Record {
uid: "test-uid-1".to_string(),
title: "Test".to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict: record_dict.clone(),
password: None,
revision: Some(1),
is_editable: true,
folder_uid: "folder-uid".to_string(),
inner_folder_uid: None,
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![],
};
assert!(record1.is_editable);
let record2 = keeper_secrets_manager_core::dto::Record {
uid: "test-uid-2".to_string(),
title: "Read Only".to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict,
password: None,
revision: Some(1),
is_editable: false,
folder_uid: "folder-uid".to_string(),
inner_folder_uid: None,
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![],
};
assert!(!record2.is_editable);
}
#[test]
fn test_record_has_inner_folder_uid_field() {
let mut record_dict = HashMap::new();
record_dict.insert("title".to_string(), json!("Test"));
record_dict.insert("type".to_string(), json!("login"));
record_dict.insert("fields".to_string(), json!([]));
record_dict.insert("innerFolderUid".to_string(), json!("inner-folder-123"));
let record = keeper_secrets_manager_core::dto::Record {
uid: "test-uid".to_string(),
title: "Test".to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict,
password: None,
revision: Some(1),
is_editable: true,
folder_uid: "parent-folder-uid".to_string(),
inner_folder_uid: Some("inner-folder-123".to_string()),
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![],
};
assert_eq!(
record.inner_folder_uid,
Some("inner-folder-123".to_string())
);
assert_eq!(record.folder_uid, "parent-folder-uid");
}
#[test]
fn test_title_search_case_sensitivity() {
let titles = vec!["Production DB", "production db", "PRODUCTION DB"];
assert_ne!(titles[0], titles[1]);
assert_ne!(titles[0], titles[2]);
assert_ne!(titles[1], titles[2]);
}
#[test]
fn test_secrets_manager_response_has_expires_on() {
let response = keeper_secrets_manager_core::dto::SecretsManagerResponse {
app_data: keeper_secrets_manager_core::dto::AppData::default(),
folders: vec![],
records: vec![],
expires_on: 1699999999000, warnings: None,
just_bound: false,
};
assert_eq!(response.expires_on, 1699999999000);
let formatted = response.expires_on_str(None);
assert!(!formatted.is_empty());
}
#[test]
fn test_custom_post_function_integration() {
use std::sync::atomic::{AtomicBool, Ordering};
let storage = create_test_storage().expect("Failed to create storage");
let mut client_options = ClientOptions::new_client_options(storage);
thread_local! {
static WAS_CALLED: AtomicBool = AtomicBool::new(false);
}
fn tracking_mock(
_url: String,
transmission_key: TransmissionKey,
_encrypted_payload: EncryptedPayload,
) -> Result<KsmHttpResponse, KSMRError> {
WAS_CALLED.with(|called| called.store(true, Ordering::SeqCst));
let encrypted_response =
CryptoUtils::encrypt_aes_gcm(&[], &transmission_key.key, None)?;
Ok(KsmHttpResponse {
status_code: 200,
data: encrypted_response,
http_response: None,
})
}
client_options.set_custom_post_function(tracking_mock);
let mut secrets_manager = SecretsManager::new(client_options).expect("Failed to create");
let mut record_dict = HashMap::new();
record_dict.insert("title".to_string(), json!("Test"));
record_dict.insert("type".to_string(), json!("login"));
record_dict.insert("fields".to_string(), json!([]));
let record = keeper_secrets_manager_core::dto::Record {
uid: "test-uid".to_string(),
title: "Test".to_string(),
record_type: "login".to_string(),
files: vec![],
raw_json: serde_json::to_string(&record_dict).unwrap(),
record_dict,
password: None,
revision: Some(1),
is_editable: true,
folder_uid: "folder-uid".to_string(),
inner_folder_uid: None,
record_key_bytes: vec![0; 32],
folder_key_bytes: None,
links: vec![],
};
secrets_manager.update_secret(record).ok();
let was_called = WAS_CALLED.with(|called| called.load(Ordering::SeqCst));
assert!(was_called, "Custom post function should have been called");
}
#[test]
fn test_new_from_json_parses_links_from_server_response() {
use keeper_secrets_manager_core::crypto::CryptoUtils;
use keeper_secrets_manager_core::dto::Record;
use keeper_secrets_manager_core::utils::bytes_to_base64;
let secret_key = keeper_secrets_manager_core::utils::generate_random_bytes(32);
let record_key = keeper_secrets_manager_core::utils::generate_random_bytes(32);
let encrypted_record_key = CryptoUtils::encrypt_aes_gcm(&record_key, &secret_key, None)
.expect("encrypt record key");
let record_key_b64 = bytes_to_base64(&encrypted_record_key);
let record_data = json!({
"title": "Linked Record Test",
"type": "login",
"fields": [
{"type": "login", "value": ["user@example.com"]},
{"type": "password", "value": ["secret123"]}
],
"custom": []
});
let record_data_bytes = record_data.to_string().into_bytes();
let encrypted_data = CryptoUtils::encrypt_aes_gcm(&record_data_bytes, &record_key, None)
.expect("encrypt record data");
let data_b64 = bytes_to_base64(&encrypted_data);
let mut server_response: HashMap<String, serde_json::Value> = HashMap::new();
server_response.insert("recordUid".to_string(), json!("rec-uid-001"));
server_response.insert("recordKey".to_string(), json!(record_key_b64));
server_response.insert("data".to_string(), json!(data_b64));
server_response.insert("revision".to_string(), json!(5));
server_response.insert("isEditable".to_string(), json!(true));
server_response.insert(
"links".to_string(),
json!([
{"recordUid": "linked-uid-1", "data": "enc-data-1", "recordKey": "enc-key-1"},
{"recordUid": "linked-uid-2", "data": "enc-data-2", "recordKey": "enc-key-2"},
{"recordUid": "linked-uid-3", "data": "enc-data-3"}
]),
);
let record =
Record::new_from_json(server_response, &secret_key, Some("folder-001".to_string()))
.expect("new_from_json should succeed");
assert_eq!(record.links.len(), 3, "Should have 3 linked records");
assert_eq!(
record.links[0].get("recordUid").and_then(|v| v.as_str()),
Some("linked-uid-1")
);
assert_eq!(
record.links[1].get("recordUid").and_then(|v| v.as_str()),
Some("linked-uid-2")
);
assert_eq!(
record.links[2].get("recordUid").and_then(|v| v.as_str()),
Some("linked-uid-3")
);
assert_eq!(record.uid, "rec-uid-001");
assert_eq!(record.title, "Linked Record Test");
assert_eq!(record.revision, Some(5));
assert!(record.is_editable);
}
#[test]
fn test_new_from_json_empty_links() {
use keeper_secrets_manager_core::crypto::CryptoUtils;
use keeper_secrets_manager_core::dto::Record;
use keeper_secrets_manager_core::utils::bytes_to_base64;
let secret_key = keeper_secrets_manager_core::utils::generate_random_bytes(32);
let record_key = keeper_secrets_manager_core::utils::generate_random_bytes(32);
let encrypted_record_key = CryptoUtils::encrypt_aes_gcm(&record_key, &secret_key, None)
.expect("encrypt record key");
let record_key_b64 = bytes_to_base64(&encrypted_record_key);
let record_data = json!({"title": "No Links", "type": "login", "fields": [], "custom": []});
let encrypted_data =
CryptoUtils::encrypt_aes_gcm(&record_data.to_string().into_bytes(), &record_key, None)
.expect("encrypt");
let data_b64 = bytes_to_base64(&encrypted_data);
let mut server_response: HashMap<String, serde_json::Value> = HashMap::new();
server_response.insert("recordUid".to_string(), json!("rec-no-links"));
server_response.insert("recordKey".to_string(), json!(record_key_b64));
server_response.insert("data".to_string(), json!(data_b64));
let record = Record::new_from_json(server_response, &secret_key, None)
.expect("new_from_json should succeed");
assert!(
record.links.is_empty(),
"Links should be empty when not in response"
);
}
#[test]
fn test_all_new_features_compile() {
use keeper_secrets_manager_core::caching;
use keeper_secrets_manager_core::dto::payload::{UpdateOptions, UpdateTransactionType};
use keeper_secrets_manager_core::dto::QueryOptions;
let _opts = UpdateOptions::new(UpdateTransactionType::General, vec![]);
let _opts2 = UpdateOptions::with_transaction_type(UpdateTransactionType::Rotation);
let _opts3 = UpdateOptions::with_links_removal(vec!["link".to_string()]);
let _opts4 = UpdateOptions::default();
let _query = QueryOptions::with_links(vec![], vec![], true);
let _path = caching::get_cache_file_path();
let _exists = caching::cache_exists();
assert!(true);
}
#[test]
fn test_get_folders_does_not_consume_secrets_manager() {
fn mock_empty_folders(
_url: String,
transmission_key: TransmissionKey,
_encrypted_payload: EncryptedPayload,
) -> Result<KsmHttpResponse, KSMRError> {
let response = json!({"folders": [], "records": [], "expiresOn": 0, "warnings": []});
let response_bytes = response.to_string().into_bytes();
let encrypted =
CryptoUtils::encrypt_aes_gcm(&response_bytes, &transmission_key.key, None)?;
Ok(KsmHttpResponse {
status_code: 200,
data: encrypted,
http_response: None,
})
}
let storage = create_test_storage().expect("Failed to create storage");
let mut client_options = ClientOptions::new_client_options(storage);
client_options.set_custom_post_function(mock_empty_folders);
let mut sm = SecretsManager::new(client_options).expect("Failed to create SecretsManager");
let first = sm.get_folders();
let second = sm.get_folders();
assert!(
first.is_ok(),
"First get_folders() call failed: {:?}",
first
);
assert!(
second.is_ok(),
"Second get_folders() call failed: {:?}",
second
);
}
}