use super::*;
use decdn_incentive::eth_identity::generate_and_persist;
const TEST_PASSWORD: &str = "hunter2";
fn node_toml_with_data_dir(data_dir: &str) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node.toml");
std::fs::write(&path, format!("[identity]\ndata_dir = \"{data_dir}\"\n")).unwrap();
(dir, path)
}
#[test]
fn resolve_data_dir_honors_config_file() {
let (_dir, path) = node_toml_with_data_dir("/var/lib/decdn");
assert_eq!(
resolve_data_dir(None, Some(&path)).unwrap(),
Some(PathBuf::from("/var/lib/decdn"))
);
}
#[test]
fn resolve_data_dir_output_dir_overrides_config_file() {
let (_dir, path) = node_toml_with_data_dir("/var/lib/decdn");
assert_eq!(
resolve_data_dir(Some(Path::new("/opt/keys")), Some(&path)).unwrap(),
Some(PathBuf::from("/opt/keys"))
);
}
#[test]
fn resolve_data_dir_output_dir_ignores_broken_config() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node.toml");
std::fs::write(&path, "this is not = valid = toml\n").unwrap();
assert_eq!(
resolve_data_dir(Some(Path::new("/opt/keys")), Some(&path)).unwrap(),
Some(PathBuf::from("/opt/keys"))
);
}
#[test]
fn resolve_data_dir_errors_on_broken_config_without_output_dir() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node.toml");
std::fs::write(&path, "this is not = valid = toml\n").unwrap();
assert!(resolve_data_dir(None, Some(&path)).is_err());
}
#[cfg(unix)]
fn secure_tempdir() -> tempfile::TempDir {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
dir
}
#[cfg(not(unix))]
fn secure_tempdir() -> tempfile::TempDir {
tempfile::tempdir().unwrap()
}
fn file_names(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
#[test]
fn address_line_shows_generated_address_and_mutates_nothing() {
let dir = secure_tempdir();
identity::load_or_generate(dir.path()).unwrap();
let address = generate_and_persist(dir.path(), TEST_PASSWORD, false).unwrap();
let keystore = eth_identity::keystore_path(dir.path());
let before = file_names(dir.path());
let attempt = eth_address_line(
&keystore,
&KeystoreState::Present,
&KeystorePassword::Supplied(Zeroizing::new(TEST_PASSWORD.to_owned())),
);
assert_eq!(attempt.line, format!("eth address: {address}"));
assert!(!attempt.retryable, "a decrypted keystore needs no retry");
assert_eq!(
before,
file_names(dir.path()),
"whoami must not add or remove files"
);
}
#[test]
fn address_line_without_password_notes_one_is_needed() {
let keystore = Path::new("/data/keystore.json");
let attempt = eth_address_line(
keystore,
&KeystoreState::Present,
&KeystorePassword::Unavailable,
);
let line = attempt.line;
assert!(
line.starts_with("eth address:") && line.contains("password"),
"expected a password note, got {line:?}"
);
assert!(
!attempt.retryable,
"no password source was present, so there is nothing to retry with"
);
}
#[test]
fn address_line_without_keystore_notes_it_is_absent() {
let keystore = Path::new("/data/keystore.json");
let attempt = eth_address_line(
keystore,
&KeystoreState::Absent,
&KeystorePassword::Unavailable,
);
let line = attempt.line;
assert!(
line.starts_with("eth address:") && line.contains("no keystore"),
"expected a 'no keystore' note, got {line:?}"
);
assert!(!attempt.retryable, "there is no keystore to retry against");
}
#[test]
fn address_line_with_wrong_password_notes_and_asks_for_a_retry() {
let dir = secure_tempdir();
generate_and_persist(dir.path(), TEST_PASSWORD, false).unwrap();
let keystore = eth_identity::keystore_path(dir.path());
let attempt = eth_address_line(
&keystore,
&KeystoreState::Present,
&KeystorePassword::Supplied(Zeroizing::new("definitely-wrong".to_owned())),
);
assert!(
attempt.line.starts_with("eth address: (keystore at "),
"got: {:?}",
attempt.line
);
assert!(
attempt.line.contains("did not open with this password"),
"the line must say why there is no address: {:?}",
attempt.line
);
assert!(
attempt.retryable,
"a present keystore that did not open is exactly the retry case"
);
}
#[test]
fn keystore_state_classifies_present_and_absent() {
let dir = secure_tempdir();
let keystore = eth_identity::keystore_path(dir.path());
assert!(
matches!(keystore_state(&keystore).unwrap(), KeystoreState::Absent),
"absent keystore must classify as Absent"
);
generate_and_persist(dir.path(), TEST_PASSWORD, false).unwrap();
assert!(
matches!(keystore_state(&keystore).unwrap(), KeystoreState::Present),
"present keystore must classify as Present"
);
}
#[test]
fn node_id_load_errors_on_absent_key_without_writing() {
let dir = secure_tempdir();
let err = identity::load(dir.path()).expect_err("absent node key must error");
assert!(
format!("{err:#}").contains("key-gen"),
"error should point at key-gen: {err:#}"
);
assert!(
!identity::key_path(dir.path()).exists(),
"a failed whoami must not create node.secret"
);
}
#[test]
fn report_notes_absent_node_key_and_reports_client_keystore() {
let dir = secure_tempdir();
let client_dir = dir.path().join("client");
generate_and_persist(&client_dir, TEST_PASSWORD, false).unwrap();
let lines = report(dir.path(), None).unwrap();
assert!(
lines.iter().any(|l| l.starts_with("node id:")
&& l.contains("no node key")
&& l.contains("key-gen")),
"absent node key must be a note, got {lines:?}"
);
let client_keystore = eth_identity::keystore_path(&client_dir);
assert!(
lines.contains(&format!(
"client keystore path: {}",
client_keystore.display()
)),
"the client keystore path must be reported, got {lines:?}"
);
assert!(
lines.iter().any(|l| l.starts_with("client eth address:")),
"the client eth address line must be reported, got {lines:?}"
);
assert!(
!identity::key_path(dir.path()).exists(),
"report must not mint a node key"
);
assert!(
!eth_identity::keystore_path(dir.path()).exists(),
"report must not create a node keystore"
);
}
#[test]
fn report_survives_two_keystores_with_two_passwords() {
let dir = secure_tempdir();
let secret = identity::load_or_generate(dir.path()).unwrap();
let node_address = generate_and_persist(dir.path(), TEST_PASSWORD, false).unwrap();
let client_dir = dir.path().join("client");
generate_and_persist(&client_dir, "a-different-password", false).unwrap();
let pw_file = dir.path().join("password");
std::fs::write(&pw_file, TEST_PASSWORD).unwrap();
let lines = report(dir.path(), Some(&pw_file)).unwrap();
assert!(
lines.contains(&format!("node id: {}", secret.public())),
"the node id must print, got {lines:?}"
);
assert!(
lines.contains(&format!("eth address: {node_address}")),
"the keystore this password DOES open must show its address, got {lines:?}"
);
let client_line = lines
.iter()
.find(|l| l.starts_with("client eth address:"))
.expect("the client eth line must print");
assert!(
client_line.contains("did not open with this password"),
"the keystore this password does not open degrades to a note: {client_line:?}"
);
}
#[test]
fn report_shows_node_identity_and_omits_absent_client() {
let dir = secure_tempdir();
let secret = identity::load_or_generate(dir.path()).unwrap();
generate_and_persist(dir.path(), TEST_PASSWORD, false).unwrap();
let lines = report(dir.path(), None).unwrap();
assert!(
lines.contains(&format!("node id: {}", secret.public())),
"the real node id must print, got {lines:?}"
);
assert!(
!lines.iter().any(|l| l.starts_with("client ")),
"no client section without a client keystore, got {lines:?}"
);
}