#[cfg(feature = "cloud-pull-keepass")]
mod inner {
use keepass::{
db::{fields, GroupMut},
Database, DatabaseKey,
};
use tempfile::tempdir;
use tsafe_cli::tsafe_keepass::{pull_entries, KeePassConfig, KeePassError};
use tsafe_core::pullconfig::PullSource;
const PASSWORD: &str = "test-master-password";
fn make_kdbx(
password: &str,
setup: impl FnOnce(&mut Database),
) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempdir().unwrap();
let path = dir.path().join("test.kdbx");
let mut db = Database::new();
setup(&mut db);
let key = DatabaseKey::new().with_password(password);
let mut file = std::fs::File::create(&path).unwrap();
db.save(&mut file, key).unwrap();
(dir, path)
}
fn add_entry(
group: &mut GroupMut<'_>,
title: &str,
username: &str,
password: &str,
url: Option<&str>,
) {
let mut entry = group.add_entry();
entry.set_unprotected(fields::TITLE, title);
entry.set_unprotected(fields::USERNAME, username);
entry.set_protected(fields::PASSWORD, password);
if let Some(u) = url {
entry.set_unprotected(fields::URL, u);
}
}
fn cfg_from_path(path: &std::path::Path) -> KeePassConfig {
KeePassConfig::from_pull_source(&PullSource::Keepass {
name: None,
ns: None,
path: path.to_string_lossy().into_owned(),
password_env: Some("TSAFE_TEST_KP_PASSWORD".to_string()),
keyfile_path: None,
group: None,
recursive: None,
overwrite: false,
})
.expect("config should be valid when env var is set")
}
fn cfg_from_path_with_group(path: &std::path::Path, group: &str) -> KeePassConfig {
KeePassConfig::from_pull_source(&PullSource::Keepass {
name: None,
ns: None,
path: path.to_string_lossy().into_owned(),
password_env: Some("TSAFE_TEST_KP_PASSWORD".to_string()),
keyfile_path: None,
group: Some(group.to_string()),
recursive: None,
overwrite: false,
})
.expect("config should be valid when env var is set")
}
#[test]
fn keepass_pull_reads_username_and_password() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
add_entry(&mut root, "Database Creds", "admin", "s3cr3t", None);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = cfg_from_path(&path);
let entries = pull_entries(&cfg).expect("pull_entries should succeed");
let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
let values: std::collections::HashMap<&str, &str> = entries
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
assert!(
keys.contains(&"DATABASE_CREDS_USERNAME"),
"expected DATABASE_CREDS_USERNAME in keys, got {keys:?}"
);
assert!(
keys.contains(&"DATABASE_CREDS_PASSWORD"),
"expected DATABASE_CREDS_PASSWORD in keys, got {keys:?}"
);
assert_eq!(
values.get("DATABASE_CREDS_USERNAME").copied(),
Some("admin")
);
assert_eq!(
values.get("DATABASE_CREDS_PASSWORD").copied(),
Some("s3cr3t")
);
});
}
#[test]
fn keepass_pull_reads_url_field() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
add_entry(
&mut root,
"My Service",
"user",
"pass",
Some("https://example.com"),
);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = cfg_from_path(&path);
let entries = pull_entries(&cfg).expect("pull_entries should succeed");
let values: std::collections::HashMap<&str, &str> = entries
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
assert_eq!(
values.get("MY_SERVICE_URL").copied(),
Some("https://example.com")
);
});
}
#[test]
fn keepass_pull_normalises_title_with_spaces() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
add_entry(&mut root, "db prod server", "dbadmin", "dbpass", None);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = cfg_from_path(&path);
let entries = pull_entries(&cfg).expect("pull_entries should succeed");
let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
assert!(
keys.iter().any(|k| k.starts_with("DB_PROD_SERVER_")),
"expected normalised key prefix DB_PROD_SERVER_, got {keys:?}"
);
});
}
#[test]
fn keepass_pull_group_filter_returns_only_group_entries() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
add_entry(&mut root, "Root Entry", "root-user", "root-pass", None);
let mut infra = root.add_group();
infra.name = "Infrastructure".to_string();
add_entry(&mut infra, "Infra Entry", "infra-user", "infra-pass", None);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = cfg_from_path_with_group(&path, "Infrastructure");
let entries = pull_entries(&cfg).expect("pull_entries should succeed");
let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
assert!(
keys.contains(&"INFRA_ENTRY_USERNAME"),
"expected INFRA_ENTRY_USERNAME in keys, got {keys:?}"
);
assert!(
!keys.contains(&"ROOT_ENTRY_USERNAME"),
"expected ROOT_ENTRY_USERNAME to be absent from keys, got {keys:?}"
);
});
}
#[test]
fn keepass_pull_group_filter_is_case_insensitive() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
let mut grp = root.add_group();
grp.name = "MyGroup".to_string();
add_entry(&mut grp, "Group Entry", "g-user", "g-pass", None);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = cfg_from_path_with_group(&path, "mygroup");
let entries = pull_entries(&cfg).expect("pull_entries should succeed");
let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
assert!(
keys.contains(&"GROUP_ENTRY_USERNAME"),
"expected GROUP_ENTRY_USERNAME in keys with case-insensitive match, got {keys:?}"
);
});
}
#[test]
fn keepass_pull_wrong_password_errors() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
add_entry(&mut root, "Entry", "user", "pass", None);
});
temp_env::with_var(
"TSAFE_TEST_KP_PASSWORD",
Some("definitely-wrong-password"),
|| {
let cfg = cfg_from_path(&path);
let result = pull_entries(&cfg);
assert!(
matches!(result, Err(KeePassError::Auth)),
"expected KeePassError::Auth for wrong password, got {result:?}"
);
},
);
}
#[test]
fn keepass_pull_missing_file_errors() {
let dir = tempdir().unwrap();
let nonexistent_path = dir.path().join("does_not_exist.kdbx");
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = KeePassConfig::from_pull_source(&PullSource::Keepass {
name: None,
ns: None,
path: nonexistent_path.to_string_lossy().into_owned(),
password_env: Some("TSAFE_TEST_KP_PASSWORD".to_string()),
keyfile_path: None,
group: None,
recursive: None,
overwrite: false,
})
.expect("config is valid");
let result = pull_entries(&cfg);
assert!(
matches!(result, Err(KeePassError::Open(_))),
"expected KeePassError::Open for missing file, got {result:?}"
);
});
}
#[test]
fn keepass_pull_nonexistent_group_errors() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
add_entry(&mut root, "Entry", "user", "pass", None);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = cfg_from_path_with_group(&path, "NoSuchGroup");
let result = pull_entries(&cfg);
assert!(
matches!(result, Err(KeePassError::GroupNotFound(_))),
"expected KeePassError::GroupNotFound, got {result:?}"
);
});
}
#[test]
fn keepass_config_missing_password_env_errors() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.kdbx");
let result = temp_env::with_var("TSAFE_TEST_KP_PASSWORD_ABSENT", None::<&str>, || {
KeePassConfig::from_pull_source(&PullSource::Keepass {
name: None,
ns: None,
path: path.to_string_lossy().into_owned(),
password_env: Some("TSAFE_TEST_KP_PASSWORD_ABSENT".to_string()),
keyfile_path: None,
group: None,
recursive: None,
overwrite: false,
})
});
assert!(
matches!(result, Err(KeePassError::PasswordRequired(_))),
"expected KeePassError::PasswordRequired when env var is unset, got {result:?}"
);
}
#[test]
fn keepass_config_no_credentials_errors() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.kdbx");
let result = KeePassConfig::from_pull_source(&PullSource::Keepass {
name: None,
ns: None,
path: path.to_string_lossy().into_owned(),
password_env: None,
keyfile_path: None,
group: None,
recursive: None,
overwrite: false,
});
assert!(
matches!(result, Err(KeePassError::PasswordRequired(_))),
"expected PasswordRequired when neither password_env nor keyfile_path is set, got {result:?}"
);
}
#[test]
fn keepass_pull_recursive_traversal_includes_descendant_groups() {
let (_dir, path) = make_kdbx(PASSWORD, |db| {
let mut root = db.root_mut();
let mut parent = root.add_group();
parent.name = "ParentGroup".to_string();
let mut child = parent.add_group();
child.name = "ChildGroup".to_string();
add_entry(&mut child, "Child Entry", "child-user", "child-pass", None);
});
temp_env::with_var("TSAFE_TEST_KP_PASSWORD", Some(PASSWORD), || {
let cfg = KeePassConfig::from_pull_source(&PullSource::Keepass {
name: None,
ns: None,
path: path.to_string_lossy().into_owned(),
password_env: Some("TSAFE_TEST_KP_PASSWORD".to_string()),
keyfile_path: None,
group: Some("ParentGroup".to_string()),
recursive: Some(true),
overwrite: false,
})
.expect("config should be valid");
let entries = pull_entries(&cfg).expect("pull_entries should succeed");
let keys: Vec<&str> = entries.iter().map(|(k, _)| k.as_str()).collect();
assert!(
keys.contains(&"CHILD_ENTRY_USERNAME"),
"expected CHILD_ENTRY_USERNAME with recursive=true, got {keys:?}"
);
});
}
#[test]
fn keepass_pull_source_yaml_parses() {
let yaml = r#"
pulls:
- source: kp
path: /tmp/test.kdbx
password_env: TSAFE_KP_PASSWORD
group: Infrastructure
recursive: false
ns: infra
"#;
let cfg: tsafe_core::pullconfig::PullConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(cfg.pulls.len(), 1);
match &cfg.pulls[0] {
PullSource::Keepass {
path,
password_env,
group,
recursive,
ns,
..
} => {
assert_eq!(path, "/tmp/test.kdbx");
assert_eq!(password_env.as_deref(), Some("TSAFE_KP_PASSWORD"));
assert_eq!(group.as_deref(), Some("Infrastructure"));
assert_eq!(recursive, &Some(false));
assert_eq!(ns.as_deref(), Some("infra"));
}
other => panic!("expected Keepass variant, got {other:?}"),
}
}
#[test]
fn keepass_pull_source_accessors() {
use tsafe_core::pullconfig::PullSource;
let src = PullSource::Keepass {
name: Some("dev-kp".into()),
ns: Some("dev".into()),
path: "/tmp/dev.kdbx".into(),
password_env: Some("KP_PW".into()),
keyfile_path: None,
group: None,
recursive: None,
overwrite: false,
};
assert_eq!(src.name(), Some("dev-kp"));
assert_eq!(src.ns(), Some("dev"));
assert_eq!(src.provider_type(), "kp");
}
}