use sysknife_daemon::actions::{ssh, ActionMechanism};
use sysknife_daemon::executor::execute_spec;
use tempfile::tempdir;
const TEST_KEY: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKeyForSysknifeUnitTestsDoNotUse testuser@sysknife-test";
const OTHER_KEY: &str =
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtherKeyForSysknifeUnitTestsOnly other@sysknife-test";
const USERNAME: &str = "testuser";
fn redirect_spec_path(
mut spec: sysknife_daemon::actions::ActionSpec,
temp_path: &str,
) -> sysknife_daemon::actions::ActionSpec {
let real_path = format!("/home/{USERNAME}/.ssh/authorized_keys");
if let ActionMechanism::Command {
ref mut program,
ref mut args,
..
} = spec.mechanism
{
if *program == "sudo" && args.first().map(String::as_str) == Some("sh") {
*program = "sh";
args.remove(0);
}
for arg in args.iter_mut() {
if arg.contains(&real_path) {
*arg = arg.replace(&real_path, temp_path);
}
}
}
spec
}
#[tokio::test]
async fn add_authorized_key_appends_key_to_existing_empty_file() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, "").unwrap();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(TEST_KEY),
"key must appear in authorized_keys after add: {content:?}"
);
}
#[tokio::test]
async fn add_authorized_key_creates_file_when_absent() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0);
assert!(
std::path::Path::new(&keys_path).exists(),
"authorized_keys must be created when absent"
);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(TEST_KEY),
"key must be in newly created file: {content:?}"
);
}
#[tokio::test]
async fn add_authorized_key_is_idempotent() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
execute_spec(&spec).await.unwrap();
let content = std::fs::read_to_string(&keys_path).unwrap();
let count = content.lines().filter(|line| *line == TEST_KEY).count();
assert_eq!(
count, 1,
"key must appear exactly once after idempotent add: {content:?}"
);
}
#[tokio::test]
async fn add_authorized_key_preserves_other_keys() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::add_authorized_key(USERNAME, TEST_KEY), &keys_path);
execute_spec(&spec).await.unwrap();
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(OTHER_KEY),
"pre-existing key must not be removed: {content:?}"
);
assert!(
content.contains(TEST_KEY),
"new key must also be present: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_deletes_exact_matching_line() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::remove_authorized_key(USERNAME, TEST_KEY), &keys_path);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(out.exit_code, 0);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
!content.contains(TEST_KEY),
"removed key must not remain in authorized_keys: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_preserves_other_keys() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{TEST_KEY}\n{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(ssh::remove_authorized_key(USERNAME, TEST_KEY), &keys_path);
execute_spec(&spec).await.unwrap();
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
!content.contains(TEST_KEY),
"target key must be removed: {content:?}"
);
assert!(
content.contains(OTHER_KEY),
"other key must remain untouched: {content:?}"
);
}
#[tokio::test]
async fn remove_authorized_key_is_noop_when_key_absent() {
let dir = tempdir().unwrap();
let keys_path = dir
.path()
.join("authorized_keys")
.to_string_lossy()
.into_owned();
std::fs::write(&keys_path, format!("{OTHER_KEY}\n")).unwrap();
let spec = redirect_spec_path(
ssh::remove_authorized_key(USERNAME, TEST_KEY), &keys_path,
);
let out = execute_spec(&spec).await.unwrap();
assert_eq!(
out.exit_code, 0,
"remove when key is absent must exit 0 (no-op)"
);
let content = std::fs::read_to_string(&keys_path).unwrap();
assert!(
content.contains(OTHER_KEY),
"unrelated key must not be affected by no-op remove: {content:?}"
);
}